1. 先把指针的底层逻辑梳理清楚再做题
1.1 为什么这一章会成为C语言的分水岭
很多初学者学到第八章会突然觉得C语言变难了,原因不是这章内容本身有多高深,而是前面几章积累的很多“默认认识”在这一章被打破了。比如学数组的时候,我们习惯说a[0]、a[1]是数组的各个元素,但到这一章你会发现,数组名a本身在很多场景下其实是一个地址;再比如前面调用函数传参数,你老老实实地把值传进去,函数内部怎么折腾都不影响外面的变量,但这章开始,你可以把变量的地址传进函数,让函数直接修改外部变量。第八章就是把这些“原来如此”全部揭开的一章。
如果前面数组、函数的基础不牢,做这章习题会非常吃力。我建议动手做题之前,先花半小时把三块内容重新复习一遍:数组元素地址的计算方式、字符串的存储与结束符'\0'、函数的传值和返回值机制。这三块在第八章里都会被反复使用,哪一块含糊,题目做到一半就会卡住。
这一章的核心关键词就是“指针”。它到底指什么?一句话:指针就是地址,指针变量就是用来存放地址的变量。你用&取一个变量的地址,把它存进指针变量,之后通过*就能找到并操作那个变量。很多习题的答案看起来千变万化,本质上都是在做“通过地址找到房间,再对房间里存的东西做操作”这件事。
1.2 第八章知识点的整体地图
做题之前,先用自己的话把这章的框架说出来。说不上来的地方,就是你的薄弱点。下面是这章常见知识点的清单,你可以对照自查:
| 知识点 | 核心内容 | 常见出错点 |
|---|---|---|
| 指针变量定义 | 类型 *变量名 | 星号位置、类型不匹配 |
| 取地址与间接访问 | & 和 * | 两者混用、写反 |
| 指针运算 | 加减、比较、自增自减 | 步长计算、与普通整数混淆 |
| 指针与一维数组 | 数组名即首元素地址 | &a 和 a 混淆 |
| 指针与二维数组 | 行指针、列指针 | *(p+i) 和((p+i)+j) 绕晕 |
| 指针与字符串 | 地址代表字符串 | 字符串常量不可修改 |
| 指针作函数参数 | 传址修改外部变量 | swap写错、返回局部地址 |
| 函数指针 | 函数名也是地址 | 声明语法记不住 |
| 二级指针 | 指向指针的指针 | 需要但没用 |
| 动态内存 | malloc/free | 忘记释放、越界访问 |
把这十项过一遍,你会发现习题再怎么变,考的就是表里的内容翻来覆去地组合。
1.3 建立两个直觉:看到&想到房间号,看到*想到进房间
学习指针最大的障碍是抽象。我的经验是把它落到生活里,哪怕这个比喻不够严谨,用来做题足够了:内存是一排房间,变量名是贴在房间门上的标签,&就是“告诉我这个房间的门牌号”,*就是“推开门,看看里面存了什么”。
比如int a = 10; int *p = &a;这句话翻译过来就是:房间a里放了10这个数,p记下了a房间的门牌号。想通过p看房间里的值,就写*p,结果是10。想换个值,就写*p = 20;,相当于推门进去,把10换成20。
做题的时候,遇到任何一条语句,先在草稿纸上画一个这样的“房间图”:左边是指针变量,右边是它指向的目标变量,中间画箭头。这个方法看起来笨,但20道题画下来,你对指针的直觉就比那些光看不练的人强出一大截。
2. 第八章课后题的题型分布与刷题策略
2.1 题目类型的实际分布
把这一章的课后题按照“考的到底是什么”来分,大致是五类。不同版次的具体题号可能略有差别,但分布基本稳定:
| 题型 | 高频考点 | 大概占比 |
|---|---|---|
| 基础概念与辨析 | 指针含义、&与*、空指针 | 15% |
| 指针与一维/二维数组 | 数组元素遍历、二维数组输出、排序 | 30% |
| 指针与字符串 | 复制、比较、长度、连接 | 25% |
| 指针与函数 | swap、数组传参、函数指针 | 20% |
| 综合应用 | 二级指针、动态内存分配 | 10% |
这个分布告诉我们一个事实:这章考的不是“指针语法背得有多熟”,而是“能不能用指针解决数组和字符串的处理问题”。所以前期概念题做错不要慌,真正的分水岭在数组和字符串那些题上。
2.2 推荐的刷题顺序
我见过很多同学做题很随意,从第1题做到最后,结果卡在中间一道二维数组题上足足耗掉一个晚上。更高效的做法是分三轮进行:
第一轮:只做基础概念题,目标是搞清楚指针变量的定义、赋值、&和*的使用。这类题花的时间最短,却能在潜意识里建立“指针=地址”的认知。
第二轮:集中做指针与一维数组、指针与字符串的题目。这两块是核心,因为有了数组名的退化、字符串地址传递这些概念,后面理解函数参数和二维数组才顺理成章。
第三轮:最后攻二维数组、指针与函数、二级指针和动态内存分配。这些题综合性最强,需要前面的基础全部到位才不会一头雾水。
2.3 草稿纸上的内存图法
这一章做题时最忌讳“光看代码想象运行结果”。C语言里指针的行为非常具体,具体到每一个字节,草稿纸就是你的好朋友。
画法很简单:遇到一批变量,就画几个方框,方框上方写变量名,方框内写值。指针变量的值是地址,可以写在方框内,然后用箭头指向对应变量的方框。每次执行一条语句,就更新一次方框里的内容。比如int a = 3; int *p = &a;画出来就是两个方框:a的方框里是3,p的方框里写着a的地址,箭头指向a。再执行*p = 5;,你就把a方框里的3改成5。
这个方法在处理*p++、*(p+2)、*(*(p+i)+j)这类复杂表达式时特别好用,因为每一层*都在对应一个“沿着箭头走一步”的动作。纸上画二十次,胜过看视频一百遍。
3. 基础概念题:定义、运算与优先级陷阱
3.1 定义指针时的写法差异
指针变量的定义书上有标准写法:int *p;。不过实际使用中你可能见过三种写法:int *p、int* p、int * p。它们对编译器来说完全一样,但理解上有细微差别。
int *p的读法是把*p看成一个整体:*p是int类型,所以p是指向int的指针。int* p的读法是“int星号”这个类型修饰了p,视觉上更强调指针类型。两种都没有错,但当你连续定义多个变量时要格外小心:int* p, q;里,p是指针,q只是普通int变量。如果你想定义两个指针,应该写成int *p, *q;。
基础题里经常考这种细节,我就见过不少人在这里丢分。另外要注意,定义指针的时候最好同时初始化,比如int *p = &a;,否则p是一个未初始化的指针,指向哪里完全随机,后面再用很可能导致程序崩溃。
3.2 &与*的关系,以及交换函数为什么经常写错
&和*是两个互逆的操作:&给出地址,*从地址取值。*(&a)等于a,&(*p)等于p所指对象的地址。这个互逆关系在题目里经常以“写出下列表达式的结果”的形式出现。
还有一个绕不过去的基础题:交换两个变量的值。很多人一开始都写过这样的版本:
void swap(int a, int b) { int temp = a; a = b; b = temp; }这个写法错得特别典型。调用swap(x, y)时,函数拿到的只是x和y的“复印件”,函数内部确实交换了,但外面的x、y纹丝不动。正确写法必须把地址传进去:
void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; }这里的关键是:函数参数本身还是传值,但传的值是地址。有了地址,函数就能顺着地址找到外面的变量,直接修改。很多同学学完这章还会在这道题上栽跟头,因为下意识把“传地址”和“传值”混为一谈。记住一句话:C语言函数参数永远是传值,所谓传址只是把地址作为值传进去。
3.3 指针自增自减与表达式优先级
这是基础题里错误率最高的一类。*p++和(*p)++看起来就差个括号,含义完全不同。
根据运算符优先级,自增运算符++和间接访问运算符*的优先级是相同的,方向是“从右向左”结合,所以*p++实际上被解释为*(p++):先取p当前指向的值作为表达式的值,然后p自己往后挪一个位置。这个操作在数组遍历里非常常见,遍历完后p就指向了数组的末尾,而不是遍历过程中一直指向第一个元素。
而(*p)++因为有括号,先对p取地址的内容进行自增,也就是“把p指向的那个变量的值加1”,p本身不动。
这里可以用排队打饭来类比:*p++就像你先记下当前排队的人是谁,然后他打完饭走了,队伍前移;(*p)++则是当前这个人没走,你给他碗里多打了一份饭。做题时遇到这类表达式,先在草稿纸上画好指向关系,再一步步算,比凭记忆猜结果可靠得多。
4. 指针与一维/二维数组:重头戏的拆解
4.1 一维数组名到底是什么
数组和指针的关系这章讲得非常细。数组名a代表数组首元素的地址,即a等价于&a[0]。但有个地方容易混:&a取的是整个数组的地址,虽然数值上和&a[0]相同,但类型完全不同。&a的类型是“指向包含N个int元素的数组的指针”,所以&a + 1会跳过整个数组,而不是跳过1个int元素。
很多趣味题和概念题就考这一点。比如:
int a[5] = {1, 2, 3, 4, 5}; int *p = a; printf("%d\n", *(p + 3)); printf("%d\n", *(a + 3));这两行输出都是4。a在表达式中自动退化为指向首元素的指针,所以a + 3就是首元素地址往后挪3个int的位置,再取值就是a[3]。这里“退化”这个词是理解数组与指针关系的关键:数组名在大部分表达式中都会变成“指向第一个元素的指针”,唯一例外是 sizeof 和 & 操作符。
4.2 二维数组的行指针和列指针
二维数组是这章公认的难点。核心要搞清楚两个概念:列指针和行指针。
列指针就是普通的int *,指向一个int元素。它用来逐个遍历二维数组的所有元素,遍历方式就是把二维数组看成一串连续的一维数组。比如:
int a[2][3] = {{1, 2, 3}, {4, 5, 6}}; int *p = &a[0][0]; for (int i = 0; i < 6; i++) { printf("%d ", *(p + i)); }这能依次输出1到6,因为数组在内存中是连续存放的,从a[0][0]开始数6个int,刚好把两行都数完。
行指针的声明长这样:
int (*p)[3]; p = a;这里的p指向一个“长度为3的int数组”,也就是指向一行。所以 p 指向第0行,p+1 指向第1行。要访问第i行第j列的元素,写*(*(p + i) + j)。拆开看:*(p + i)找到第i行这个一维数组,得到它的首元素地址,也就是列指针;再加j,得到第i行第j列元素的地址;再取*,就是元素的值。每一层*都对应一步“先找行,再找列”的过程。
习题里让用指针法输出二维数组、求二维数组的最大值最小值,基本都是这两类:要么用列指针走一遍,要么用行指针按行列访问。
4.3 经典题:用指针法输出二维数组全部元素
这类题有几个写法,我比较推荐这种逻辑清晰、不容易错的版本:
#include <stdio.h> int main() { int a[2][3] = {{1, 2, 3}, {4, 5, 6}}; int i, j; int (*p)[3]; // 行指针 p = a; for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { printf("%4d", *(*(p + i) + j)); } printf("\n"); } return 0; }也可以把*(*(p + i) + j)替换成p[i][j],结果完全一样。这里顺便解释一下为什么可以替换:既然 p 指向一行,p[i] 就是第i行这个一维数组,p[i][j] 就是第i行第j列元素。对编译器来说,p[i] 已经是一个“数组名”,会继续退化为指针,所以 p[i][j] 和((p+i)+j) 本质相同。
做题的时候我更建议大家先写p[i][j]这种直观写法,保证逻辑正确,再改成指针写法作为练习。两种写法都过一遍,才算真正掌握了这个知识点。
4.4 数组名作为函数参数时的退化问题
函数参数里写void f(int arr[], int n)和写void f(int *arr, int n),对编译器来说是一样的。数组名作为参数时退化为指针,所以在函数内部用sizeof(arr)得到的不是整个数组的字节数,而是一个指针的大小。这也是为什么数组做参数时通常还要再传一个长度n。
这个知识点在习题里经常以“补全函数”“判断对错”的形式出现。出错的原因大多是:写函数时总觉得传进来的还是完整数组,于是放心地写sizeof(arr)/sizeof(arr[0])去算长度,结果得到的不是数组长度而是指针大小,程序跑起来就出问题。记住:一旦数组名进了函数参数,它就只是指针,长度必须另外传。
5. 指针与字符串:地址操作为主的题目解法
5.1 字符串在内存中的两种不同表现
C语言没有单独的字符串类型,字符串就是用char数组或char指针表示的。习题里最常见的两种写法:
char str1[] = "hello"; char *str2 = "hello";str1是一个字符数组,内容存在自己的一片内存里,可以修改,比如str1[0] = 'H';完全合法。str2是一个指针,指向字符串常量"hello"在内存中的位置,字符串常量存在只读区,试图通过str2[0] = 'H';去修改会导致未定义行为,多数情况下程序直接崩溃。
这个区别几乎每年都考。理解方式很简单:数组是“自己盖了一间房放东西”,指针是“知道别人那间房里有东西”。自己的房子怎么改都行,别人的房子你不能说改就改。
5.2 字符串复制的经典题写法
教材里大概率有一道题:把字符串a复制到字符串b,要求用指针实现。参考写法:
void my_strcpy(char *to, char *from) { while (*from != '\0') { *to = *from; to++; from++; } *to = '\0'; }很多同学会问:为什么最后还要补一个'\0'?因为while循环遇到 '\0' 就停了,但 '\0' 本身没有拷贝过去。目标字符串必须以 '\0' 结尾,否则后续用 printf("%s") 输出时会继续向后读内存,直到碰巧遇到一个0,输出就会出现“乱码尾巴”。
这道题的更简洁写法是:
while ((*to++ = *from++) != '\0');这里把赋值、自增、比较全部塞进一行。体会一下:先执行*to = *from,然后两个指针都往后挪一位,然后判断赋回来的字符是不是 '\0'。如果是,循环结束。这个写法看起来炫酷,但我建议你先写完整版,理解每一步之后再去压缩它。考试或面试时,完整版更好讲清楚,行数多没关系。
5.3 字符串比较与求长度的指针思路
字符串比较的习题,核心思路是逐个字符比较,直到出现不同或遇到 '\0':
int my_strcmp(char *s1, char *s2) { while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) { s1++; s2++; } return *s1 - *s2; }返回值大于0表示s1大,小于0表示s2大,等于0表示相同。这个return *s1 - *s2;处理了三种情况:两个都到结尾,相减为0;中途遇到不同字符,差值体现大小;一个先到结尾,先到结尾的字符是 '\0',ASCII码为0,比任何普通字符都小,所以短字符串自然“小于”长字符串。
求字符串长度的指针写法是:
int my_strlen(char *s) { char *p = s; while (*p != '\0') { p++; } return p - s; }这里用到一个重要性质:同一个数组内的两个指针相减,结果是一个整数,表示它们之间相隔多少个元素。p从头走到 '\0',p - s 就是字符个数。这题提醒我们,指针不只能加减整数,两个指针本身也可以相减,这在数组、字符串处理题里非常实用。
6. 指针作为函数参数:传值传址的本质
6.1 传地址也是传值,别被概念绕晕
前面说过swap的正确写法,这里再往深挖一步:为什么传地址就能修改外部变量?因为函数内部拿到的是外部变量的地址,通过*p就能访问那个地址上存储的对象。但指针变量本身在函数内部如果被修改,比如执行p++,这个修改不会传回调用者,因为指针变量p本质上还是一个普通的局部变量。
有一个经典陷阱题能帮你彻底领悟这一点。看下面的代码:
void get_memory(char *p) { p = (char *)malloc(100); } int main() { char *str = NULL; get_memory(str); // 此时str仍然是NULL return 0; }很多人以为调用后str就有了一百字节的内存,实际上str始终是NULL。原因就是str本身的值(NULL)传给了形参p,函数里重新给p赋值,只是改变了形参p的指向,外部str根本不受影响。如果需要在函数中改变外部指针本身,就必须用二级指针,也就是把指针的地址传进去。这类题在第八章的综合题里经常出现。
6.2 用指针参数完成排序与查找
习题里常见的排序题,函数原型一般是void sort(int a[], int n)或void sort(int *a, int n),两种写法等价。以选择排序为例:
void sort(int *a, int n) { int i, j, min_idx, temp; for (i = 0; i < n - 1; i++) { min_idx = i; for (j = i + 1; j < n; j++) { if (a[j] < a[min_idx]) { min_idx = j; } } if (min_idx != i) { temp = a[i]; a[i] = a[min_idx]; a[min_idx] = temp; } } }这里数组名退化为指针,所以函数内部对a[i]的修改,本质上就是*(a + i)的修改,直接影响main里的原数组。你要理解一件事:为什么排序函数不用写返回值?因为修改发生在调用者那块内存上,函数只需要“知道地址”就够了。这正好呼应了前面swap的例子——通过地址访问,就能修改外部对象。
6.3 函数返回地址的两个大坑
习题里还有一种题:写一个函数返回某个字符在字符串中的位置,或者返回数组中的最大值地址。这类题本身不难,但涉及“函数返回指针”时有两个坑。
第一,不要返回局部变量的地址。比如:
int *bad() { int local = 100; return &local; }local是函数内部的局部变量,函数结束后这块内存就被回收复用,返回的地址指向的内容随时可能被其他代码改写。运行结果完全不可预测。
第二,返回字符串常量地址没问题,返回动态分配的内存地址也没问题,但使用完要记得free。判断“能不能返回”的标准只有一个:那块内存的生存期是否在函数结束后仍然有效。局部变量的生存期在函数返回时就结束了,所以不行;malloc出来的堆内存直到free才消失,所以可以。
6.4 函数指针的概念题
第八章如果涉及到函数指针,通常只是初识。函数指针的声明长得比较吓人:
int (*p)(int, int);p是一个指针,指向一个“接收两个int参数、返回int”的函数。为什么函数名可以赋给函数指针?因为函数本身在内存中也有起始地址,函数名就是它的地址,就像数组名是数组首地址一样。
一个简单的例子:
#include <stdio.h> int add(int a, int b) { return a + b; } int main() { int (*p)(int, int); p = add; printf("%d\n", p(3, 4)); return 0; }p(3,4) 和 add(3,4) 等价。这个概念题主要考察声明语法能不能看懂,实际用到它的题目不多。如果书上只是提了一句,你不必在这里花太多时间,先认识语法结构,等以后真的写回调函数时再深入也不迟。
7. 二级指针与动态内存分配:综合题的突破口
7.1 什么时候需要二级指针
二级指针就是“指向指针的指针”,声明写int **p;。它最常见的用途就是前面提到的:在函数里修改外部指针变量的值。比如动态分配内存并把这个指针带回调用者:
void init_ptr(int **p, int size) { *p = (int *)malloc(size * sizeof(int)); } int main() { int *arr = NULL; init_ptr(&arr, 10); // 此时arr已经指向一块10个int大小的堆内存 if (arr != NULL) { arr[0] = 10; printf("%d\n", arr[0]); } free(arr); return 0; }这里的&arr是 int **,形参 p 是 int **。函数里*p = malloc(...)等价于把malloc的结果写回arr。拆开看:p得到的是arr,赋值给p就是给arr赋值。想明白这个链条,二级指针就不再神秘。
7.2 malloc的使用全套
动态内存的题几乎离不开malloc。最稳的写法是:
int *p = (int *)malloc(n * sizeof(int)); if (p == NULL) { return 1; }判空这一步很多人偷懒省略。在内存不足时malloc返回NULL,如果不检查就对NULL解引用,程序会直接崩溃。习题里这个检查一般不会扣分,但好的习惯是写上。
配套函数还有几个:calloc会在分配后清零,realloc可以调整已有内存块的大小。教材里主要用malloc,如果书上提到了realloc,注意realloc失败时返回NULL,原来的内存仍然有效,不能直接p = realloc(p, new_size);了事,否则新指针没拿到,旧指针还给丢了,内存就泄漏了。
7.3 典型综合题:动态分配学生成绩表
这类题的形态通常是这样:某班级有m个学生,每个学生有n门成绩,要求动态分配存储空间并计算平均分。完整参考思路如下:
#include <stdio.h> #include <stdlib.h> int main() { int m, n, i, j; printf("请输入学生数和课程数:"); scanf("%d %d", &m, &n); int **score = (int **)malloc(m * sizeof(int *)); if (score == NULL) { return 1; } for (i = 0; i < m; i++) { score[i] = (int *)malloc(n * sizeof(int)); if (score[i] == NULL) { return 1; } } for (i = 0; i < m; i++) { printf("请输入第%d个学生的%d门成绩:", i + 1, n); for (j = 0; j < n; j++) { scanf("%d", &score[i][j]); } } for (i = 0; i < m; i++) { int sum = 0; for (j = 0; j < n; j++) { sum += score[i][j]; } printf("第%d个学生的平均分:%.2f\n", i + 1, sum / (double)n); } for (i = 0; i < m; i++) { free(score[i]); } free(score); return 0; }这个例子把二级指针、动态分配、双重循环、free全部串起来了。值得注意的是释放顺序:先释放每一行的score[i],最后释放score本身。顺序反过来会导致内存泄漏或非法访问。
7.4 内存释放的写码习惯
很多同学在习题里能想到malloc,却总忘free。习题代码跑一次就结束,不free好像也没什么影响,但一旦养成这个坏习惯,以后写真实项目时会不停出现内存泄漏。
我建议的写法是:在申请内存的那一刻,就顺手在代码注释里写上“这里要free”,或者写完malloc立刻把对应的free写出来,再回来补中间逻辑。free之后,最好再把指针置为NULL:free(p); p = NULL;。这样可以防止“悬空指针”——即指针还保存着已释放内存的地址,万一后面误用,程序崩溃的概率非常高。置空之后,哪怕误用也能快速定位到空指针问题。
8. 刷题过程中的常见报错与排查方法
8.1 编译警告里藏着答案
自己写完代码编译时,一定要开高警告级别。在命令行下用gcc或者常见的IDE编译时,加上-Wall选项,编译器会输出很多平时不显示的警告。
最常见的警告是“解引用未初始化的指针”“赋值类型不匹配”“函数参数类型不一致”。这些警告往往直接指出题目答案里的坑。比如声明int *p;后直接*p = 5;,编译器会提示p未初始化。这时候你就应该意识到:p没有指向任何合法内存,你是在往一个随机的地址写数据,后果可能是段错误,也可能悄悄改坏了别的变量。习题里这种“能跑但结果不对”的经典错误,基本都是这么来的。
8.2 printf大法和画图法结合使用
程序结果不对的时候,最直接的调试方法就是“到处打印”。在指针的习题里,我强烈建议多打印几个东西:变量本身的地址、指针的值、指针指向的值。
比如你写了一个排序函数,结果输出乱序,可以在关键位置加上:
printf("i=%d, a[i]=%d, p=%p\n", i, a[i], (void*)a);地址的格式用%p,打印出来的十六进制数能帮你确认指针确实在数组区域内移动。一旦发现地址跳出了数组范围,八成是下标越界或者指针加减的步数算错了。
画图法和printf并不冲突:先听printf输出的地址和数据,再在草稿纸上把内存关系画出来,很多问题能直观地暴露出来。尤其是二维数组相关的题,画出一个3行4列的格子,把每个元素的地址标出来,你立刻能分清行指针和列指针的差别。
8.3 段错误常见的几种来源
程序一运行就崩溃,最常见的是这几种:
| 可能原因 | 典型代码 | 解决办法 |
|---|---|---|
| 对未初始化指针解引用 | int *p; *p = 5; | 初始化或分配内存 |
| 数组越界访问 | 循环到下标n而不是n-1 | 检查循环边界 |
| 字符串常量被修改 | 通过char *改字符串常量 | 用char数组存储 |
| 释放内存后继续使用 | free后不置空仍访问 | free后置NULL |
| scanf缺少地址符 | scanf("%d", a);而不是scanf("%d", &a); | 加取地址符 |
第五个看着和本章无关,但实际上很多同学在做“读入数组再用指针处理”的习题时,卡在了输入的scanf上。值得提醒一句:scanf读取普通变量时是要传地址的,这个“地址”和本章指针的概念本来就是同一回事。
8.4 我的调试习惯与最终检查流程
我刷第八章习题时,总结了一套流程,每一步都能省下大量时间:
第一步,写完代码先不求能跑,而是逐行读一遍,把每个指针当前的指向在纸上画出来。第二步,开警告编译,把警告全部清零再运行。第三步,用最简单的小数据测试,比如3个数的排序、2行3列的数组,先确认逻辑正确,再换大数据验证。第四步,程序结果正确后,再把代码里的临时printf删掉,确保最终提交版本干净整洁。
这套流程对习题和以后做项目都适用。尤其是“先小数据再大数据”这点,我见过太多人拿一个很大的测试用例去调试,输出一堆数据,根本看不出哪里错了,还不如从最小的用例开始手动追踪。指针这章练的就是对内存的敏感度,数据越小你越容易手算出答案,也越容易发现代码和手算结果不一致的位置。
我个人学这章时最深的体会是:不要怕出错,反而要珍惜犯错的机会。一道二维数组题想不明白,画图、打印、慢慢追,一次彻底弄懂,比稀里糊涂做对十道题都强。指针这个东西,一旦底层逻辑通了,后面再看链表、二叉树、结构体指针都会轻松很多,因为内存模型是相通的。先把第八章的实验题吃透,你的C语言才算真正入了门。