很多人学C语言,学到“数据类型和变量”这一节就觉得索然无味,不就是int a = 10;嘛,声明、赋值、打印,三分钟看完。但等到真正写代码的时候,被坑得最惨的恰恰就是这些基础概念。我在整理C语言学习笔记时,也经历过从“自我感觉良好”到“一脸懵”的过程,尤其是在内存视角下去看变量、在边界条件下去测数据类型,才发现原来很多东西只是“以为懂了”。这篇笔记就是围绕C语言数据类型和变量展开的,把变量本质、整数/浮点数的存储细节、类型转换规则、作用域与生命周期这些核心内容,用更容易理解的方式重新捋一遍。不管你是刚开始学C语言的小白,还是学了一阵子但总觉得根基不稳的人,这篇内容都值得你静下心来看完。
1. 变量到底是什么:地址、内存和解释方式
1.1 变量名只是给程序员看的名字
先说一个很多初学者容易忽略的事实:C语言里的变量名,纯粹是给人类看的。编译器拿到你的.c源文件之后,第一步会做语法分析、符号收集,把你定义的a、b、count这些名字记录在符号表里,并给它们分配“对应的地址”。到了最终生成机器码的阶段,变量名基本就消失了,取而代之的是一串串内存地址和偏移量。
所以你在代码里写:
int a = 5;这句话真正干的事是什么?是向编译器申请一块大小为sizeof(int)的内存空间,然后把二进制形式的“5”写到这块内存里,同时让符号表中的a指向这块内存的起始地址。后面你再写a = 10;,程序做的事情是:找到a对应的地址,把这块内存里的值改成10,而不是“把一个叫a的盒子里的东西换掉”。
理解这一点非常重要,因为它直接决定了你后面学指针、学数组、学结构体时能不能建立起正确的心智模型。很多人都说指针难,其实难就难在没想明白:指针变量也是变量,它存的值也是一个数字,只不过这个数字被类型约定为“地址”。如果你清楚变量本身的底层是“地址+内存+解释方式”,指针的神秘感立刻就少了一半。
1.2 同一串二进制,换一种类型解释就完全不一样
“解释方式”是什么概念?我举个例子,你立刻就能明白:
#include <stdio.h> int main(void) { char c = 'A'; printf("c 按 %%c 打印: %c\n", c); printf("c 按 %%d 打印: %d\n", c); printf("c 的十六进制: 0x%02X\n", (unsigned char)c); return 0; }在常见的ASCII环境下,'A'对应的数值是65。也就是说,变量c的内存里存的二进制就是0100 0001(也就是十进制65)。你用%c打印它,它显示成字符A;你用%d打印它,它显示成数字65。内存里的数据没有变,变的是你怎么解释这一串二进制。
这就是数据类型的核心作用:类型决定了编译器如何解释一块内存,以及允许你对这块内存做哪些操作。int告诉你这块内存按4字节有符号整数来读,float告诉你这块内存按IEEE 754格式来读,char *告诉你这块内存里存的是一个地址,并且以这个地址为起点可以继续读字节。
还有一个特别容易混淆的细节:为什么同样是4字节,int能表示的最大正整数大约是21亿,而float却能表示到3.4×10^38?因为它们的解释方式不同,float把32位拆成了符号位、指数位和尾数位,用“科学计数法”的套路来表示数值,所以范围大但精度有限。这一点下一章讲浮点数的时候再细说,这里先建立“同一块内存,不同解释,得到完全不同的数值”这个认知。
1.3 用printf把变量“底裤”扒出来看一下
学习变量最有效的方法,就是别光在纸上推导,直接上手打印。我写C这么多年,遇到任何拿不准的类型问题,第一反应永远是写个小程序去验证。比如你想搞清楚自己的平台上int到底占几个字节、变量地址长什么样,就写这段:
#include <stdio.h> int main(void) { int a = 5; char b = 'A'; double c = 3.14; printf("sizeof(int) = %zu\n", sizeof(int)); printf("sizeof(char) = %zu\n", sizeof(char)); printf("sizeof(double) = %zu\n", sizeof(double)); printf("&a = %p\n", (void *)&a); printf("&b = %p\n", (void *)&b); printf("&c = %p\n", (void *)&c); return 0; }注意,打印sizeof的结果时一定要用%zu,因为sizeof运算符的返回值类型是size_t,它在不同平台上可能是unsigned long或unsigned long long。用%d去打印,严格来说属于格式说明符和参数类型不匹配,在某些编译器上会报警告,在64位平台上甚至可能打出错误的值。很多人说“我机器上sizeof(int)打印出来是8”,多半就是犯了这种错误。先别急着怀疑标准,先去查你的格式说明符对不对。
2. 整数类型家族:从char到long long的字节与范围
2.1 一张表看懂常见整数类型的范围和大小
C语言标准规定了5种标准有符号整数类型:char、short int、int、long int、long long int,以及它们对应的无符号版本。注意,标准只规定了“最小范围”,没有硬性规定每种类型必须占多少字节。说得直白一点:int在16位平台、32位平台、64位平台上的大小可能是不同的,但标准保证int至少能表示 -32767 到 32767 的范围。
在我常用的64位Linux + GCC环境下,各类型实际大小如下:
| 类型 | 典型大小(64位Linux) | 取值范围示例 | printf格式 |
|---|---|---|---|
char | 1字节 | -128 ~ 127(有符号时) | %c/%d |
unsigned char | 1字节 | 0 ~ 255 | %u或%02X |
short | 2字节 | -32768 ~ 32767 | %hd |
unsigned short | 2字节 | 0 ~ 65535 | %hu |
int | 4字节 | -2147483648 ~ 2147483647 | %d |
unsigned int | 4字节 | 0 ~ 4294967295 | %u |
long | 8字节(64位Linux) | -9223372036854775808 ~ 9223372036854775807 | %ld |
unsigned long | 8字节 | 0 ~ 18446744073709551615 | %lu |
long long | 8字节 | 同上 | %lld |
unsigned long long | 8字节 | 同上 | %llu |
这张表看起来简单,但有几个值得划重点的地方。
第一,long在不同平台上的大小不一致。在64位Linux上long是8字节,但在64位Windows上long是4字节。所以跨平台写代码时,如果你想确保一个整数是8字节,直接写long long,别写long。这也是C语言被人诟病“可移植性差”的一个典型体现。
第二,char类型到底有没有符号,标准没规定,由实现决定。大多数x86平台上有符号char的范围是 -128 到 127,但有些嵌入式平台(比如ARM默认)char是无符号的。所以当你要用char存非负的小数(比如字节数据、ASCII码)时,要么显式写unsigned char,要么做好跨平台时行为可能不一致的心理准备。
2.2 有符号和无符号:最高位决定了一切
整数在内存里是怎么存的?这里必须提补码(two's complement)。简单说,正数的补码就是它本身的二进制;负数的补码是“取反加一”。比如8位有符号整数 -1,内存里是1111 1111,也就是0xFF。而unsigned char类型的255,内存里也是1111 1111。同样的8个1,按有符号char解释是 -1,按无符号char解释是255。这就是前面说的“类型决定解释方式”的又一个活例子。
为什么要用补码?因为计算机做减法的时候不需要单独设计一套减法电路,直接“加一个负数”就行。比如 5 - 3,本质上就是 5 + (-3)。补码让符号位也能参与正常的二进制加法运算,而且进位自然消失,结果依然正确。这是计算机组成原理里的基础内容,但作为一个C语言开发者,理解它能帮你解释很多“奇怪”的现象。
关于有符号和无符号,有一个几乎所有初学者都会踩的坑:有符号整数和无符号整数混合运算时,有符号整数会被隐式转换成无符号整数。看这段代码:
#include <stdio.h> int main(void) { int a = -1; unsigned int b = 1; if (a < b) { printf("a < b\n"); } else { printf("a >= b\n"); } return 0; }凭直觉,-1 肯定小于 1,对吧?但运行结果输出的是a >= b。原因就是比较时a先从int被隐式转换为unsigned int,-1 变成了UINT_MAX(4294967295),4294967295 当然不小于 1。这也是为什么我写代码时,for循环里的计数器、数组下标这类东西,能不用无符号就不用无符号,尤其要避免“无符号变量和负数比较”这种局面。
2.3 数值溢出:有符号无符号的区别
溢出的行为,有符号和无符号完全是两回事。
无符号整数溢出是明确定义的:按2^N取模。比如unsigned char x = 255; x = x + 1;结果就是0,标准保证这一点。如果你刻意需要“环绕”效果(比如实现一个循环计数器),用无符号整数就够了。
有符号整数溢出是未定义行为(undefined behavior)。比如:
int a = INT_MAX; printf("%d\n", a + 1);这段代码在标准层面没有规定输出什么。在某些编译器上它可能输出-2147483648(看起来像“环绕”),但在开启优化的情况下,编译器可能直接假设“有符号溢出永远不会发生”,然后做各种你想象不到的优化,导致结果非常离谱。所以千万不能依赖有符号溢出的“环绕”行为来写业务逻辑,这是C语言初学者最容易养成的坏习惯之一。
limits.h头文件里定义好了各种类型的边界值,比如INT_MAX、INT_MIN、UINT_MAX、CHAR_BIT。写可移植代码时,不要自己硬编码2147483647这种数字,用标准宏来代替。顺便说一句,CHAR_BIT表示一个字节有多少位,绝大多数平台是8,但在一些古老的DSP平台上可能是16,用这个宏能让你的代码更稳健。
3. 浮点数的真相:为什么0.1加0.2不等于0.3
3.1 IEEE 754:浮点数是怎么存成一串二进制的
如果你第一次运行下面这段代码:
#include <stdio.h> int main(void) { float a = 0.1f; float b = 0.2f; float c = a + b; printf("a + b = %.20f\n", c); return 0; }看到输出结果约等于0.30000000447034835815,你可能会怀疑自己写错了。其实没写错,这就是浮点数存储的天然缺陷。
原因很简单:二进制无法精确表示 0.1 或 0.2,就像十进制无法用有限小数表示 1/3 一样。0.1 转换成二进制是0.0001100110011001100...,是一个无限循环小数。C语言里的float只有32位,double只有64位,存不下这个无限循环,只能截断或舍入到某个精度。因此你在内存里存的0.1,其实是一个非常接近但不完全等于0.1的二进制近似值。
IEEE 754标准把浮点数拆成三部分:符号位、指数位、尾数位。float是1位符号 + 8位指数 + 23位尾数,double是1位符号 + 11位指数 + 52位尾数。用类似“科学计数法”的方式来表示一个数:尾数存有效数字,指数存小数点移动的位数。这种设计让浮点数能表示极大和极小的数值范围,但也意味着它只能精确表示“2的整数次幂的有限组合”,比如 0.5、0.25、0.125 都能精确,而 0.1、0.2、0.3 都不能精确。
3.2 float和double怎么选:精度才是关键
float的有效精度大约是6到7位十进制数字,double大约是15到16位。什么叫有效精度?就是说一个数从左边第一个非零数字开始,能保证多少位是可信的。比如float存 123456789,打印出来可能是 123456792,后面几位就不准了;存 0.123456789,能信的也只有前7位左右。
很多初学者写科学计算题、物理模拟题时习惯声明float,原因可能是教材里老用float举例。但现在的个人电脑内存动辄8GB、16GB,普通程序里多占4个字节根本无感,而精度损失却是实打实的。除非你在写单片机、嵌入式、GPU渲染这些对内存带宽极其敏感的场景,否则优先double没毛病。
还有一个细节:printf打印浮点数时,默认只显示6位小数。你看到0.300000不代表它真的就是精确的0.3,只是默认精度只显示到6位。想看真实情况,就用%.10f、%.20f这样的格式控制多打几位。反过来,scanf读float用%f,读double必须用%lf。很多人把printf和scanf的格式说明符搞混,因为printf里%f和%lf效果差不多,但在scanf里用错是会导致内存写坏的典型错误。
3.3 千万别用 == 直接比较浮点数
浮点数不能直接用==比较相等,这是无数人踩出来的教训。原因还是那个:0.1 + 0.2 和 0.3 在二进制浮点表示里并不相等,哪怕它们肉眼看起来应该相等。正确做法是比较两个数的差的绝对值是否小于某个允许的误差范围(epsilon):
#include <stdio.h> #include <math.h> int main(void) { double x = 0.1 + 0.2; double y = 0.3; double eps = 1e-9; if (fabs(x - y) < eps) { printf("x 和 y 在误差范围内相等\n"); } else { printf("x 和 y 不相等\n"); } return 0; }eps取多少要看具体场景。如果是普通计算题,1e-9基本够用;如果涉及放大倍数很大的计算(比如天文数字),可能要动态调整误差。总之,把“浮点数不能直接等值比较”这条刻在脑子里,能帮你避开一大批看似莫名其妙的问题。
更重要的实践建议:涉及钱、金额、账户余额等场景,绝对不要用浮点数。金融计算要求的是精确十进制舍入规则,浮点数的二进制近似会让你在结账时多出0.00000001元。正确做法是用整数存“分”,或者用专门的十进制高精度库。这个话我在很多技术群里重复过无数遍,但仍然能看到有人用double存金额,然后对着“为什么算出来差一分钱”发愁。
4. 类型转换:编译器替我们做的决定,和我们替编译器背的锅
4.1 隐式转换:C语言悄悄替你做的数学题
C语言有一个隐式类型转换规则,几乎所有写C的人都被它“偷袭”过。核心规则可以总结成一条链:
当表达式里操作数类型不一致时,编译器会把“较低等级”的类型自动提升为“较高等级”的类型。等级大致是:
int<unsigned int<long<unsigned long<long long<unsigned long long<float<double<long double
注意,int和unsigned int混用时,int会被转成unsigned int;int和long混用时,int会转成long。规则本身不复杂,后果却很迷惑人。
举一个面试高频题:
#include <stdio.h> int main(void) { int a = -1; unsigned int b = 1; if (a < b) { printf("yes\n"); } else { printf("no\n"); } return 0; }前面已经讲过了,输出是no。这里再从转换规则角度重复一遍:比较的时候a被转换成unsigned int,变成 4294967295,所以是“大数”和1比。你以为在比大小,实际上编译器帮你做了一次“无符号化”。
更隐蔽的还有整数提升(integer promotion)。C语言规定:char、short参加算术运算时,会先被提升成int。这意味着char c1 = 127; char c2 = 1; int sum = c1 + c2;这个计算是手打int加法,不会发生溢出,因为提升后已经不再是char运算了。但对初学者来说,最容易出问题的地方是sizeof('a')。在C语言里,字符常量的类型是int,不是char,所以sizeof('a')的结果在32位/64位平台上通常是4,而不是1。很多人乍一看会愣住,但这就是标准。
这一套隐式转换不是坏事,它让C语言写起来更简洁。但问题是它太“安静”了,经常在你没注意到的时候就改变了结果。我的建议是:在关键计算里,显式写出你认为应该参与运算的类型,别把正确性寄托在隐式转换上。
4.2 强制转换:告诉编译器“我知道我在干什么”
显式强制转换的语法是(type)expression,比如(int)x、(double)count。它的作用是“我就要把这份数据按另一种类型来解释/转换”。但强制转换有一个很不直观的特点:它不修改原变量,而是生成一个新的值。
比如:
double d = 3.99; int n = (int)d; printf("%f %d\n", d, n);输出3.990000 3。d还是3.99,(int)d只是把小数部分截断了。这个截断是向零方向截断,不是四舍五入。如果你需要四舍五入,得自己写(int)(d + 0.5)或者用round()函数。
还有一个初学者必踩的经典坑,和整数除法有关:
float f = 1 / 3; printf("%f\n", f);输出是0.000000。原因是1和3都是int,1 / 3先按整数除法计算,结果是0,然后才把0赋给浮点变量f。要写出浮点数,至少要让其中一个操作数是浮点类型,比如1.0 / 3、1 / 3.0,或者(float)1 / 3。很多人问“为什么我算除法总得到0”,十有八九就是这个问题。
4.3 指针和数组相关的转换:别把地址当数值用
前面讲过的内容在这里会换个角度出现:数组名在很多情况下会“退化”成指向首元素的指针。比如:
int arr[5] = {1, 2, 3, 4, 5}; printf("%p\n", (void *)arr);arr的本质是“数组首元素的地址”,它的类型是int *。如果你试图对数组名做数值转换,比如(long)arr,你得到的是那个地址的整数值,而不是把数组里每个元素“变成”long。这与热搜里经常有人问的“c语言数组变量的类型转换”是同一个问题:数组的“类型”和“元素类型”是两回事,转换数组名不会转换数组内容,只会转换地址的“身份”。
指针类型之间的强制转换也存在类似的坑。比如int *p和char *q指向同一个地址,但p + 1移动4个字节,q + 1移动1个字节,因为指针算术的步长取决于指针指向的类型。如果你把一个int *强转成char *,后续所有指针运算的步长都变了,这既可能是有意为之(比如按字节分析内存),也可能导致你读错数据。所以做指针强转前,先问自己一句:你到底想干嘛?
5. 作用域与生命周期:变量不是一直存在的
5.1 局部变量、全局变量和那块“栈”内存
按位置划分,变量大致可以分成局部变量、全局变量、静态变量、外部变量。它们的区别不仅在于“能不能访问”,还在于“什么时候创建、什么时候销毁”。
局部变量定义在函数内部或块内部(花括号包裹的范围内),它的生命周期从程序执行到定义处开始,到离开所在的块为止。局部变量一般存放在栈区,函数调用时在栈上分配,函数返回时自动释放。典型的陷阱是返回局部变量的地址:
int *foo(void) { int x = 10; return &x; // 危险!x在函数返回后就被销毁了 }这个代码编译时可能会给个警告,运行后通过返回的指针去读x的值,读到的可能是垃圾值,也可能碰巧还是10,但这是未定义行为,随时会崩。很多人学指针时在这里吃过亏,根源就是没理解局部变量的生命周期。
全局变量定义在所有函数之外,存放在数据段,从程序启动到程序结束一直存在。它的特点是默认初始化为0(静态存储期变量默认清零),作用域是整个源文件甚至可以被extern扩展到其他文件。但全局变量也有代价:多文件编程时如果命名不好,容易互相冲突;程序逻辑里隐藏的全局状态越多,越难排查问题。
还有一个细节:如果你同时有全局变量和局部变量同名,在函数内部,局部变量会遮蔽(shadow)全局变量。想在这种情况下访问全局变量,可以在局部变量名前加上作用域解析符,但在纯C里其实没有这种语法,只能换名字。我见过有人靠“全局变量叫count,局部变量也叫count”来炫技,结果代码review时被骂得狗血淋头,所以给变量起名时务必注意避免遮蔽。
5.2 static关键字:一副面孔,两种效果
static是C语言里最容易被误解的关键字之一,因为它有两个截然不同的作用。
第一,static修饰局部变量:把变量的存储期从“自动”改为“静态”。函数里写:
int counter(void) { static int count = 0; return ++count; }第一次调用counter()时count被初始化为0,之后每次调用,count的值都会保留上次的结果,第二次调用返回2,第三次返回3。它的生命周期变成了整个程序运行期间,但作用域依然是函数内部,外面访问不到。这个特性很适合做“调用次数统计”“全局唯一计数器”之类的功能,但也正因为它的状态不会自动清零,多线程环境下使用需要额外注意线程安全问题。
第二,static修饰全局变量:把变量的作用域限制在当前源文件(编译单元)内,外部文件即使写了extern也无法访问。这个用途在写多文件项目时很常用,可以避免不同文件之间因同名全局变量发生链接冲突。相当于给全局变量加了一层“私密性”。
5.3 extern:把变量的世界扩大到多个文件
一个常见的多文件代码结构是:
helper.h里写声明:
#ifndef HELPER_H #define HELPER_H extern int g_counter; #endifhelper.c里写定义:
#include "helper.h" int g_counter = 0;main.c里使用:
#include <stdio.h> #include "helper.h" int main(void) { g_counter = 100; printf("%d\n", g_counter); return 0; }这里的逻辑是:extern int g_counter;是“声明”,告诉编译器这个变量在别的文件里定义了,你先让我用;int g_counter = 0;是“定义”,编译器为它分配内存。声明和定义的区别,很多初学者分不清,但它恰恰是解决“头文件里到底该写什么”的关键。如果你把定义写进了头文件,然后在多个.c文件里#include同一个头文件,链接器会报“重复定义”的错误。
另一个常在热搜里出现的相关问题:“如何查看堆内变量”。用C语言的malloc在堆区分配内存时,指针变量本身可能是栈上的局部变量,但它指向的内存块在堆上,需要手动用free释放。堆上的数据生命周期由程序员控制,不像局部变量那样自动销毁。这跟变量的生命周期也有密切关系:全局/static变量的生命周期是“程序级”,局部变量是“函数级”,堆内存的生命周期是“手动级”。搞清楚这三者的区分,很多“内存崩溃”“内存泄漏”问题就能快速定位。
结构体变量的定义也遵循同样的变量规则。比如struct Student stu;这里的stu就是一个占一块连续内存的变量,它的总大小是各成员大小之和(还会考虑对齐填充)。你可以先把结构体变量理解成一个“打包好的变量”,本质依然是“地址+内存+解释方式”,只是C语言还额外告诉了你内部怎么分区。这块内容等讲结构体的时候再展开,但底层的变量观念现在就可以建立。
6. 常见编译错误与自查清单
6.1 “未声明的标识符”其实在说什么
我见过很多人刚学C语言时,被编译器的报错吓住,其实错误信息本身已经把问题说得很清楚了。最常见的错误之一:
error: 'x' undeclared (first use in this function)翻译成大白话就是:“编译器在处理你这行代码的时候,没见过叫x的东西。”可能的原因有这么几个:
- 变量名拼写错误,
count写成了conut; - 变量声明写在了使用它的代码之后;
- 变量声明在一个块
{}内部,而你在块外使用; - 根本没有声明这个变量。
排查方法很机械但很有效:从上往下看,找到第一次使用该变量的行,然后往上找最近的一次声明。如果找不到,补上声明;如果声明处的作用域和你使用处不一致,调整声明的位置。在IDE里这类错误一般还会给红色波浪线,定位更快。用命令行编译的话,配合gcc -Wall -Wextra能看到更多潜在问题。
还有个很类似的报错是:“'xxx' was not declared in this scope”。别紧张,scope就是作用域,这个报错本质上也是在说你在一个看不到xxx的区域内使用了它。
6.2 格式说明符与参数类型不匹配
int x = 42; printf("%f\n", x); // 错误:x是int,%f期望double这个错误在printf中可能不报编译错误,因为printf是可变参数函数,编译器不一定能检查出每个参数的类型是否与格式说明符匹配。但运行结果经常非常离谱,有时候打印出0.000000,有时候打印出天文数字。原因在于函数调用时参数按二进制压栈,而printf按%f的要求去解释那一串二进制,解释方式错了,数字自然就错了。
用gcc编译时,如果开启了-Wall(强烈建议),编译器能帮你检查出部分格式串和参数不匹配的问题,类似:
warning: format '%f' expects argument of type 'double', but argument 2 has type 'int'看到这类警告不要无视,哪怕程序“碰巧”输出正常,也说明你的代码存在隐性问题。而对于scanf,格式串写错更严重,因为它是往你传入的指针指向的内存里写数据。比如double d; scanf("%f", &d);就是错误用法,应该用scanf("%lf", &d);。
6.3 链接阶段的“未定义引用”和“重复定义”
编译错误分两个阶段:编译期和链接期。编译期错误通常在你写的源文件内部;链接期错误则出现在把多个目标文件合并成可执行文件时。
典型报错:
undefined reference to 'helper_func'大白话是:helper_func这个函数的声明(在头文件里)你已经看到了,代码里也调用了,但是在链接时,链接器翻遍了所有参与链接的目标文件,都没找到helper_func的实现。可能的原因是你忘了把helper.c编进去,也可能helper_func根本没有定义。解决办法:检查编译命令,确保相关源文件都参与了编译,比如:
gcc main.c helper.c -o program另一个典型报错是“重复定义”:
multiple definition of 'g_counter'通常是因为同一个全局变量在多个源文件里各定义了一遍,或者定义被写进了头文件并被多个源文件包含。解法请参考上一章extern的用法,头文件里只放声明,不放定义。
6.4 给新手的一份自查清单
最后给你一个我写C代码时经常过一遍的清单,适合在提交代码或运行崩溃前快速自查:
| 检查项 | 说明 |
|---|---|
| 变量是否存在且拼写正确 | 防止“未声明的标识符” |
| 变量声明是否在使用之前 | C89之前要求所有声明在语句前,现代C虽然放开了,但声明靠前仍更清晰 |
| 变量类型是否和表达式一致 | 防止隐式转换带来的意外结果 |
| 格式说明符是否和参数类型匹配 | printf/scanf的经典坑 |
| 数组下标是否越界 | 下标从0开始,边界用< 长度 |
| 有符号和无符号是否混用 | 避免负数被解释成巨大正数 |
| 指针指向的内存是否仍有效 | 别返回局部变量地址 |
| 强制转换是否真的需要 | 别为了消除警告而乱强转 |
这些自查项看起来基础,但我在读别人代码、帮人排查bug的时候,遇到的绝大多数“诡异问题”都能落回到这几点上。C语言的变量系统就是这么个东西,规则不算多,但每条绑定在内存模型上,忘记哪一环都可能出大问题。
我自己学C语言最深的体会是:千万别把变量和数据类型当成“背知识点”的内容,一定要动手。每学一个新类型,就写个小程序去printf它的sizeof、它的地址、它的边界值;看到一份不理解的代码,先猜它内存长什么样,再写代码验证。纸上得来终觉浅,这句老话放在C语言身上比任何一门语言都合适。多踩几个坑,多看几次打印结果,类型和变量的那点事儿自然而然就通了。