C语言:那些让你摔过跤的坑,其实都很眼熟

学 C 语言这件事,说白了就是跟内存和类型死磕,磕着磕着你就会发现,很多错误不是因为你不懂,而是你以为你懂了。

一、变量不是盒子,是标签

很多人刚开始学的时候,脑子里有个画面:变量是个小盒子,数据装在里面,这个画面害人不浅,我也是这么认为的,但是后来我才意识到这个观点是错误的。

变量本质上是个名字,是给内存地址起的外号。你写 int a = 10,编译器在背后干的事是:找一块能装下整数的地方,把 10 塞进去,然后告诉你这块地方以后叫 a。所以当你写 int a = 10, b = a 的时候,别想着 b 和 a 共享什么,它们就是两块独立的内存,值碰巧一样而已。

变量命名也有规矩。intcharreturn 这些关键字是编译器预留的,你不能拿它们当变量名。_2b 以下划线加数字开头是合法的,2b 以数字开头不行,编译器直接报错。变量名只能由字母、数字和下划线组成,必须以字母或下划线开头,这是硬性规则,没得商量

另外 float score; 这种只声明不初始化的写法,score 里面存的是垃圾值,不是 0,不是空,是内存里之前留下的随机数据。你以为它默认是 0,编译器可没这个义务帮你清零。全局变量倒是会自动初始化为 0,但局部变量不会,局部变量里存的是那块内存之前留下的垃圾。所以局部变量使用前一定要初始化,这不是建议,而是一个作为程序员的好习惯,是一个保命的技巧

二、数据类型的大小,别猜

C 语言的数据类型看着简单,实际上坑很多。char 占 1 个字节,short 占 2 个,intlongfloat 在 VS 上都是 4 个,long longdouble 是 8 个,long double 在 VS 上是 8 个,在 gcc 上可能是 16 个。你以为是固定的?错了,C 语言只规定 sizeof(long) >= sizeof(int) 以及 sizeof(long double) >= sizeof(double),具体等于多少,编译器说了算

所以跨平台的时候别硬编码类型大小,想知道当前环境某个类型多大,直接用 sizeofsizeof 是个运算符,不是函数,所以 sizeof asizeof(a) 效果一样,括号可以省。但 sizeof(3 + 3.5) 这种写法要注意,3 是 int,3.5 是 double,相加结果是 double,所以 sizeof 返回的是 double 的大小,也就是 8。

还有一个特别坑的点:sizeof 括号里的表达式不会真的执行。

例如以下代码:

#include <stdio.h>
int main() {
int s = 12;
// sizeof 括号里的表达式不会真的执行
int size = sizeof(s = 20);
printf("s = %d\n", s);  // s 还是 12,不是 20
printf("sizeof(s = 20) = %zu\n", size);  // 返回 int 类型的大小
return 0;
}

你以为 s 会变成 11?错了,s 还是 12。因为 sizeof 在编译期就确定了,它只看表达式的类型,不会真的去执行赋值。

因为 sizeof 在编译期就确定了,它只看表达式的类型,不会真的去执行赋值。sizeof 的返回值类型是 size_t,打印的时候用 %zu 最标准,%zd 也行但 %zu 才是标准写法。用 %d 打印 size_t 类型在 64 位系统上可能出错,因为 size_t 在 64 位系统上是 8 字节,而 %d 只认 4 字节。

sizeof 在编译期就确定了,不会真的去读取内存。所以对数组用 sizeof,得到的是整个数组占的字节数。

例如:

#include <stdio.h>
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 对数组用 sizeof,得到的是整个数组占的字节数
printf("sizeof(arr) = %zu\n", sizeof(arr));  // 输出 40 (10 * 4)
printf("sizeof(arr[0]) = %zu\n", sizeof(arr[0]));  // 输出 4
printf("数组元素个数 = %zu\n", sizeof(arr) / sizeof(arr[0]));  // 输出 10
return 0;
}

输出结果是:

sizeof(arr) = 40
sizeof(arr[0]) = 4
数组元素个数 = 10

这个特性经常用来算数组元素个数:sizeof(arr) / sizeof(arr[0])。但一旦数组作为参数传给函数,数组会退化成指针,sizeof 就只能得到指针的大小,数组长度信息直接消失。所以数组传参的时候,长度必须另外传,C 语言不会帮你记着。

三、char 的符号,编译器说了算

int 默认就是 signed intshortlonglong long 也一样。但 char 不一样,它到底是 signed char 还是 unsigned char,C 语言标准没说,编译器自己决定。大多数编译器默认是 signed char,但这不是保证。如果你需要确定的行为,请显式写成 signed charunsigned char

比如说 char c = 200 在某些编译器上是 200,在另一些上是 -56。标准没规定 char 默认是 signed 还是 unsigned,所以如果需要确定行为,显式的写成 signed charunsigned char。不要偷懒,偷懒的代价是调试好几个小时。

四、浮点数默认是 double,不是 float

float weight = 55.5f 的时候,那个 f 可不是装饰,是必须的。因为在 C 语言里,直接写 55.5 这种小数常量,编译器默认把它当成 double 类型。如果你把它赋给 float 变量,会发生隐式类型转换,有些编译器会报警告甚至报错。所以养成习惯:float 常量后面加 f,double 常量后面加 d,当然,不加也行,因为默认就是 double

另外 floatdouble 不是给你做精确计算的,它们是给你做"差不多"计算的。0.1 + 0.2 == 0.3 在大多数语言里都是假的,C 语言也不例外。因为 0.1 在二进制浮点里是个无限循环小数,存不下,只能近似。所以判断浮点数相等的时候,别用 ==,用 fabs(a - b) < 1e-6 这种写法。如果你需要精确计算,比如钱,用整数存分,别用浮点数存元。

五、取值范围别硬背,查头文件

limits.h 这个头文件里定义了所有整型类型的极限值,float.h 里定义了浮点型的。比如 INT_MAXINT_MINSCHAR_MAXUSHRT_MAX 这些,写代码的时候直接用,别自己硬背数字。不同平台上这些值可能不一样,硬背容易出错。为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。

六、转义字符,\ 不是装饰

\n 是换行,\t 是制表符,\r 是回车,\b 是退格(光标回退一个字符但不删除),\f 是换页符(光标移到下一页,在现代系统上基本看不出来),\v 是垂直分隔符(光标移到下一个垂直制表位,通常是下一行的同一列)。\' 用来在字符常量里写单引号,\" 用来在字符串里写双引号,\\ 用来写反斜杠本身。

例如:

#include <stdio.h>
int main() {
// 转义字符示例
printf("转义字符演示:\n");
printf("1. 换行符:Hello\nWorld\n");
printf("2. 制表符:Name\tAge\tCity\n");
printf("3. 回车符:\r(光标回到行首)\n");
printf("4. 退格符:Hello\bWorld(删除前一个字符)\n");
printf("5. 单引号:It\'s a test\n");
printf("6. 双引号:He said \"Hello\"\n");
printf("7. 反斜杠:路径:C:\\Program Files\\Test\n");
printf("8. 警报符:\a(终端会发出声音)\n");
printf("9. 垂直制表符:\v(垂直对齐)\n");
printf("10. 换页符:\f(分页符)\n");
// 八进制和十六进制转义示例
printf("\n八进制和十六进制转义:\n");
printf("八进制 \\130 表示字符:%c\n", '\130');  // 八进制的130是十进制的88,对应'X'
printf("十六进制 \\x30 表示字符:%c\n", '\x30'); // 十六进制的30是十进制的48,对应'0'
printf("空字符 \\0 的ASCII值:%d\n", '\0');
return 0;
}

输出:

转义字符演示:
1. 换行符:Hello
World
2. 制表符:Name    Age     City
3. 回车符:(光标回到行首)
4. 退格符:HellWorld(删除前一个字符)
5. 单引号:It's a test
6. 双引号:He said "Hello"
7. 反斜杠:路径:C:\Program Files\Test
8. 警报符:(终端会发出声音)
9. 垂直制表符:(垂直对齐)
10. 换页符:(分页符)
八进制和十六进制转义:
八进制 \130 表示字符:X
十六进制 \x30 表示字符:0
空字符 \0 的ASCII值:0

还有两个八进制和十六进制的转义形式:\ddd(1 到 3 个八进制数字)和 \xdd(2 个十六进制数字)。比如 \130 表示字符 X,\x30 表示字符 0。\0 是 null 字符,ASCII 码值为 0,它是字符串的结束标志。这些转义字符在 printf 和字符串里经常用到,比如写文件路径的时候 c:\\test\\code\\test.c,每个反斜杠都要写成 \\

\? 在书写连续多个问号时使用,防止它们被解析成三字母词,在新的编译器上没法验证了。\a 是警报,会使得终端发出警报声或出现闪烁,或者两者同时发生。

七、注释有个坑

C 语言有两种注释:/* 这是块注释 */// 这是行注释(C99 标准新增)。块注释不能嵌套,行注释到行尾结束,可以放在一行语句的结尾。但有个特别坑的点:注释在编译时会被替换成一个空格。所以 min/*这里是注释*/Value 会变成 min Value,而不是 minValue

另外注释不能放在双引号字符串里面,双引号里的 ///* */ 会被当成普通字符处理,失去注释作用。printf("// hello /* world */ "); 这个字符串里的注释符号都会被视为普通字符,没有注释作用。

八、printf 和 scanf 的格式控制

printf("%6.2f\n", 0.5) 里的 %6.2f 表示最小宽度 6,小数位数 2,所以输出前面会补两个空格,变成 0.50。宽度和精度可以用 * 代替,通过参数传入,比如 printf("%*.*f\n", 6, 2, 0.5) 效果一样。上面示例中 %*.*f 的两个星号通过 printf 的两个参数 6 和 2 传入,等同于 printf("%6.2f\n", 0.5)

输出字符串的时候,%.[m]s 可以限定输出长度,比如 %.3s 只输出前 3 个字符。%s 占位符用来输出字符串,默认是全部输出。如果只想输出开头的部分,可以用 %.[m]s 指定输出的长度,其中 [m] 代表一个数字,表示所要输出的长度。

scanf 有个特别坑的地方:%c 前面如果加个空格,比如 scanf(" %c", &c),这个空格表示忽略输入开始的所有空白字符(空格、换行、制表符)。如果不加空格,scanf("%c", &c) 会把上一个输入残留的换行符直接读进来,这是很多新手踩过的坑。

例如:

#include <stdio.h>
int main() {
int num;
char ch;
printf("请输入一个整数: ");
scanf("%d", #);
printf("请输入一个字符: ");
scanf(" %c", &ch);  // 注意 %c 前面的空格,用于忽略空白字符
printf("你输入的整数是: %d\n", num);
printf("你输入的字符是: %c\n", ch);
return 0;
}

输出:

请输入一个整数: 42
请输入一个字符: A
你输入的整数是: 42
你输入的字符是: A

而不加空格则是:

#include <stdio.h>
int main() {
int num;
char ch;
printf("请输入一个整数: ");
scanf("%d", #);
printf("请输入一个字符: ");
scanf("%c", &ch);  // 没有空格,会读取上一个输入残留的换行符
printf("你输入的整数是: %d\n", num);
printf("你输入的字符是: %c (ASCII: %d)\n", ch, ch);
return 0;
}

输出:

请输入一个整数: 42
请输入一个字符: 
你输入的整数是: 42
你输入的字符是: 
 (ASCII: 10)  // 读取到了换行符

总之,%c 前面的空格表示忽略开始的字符,这是 scanf 的一个实用技巧。

九、类型转换的隐式规则

C 语言的隐式类型转换规则复杂到可以单独写一本书,但你只需要记住一条:小类型往大类型转的时候没事,大类型往小类型转的时候丢数据。intlong 安全,longint 危险,doublefloat 丢精度,floatint 直接截断小数。

比如:

#include <stdio.h>
int main() {
// 隐式类型转换示例
int a = 10;
float b = 3.14f;
double c = 2.71828;
// 小类型往大类型转 - 安全
long long big_num = a;  // int 转 long long
double big_float = b;   // float 转 double
// 大类型往小类型转 - 危险,可能丢失数据
int small_int = c;      // double 转 int,小数部分被截断
float small_float = c;  // double 转 float,精度丢失
printf("int a = %d\n", a);
printf("float b = %.2f\n", b);
printf("double c = %.5f\n", c);
printf("\n");
printf("int 转 long long: %lld\n", big_num);
printf("float 转 double: %.5f\n", big_float);
printf("double 转 int (截断): %d\n", small_int);      // 输出 2,不是 2.71828
printf("double 转 float (精度丢失): %.5f\n", small_float); // 精度降低
return 0;
}

输出:

int a = 10
float b = 3.14
double c = 2.71828
int 转 long long: 10
float 转 double: 3.14000
double 转 int (截断): 2
double 转 float (精度丢失,但显示值相同): 2.71828

更阴险的是运算中的隐式转换。char + int 会先提升成 int 再算,int + float 会先提升成 float。这些规则不会报错,但会让你的结果莫名其妙地变。所以养成习惯:运算前手动强制转换,让代码读起来就是你想要的逻辑,别指望编译器猜你的心思。

十、有符号和无符号混用,一场静默的灾难

unsigned intint 混用的时候,编译器不会报错,程序也不会崩溃,但结果会错得让你怀疑人生。比如 unsigned int a = 5; int b = -1; if (a < b) 这个判断,在大多数人的直觉里应该是 true(因为 -1 小于 5),但实际上因为 b 会被隐式转换为 unsigned int,-1 变成了一个巨大的正数(通常是 4294967295),导致 a < b 的结果是 false。这种静默的类型提升和转换,是 C 语言里最隐蔽的 bug 来源之一。所以,在涉及有符号和无符号的运算或比较时,我们要注意保持类型一致,以避免这种错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值