C语言指针知识点
C语言指针知识点前言指针是C语言的灵魂也是无数初学者心中难以逾越的高山。有人说“理解了指针就理解了C语言的一半”这话一点都不夸张。本文将从最基础的内存概念开始循序渐进地讲解字符指针、指针数组、数组指针以及它们之间错综复杂的关系配合大量的代码示例和内存图示帮助你真正掌握C指针的核心知识。第一章内存与地址——指针的物理基础1.1 内存就是一个个带编号的小格子想象一下计算机的内存就像一栋巨大的公寓楼每个房间都有唯一的门牌号。这些“房间”就是内存单元而“门牌号”就是地址。text内存示意图每个格子1字节 地址: 1000 1001 1002 1003 1004 1005 ... 内容: ??? ??? ??? ??? ??? ??? ...当我们定义一个变量时编译器会为这个变量分配一个或多个这样的“房间”。指针变量特殊的地方在于它里面存放的不是普通数据而是另一个房间的门牌号地址。1.2 为什么需要指针没有指针时函数只能通过返回值来传递数据而且只能传递一份。有了指针我们可以在函数内部修改外部的变量动态申请内存malloc构建链表、树等复杂数据结构高效地操作数组和字符串实现回调函数可以说没有指针C语言能做的工作会大打折扣。第二章字符指针——最简单的指针2.1 字符指针的定义字符指针就是指向字符类型数据的指针变量用char*表示。它是最基础的指针类型之一也是理解其他复杂指针的基石。cchar ch A; char *p ch; // p 指向 chp 里存放的是 ch 的地址2.2 字符指针的两种核心用法用法一指向单个字符cchar ch w; char *pc ch; // pc 是指针变量指向 ch *pc W; // 通过指针修改 ch 的值 printf(%c\n, ch); // 输出 W关键点普通变量不会自动变成指针必须用取地址。cchar ch A; char *p1 ch; // ✅ 正确必须用 char *p2 ch; // ❌ 错误ch 的值是 AASCII 65不是地址用法二指向字符串最常见C语言中没有专门的字符串类型字符串是以\0结尾的字符数组。字符指针可以指向这个数组的第一个字符。c// 方式A指向可修改的字符数组 char arr[10] abcdef; char *p1 arr; // arr 是数组名会退化为指针 *p1 w; // 修改第一个字符 printf(%s\n, p1); // 输出 wbcdef // 方式B指向字符串常量只读 const char *p2 abcdef; // *p2 w; // 错误不能修改常量区的内容 printf(%s\n, p2); // 输出 abcdef2.3 指针的移动指针变量本身的值它所存储的地址是可以改变的这让我们可以遍历字符串中的每一个字符。cconst char *p abc; printf(%s\n, p); // 输出 abc printf(%s\n, p1); // 输出 bc从 b 开始输出 printf(%s\n, p2); // 输出 c从 c 开始输出注意这里并没有修改字符串的内容只是让 p 指向了不同的位置。字符串 abc 本身仍然完好无损地躺在内存中。2.4 const 关键字的位置含义——一个常见的困惑点很多初学者对const放在*的左边还是右边感到困惑。记住这个规律cconst char *p abc; // const 在左边指向的内容是常量不能改内容 p def; // ✅ 可以改变指向 // *p x; // ❌ 错误 char * const p abc; // const 在右边指针本身是常量不能改变指向 // p def; // ❌ 错误 *p x; // ❌ 危险指向常量区 // 更常见且安全的写法 const char * const p abc; // 内容和指向都不能改2.5 字符指针 vs 字符数组——最本质的区别这是面试中经常被问到的问题。两者最本质的区别在于内存位置和可变性。特性char *str abcchar arr[] abc存储位置指针在栈/全局字符串在常量区整个数组在栈/全局是否可修改内容❌ 不可改未定义行为程序可能崩溃✅ 可改可重新指向其他字符串✅ 可以指针本身是变量❌ 不可以数组名是常量地址大小指针占8字节64位系统数组占4字节abc\0验证代码c#include stdio.h int main() { char str1[] hello bit.; char str2[] hello bit.; const char *str3 hello bit.; const char *str4 hello bit.; if (str1 str2) printf(str1 and str2 are same\n); else printf(str1 and str2 are not same\n); // 输出这个 if (str3 str4) printf(str3 and str4 are same\n); // 输出这个 else printf(str3 and str4 are not same\n); return 0; }为什么结果不同因为str1和str2是栈上两个不同的数组各自占用独立的内存空间地址自然不同。而str3和str4指向的是常量区的同一块内存编译器会优化相同的字符串常量只存一份。第三章深入理解 printf 的 %s——从地址到字符串3.1 %s 的工作原理printf的格式控制符决定了它怎么处理传入的参数格式符参数类型要求工作原理%schar*从传入的地址开始逐个字节读取直到遇到\0停止%pvoid*直接把地址值以十六进制形式打印出来%cchar值打印单个字符%dint值打印整数值cconst char *p hello; printf(%s\n, p); // 传入地址0x1000 → 去0x1000读内容 → 输出 hello printf(%c\n, *p); // 传入字符h值 → 直接输出 h printf(%p\n, p); // 传入地址0x1000 → 输出 0x10003.2 什么类型可以用 %s✅ 可以用 %s 的情况c// 1. 字符指针 char *p hello; printf(%s, p); // ✅ // 2. 字符数组名退化为指针 char arr[] hello; printf(%s, arr); // ✅ // 3. 指针数组的单个元素 char *arr[] {abc, def}; printf(%s, arr[0]); // ✅ 输出 abc // 4. 数组指针的解引用 char arr[10] hello; char (*ptr)[10] arr; printf(%s, *ptr); // ✅ *ptr 的类型是 char*❌ 不能用 %s 的情况c// 1. 指针数组名本身 char *arr[] {abc, def}; printf(%s, arr); // ❌ arr 退化后是 char**类型不匹配 // 2. 数组指针本身 char (*ptr)[10] arr; printf(%s, ptr); // ❌ ptr 的类型是 char(*)[10] // 3. 整型数组 int intArr[] {1,2,3}; printf(%s, intArr); // ❌ intArr 退化后是 int*第四章数组名与取地址——一个容易混淆的区别4.1 三种地址的区别对于char arr[10] abcdef我们有三种方式获取地址表达式含义类型值1 后的偏移arr数组首元素的地址char*0x1000x1011字节arr[0]首元素的地址同上char*0x1000x1011字节arr整个数组的地址char(*)[10]0x1000x10A10字节4.2 为什么要用 arr这是理解数组指针的关键问题。核心原因数组名在表达式中会自动退化为指向首元素的指针。如果你想要“指向整个数组的指针”就必须用来“阻止”退化并提升类型。cchar arr[10]; // 不用 类型是数组首元素的指针 char *p1 arr; // arr 退化 → char*指向第一个字符 // 用 类型是指向整个数组的指针 char (*p2)[10] arr; // arr → char(*)[10]指向整个数组类比理解把数组想象成一栋10层楼每层1个字符表达式含义类比arr指向一楼的指针“这栋楼的一楼在哪儿”arr指向整栋楼的指针“这栋楼整体在哪儿”两个指针的数值相同都是楼的位置但类型不同arr是“楼层指针”1 到二楼arr是“整楼指针”1 到下一栋楼4.3 代码验证c#include stdio.h int main() { char arr[10] abcdef; printf(arr %p\n, arr); // 0x100 printf(arr1 %p\n, arr1); // 0x101 printf(arr %p\n, arr); // 0x100 printf(arr1 %p\n, arr1); // 0x10A return 0; }4.4 普通变量 vs 数组取地址的区别重要澄清只有数组才有“退化”普通变量没有退化。情况代码原因普通变量char *p ch;变量不会退化必须用取地址一维数组退化char *p arr;数组名退化为指针不用一维数组取整个数组char (*p)[10] arr;要用类型才是char(*)[10]cchar ch A; char *p1 ch; // ✅ 普通变量必须用 char *p2 ch; // ❌ 错误ch 的值是 A不是地址 char arr[10]; char *p3 arr; // ✅ 数组名退化不需要 char (*p4)[10] arr; // ✅ 取整个数组的地址需要 第五章指针数组——存放指针的数组5.1 什么是指针数组指针数组本质上是一个数组只不过数组里存放的不是整数或字符而是指针地址。cint *p[5]; // p 是一个数组有5个元素每个元素是 int* 类型 char *arr[3]; // arr 是一个数组有3个元素每个元素是 char* 类型5.2 指针数组的核心作用指针数组最大的优势是让多个长度不同的字符串可以像普通数组一样被统一管理。cchar *fruits[] {apple, banana, cherry, durian}; // 这四个字符串长度分别为5、6、6、61的\0 // 但每个指针只占8字节总共32字节 // 如果使用二维数组 char[4][10]需要40字节浪费空间5.3 内存布局详解cchar *arr[3] {hello, world, nice};text内存布局 指针数组 arr位于栈或全局数据区 ┌─────────────────────────────────────────┐ │ 地址: 2000 2008 2016 │ │ 内容: 3000 3100 3200 │ │ 含义: 指向hello 指向world 指向nice │ └─────────────────────────────────────────┘ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 实际字符串位于常量区位置不固定 ┌──────┐ │ 3000 │ h e l l o \0 ├──────┤ │ 3100 │ w o r l d \0 ├──────┤ │ 3200 │ n i c e \0 └──────┘关键特点指针数组本身在内存中连续存储2000, 2008, 2016字符串分散在内存的其他位置3000, 3100, 3200可能相隔很远每个字符串只占用实际长度1的空间没有浪费5.4 指针数组 vs 二维数组特点指针数组char *a[]二维数组char a[3][10]内存布局指针连续 字符串分散所有字符连续存储字符串长度可以不同每行固定长度内存利用率高无浪费低预分配固定长度修改字符串可以指向新字符串或修改内容只能修改内容常用场景字符串列表、命令行参数固定格式数据如棋盘第六章数组指针——指向数组的指针6.1 什么是数组指针数组指针本质上是一个指针只不过它指向的是一个数组整体而不是单个元素。cint (*p)[5]; // p 是一个指针指向一个包含5个int元素的数组 char (*ptr)[10]; // ptr 是一个指针指向一个包含10个char元素的数组6.2 为什么需要数组指针数组指针的核心价值在于能够以“整个数组”为单位进行移动或操作。cint arr[3][4] {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; int (*p)[4] arr; // p 指向第一行 // p 是一个指针指向一个长度为4的int数组 // p1 会跳过一整行16字节而不是一个元素4字节6.3 数组指针的三种核心用途用途1遍历二维数组的行c#include stdio.h int main() { int arr[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int (*p)[4] arr; // p 指向第一行 for (int i 0; i 3; i) { for (int j 0; j 4; j) { printf(%d , p[i][j]); // 方式1 } printf(\n); } return 0; }用途2函数参数传递二维数组保留列信息cvoid printMatrix(int (*arr)[5], int rows) { for (int i 0; i rows; i) { for (int j 0; j 5; j) { printf(%d , arr[i][j]); } printf(\n); } }用途3动态分配二维数组cint (*matrix)[5] malloc(10 * sizeof(*matrix)); // 10行每行5个int6.4 指针运算演示c#include stdio.h int main() { int arr[10] {0}; // 普通指针 int *p1 arr; printf(p1 %p\n, p1); // 0x100 printf(p11 %p\n, p11); // 0x104移动4字节 // 数组指针 int (*p2)[10] arr; printf(p2 %p\n, p2); // 0x100 printf(p21 %p\n, p21); // 0x128移动40字节 10*4 return 0; }第七章深入理解“退化”——C语言最隐蔽的特性7.1 什么是退化退化Decay是C语言中一个非常重要的概念数组名在大多数情况下会自动转换为指向其首元素的指针。7.2 什么时候不退化只有两种情况下数组名不会退化cint arr[10]; // 情况1作为 sizeof 的操作数 size_t s sizeof(arr); // s 4010 * 4不是 8指针大小 // 情况2作为 的操作数 int (*p)[10] arr; // arr 的类型是 int(*)[10]不是 int*7.3 什么时候会退化几乎所有其他情况cint arr[10]; int *p arr; // ✅ arr 退化 arr[0]; // ✅ arr 退化等价于 *(arr0) func(arr); // ✅ 传给函数时退化 arr 1; // ✅ 运算时退化7.4 不同维度数组的退化规律数组定义完整类型退化后的类型匹配的指针类型int a[5]int[5]int*int(*)[5]int a[3][4]int[3][4]int(*)[4]int(*)[4]int a[3][4][5]int[3][4][5]int(*)[4][5]int(*)[4][5]7.5 为什么二维数组名可以直接赋值给数组指针cint arr[3][5]; int (*ptr)[5] arr; // ✅ 可以不需要 原因类型匹配arr的类型是int[3][5]在表达式中arr退化为int(*)[5]指向有5个int的数组的指针ptr的类型正好是int(*)[5]对比一维数组cint a[5]; int (*ptr)[5] a; // ✅ 必须用 // int (*ptr)[5] a; // ❌ a 退化后是 int*类型不匹配第八章指针数组与数组指针的终极对比8.1 语法对比c// 指针数组先看到 []后看到 * int *p[5]; // p 是一个数组包含5个 int* 类型的元素 // 数组指针先看到 *后看到 [] int (*p)[5]; // p 是一个指针指向 int[5] 类型的数组8.2 大小对比cprintf(sizeof(p1) %lu\n, sizeof(p1)); // 40 (5 * 8) printf(sizeof(p2) %lu\n, sizeof(p2)); // 8 (指针大小)8.3 用途对比场景使用指针数组使用数组指针存储多个字符串✅ 最佳选择❌ 不合适函数传递二维数组❌ 不合适✅ 最佳选择字符串排序✅ 高效❌ 不合适动态分配二维数组❌ 需要多次分配✅ 一次分配第九章常见错误与调试技巧9.1 常见错误清单错误1混淆普通变量和数组的取地址cchar ch A; char *p1 ch; // ❌ 错误普通变量不会退化 char *p2 ch; // ✅ 正确 char arr[10]; char *p3 arr; // ✅ 数组名退化正确 char (*p4)[10] arr; // ❌ 类型不匹配 char (*p5)[10] arr; // ✅ 正确错误2修改字符串常量cchar *p hello; p[0] H; // ❌ 危险错误3返回局部数组的地址cchar* getString() { char arr[] hello; return arr; // ❌ 返回后 arr 被销毁 }9.2 调试技巧技巧1使用 %p 打印地址cprintf(arr %p\n, arr); printf(arr1 %p\n, arr1); printf(arr %p\n, arr);技巧2使用 sizeof 区分数组和指针cint arr[10]; int *p arr; printf(%lu, %lu\n, sizeof(arr), sizeof(p)); // 40, 8第十章记忆口诀与总结10.1 核心记忆口诀text指针数组存指针数组指针指数组。 左看右看定类型括号位置分清楚。 普通变量取地址必须加上 符号。 数组名会自己退退成指针不需要。 要想取到整个数组 符号不能少。 一维数组要加 二维数组退化了。 数组名用 sizeof 和 是本体 其他场合都退化莫迟疑。 const 在左内容不变 const 在右指向不变。 %s 要地址%c 要字符 传对类型才不会出错。10.2 终极对比表概念语法本质大小取地址方式普通变量char ch变量1字节必须用ch字符指针char *p指针8字节本身已是地址字符数组char arr[10]数组10字节arr退化arr取整体指针数组int *p[5]数组40字节数组名退化数组指针int (*p)[5]指针8字节指向数组10.3 最后的话指针是C语言的精髓也是通向高级C编程的必经之路。理解指针不是一蹴而就的需要在实践中反复练习、不断思考。记住这几个最关键的要点普通变量不会退化必须用取地址数组名会退化变成指向首元素的指针要取整个数组的地址一维数组必须用二维及以上可以直接用数组名%s要求char*传对类型才不会出错建议你多画内存图遇到指针问题先在纸上画出内存布局多用 printf 打印地址验证自己的理解是否正确多写测试代码把本文的每个例子都亲手跑一遍希望这篇指南能够帮助你彻底掌握C指针。如果你有任何疑问欢迎继续探讨