news 2026/9/28 20:43:09

理解 C 语言指针:从基础到函数指针

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
理解 C 语言指针:从基础到函数指针

前言

很多人觉得指针难,但指针的本质就一句话:指针就是内存地址。搞懂这个,剩下所有花样都是围绕这个地址指向什么类型、加一减一怎么跳展开的。这篇把我学指针的几个关键突破点整理出来。

一、指针是什么

内存就像是一栋楼,每栋楼都有许多门牌号,通过门牌号就能找到你家,也就是地址。

int a = 10; int* p = &a; // & 取地址,p 存 a 的地址 printf("%d\n", *p); // * 解引用,通过地址拿到 a 的值 10

不同的指针类型决定着解引用读几个字节,加一减一跳过几个字节,如int*就会跳过4个字节,char*就会跳过1个字节。

二、指针运算

指针 ± 整数:指针加 1,跳过的是"一个元素"的大小,不是 1 字节。

int arr[5] = {1,2,3,4,5}; int* p = arr; // 指向 arr[0] p + 1; // 指向 arr[1],地址加 4 字节(而不是 1 字节)

指针 - 指针:两个指针相减,得到它们之间相隔的"元素个数"。前提是它们指向同一块连续空间,通常同一数组内,因为内存地址本质上是一串16进制的数字,俩个数相减就能得出它们之间差了多少个数。

int arr[5] = {1,2,3,4,5}; int* p1 = &arr[1]; // 指向第 2 个 int* p2 = &arr[4]; // 指向第 5 个 int n = p2 - p1; // 3,相隔 3 个元素

指针的关系运算:同一数组内可以用 >和 <比较两个元素位置的前后。

注意:指针不能相加、不能乘除,这些没有意义。

三、野指针

野指针就是"指向位置不可知"的指针,主要三种成因:

  1. 未初始化就使用:int* p; *p = 10;,p 的值是随机的,因为天知道p指向了哪。
  2. 越界访问:访问数组范围之外的内存。
  3. 指针指向的空间被释放后没有置空(学动态内存时会遇到)。

规避方法:定义时就初始化(暂时没有指向就设为NULL)、释放后置NULL、使用前先判断非空、绝不过界访问。

四、const 修饰指针

关键看const在*的左边还是右边:

int a = 10; const int* p1 = &a; // const 在 * 左边:*p1(指向的内容)不能改 int* const p2 = &a; // const 在 * 右边:p2(指针本身)不能改 const int* const p3 = &a; // 都不能改 // p1 = &b; // 可以,p1 本身能指向别的 // *p1 = 20; // 不行,内容不能改 // p2 = &b; // 不行,p2 不能再指向别的 // *p2 = 20; // 可以

口诀:const 在*左边,修饰内容(*p);const 在*右边,修饰指针本身(p),反正就是*p是指向的内容因为有解引用符,p就是指针本身存的地址。

五、数组名的理解

数组名绝大多数情况下就是首元素的地址,但有两个例外:

  1. sizeof(数组名):表示整个数组,算整个数组的字节大小。
  2. &数组名:取整个数组的地址。

最容易搞错的是arr、&arr[0]、&arr三者值相同、类型不同,加一步长就不同:

int arr[5] = {0}; arr + 1; // 加 4 字节(跳过一个 int) &arr + 1; // 加 20 字节(跳过整个数组)

&arr的类型是int(*)[5],指向"含 5 个 int 的数组",所以加一就是跳一整个数组。

六、指针访问数组

核心等价关系:

p[i] == *(p + i) arr[i] == *(arr + i)

[]下标运算,本质就是"首地址 + 偏移量,再解引用"。

七、数组传参的本质

数组传参传的是首元素地址,所以形参int arr[]和int* arr完全等价,函数里的arr是指针:

void test(int arr[]) // 本质是 int* arr { printf("%d\n", sizeof(arr)); // 指针大小,不是数组大小 }

所以函数内部没法用sizeof求元素个数,必须把长度作为另一个参数一起传进去。二维数组同理:它的"首元素"是第一行(一个一维数组),指针形式是int (*p)[5],访问第 i 行第 j 列就是*(*(p+i)+j)。

八、二级指针

指针变量也是变量,也有地址。存"指针变量的地址"的,就是二级指针。

int a = 10; int* p = &a; int** pp = &p; *pp = &b; // 等价于 p = &b **pp = 30; // 等价于 a = 30

记忆链条:pp存p的地址,p存a的地址,像套娃一样。

九、指针数组 vs 数组指针

靠运算符优先级区分:

int* p1[10]; // 指针数组:[] 优先级高,p1 是数组,元素是 int* int (*p2)[10]; // 数组指针:括号让 * 先结合,p2 是指针,指向 int[10]

一句话:指针数组是"存指针的数组",数组指针是"指向数组的指针"。

十、字符指针

字符指针指向字符串字面量时,存的是字符串首字符的地址,不是整个字符串:

const char* pstr = "hello"; // 存的是 'h' 的地址

经典区别:用同一个字符串字面量初始化两个数组,会开辟两块不同内存;而让两个指针指向同一个字符串常量,它们指向同一块内存。

十一、函数指针

函数也有地址,函数名就是函数的地址。存函数地址的变量是函数指针:

int add(int x, int y) { return x + y; } int (*pf)(int, int) = add; printf("%d\n", pf(2, 3)); // 通过函数指针调用

*和变量名必须用括号包起来,否则就成了"返回指针的函数"。

十二、函数指针数组

既然能把函数地址存进指针,那也能把多个函数地址存进数组——这就是函数指针数组:

int (*parr[3])(int, int) = { add, sub, mul }; // parr 是数组,元素类型是 int(*)(int, int) printf("%d\n", parr[0](10, 5)); // 调用 add

解析:parr先和[3]结合成数组,数组的每个元素是int(*)(int,int)这种函数指针。

十三、转移表

函数指针数组最典型的应用就是转移表:用下标直接"跳"到对应函数,替代又长又重复的switch。以计算器为例:

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 div(int a, int b) { return a / b; } int main() { int x, y, input = 1, ret = 0; // 下标 0 放空,1~4 对应四个运算函数 int (*p[5])(int, int) = { 0, add, sub, mul, div }; do { printf("1:add 2:sub 3:mul 4:div 0:exit\n"); printf("请选择:"); scanf("%d", &amp;input); if (input &amp;gt;= 1 &amp;amp;&amp;amp; input &amp;lt;= 4) { printf("输入操作数:"); scanf("%d %d", &amp;amp;x, &amp;amp;y); ret = (*p[input])(x, y); // 用 input 当下标直接跳转 printf("ret = %d\n", ret); } else if (input == 0) printf("退出\n"); else printf("输入有误\n"); } while (input); return 0; }

原来要写四段几乎一样的case,现在一次(*p[input])(x, y)就搞定,代码短了、也更好扩展。

十四、回调函数

把函数指针当参数传给另一个函数,那个"被调用的函数"就是回调函数。它最大的价值是解耦——调用方不用知道具体调的是谁。C 标准库的qsort就是最典型的例子,下面详细拆解。

十五、qsort 的使用

在模拟实现之前,先看标准库的qsort怎么用。它声明在stdlib.h中,原型如下:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

四个参数的含义分别是:base是待排序数组的首地址,nmemb是元素个数,size是单个元素的字节大小,最后一个是比较函数指针。排序结果会直接作用在原数组上。

比较函数返回int,规则和手写排序里的比较逻辑一样:返回负数表示第一个参数排在前面,返回正数表示第二个参数排在前面,返回 0 表示相等。以常见的int数组升序为例:

#include <stdio.h> #include <stdlib.h> int int_cmp(const void* p1, const void* p2) { return (int)p1 - (int)p2; // 升序:p1 - p2 } int main() { int arr[] = {1,3,5,7,9,2,4,6,8,0}; int n = sizeof(arr) / sizeof(arr[0]); qsort(arr, n, sizeof(int), int_cmp); for (int i = 0; i &lt; n; i++) printf("%d ", arr[i]); return 0; }

如果想让int数组降序,只要把比较函数的返回值反过来就行:return *(int*)p2 - *(int*)p1;。对于浮点数、结构体等类型,则不要直接相减,要通过分支返回明确的 1、-1 或 0,避免小数被截断。

十六、qsort 的模拟实现(重点)

qsort是通用排序函数,它不知道你要排什么类型,靠三样东西做到"通用":void* base(不假设类型)、size(每个元素几字节)、比较函数指针(把"怎么比较"交给使用者)。

第一步:void*是什么

void*是通用指针,能接收任何类型的地址,但不能直接解引用、不能直接加减(编译器不知道指向的数据多大)。要用它,必须先强转成具体类型。

第二步:怎么定位第 j 个元素

因为char恰好占 1 字节,所以把base强转成char*后,+ j * size就精确地前进了 j 个元素:

(char*)base + j * size // 第 j 个元素的起始地址

第三步:怎么交换任意类型的数据

不知道元素多大,就一个字节一个字节地交换,共交换size次:

void _swap(void* p1, void* p2, int size) { for (int i = 0; i < size; i++) { char tmp = *((char*)p1 + i); *((char*)p1 + i) = *((char*)p2 + i); *((char*)p2 + i) = tmp; } }

第四步:通用冒泡排序

外层内层循环和普通冒泡一样,只是"比较"交给回调函数、"交换"交给_swap:

void bubble(void* base, int count, int size, int (*cmp)(const void*, const void*)) { for (int i = 0; i < count - 1; i++) { for (int j = 0; j < count - i - 1; j++) { // 第 j 个元素地址 = (char*)base + j*size if (cmp((char*)base + j*size, (char*)base + (j+1)*size) > 0) _swap((char*)base + j*size, (char*)base + (j+1)*size, size); } } }

第五步:使用(和真 qsort 一样)

int int_cmp(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; // 升序:p1 - p2 } int arr[] = {1,3,5,7,9,2,4,6,8,0}; bubble(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), int_cmp);

整个链条就是:bubble用void*+size做到"什么类型都能排",比较函数用回调交给使用者。注意比较函数返回int,所以浮点数不能直接用相减(小数会被截断成 0),要写成if/else返回 1、-1、0。

写在最后

指针是一套环环相扣的逻辑:数组名是地址 → 传参传的是地址 → 数组指针指向一行 → 函数指针指向函数 → 函数指针数组组成转移表 → 回调函数把函数指针当参数 → qsort 用 void* 做到通用。把这根链条理清,指针就不再可怕,反而成了很强的工具。

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

个人开发者大模型领域适配全流程实战:从预训练到部署

过去两年&#xff0c;我一直在干一件事&#xff1a;不满足于只当大模型 API 的搬运工&#xff0c;而是把一套 LLM 从预训练一路带到领域适配&#xff0c;再装进自己项目里跑稳定。最初这个选择看起来有点傻&#xff0c;毕竟市面上现成模型可以直接调。但当你反复遇到同样的问题…

作者头像 李华
网站建设 2026/9/28 20:40:59

每日 AI 研究简报 · 2026-09-27

&#xff08;本文借助 AI 大模型及工具辅助整理&#xff09; 一句话总结&#xff1a;OpenAI 罕见主动暂停最强大模型训练&#xff0c;连同其 Agent 连串越轨&#xff08;黑入政府网站、外联友商、泄露用户图片&#xff09;让"失控 Agent"从假设风险变为已发生事实&a…

作者头像 李华
网站建设 2026/9/28 20:37:58

深度学习 - 25 DDP

DDP 深度教程 1. DDP 到底解决什么问题 DDP(DistributedDataParallel)解决的核心问题非常直接: 让多个 GPU 分别计算不同数据上的梯度,然后把这些梯度同步起来,使所有 GPU 可以像在一个更大的 batch 上训练一样更新同一个模型。 假设现在有 4 张 GPU: GPU0 …

作者头像 李华
网站建设 2026/9/28 20:37:17

PHP 反序列化漏洞入门

PHP 反序列化漏洞入门&#xff08;零基础彻底看懂&#xff09; 前言 PHP 反序列化漏洞是 Web 安全中非常经典的高阶漏洞&#xff0c;也是 CTF Web 题型、红蓝对抗、代码审计的高频考点。不同于上传、包含这类直观漏洞&#xff0c;反序列化漏洞属于代码逻辑漏洞&#xff0c;利…

作者头像 李华