1. 题目背景与需求分析
这道PTA题目"L1-016 查验身份证"是一个典型的字符串处理问题,主要考察C语言中对字符串的基本操作和校验逻辑的实现能力。身份证号码作为我国公民的重要身份标识,其编码规则有着严格的标准,这道题目正是基于真实场景设计的编程练习。
在实际业务场景中,身份证校验是金融、政务、电商等系统中必不可少的功能模块。一个合格的身份证号码需要满足以下基本规则:
- 长度必须为18位(新标准)
- 前17位必须全部是数字
- 最后一位可能是数字或字母X(代表罗马数字10)
- 需要符合国家标准的校验码计算规则
题目要求我们编写程序,输入若干身份证号码,输出其中不符合规则的号码。这在实际开发中对应着数据清洗的场景,比如用户注册时验证身份证有效性,或者批量导入数据时的校验环节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 身份证校验规则详解
2.1 基本格式校验
首先我们需要对身份证号码进行基本格式检查:
c复制int checkFormat(const char* id) {
// 检查长度是否为18位
if(strlen(id) != 18) {
return 0;
}
// 检查前17位是否为数字
for(int i = 0; i < 17; i++) {
if(!isdigit(id[i])) {
return 0;
}
}
// 检查最后一位是否为数字或X
char last = id[17];
if(!(isdigit(last) || last == 'X' || last == 'x')) {
return 0;
}
return 1;
}
2.2 校验码计算规则
身份证的第18位是校验码,计算规则如下:
- 将前17位数字分别乘以不同的权重系数:
- 权重系数数组:
- 将17个乘积相加得到总和S
- 计算S除以11的余数:Y = S % 11
- 根据余数Y查找对应的校验码:
- 对应关系表:
实现代码如下:
c复制char calculateCheckCode(const char* id) {
int weights[] = {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2};
char m[] = {'1','0','X','9','8','7','6','5','4','3','2'};
int sum = 0;
for(int i = 0; i < 17; i++) {
sum += (id[i] - '0') * weights[i];
}
int y = sum % 11;
return m[y];
}
3. 完整解决方案实现
3.1 主程序逻辑
基于上述分析,我们可以构建完整的解决方案:
c复制#include <stdio.h>
#include <string.h>
#include <ctype.h>
// 函数声明
int checkFormat(const char* id);
char calculateCheckCode(const char* id);
int main() {
int N;
scanf("%d", &N);
getchar(); // 消耗换行符
char ids[100][19]; // 假设最多100个身份证号
int invalidCount = 0;
for(int i = 0; i < N; i++) {
scanf("%18s", ids[i]);
// 检查格式
if(!checkFormat(ids[i])) {
printf("%s\n", ids[i]);
invalidCount++;
continue;
}
// 检查校验码
char checkCode = calculateCheckCode(ids[i]);
char lastChar = toupper(ids[i][17]);
if(lastChar != checkCode) {
printf("%s\n", ids[i]);
invalidCount++;
}
}
if(invalidCount == 0) {
printf("All passed\n");
}
return 0;
}
// 前面定义的checkFormat和calculateCheckCode函数
3.2 输入输出处理注意事项
在实际编程中,输入处理有几个关键点需要注意:
- 使用
getchar()消耗换行符,避免影响后续输入 - 限制
scanf读取的字符数,防止缓冲区溢出 - 对最后一位校验码进行大小写不敏感比较(X和x都有效)
提示:在真实项目中,建议使用更安全的输入函数如
fgets配合sscanf,但PTA平台通常对输入格式有严格要求,按题目要求使用scanf即可。
4. 常见错误与调试技巧
4.1 典型错误分析
在实现身份证校验程序时,新手常犯的错误包括:
-
数组越界:忘记C语言字符串需要额外的null终止符,声明数组时长度不足
- 错误示例:
char id[18](无法存储18位身份证+'\0') - 正确做法:
char id[19]
- 错误示例:
-
权重系数错误:混淆权重系数的顺序或数值
- 建议将权重数组声明为常量,避免硬编码
-
校验码计算错误:余数与校验码对应关系搞错
- 可以使用已知有效的身份证号进行测试验证
4.2 测试用例设计
完善的测试用例应该包括:
c复制// 格式错误测试
"12345678901234567" // 长度不足
"1234567890123456789" // 长度过长
"12345678901234567X" // 第17位不是数字
"12345678901234567A" // 最后一位无效
// 校验码错误测试
"11010519491231002X" // 正确应为110105194912310027
"440524188001010014" // 正确应为440524188001010018
// 正确样例
"110105194912310027" // 北京朝阳区
"440524188001010018" // 广东汕头
5. 性能优化与扩展思路
5.1 性能优化
对于大规模数据处理,可以考虑以下优化:
- 提前终止检查:在格式检查阶段发现错误后立即返回,不进行后续计算
- 查表法优化:将权重计算和校验码映射预先计算并存储
- 并行处理:对于海量数据,可以使用多线程分批处理
5.2 功能扩展
实际项目中可能需要更复杂的校验:
- 地区码校验:检查前6位是否为有效的行政区划代码
- 生日校验:检查第7-14位是否为有效日期
- 顺序号校验:检查第15-17位是否符合规则(奇数男性,偶数女性)
例如,生日校验函数可以这样实现:
c复制int checkBirthday(const char* id) {
char year[5] = {0};
char month[3] = {0};
char day[3] = {0};
strncpy(year, id+6, 4);
strncpy(month, id+10, 2);
strncpy(day, id+12, 2);
int y = atoi(year);
int m = atoi(month);
int d = atoi(day);
// 简单校验
if(y < 1900 || y > 2100) return 0;
if(m < 1 || m > 12) return 0;
if(d < 1 || d > 31) return 0;
// 更精确的月份天数校验可以在这里添加
return 1;
}
6. 工程实践建议
在实际项目中实现身份证校验时,建议:
- 模块化设计:将校验功能封装成独立函数或模块
- 错误码设计:区分不同类型的错误(格式错误、校验码错误等)
- 日志记录:记录校验失败的身份证号和具体原因
- 单元测试:为每个校验函数编写全面的测试用例
例如,可以设计如下的错误码枚举:
c复制typedef enum {
ID_OK = 0,
ID_ERR_LENGTH,
ID_ERR_FORMAT,
ID_ERR_CHECKCODE,
ID_ERR_REGION,
ID_ERR_BIRTHDAY
} ID_CHECK_RESULT;
在C语言实际开发中,处理字符串时要特别注意内存安全和边界检查。对于身份证校验这种确定长度的字符串,使用固定大小的缓冲区是合理的选择,但一定要确保缓冲区足够大(18位身份证需要19字节的存储空间,包括终止符)。
我在实际项目中遇到过因为少算一个字节导致的内存越界问题,这种bug往往难以追踪。建议在代码中加入明确的长度检查断言:
c复制#define ID_LENGTH 18
void processID(const char* id) {
assert(strlen(id) == ID_LENGTH);
// 其他处理逻辑
}
