news 2026/10/3 4:39:43

C语言指针五兄弟:指针数组、数组指针、函数指针、函数指针数组一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言指针五兄弟:指针数组、数组指针、函数指针、函数指针数组一次讲透

搞C/C++的人,早晚都要过指针这一关。尤其是标题里这串名词——指针数组、数组指针、指针函数、函数指针、函数指针数组——光看名字就让人头大,不少人在面试前背了又背,一到写代码还是分不清int *p[3]和int (*p)[3]到底谁是谁。这篇文章我就把这几个硬骨头串在一起讲清楚,从指针和变量、一维数组、二维数组开始,一路拆到函数指针数组,顺带把声明怎么读、传参怎么写、底层的寻址逻辑是什么一次说透。文章适合正在学C语言的同学,也适合学了一段时间依然对指针和数组的结合犯迷糊的人,只要跟着例子自己敲一遍,这几个概念基本就不会再混了。

1. 指针与变量:先搞清楚指针到底存了什么

1.1 指针的本质是一个普通变量,只不过存的是地址

很多初学者的误区是觉得指针很"玄",好像变量前面加个*就变成了某种特殊生物。其实指针就是一个普通的变量,它里面存的东西不是数值本身,而是另一个变量的地址。也就是说,指针变量和int、char变量没什么本质区别,都是先开辟一块内存,往里存数据,唯一的不同是它存的数据恰好是一个地址编号。

举个例子:

int a = 10; int *p = &a;

这段代码里,a在内存中占4个字节,里面存的是数值10;p也占内存空间,里面存的是a的首字节地址。&是取地址操作符,它的作用就是把a在内存中的地址编号"取"出来,放进p。

那么*p又是什么?*在这里是间接访问操作符(解引用),意思是"根据p里面存的地址,去访问那块内存里的数据"。所以*p的值就是10,而且*p和a是同一块内存的两种叫法。你可以直接*p = 20;,然后打印a会发现它变成了20。

我在带新人时最爱打一个比方:变量名是人的名字,地址是门牌号,指针变量是一张写着别人门牌号的纸条。你拿着纸条就能找到那个人的家,还能进屋改东西。这跟"指针指向谁"是同一个意思。

1.2 指针的大小和步长:为什么int*加1跳4个字节

这是很多新手忽略的细节。一个指针变量本身占多大内存?答案是固定的,取决于平台:32位程序里所有指针占4字节,64位程序里占8字节,跟你指向的是int还是char没关系。你可以用printf("%zu\n", sizeof(int*));自己验证。

但指针的"步长"跟它指向的类型直接相关:

char *pc = NULL; int *pi = NULL; double *pd = NULL; pc + 1; // 地址加1个字节 pi + 1; // 地址加4个字节 pd + 1; // 地址加8个字节

p + 1并不表示"地址数值加1",而是"跳到下一个同类型变量"。所以int *pi加1,实际上是地址值加sizeof(int),也就是4。这个特性是后续数组遍历的理论基础,也解释了为什么不同类型的指针不能随便互相赋值——步长定义完全不同。

常见一个低级错误是:

int a = 5; char *p = &a; // 编译会警告

就算你用强制类型转换强行编译过去,*p也只能读到a低地址的1个字节,剩下的3字节被丢弃了。这属于类型不匹配,不是我这里说的步长问题,但原理是相通的:指针的类型决定了你通过它能"看到"多大范围的内存。

1.3 指针的初始化和空指针

用指针前必须初始化,否则它里面是垃圾值,解引用不知道会踩到哪块内存。两种规范写法:

int a = 10; int *p = &a; // 指向变量 int *q = NULL; // 或者暂时不指向任何对象

NULL在C语言里就是(void*)0,代表地址0。地址0在绝大多数平台上是被系统保护的内存,不允许读写。好处是你可以用if (p != NULL)来判断指针是否有效。坏处是如果你对NULL解引用,程序会直接崩溃(段错误),不过崩溃总比乱写内存强——后者通常表现为数据莫名被篡改,排查起来痛苦得多。

2. 指针与一维数组:数组名为什么能"假装"是指针

2.1 数组名在大多数表达式中会退化成首元素地址

一维数组和指针结合,核心就一句话:数组名在绝大多数情况下会退化成指向首元素的指针。

int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; // 这里arr退化为&arr[0]

arr的类型是int[5],但在表达式里赋值给int*时,编译器会把arr退化成int*,也就是&arr[0]。所以p和arr指向同一个地址,但注意,它们不是同一种类型。arr是数组类型,长度为5的元素序列;p是指针类型,只存了一个地址。

这个退化规则有两个例外,一个是sizeof,一个是&:

int arr[5]; printf("%zu\n", sizeof(arr)); // 20,整个数组的大小,不是4或8 printf("%p %p\n", arr, &arr); // 数值相同,但类型不同

sizeof(arr)算出的是数组总字节数(5 × 4 = 20),说明在这里arr保持了数组的身份。&arr的类型是int(*)[5],指向整个数组的指针,后面会细讲。

2.2 下标访问的本质:arr[i]就是*(arr+i)

C语言的数组下标[]只是一个语法糖,编译器的处理逻辑是:

arr[i] 等价于 *(arr + i)

怎么理解?arr退化为首元素地址后,arr + i就是地址往后跳i个int的长度,然后*解引用取出那个位置的值。这个转换是双向的:arr[i]可以写成*(arr + i),也可以写成i[arr]。听起来匪夷所思,但编译器就是这么处理的,i[arr]会被翻译成*(i + arr),结果一模一样。

我遇到过有面试官喜欢考这个,其实它就是考你有没有真正理解下标的本质。

对于一个变量int x = 10; int *p = &x;来说,p[0]也合法,因为p[0]等价于*(p + 0),也就是*p。很多人在做链表或者动态数组时看到p[0]不理解为什么数组下标能用在指针上,这个转换就是答案。

2.3 遍历数组的几种写法对比

int arr[5] = {1, 2, 3, 4, 5}; // 写法1:下标 for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } // 写法2:指针遍历 for (int *p = arr; p < arr + 5; p++) { printf("%d ", *p); } // 写法3:直接操纵首地址 for (int i = 0; i < 5; i++) { printf("%d ", *(arr + i)); }

三种写法输出一样。写法2在C语言底层很常见,比如操作字符串时,常用while (*s) s++;来走完整个字符串,因为在C里字符串就是char数组,末尾有个'\0'作为哨兵。

边界判断p < arr + 5是可行的,因为指针大小比较的前提是"指向同一个数组内部或末尾之后一个位置"。如果你乱指到别的内存去比较,那属于未定义行为。

3. 指针与二维数组:行指针才是理解的关键

3.1 二维数组在内存里其实是一维的

很多新手把二维数组想象成一个矩阵,其实内存里根本没有"二维"这个概念。C语言的二维数组是连续存放的线性内存,int a[3][4]就是先放第0行(4个int),紧接着放第1行(4个int),再放第2行。总字节数3 * 4 * 4 = 48字节,在内存里是连续的48个字节。

之所以用a[row][col]访问,是因为编译器帮你做了换算:a[row][col]等价于*(*(a + row) + col)。先理解这个换算逻辑:

  • a的类型是int (*)[4],指向一行(含4个int的数组)。
  • a + row:按"行"为步长跳转,跳到第row行的首地址,类型依然是int (*)[4]。
  • *(a + row):解引用这一行,得到第row行的首元素地址,类型是int*。
  • *(a + row) + col:按"列"(int)为步长跳转,跳到第col个元素。
  • 再解引用*(*(a + row) + col)得到具体的int值。

这就是为什么二维数组名不能直接赋值给int**,下面细说。

3.2 指向二维数组的指针怎么写

看一个非常容易混淆的声明对比表:

声明类型含义典型用途
int *p[4]指针数组:p先和[4]结合,是数组,数组每个元素的类型是int*存放多个字符串首地址
int (*p)[4]数组指针:p先和*结合,是指针,指向一个含4个int的数组用来指向二维数组的某一行
int **pp二级指针:pp是指针,指向一个int*用于指针数组的头指针

用代码看数组指针怎么用:

int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int (*p)[4] = a; // p指向第0行 printf("%d\n", (*p)[2]); // 1,*p得到第0行首元素地址,[2]取第3列 printf("%d\n", *(*(p + 1) + 3)); // 8,第1行第3列

p + 1为什么能跳到第1行?因为p的类型是int (*)[4],指针步长是"一个含4个int的数组"的大小,也就是16字节。这就是行指针的精髓:它把"行"作为移动单位。

3.3 二维数组当参数传递时的写法

写函数时,接收一个二维数组有好几种等价的声明方式:

void func1(int a[3][4]) {} // 第1种:直接写完整维度 void func2(int a[][4]) {} // 第2种:省略第一维,第二维必须写 void func3(int (*a)[4]) {} // 第3种:使用数组指针

三种写法在编译器层面完全相同,int a[3][4]和int a[][4]都会被调整为int (*a)[4]。这也是一个理解要点:数组参数本质上是通过指针传递的,数组名退化为指针,所以函数内没法通过sizeof(a)获得数组大小,必须在参数里额外传行列数。

初学者常犯的错是把二维数组名a直接传给int**类型的形参:

void func(int **a); func(a); // 编译警告,类型不匹配

原因就是前面分析的:a的类型是int (*)[4],它退化成指针后指向的是"含4个int的行数组",不是指向int*。int **要求先按int*的步长移动,再按int的步长移动,跟二维数组的实际内存布局对不上。

3.4 二级指针与二维数组:为什么它们不是一回事

二级指针int **pp指向的是一个int*类型的变量,常用于动态分配的指针数组,比如:

int row0[3] = {1, 2, 3}; int row1[3] = {4, 5, 6}; int *rows[2] = {row0, row1}; int **pp = rows; // 合法,rows是int*数组,首元素类型是int*

这里rows是"数组里的元素本身是指针",每个元素指向一块独立的内存,这块内存之间不保证连续。而二维数组int a[3][2]是一整块连续内存。两者的内存布局有本质区别:

  • 二维数组:一行紧挨一行,连续存储。
  • 指针数组(或者二级指针访问的对象):每行可能分布在内存的不同角落。

所以访问方式都有差异:用int **pp访问时,第一跳取到rows[i](指向某行的指针),第二跳rows[i][j]才取到具体数值;而二维数组a[i][j]是纯索引换算。

4. 指针数组与数组指针:一字之差,方向相反

4.1 怎么读声明:优先级和结合性是突破口

区分int *p[3]和int (*p)[3]有一个非常实用的方法,叫"优先级读法",比死记硬背靠谱得多:

  1. 先看标识符(变量名)左边有没有*,右边有没有[]或()。
  2. []的优先级高于*,所以如果没有括号,p先和[3]结合,p是数组。
  3. 如果加了括号让*先和p结合,p就是指针。

具体来说:

int *p[3]; // p先和[3]结合 => p是一个数组,数组里有3个元素 // 元素类型是int* => "存放int指针的数组",即指针数组 int (*p)[3]; // 括号使p先和*结合 => p是指针 // 指向的类型是int[3] => "指向含3个int数组的指针",即数组指针

面试时考的最多的就是这两种。你可以默念口诀:"没括号,数组优先;有括号,指针优先"。

4.2 指针数组的实际用途:字符串表、参数列表

指针数组最经典的应用是保存多个字符串的首地址:

char *str_arr[] = { "hello", "world", "C language" };

这里有三个字符串字面量,它们存储在静态存储区,str_arr是一个数组,每个元素是一个char*,分别指向三个字符串的首字符。这种写法比char str[3][20]省内存,因为每行不用固定申请20字节,字符串多长就占多少空间。代价是指向的字符串字面量是只读的,不能通过str_arr[0][0] = 'H'修改。

程序入口main函数的参数其实也是这个结构:

int main(int argc, char *argv[])

argv就是一个char*数组,argv[0]是程序路径,argv[1]及以后是命令行传入的参数。理解指针数组后,操作argv跟操作str_arr没区别,你会遍历它就能处理任意数量的命令行参数。

4.3 数组指针的实际用途:二维数组传参和矩阵运算

数组指针主要用来指向二维数组的某一行,或者干脆作为二维数组的"行指针"来操作整个矩阵。最常见的场景是编写矩阵运算函数:

void print_matrix(int rows, int cols, int (*mat)[cols]) { for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { printf("%5d ", mat[i][j]); } printf("\n"); } }

这里int (*mat)[cols]是可变长数组的写法,C99支持,cols在运行时传入。如果编译器不支持可变长数组,那就老老实实写固定列数或者使用int**配合手动索引。

数组指针还有一个容易踩的坑:

int a[3][4]; int (*p)[4] = a; p++; // 跳过整整一行,地址加16字节 (*p)[0]; // 访问下一行的第一个元素

很多新手拿p++当"移动一个元素"来理解,结果发现地址跳了4个int那么多,就是因为没意识到int (*)[4]的步长是"一个含4个int的数组",16字节。这是一个非常典型的指针步长问题。

5. 指针函数与函数指针:返回指针,还是用指针调函数?

5.1 指针函数:返回值是地址的函数

名字里带"函数"结尾的,本质上是个函数,只不过它的返回值是个指针。声明格式:

int *find_max(int *arr, int size) { int *max = arr; for (int i = 1; i < size; i++) { if (arr[i] > *max) { max = &arr[i]; } } return max; }

int *find_max(...)的意思是:find_max是一个函数,它的返回值类型是int*。调用方式:

int nums[] = {3, 7, 2, 9, 5}; int *p = find_max(nums, 5); printf("%d\n", *p); // 9

指针函数最常见的一个坑是返回局部变量的地址。局部变量在函数结束后内存就被回收了,你再拿着那个地址去解引用,读出来的是垃圾值。比如:

int *bad_func(void) { int x = 10; return &x; // 悬垂指针,x已经生命周期结束 }

解决办法有三条路:返回静态局部变量的地址(static int x)、由调用者传入缓冲区地址、或者用动态分配内存malloc(记得释放)。这是面试常考点,也是实际开发中非常容易出bug的地方。

5.2 函数指针:把函数当成数据来传递

函数指针的声明语法是三者中最抽象的:

int (*fp)(int, int);

读法是:fp先和*结合,说明fp是指针;往外一层,fp指向一个函数,这个函数有两个int参数,返回int。它最典型的应用是回调函数——把一个函数作为参数传给另一个函数,让框架在特定时机调用。

举个例子,自定义排序比较函数:

#include <stdio.h> #include <stdlib.h> int compare_int(const void *a, const void *b) { int ia = *(const int*)a; int ib = *(const int*)b; return ia - ib; } int main(void) { int nums[] = {5, 2, 8, 1, 9, 3}; qsort(nums, 6, sizeof(int), compare_int); for (int i = 0; i < 6; i++) { printf("%d ", nums[i]); } return 0; }

这里qsort的第四个参数就是函数指针,类型是int (*)(const void*, const void*)。没有函数指针的话,你还得写几个不同的排序函数来应对不同的比较规则;有了它,排序算法本体只需要实现一次,比较逻辑由调用方随时注入。这就是"把变化的部分参数化"的设计思路。

5.3 区分记忆技巧:从名字位置和*配合来看

类别声明示例读法本质
指针函数int *func(int a)func是函数,返回int*一个返回指针的函数
函数指针int (*fp)(int a)fp是指针,指向函数一个指向函数的指针变量

这里又可以用优先级读法:*func和*fp的位置关系。

int *func(int a)中,func先和(int a)结合,说明func是函数,*只是修饰返回值。

int (*fp)(int a)中,括号让fp先和*结合,说明fp是指针,外面的(int a)说明它指向的是一个接受int参数、返回int的函数。

注意一个细节:fp = func;和fp = &func;两种写法在C语言里完全等价,因为函数名在表达式中会退化为指向函数的指针。调用时fp(10)和(*fp)(10)也都行,日常习惯写fp(10)就够了。

6. 函数指针数组:转移表与回调机制的基石

6.1 声明、初始化和调用

函数指针数组就是把多个函数指针放进一个数组。声明语法看着吓人,但一层层剥开并不难:

int (*fp_array[4])(int, int);

按优先级读法拆解:fp_array先和[4]结合,所以它是数组;数组元素类型是int (*)(int, int),也就是每个元素是一个函数指针。整个声明的意思是:一个长度为4的数组,里面存了4个函数指针。

初始化并调用:

#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b == 0 ? 0 : a / b; } int main(void) { int (*oper[4])(int, int) = {add, sub, mul, divide}; for (int i = 0; i < 4; i++) { printf("%d\n", oper[i](10, 3)); } return 0; }

这段代码定义了4个二元运算函数,把它们放进数组oper,然后用oper[i](10, 3)直接调用。这是函数指针数组最直白的用途:根据下标选函数,而不是根据条件写一长串if-else。

6.2 用函数指针数组替换switch-case:以计算器为例

很多教材讲计算器都会先写一版switch:

char opt; int a = 5, b = 3; switch (opt) { case '+': result = a + b; break; case '-': result = a - b; break; case '*': result = a * b; break; case '/': result = a / b; break; }

这种写法本身没错,但当你需要支持几十个操作时,switch会膨胀得很厉害。用函数指针数组可以把"操作符映射"和"运算逻辑"彻底分离:

#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b ? a / b : 0; } typedef int (*op_func)(int, int); int main(void) { op_func ops[128] = {0}; ops['+'] = add; ops['-'] = sub; ops['*'] = mul; ops['/'] = divide; char opt = '+'; int a = 9, b = 4; if (ops[(int)opt] != NULL) { printf("%d\n", ops[(int)opt](a, b)); // 13 } return 0; }

这里使用了typedef给函数指针类型起别名op_func,可读性大大提高。数组大小为128是因为char的下标最大到127,直接用ASCII码做索引,查表代替分支判断。这种"表驱动"的设计在嵌入式菜单系统、命令解析器、图形界面事件分发中非常常见,本质就是一种轻量的回调机制。

6.3 从计算器到命令解析:实际的进阶应用

我在实际项目中用过一次这样的设计:一个简单的调试终端,要解析set、get、help、dump、reset这几种命令。如果每个命令写一个case分支,后期命令越来越多,每个case里还要处理参数解析、错误返回,函数会变得又长又难维护。

用函数指针数组之后,每个命令就是一个独立的函数,命令名到函数的映射单独成一张表,新增命令只需要加一个函数、加一行映射:

struct command { const char *name; void (*handler)(int argc, char *argv[]); }; struct command cmd_table[] = { {"help", help_handler }, {"set", set_handler }, {"get", get_handler }, {"dump", dump_handler }, {"reset", reset_handler}, }; void dispatch(const char *cmd, int argc, char *argv[]) { for (size_t i = 0; i < sizeof(cmd_table) / sizeof(cmd_table[0]); i++) { if (strcmp(cmd, cmd_table[i].name) == 0) { cmd_table[i].handler(argc, argv); return; } } printf("unknown command\n"); }

这种写法在C语言世界里非常成熟,也是很多C++里"策略模式"的底层原型。值得一提的是,struct里面放函数指针,很多数据结构的教科书不会展开讲,但你在阅读开源C项目时会频繁遇到。

6.4 函数指针最难的点:类型太长了怎么办

很多人学到这里会崩溃,因为函数指针类型写起来太啰嗦,比如:

int (*fp)(int, int);

如果你有一堆这种变量,每次都写完整类型非常痛苦。解决办法是typedef,除了给函数指针起别名,也可以给函数签名起别名:

typedef int (*binary_op)(int, int); typedef int (*unary_op)(int);

然后声明就清爽多了:

binary_op ops[100];

注意typedef和变量声明的区别:typedef把"一个新的类型名"放在了原来写"变量名"的位置。刚开始可能不习惯,多用几次就好。实际上,在很多C语言项目中,函数指针都是藏在typedef后面的,直接看到的都是简洁的别名。

6.5 使用函数指针数组时常见的坑

几个实战中容易踩的坑,提前提醒一下:

第一,数组下标越界。用操作符字符做下标(如ops['+'])没问题,但如果你用数字索引(比如ops[1000])就直接越界了。务必保证下标范围合法,或者用if (idx >= 0 && idx < size)做防护。

第二,空指针调用。如果数组里某些位置没初始化,或者初始化为NULL,调用时就会崩溃。像上面的例子,调用前先检查ops[(int)opt] != NULL。

第三,函数签名必须完全匹配。函数指针的类型严格限制了参数个数和返回值,不匹配会编译报错,比如int add(int, int)不能直接塞进int (*fp)(int)类型的数组里。解决办法是统一函数签名,让所有函数都用int func(int, int)或者带void*参数的形式,然后内部强转。

第四,别忘记数组名本身也是一个指针,这跟你前面学的数组和指针的关系一样。oper[i](a, b)本质上等价于(*(oper + i))(a, b)。这行代码能把前面所有知识串起来:oper + i是数组内第i个元素的地址,*(oper + i)取到那个函数指针,再对函数指针调用(a, b)。

7. 结合实战:把这几层关系放在一起写段能跑的代码

7.1 一个综合示例:用二维数组、数组指针、函数指针数组做一个迷你菜单程序

光讲概念容易飘,我写一个相对完整的小例子,尽量把前面所有知识点串在一起。代码不长,但覆盖了二维数组、数组指针、指针函数、函数指针数组这些概念。

#include <stdio.h> #include <string.h> // 功能函数 int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return b == 0 ? 0 : a / b; } typedef int (*binary_op)(int, int); // 函数指针数组,下标0-3对应4种操作 binary_op ops[4] = {add, sub, mul, divide}; // 操作符表:二维数组,每行存一个操作符字符和名字 char op_names[4][8] = { "+", "-", "*", "/" }; // 指针函数:接收行指针,返回该行操作符字符 char get_op(char (*row)[8]) { return (*row)[0]; } int main(void) { // 用数组指针遍历操作符表 char (*row_ptr)[8] = op_names; for (int i = 0; i < 4; i++, row_ptr++) { printf("操作符: %c\n", get_op(row_ptr)); } int a = 12, b = 5; printf("12 + 5 = %d\n", ops[0](a, b)); printf("12 - 5 = %d\n", ops[1](a, b)); printf("12 * 5 = %d\n", ops[2](a, b)); printf("12 / 5 = %d\n", ops[3](a, b)); return 0; }

这里组合了:

  • 二维字符数组op_names存放操作符信息;
  • 函数指针数组ops存放运算函数;
  • get_op是返回char的普通函数,形参用了数组指针char (*row)[8];
  • main里用char (*row_ptr)[8]遍历二维数组的每一行。

这个例子虽然小,但把"数组指针、二维数组、函数指针数组"这三者放在同一个程序里,彼此的角色一对比就直观多了:数组指针负责"行"级别的移动,函数指针数组负责"操作"级别的分发。

7.2 建议的练习路径

学习这些概念没有捷径,但有一条我自己验证过还算高效的路径,按顺序来:

  1. 在纸上手写出int *p[5]、int (*p)[5]、int *fp(int)、int (*fp)(int)四个声明,并用优先级读法解释含义,直到不看答案能写对。
  2. 自己写一个程序,用三种不同方式遍历一维数组(下标、指针、*(arr+i)),打印结果确认一致。
  3. 写一个3行4列的二维数组,分别用a[i][j]、*(*(a+i)+j)、*(a[i]+j)访问同一个元素,对比结果。
  4. 写一个指针数组存放3个字符串,再写一个数组指针指向一个3行4列的二维数组,分别遍历输出。
  5. 实现一个计算器程序,先写switch版本,再改用函数指针数组版本,对比两者的代码结构。
  6. 最后用typedef给函数指针起别名,重构你的计算器。

这套练习做完,基本就能把指针和数组、函数的结合体系打通。我的建议是每个例子都自己敲一遍,别光看书,尤其是声明读法,手一写就出真知。

8. 个人体会与经常被问到的几个问题整理

8.1 为什么学了这么多年还是容易混

以我带过那么多新人的经验来看,最容易造成混淆的不是单个概念,而是概念之间容易互相暗示。比如int *p[3]和int (*p)[3],名字就差一个括号;int *function()和int (*fp)(),名字里都有function,位置不一样含义就完全不同。

我的建议是:不要在脑子里同时背好几个名字,只记住优先级读法和"右左法则"这一个工具就够了。拿到任何声明,先看标识符右边有没有[]或(),再看左边有没有*,括号改变结合顺序,每一步都按这个规则来,所有复杂声明都能拆开。语言本身就是一套规则系统,理解了规则就没必要死记硬背。

8.2 指针到底要不要用,学C++用得多吗

在很多开发场景里,现代C++推荐用引用和智能指针,但这不意味着C的原始指针不用学。理由很简单:第一,C++里指针依然无处不在,数组参数传递、回调、面向对象底层实现都离不开指针;第二,理解指针是理解内存布局最快的方式,而内存布局会影响你对性能、生命周期的判断。很多C++代码里的隐蔽bug追溯到底,都是"指针对内存的错误操作"在前。先把C的指针啃明白,后面学C++的引用、容器,思路会顺很多。

8.3 一道综合题型的思考过程

面试里最常考的类似题:char *p1[4]、char (*p2)[4]、char *p3()、char (*p4)(),问各是什么。

我的拆法:

  • p1:右边先遇到[4],是数组,元素类型char*,是指针数组。
  • p2:括号让左边*先发挥作用,是指针,指向char[4],是数组指针。
  • p3:右边先遇到(),是函数,返回值char*,是指针函数。
  • p4:括号让*先发挥作用,是指针,指向一个返回char的函数,是函数指针。

整个过程完全不靠背名字,纯粹是语法解析。这也是真气人但又很有意思的C语言特色:声明本身就是一道编程题,破解它你会非常有成就感。

8.4 最后分享一个调试小技巧

如果程序崩溃但你怀疑是指针问题,有个土办法很好用:把指针打印出来,然后跟一个已知的数组地址对比。比如你怀疑p越界了,先打印p,再打印arr和arr+4,看看p是否落在这个区间之外。C语言没有运行时数组边界检查,养成"打印地址、确认范围"的习惯能帮你省下大量排查时间。我工作这么些年,遇到莫名其妙的段错误,第一反应永远是"把指针和数组边界打出来看",而不是翻代码逻辑。

搞懂指针和数组、函数的结合,其实不只是为了应付考试或者面试,它直接决定你能不能读懂开源项目里那些错综复杂的函数指针表、回调结构、插件系统。这条路没有捷径,但只要你把"优先级读法"和"指针步长"这两个基本功打磨扎实,再顺着本文的路线把几个概念逐个吃透,后续再看任何复杂的C代码,都会觉得顺眼很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:39:43

最大序列和详解:从Kadane算法到五种变体与面试避坑指南

1. 破题&#xff1a;最大序列和到底在问什么“3393. 最大序列和”这个题号&#xff0c;大概率来自某个算法题库或线上练习平台的题目编号&#xff0c;但真正值钱的不是编号本身&#xff0c;而是“最大序列和”这五个字背后的那个经典问题&#xff1a;在一个整数数组里&#xff…

作者头像 李华
网站建设 2026/10/3 4:39:28

生成式召回在交易搜索中的工程实践与避坑指南

1. 从“卷向量”到“生成式召回”的范式切换1.1 为什么传统向量检索在交易搜索里越来越吃力做电商搜索的人都有一个共同感受&#xff1a;向量检索这几年被卷到了极致。从双塔模型到多负样本训练&#xff0c;从ANN索引调参到量化压缩&#xff0c;能榨的油水基本榨干了。但真正落…

作者头像 李华
网站建设 2026/10/3 4:39:03

从零搭建AI工程能力:模型部署、监控与迭代的完整路线

开篇先说明一件事&#xff1a;现在网上聊 AI 的内容&#xff0c;十篇里有八篇在放大模型的“魔法”&#xff0c;但真正让模型在业务里稳定跑起来、让团队能持续迭代、让老板愿意为算力买单的&#xff0c;往往是那些听起来不那么性感的工程问题。我接触 AI 工程&#xff08;ai e…

作者头像 李华
网站建设 2026/10/3 4:38:17

纳什谈判理论下的风光氢多主体合作博弈运行优化与仿真

看到这个标题你可能会觉得又是一篇论文仓库里抠出来的学术黑话&#xff0c;但它其实是近两年新能源领域特别值得落到实处的方向。我最近完整跑过一个“基于纳什谈判理论的风光氢多主体能源系统合作博弈运行策略优化与仿真实现”项目&#xff0c;从模型、算法到代码从零搭了一遍…

作者头像 李华
网站建设 2026/10/3 4:38:15

2025情感分析综述阅读报告:从LSTM到LLM的技术演进与落地实践

这周我把手头攒的一堆情感分析综述论文一次性过了一遍&#xff0c;特别是几篇2025年前后发出的survey&#xff0c;读完之后最大的感觉是——情感分析&#xff08;SA&#xff09;这个领域&#xff0c;已经不是十年前那个“用词典判个正负”的简单任务了。从词典匹配到LSTM中文文…

作者头像 李华
网站建设 2026/10/3 4:37:34

Halcon双目立体视觉引导机械手实战方案

1. 项目概述&#xff1a;为什么双目立体视觉机械手的组合在产线里越来越“吃香”最近三个月&#xff0c;我连续接手了三家电机壳体装配厂的视觉引导改造项目&#xff0c;核心诉求高度一致&#xff1a;让机械手不再靠“蒙”和“试”&#xff0c;而是真正“看见”工件的空间位置&…

作者头像 李华