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

C语言:那些让你摔过跤的坑,其实都很眼熟
学 C 语言这件事说白了就是跟内存和类型死磕磕着磕着你就会发现很多错误不是因为你不懂而是你以为你懂了。一、变量不是盒子是标签很多人刚开始学的时候脑子里有个画面变量是个小盒子数据装在里面这个画面害人不浅我也是这么认为的但是后来我才意识到这个观点是错误的。变量本质上是个名字是给内存地址起的外号。你写int a 10编译器在背后干的事是找一块能装下整数的地方把 10 塞进去然后告诉你这块地方以后叫 a。所以当你写int a 10, b a的时候别想着 b 和 a 共享什么它们就是两块独立的内存值碰巧一样而已。变量命名也有规矩。int、char、return这些关键字是编译器预留的你不能拿它们当变量名。_2b以下划线加数字开头是合法的2b以数字开头不行编译器直接报错。变量名只能由字母、数字和下划线组成必须以字母或下划线开头这是硬性规则没得商量。另外float score;这种只声明不初始化的写法score 里面存的是垃圾值不是 0不是空是内存里之前留下的随机数据。你以为它默认是 0编译器可没这个义务帮你清零。全局变量倒是会自动初始化为 0但局部变量不会局部变量里存的是那块内存之前留下的垃圾。所以局部变量使用前一定要初始化这不是建议而是一个作为程序员的好习惯是一个保命的技巧。二、数据类型的大小别猜C 语言的数据类型看着简单实际上坑很多。char占 1 个字节short占 2 个int、long、float在 VS 上都是 4 个long long和double是 8 个long double在 VS 上是 8 个在 gcc 上可能是 16 个。你以为是固定的错了C 语言只规定sizeof(long) sizeof(int)以及sizeof(long double) sizeof(double)具体等于多少编译器说了算。所以跨平台的时候别硬编码类型大小想知道当前环境某个类型多大直接用sizeof。sizeof是个运算符不是函数所以sizeof a和sizeof(a)效果一样括号可以省。但sizeof(3 3.5)这种写法要注意3 是 int3.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 intshort、long、long long也一样。但char不一样它到底是signed char还是unsigned charC 语言标准没说编译器自己决定。大多数编译器默认是signed char但这不是保证。如果你需要确定的行为请显式写成signed char或unsigned char。比如说char c 200在某些编译器上是 200在另一些上是 -56。标准没规定 char 默认是 signed 还是 unsigned所以如果需要确定行为显式的写成signed char或unsigned char。不要偷懒偷懒的代价是调试好几个小时。四、浮点数默认是 double不是 float写float weight 55.5f的时候那个f可不是装饰是必须的。因为在 C 语言里直接写55.5这种小数常量编译器默认把它当成double类型。如果你把它赋给float变量会发生隐式类型转换有些编译器会报警告甚至报错。所以养成习惯float 常量后面加fdouble 常量后面加d当然不加也行因为默认就是 double。另外float和double不是给你做精确计算的它们是给你做差不多计算的。0.1 0.2 0.3在大多数语言里都是假的C 语言也不例外。因为 0.1 在二进制浮点里是个无限循环小数存不下只能近似。所以判断浮点数相等的时候别用用fabs(a - b) 1e-6这种写法。如果你需要精确计算比如钱用整数存分别用浮点数存元。五、取值范围别硬背查头文件limits.h这个头文件里定义了所有整型类型的极限值float.h里定义了浮点型的。比如INT_MAX、INT_MIN、SCHAR_MAX、USHRT_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. 单引号Its a test 6. 双引号He said Hello 7. 反斜杠路径C:\Program Files\Test 8. 警报符终端会发出声音 9. 垂直制表符垂直对齐 10. 换页符分页符 八进制和十六进制转义 八进制 \130 表示字符X 十六进制 \x30 表示字符0 空字符 \0 的ASCII值0还有两个八进制和十六进制的转义形式\ddd1 到 3 个八进制数字和\xdd2 个十六进制数字。比如\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 语言的隐式类型转换规则复杂到可以单独写一本书但你只需要记住一条小类型往大类型转的时候没事大类型往小类型转的时候丢数据。int转long安全long转int危险double转float丢精度float转int直接截断小数。比如#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 int和int混用的时候编译器不会报错程序也不会崩溃但结果会错得让你怀疑人生。比如unsigned int a 5; int b -1; if (a b)这个判断在大多数人的直觉里应该是true因为 -1 小于 5但实际上因为b会被隐式转换为unsigned int-1 变成了一个巨大的正数通常是 4294967295导致a b的结果是false。这种静默的类型提升和转换是 C 语言里最隐蔽的 bug 来源之一。所以在涉及有符号和无符号的运算或比较时我们要注意保持类型一致以避免这种错误。