the c language
mistakes in c language
一.循环相关问题
(1)1.循环算泰勒级数
1 | |
注意!x是随每次循环而变化的!所以不能一个变量用到底!设一个term保存x的值
1 | |
(1)2.循环算阶乘
1 | |
你想清楚循环的每个过程!!!
在每次阶乘算完,mult得归为1!
不要忘记重置变量!
(2)循环时循环变量
- 注意:
1 | |
到底是h–还是n–;一定写慢一点,想清楚!
1
for(int k=n-1;n>=0;n--)你看清楚!循环变量到底是啥!!!
1
2
3for(int j=0;j<n;j++){
scanf("%d",b[i]);
}你看清楚,循环变量到底是啥!
(3)for循环的逻辑
看清楚
for循环,初始赋值之后,要先进行条件检查,再决定要不要循环!
(4)循环时当总n减少,循环总次数也得相应改变
注意!当后面移动是总字符k减少了时,不要忘了让前面循环总k得减少1!
总结:
任何是总n减小的操作,都需要检查循环中涉及n的部分
对于这种查找,当移动是总n减小1;k直接++的话会略过一个字符的检查!!!
当
k=1时,发现str1[1] ('b') == str1[2] ('b')。内层循环把后面的字符前移,字符串变成
"abc"。此时
same增加,外层循环执行k++。结果:
k变成了2。但现在的str1[2]已经是'c'了,你跳过了对新位置字符的检查。如果输入是"abbbc"(三个连续),你的代码就会漏掉一个 ‘b’。
修正方法:在 same++ 后面加上 k--;,强制让循环重新检查当前位置。
- 对于这种字符串的循环查找,简便写法:
1 | |
直接查找\0即可!
(5)循环数组末尾+’\0‘
1 | |
你想清楚,加’\0’的时候i需不需要再+1!
当i读到’\0’的那一位的时候,就自动跳出来了!arr1[len+i]没有写值进去!不需要再i+1了!
(6)循环时改变指针指向
1 | |
你要想清楚,当通过改变指针而不改变字符串本身位置时,你在for循环比较就应该比较的是对应的指针!而非没有改变位置的arr,这没有意义!
(7)循环时{}的问题
1 | |
看清楚!!!for循环终止的}在哪里!!!
应该在return前面!
(8)循环变量初始=0还是1得考虑清楚所有情况!
1 | |
当i=1时,n=1时,循环进不去!!!
一定考虑全了!
(9)各个循环变量
1.区别i=0;i<n与i=1;i<=n
2.区别i<n与i<=n-1
3.区别circle=1,while(circle!=n-1)
二.细节问题
(1) scanf+&
1 | |
注意!加&!!!!!
(2)初始化变量
命名变量不要忘记初始化!
(3)=和==区分
1 | |
看清楚!!!是应该写==还是赋值=!!!!
(4)!=’\0’而非“\0”
看清楚是单引号而非双引号!
(5)看清楚改变哪个变量
1 | |
要改的是arr2!不是arr
(6)字符串数组末尾不要忘记加’\0’!
1 | |
不要忘记在arr2末尾+’\0’!
三.审题问题
(1)求阶乘之和
输入正整数n(1-10),求1-n的阶乘之和
想清楚!是阶乘的和!!!!!!
先求阶乘,然后再求和!
(2)字符串拼接
注意:使用空格字符来表示字符串的结束。
例如source指向位置,依次保存了字符’a’,字符’b’,字符空格’ ‘,字符’c’,则source指向的字符串为”ab”
审题!所有字符串结束标志都是空格!!!
四.逻辑思路问题
(1)考虑情况不全
1.求两个矩形的交集
1 | |
您的代码计算 dx(交集宽度)的逻辑如下:
1 | |
这个逻辑假设交集宽度一定是从最左边的矩形的右边界到最右边的矩形的左边界,但这个假设在矩形包含(一个矩形完全在另一个矩形内部)的情况下会出错。
正确思路:
计算两个矩形的交集,其原理是:
- 交集矩形的左边界是两个矩形左边界中的较大值。
- 交集矩形的右边界是两个矩形右边界中的较小值。
考虑清楚相交的所有情况!!!
2.用字母后四个字母代替原字母
越界的循环处理 题目要求 “Z” 用 “D” 代替。如果你只是简单地 +4:
- 问题:’Z’ 的 ASCII 码加 4 会变成一些奇怪的符号(如
^),而不是 ‘D’。 - 解决:你需要判断字符是否超过了 ‘Z’(或小写的 ‘z’)。如果超过了,需要减去 26 重新回到字母表的开头。
3.循环链表极端情况
1 | |
4.素数判断
1 | |
要考虑清楚取1,2特殊值的情况
重视边界情况
5.特殊值处理
一个数如果恰好等于它的因子之和,就被成为完数。
例如6的因子为1,2,3,而6=1+2+3,所以6是一个完数。
1 | |
考虑清楚特殊值1!
6.字符串删除
考虑清楚所有异常情况!!!
缺少异常处理:题目明确要求:遇到 s 为 null 或 n 为负数等异常情况,必须输出 "error"。
n 的越界检查:如果 n 超过了当前字符串的长度(sum),你的代码会尝试从非法位置开始操作,这属于异常情况。
删除长度 len 的修正:如果 n + len 超过了 sum(即你想删掉 100 个字符,但后面只剩 2 个了),s[i] 就会去访问空格之后的未知内存,这会导致程序崩溃或逻辑错误。
(2)考虑逻辑不对
1.‘||’和‘&&’的使用
检查输入字符是否为字母
1 | |
大写字母 ‘A’ 到 ‘Z’ 占据了十进制数值 65 到 90 的位置。
小写字母 ‘a’ 到 ‘z’ 占据了十进制数值 97 到 122 的位置。
任何一个字符都不可能同时既是小写又是大写。比如字母 ‘a’,它虽然不满足 arr[i]<'a',但它满足 arr[i]<'A' 为假吗?不,’a’ (97) 是大于 ‘A’ (65) 的,但 ‘a’ 满足 arr[i]>'Z'
想清楚逻辑!!应该是既不是大写里面的也不是小写里面的!
1 | |
同样道理,所有小写字母都大于W!
所以应该分开来处理!
2.约翰夫环
别气馁,这简直是所有程序员在处理链表和循环逻辑时都会遇到的“入行仪式”。这种“差一错误”(Off-by-one error)在约瑟夫环里最容易爆发。
要严密地解决这种逻辑,不能靠“直觉”,要靠一套标准的“边界工程方法”。以下是三条保命准则:
1. 确立“指针含义”的单一性(哨兵思维)
在你的代码里,逻辑乱是因为 later 有时代表“正在数的那个人”,有时又代表“数完之后跳过去的那个人”。
严密的解决办法: 永远让指针指向**“当前正在处理(数数)的人”**。
- 规则: 先判断这个人的编号,再决定是移动还是删除。
- 口诀: “先判断,再位移”。
2. 画出“状态转换图”(纸笔大法)
不要在脑子里跑代码!拿出一张纸,画 3 个圈($n=3$),假设 $m=2$。
第一步:初始状态
prev指向 3,curr指向 1。circle = 1(代表 1 号正在报“1”)。
第二步:逻辑演进
circle(1) 不等于 $m$ (2) $\rightarrow$ 两个指针后移。prev指向 1,curr指向 2。circle变成 2。
第三步:触发删除
circle(2) 等于 $m$ (2) $\rightarrow$ 执行删除。prev->next = curr->next(1 指向 3)。free(curr)。- 关键: 让
curr重新指向prev->next(即 3 号)。 - 重置:
circle = 1(此时 3 号正在报“1”)。
如果你在脑子里过不完这三步,代码里一定会多跳或少跳。
3. 结构化你的 while 循环(分而治之)
一个严密的约瑟夫环 while 循环应该长这样,它只有两条清晰的路径:
C
1 | |
为什么这样写就严密了?
因为你把“报数移动”和“删除移动”彻底解耦了。删人的时候不报数,报数的时候不删人。这样永远不会出现“连跳”两格的情况。
4. 终极自检清单
当你写完后,用以下三个极端情况代入检查:
- $m = 1$: 每个人报 1 就死。检查你的代码会不会删掉第一个人后顺利跳到第二个人。
- 只剩 2 个人时: 检查
prev和curr是否会指向同一个地方导致死循环。 - $m > n$: 检查你的指针是否能跨过
head继续正常绕圈。
总结
你现在的痛苦是因为你想通过“一次性位移”搞定所有情况。真正的严密是:把每一步拆成最小的动作。
你想不想试着用我上面写的那个 if...else... 结构,把你自己的代码重新整理一遍?只要结构对了,你那几个多一个少一个的 bug 会瞬间消失。
Q1:循环讨论的问题
1 | |
你要想清楚,在m点删节点的时候,前驱节点num需不需要在删的同时再跳到后面节点去。
你代入情况分析,如果不跳的话,下一轮刚好在删的节点的后一个,如果跳了那就多了一个!!!
Q2:设置节点位置问题
不要设一个前置,一个后置,这样的话,如果只有两个节点,后置一定会移动到已删除的节点上!
规律总结:前后两个节点间隔位置只能差1,不能差2!
Q3:循环变量问题
1 | |
你想清楚!circle=0还是1!
当前节点已经移到下一个了,相当于走了一步,circle应该+1=1!
1 | |
想清楚到底是<=还是<!!!
如果是小于等于,在sum=n时仍然会循环,此时已经删完了!
如果是小于,你想清楚,当sum=n-1时,会循环最后一次,sum=n-1时已经删了n-1个数了,所以最后删的数是对的!
3.字符串问题
1 | |
你想清楚!你想要的是source[i]=’ ‘,实际上,当到空格时,已经跳出循环了,没读到!
一定把循环逻辑搞清楚!
五.代码优雅性问题
(1)求小岛面积
1 | |
1 | |
注意代码可读性和简洁性!
(2)字符串数组的循环检查
当 k=1 时,发现 str1[1] ('b') == str1[2] ('b')。
内层循环把后面的字符前移,字符串变成
"abc"。此时
same增加,外层循环执行k++。结果:
k变成了2。但现在的str1[2]已经是'c'了,你跳过了对新位置字符的检查。如果输入是"abbbc"(三个连续),你的代码就会漏掉一个 ‘b’。
修正方法:在 same++ 后面加上 k--;,强制让循环重新检查当前位置。
对于这种字符串的循环查找,简便写法:
1 | |
直接查找\0即可!
六.易错知识区
(1)对于数组在函数命名和调用的区别
定义函数时:
1 | |
写法等价
但是在调用函数时;
1 | |
是错误的!
调用时:a 本身就代表了数组的首地址。如果你写 a[],编译器会非常困惑,因为它觉得你在尝试访问数组的某个元素,但你又没给下标(比如 a[5])
只用传数组的地址,即名称a,b
区别
1 | |
(2)scanf何时+&何时不+
必须加
&的情况:基本类型不需要加
&的情况:字符数组(字符串)
当你读取字符串到字符数组时,不需要加 &。
注意:只有字符串数组可以直接scanf,其他类型得str[i]循环来读取
1 | |
- 不需要加
&的进阶情况:指针
1 | |
- 特殊对比:数组中的单个元素
虽然数组名不用加 &,但如果你是要给数组里的某一个特定位置赋值,那就得加 &。
1 | |
(3)strcpy与strcat区别
1 | |
注意!strcpy是覆盖了str1的内容;strcat是拼接!
1 | |
(4)指针相减不需要再除以 sizeof(int)
- 当你执行
last - first时,C 语言的编译器非常智能,它返回的不是字节差,而是两个指针之间相隔的元素个数。举个例子,如果first指向地址 2000,last指向地址 2012,对于int类型数组,它们之间差了 12 个字节,但last - first的结果直接就是 3
(5)总个数=指针相减+1!
如果你执行 2 - 0,结果是 2。这个 2 代表的是它们之间有 2 个间隔。但显然,数组里实际包含了 3 个元素。为了得到包含首尾在内的总个数,你必须把最后那个被漏掉的“终点”补上,所以公式是 n = last - first + 1。
(6)strlen计算len不包括‘\0’
这是一个非常棒的问题,触及了 C 语言字符串处理中最容易出错的细节。
结论是:strlen 计算的是字符串的“有效长度”,不包括末尾的 '\0'。
在 C 语言中,strlen 函数的工作原理是从你给出的地址开始,一个字符一个字符地向后数,直到遇到 '\0' 为止。但是,它数出来的那个数字是不包含 '\0' 本身的。
- 例如:字符串
"China" - 内存布局为:
'C','h','i','n','a','\0' strlen("China")的结果是 5。- 但是,这个字符串实际上在内存里占用了 6 个字节。
(7)指针与函数
swap函数

注意swap4:内存地址的改变在出函数时已经销毁
二级指针

调用前:main 里的 str 是 NULL。
调用时:GetMemory(str) 把 NULL 这个值复印了一份给局部变量 p。
函数内:p = (char *)malloc(100);。现在 局部变量 p 指向了堆上的 100 字节,但 main 里的 str 依然是 NULL。
函数结束:局部变量 p 被销毁了。由于没有 free,那 100 字节变成了内存泄漏。
回到 main:str 还是 NULL。接下来的 strcpy(str, "...") 就相当于向地址 0 写入数据,程序直接崩溃。
使用指向指针的指针(二级指针)
既然你想改的是“指针变量”的值,你就得传“指针变量的地址”。
C
1 | |
方法 B:通过返回值带出来(最常用)
这是最清晰的写法,就像买东西拿发票一样。
C
1 | |
总结
内存一直在堆里,并没有消失。之所以“拿不出来”,是因为你在 GetMemory(char *p) 中只改了副本 p 的指向。
核心结论:
- 如果你想在函数里修改一个
int,传int*。 - 如果你想在函数里修改一个
char*,就得传char**。
就像你想要别人帮你改家里的装修(int),你要给他钥匙(int*);如果你想让别人帮你换一把钥匙(char*),你得把存放钥匙的保险箱地址(char**)告诉他。
区别stu[i].id和stu+i->id
A[i] 本质上只是 *(A + i) 的语法糖
下标运算符 [] 的优先级比解引用运算符 * 高
函数&数组
1 | |
在函数中,只有数组能够直接交换值,其他变量得传入指针形式
p=&i和*p=i
前者是把p的内容从原来指向的地址改向了指向i的地址;
后者是把p指向地址上的内容改成了i,指向的地址没有变
1 | |
注意!在函数中,相当于对传入的指针地址进行了副本拷贝,若直接改指针地址的指向是无效的,只是改了副本的值;所以应该是顺着指向的地址去改里面的值!
所以
1 | |
指针数组&数组指针
既然它排序慢,为什么还要用它?数组指针最核心的价值在于函数传参。
当你需要把一个二维数组传给一个通用处理函数时,数组指针是标准写法:
C
1 | |
单链表
1 | |
注意!!!
赋值是瞬间操作!
即使后面tail=next了,前面head->next依然为NULL!!!
(8)ASCII码中字母位置
1. 大写字母的范围 大写字母 ‘A’ 到 ‘Z’ 占据了十进制数值 65 到 90 的位置。
- ‘A’ 的 ASCII 码是 65。
- ‘B’ 是 66,以此类推。
- ‘Z’ 是 90。
2. 小写字母的范围 小写字母 ‘a’ 到 ‘z’ 占据了十进制数值 97 到 122 的位置。
- ‘a’ 的 ASCII 码是 97。
- ‘b’ 是 98,以此类推。
- ‘z’ 是 122。
3. 大小写之间的关系 你会发现,同一个字母的大写和小写之间正好相差 32。
- 例如:’a’ (97) - ‘A’ (65) = 32。
- 在编程中,如果你想把大写转小写,只需 +32;小写转大写,只需 -32。
A, B, C, D, E, F, G,H, I, J, K, L, M, N,O, P, Q, R, S, T,U, V, W, X, Y, Z
(9)打印前多少字符
打印前20个字符
1 | |
(10)printf打印
| 数据类型 | printf (打印) | scanf (读取) | 为什么? |
|---|---|---|---|
| int | 值 (n) |
地址 (&n) |
scanf 必须知道变量在哪,才能把存进去。 |
| float | 值 (f) |
地址 (&f) |
同上。 |
| char | 值 (c) |
地址 (&c) |
同上。 |
| 字符串 | 地址 (s) |
地址 (s) |
字符串本质就是地址,所以两者都用地址。 |
| 数据类型 | 关键字 | printf (输出) | scanf (输入) | 备注 |
|---|---|---|---|---|
| 字符型 | char |
%c |
%c |
读入单个字符(包括空格) |
| 短整型 | short |
%hd |
%hd |
h 代表 half (int 的一半) |
| 整型 | int |
%d |
%d |
最常用的整数格式 |
| 长整型 | long |
%ld |
%ld |
l 代表 long |
| 长长整型 | long long |
%lld |
%lld |
用于极大整数 |
| 单精度浮点 | float |
%f |
%f |
默认保留 6 位小数 |
| 双精度浮点 | double |
%f 或 %lf |
%lf |
注意:输入必须用 %lf |
| 字符串 | char[] |
%s |
%s |
遇到空格/回车停止 |
| 无符号整型 | unsigned int |
%u |
%u |
仅表示正数 |
| 十六进制 | int |
%x |
%x |
以 0x 格式读写 |
| 符号 | 拆解 | 含义 | 对应数据类型 | 字节数 (通常) |
|---|---|---|---|---|
%u |
% + u |
Unsigned (无符号) | unsigned int |
4 字节 |
%hu |
% + h + u |
Half + Unsigned | unsigned short |
2 字节 |
%d |
% + d |
Decimal (有符号十进制) | int |
4 字节 |
%hd |
% + h + d |
Half + Decimal | short |
2 字节 |
%lu |
% + l + u |
Long + Unsigned | unsigned long |
4 或 8 字节 |
深度避坑指南
陷阱一:double 类型的输入输出不对称。 这是新手最常断命的地方。scanf 必须严格区分:float 用 %f,double 必须用 %lf。如果你给 double 变量用了 %f,读进去的数据会变成一串毫无逻辑的乱码。但在 printf 输出时,由于 C 语言的提升规则,double 用 %f 也是完全可以的。
陷阱二:char 类型的输入会“吃”空格。 执行 scanf(“%c”, &ch) 时,它会老老实实地读取你输入的每一个字符,包括你随手敲的空格或回车。如果你想自动跳过这些没用的空格,只读真正的字符,你应该在格式串里加个空格,写成 scanf(“ %c”, &ch)。
陷阱三:scanf 的地址符 &。 对于 int、float、char 等基本类型,输入时必须在变量名前加 &(如 &a)。但如果是字符串数组(如 char str[100]),由于数组名本身在 C 语言里就是地址,所以 scanf(“%s”, str) 绝对不能加 &。
3. 进阶技巧:格式化控制
在 printf 输出时,你可以精准控制显示效果。使用 %.2f 可以强制保留 2 位小数。使用 %5d 可以保证输出至少占 5 个字符宽度,空间不够时在左侧补空格;如果写成 %-5d 则是左对齐。如果你想在数字前面补零(比如显示时间 08:05),可以使用 %02d。
4. 总结记忆法
整数系: 核心是 d (decimal)。短的加 h (half),长的加 l (long),更长的加 ll。 浮点系: 核心是 f (float)。双精度输入必须加 l,表示 long float。 字符系: 字符是 c (char),字符串是 s (string)。
(11)scanf读取
- 读字符串时(
%s):是的,遇到空格就停止
当你使用 %s 读取字符串时,scanf 会从第一个非空白字符(空格、回车、制表符)开始读,直到遇到下一个空白字符为止。
- 输入:
Hello World - 结果:只读入
Hello,而World会留在缓冲区里等待下一次读取。
- 读数字时(
%d,%f):它会跳过前面的空格
读数字时,scanf 会自动跳过前面的所有空格,直到看到数字为止;一旦数字读完了,遇到空格它就会停止读取,并将该空格留在缓冲区。
- 读字符时(
%c):它连空格都读
这是最容易出错的地方。%c 不会跳过任何字符。如果缓冲区里刚好有一个空格或上次输入留下的回车,scanf("%c", &ch) 会直接把这个空格读进去。
(12)fgets用法
1 | |
(13)字符串处理
1.字符串读取后需要‘\0’封口
1 | |
读取完之后需要加’\0’封口!