一、 不能编译或能编译但运行异常
可以结合printf语句打印,缩小范围,再设置断点,逐行跟踪变量的值是否是预期值,重点关注是否是内存分配不够溢出,或者是一些逻辑问题
二、能编译能运行但提交结果部分正确,部分错误
这种一般不好直接调试,但可从以下方向进行排查:
1. 先核对 “输入输出格式”(最容易忽略的坑),如行末尾、行前是否有空格,间隔是一个还是两个空格,什么时候换行,输出格式是否需要前导0(最容易被忽略的点,一定要注意,不是什么时候都是%d输出,该用%04d输出时就要用)
另外还有一个点,就是一般最后一行输出其实不需要换行符,如果加了换行符,需要考虑像这:
输出格式如下: 5 10000 23333 44444 55555 88888 而代码写成 printf("%d\n", len); qsort(res, len, sizeof(int), compare); int space = 0; for (int i = 0; i < len; i++) { if (!space) { printf("%05d", res[i]); space = 1; } else { printf(" %05d", res[i]); } } printf("\n"); 可能会导致当len=0时,此时不应该有第二行输出,但仍会多输出一个换行符。 所以最保险的办法是不加这个换行符(不影响最终结果),注释掉即可2. 构造 “测试用例” 覆盖全场景,尤其注意题目中的边界条件检查
3. 检查 “数据类型” 和 “计算精度,注意字符串内存是否溢出, int型数据是否溢出等
4. 逻辑错误,重点关注初始化,循环,分支三类逻辑是否正确
三、可以边输入边判断并输出,不影响格式问题!
四、自己在PTA做题过程中遇到的一些借助ai才排查到的问题例子:
1.
char str[81]; fgets(str, 80, stdin); //这里的80应该改为81,是总长度! str[strcspn(str, "\n")] = '\0';应改为:
char str[81]; fgets(str, 81, stdin); //是总长度! str[strcspn(str, "\n")] = '\0';fgets会自动在末尾加'\0',所以这里的参数是包含'\0'在内的总长度,如果写成则只最多只会从stdin中读79个字符,不符合题目最多输入80个字符的意思!
2.
对于超大整数,如果超过int型范围,换long long(64位)去接收,scanf的输入格式和printf打印的占位符都是lld
3. double类型,如果要求打印整数,需要先用(int)强转,再用%d形式打印,直接打印%d不能匹配double类型 !
4. 要求输出前导0时,如4要求输出04,用%02d占位符表示,其中0表示前导0,2表示输出的总位数,如04是两位,%.05d表示输出总位数为5为,不够的左侧补0
5.
输出指定长度的字符串,用%.*s格式,其中占位符.*表示输出字符串的长度,可以截断
字符串转int,用sscanf(char*,"%d", &n),特别注意传的是char*,是地址,可以是字符数组名+3, 但是不能是s[3],这传入的是一个字符,编译器会把这个字符的ascii码值当成内存地址去访问,引起异常
若题目说该数字的位数不超过1000位这种,最好多定义几个字节,比如char str[1005]
又犯一次错: 题目说不超过 100000 个字符的串,我
char s2[100000];// 这里出了问题,不超过100000个字符,那么加上'\0'也得有100001个了,溢出! //应该换成就通过了: char s2[100005];6.
题目要求四舍五入时,用math.h里的一个round(double)函数,自动对小数作四舍五入
round(7.56)直接返回 8,因为它只做 “保留 0 位小数” 的四舍五入;- 若要保留 N 位小数,必须通过 “×10^N → round () → ÷10^N” 的缩放逻辑实现;
round()对 0.5 的处理是 “向远离 0 的方向舍入”(正数进 1,负数退 1)
7.
关于只对一个数组中的有效元素,找出最大最小值时,
如果初始化最大、最小的索引这样写:
int max_index = 0; int min_index = 0;如果索引0不是有效的,那么需要先记录下第一个有效元素的索引first_effctive
再初始化为:
int max = first_effctive;
int min = first_effctive;
8.
int n; scanf("%d", &n); getchar(); //吃掉换行符,因为后面要用fgets读取 char** str = (char**)malloc(sizeof(char*) * n); for (int i = 0; i < n; i++) { str[i] = (char*)malloc(sizeof(char) * 20); fgets(str[i], 17, stdin); str[i][strcspn(str[i], "\n")] = '\0'; }这里因为输入的字符串是一行,包含空格,所以用fgets去读,但是之前的n用scanf读取后,在缓冲区留下了换行符,导致fgets会读取留下的这个换行符而错误
所以一定要用getchar()吃掉上一步scanf留下的换行符,保证后面的fgets正确读取 !
另外,用scanf的%c格式读取时一定一定要小心,它不会跳过换行符等,会读取任意字符
如果缓冲区输入有留下换行符(是我们不期望读取的字符)需要先getchar()吃掉处理
9.
double类型数据读取时,一定用%lf格式去读才能读到!
复数乘法,模值相乘,和幅度角值可以直接相加,得到的就是相乘后的模和幅度角
即模相乘,幅度相加
三角函数sin可以直接用,参数和返回都是double,fabs(double) 用于返回浮点数,double类型数的绝对值,1e-4表示0.0001
10. 打印%需要转义,%本身表示占位符开始,所以打印%需要用专门的%%来打印
11.sscanf处理包含非法字符的字符串(比如12a3)时,并不会直接返回失败,而是会 “尽可能读取合法部分”,这就导致你无法准确判断输入是否为纯数字,比如读取为12,到a不匹配时就停下。返回2(成功读取的数量),所以在转换之前,需要先遍历字符判断是否合法
12. p=='\0' 与 p == NULL 的区别
*p == '\0'和p == NULL是完全独立的两个状态,没有任何必然联系,我用最直白的方式解释:
p == NULL:表示指针变量p本身的值是 0(空地址),它没有指向任何有效的内存空间,此时访问*p会直接导致程序崩溃;*p == '\0':表示指针p指向的有效内存地址中存储的字符是字符串结束符,此时p本身是一个有效的非空指针
13.
字符串比较一定要用strcmp,即使是字符串与单个字符串"#"这种比较,也要用strcmp
在处理输入以读取到#为结束标志时,由于#不算输入,所以注意这时count的统计不能++
当题目直说输入字符串以回车结束时,需要考虑输入可能有空格等空白字符,所以用fgets读取更好,用scanf读取会漏掉要求输入的空白字符!
14.
C 语言不会在运行时检查数组索引是否越界,访问arr[n]不会直接崩溃,而是会读取「数组内存后方的相邻垃圾内存」
内存类型越界,栈数组(在main函数里面静态定义的数组)大概率段错误,小概率逻辑错乱,关键是否触达栈保护页,报段错误
堆数组(动态分配)越界时大概率读取垃圾值,极小概率段错误是否超出进程地址空间,报答案错误!
15.
关于realloc函数使用的易错点,realloc(arr,sizeof(类型)*size)
注意第二个参数是扩容后的总大小,以字节为单位,不是以类型个数为单位,只写个size是错的!
对于动态加入链表函数,head在函数内是局部副本,需要返回类型不为空才能传给主调函数!
16.
在C语言中,NULL宏通常被定义为((void *)0)。这是一个将整数0强制转换为void类型的指针,表示一个不指向任何有效内存地址的空指针,调试时可以用%s格式把空指针打印出null !
17.
全局变量(包括静态局部变量)的 int 类型会自动初始化为 0,而普通局部变量(栈上)的 int 类型如果不手动初始化,值是未定义的垃圾值
具体来说,在主函数外定义的int类型,自动为0,而在main函数内定义的int类型变量,是垃圾值,一般根据需要手动赋0才行
18.
当使用scanf结合!=EOF判断时,输入数据按回车并不算输入/文件 末尾的结束符,scanf会跳过换行符继续阻塞I/O,等待输入数据并读取!
所以这时可手动以输入-1或换行符表示结束
或者边输入边输出运行结果!
19.
exit是库函数,用于退出程序!,编写自定义函数时注意避开系统已存在的函数名称
20.
隐藏的数组越界问题:
1) PTA天梯赛59题,题目虽告知每行字符总长度不超过100,我最开始定义的char str[105],
但是题目要求替换部分内容,而替换后的总长度超过了 105个字符,导致越界!
遇到这种问题,都先试试扩大数组能否解决问题,比如扩大char str[200]!
另外可以复习一下此题的解题思路,尤其是如何找到最后3个单词并进行拼接覆盖(相当于替换的效果)!
2)
段错误时,一般考虑数组越界,而数组越界分为内存溢出和索引异常!
如果确定数组大小足够大,那就考虑索引异常(过大或为负索引),这时候仔细审查索引的控制,是否可能出现过大/为负数的情况!
21.
如果排除了越界错误,且部分答案正确,仔细读题,重新审题,严格按照题目要求复现,同时挖掘边界和极端情况!
关于极端输入的找法:
如果从题目中不明显,顺着代码数据流向找,判断每一步是否有逻辑漏洞漏掉一些条件/极端情况!
22.
复现PTA天梯赛88题,尤其是has_extra位置的定义,控制的是不同分数而不是批次!
顺序学习思路:
对一批分数先排序,然后从小到大选,遇到同分的处理:
int last_score;
遍历分数数组,如果>last_score就取,并更新last_score
如果==last_score,继续根据条件判断是否可选,不用更新last_score
但如果要求了同一批次、同一分数下只能多选一个额外名额,则需要更新额外名额的标志位has_extra !
此方法,先升序或降序排序(让相同的值挨着排列),再更新last_number,也可用于int型数组去重!