C语言指针知识点
前言
指针是C语言的灵魂,也是无数初学者心中难以逾越的高山。有人说“理解了指针,就理解了C语言的一半”,这话一点都不夸张。本文将从最基础的内存概念开始,循序渐进地讲解字符指针、指针数组、数组指针以及它们之间错综复杂的关系,配合大量的代码示例和内存图示,帮助你真正掌握C指针的核心知识。
第一章:内存与地址——指针的物理基础
1.1 内存就是一个个带编号的小格子
想象一下,计算机的内存就像一栋巨大的公寓楼,每个房间都有唯一的门牌号。这些“房间”就是内存单元,而“门牌号”就是地址。
text
内存示意图(每个格子1字节): 地址: 1000 1001 1002 1003 1004 1005 ... 内容: ??? ??? ??? ??? ??? ??? ...
当我们定义一个变量时,编译器会为这个变量分配一个或多个这样的“房间”。指针变量特殊的地方在于:它里面存放的不是普通数据,而是另一个房间的门牌号(地址)。
1.2 为什么需要指针?
没有指针时,函数只能通过返回值来传递数据,而且只能传递一份。有了指针,我们可以:
在函数内部修改外部的变量
动态申请内存(malloc)
构建链表、树等复杂数据结构
高效地操作数组和字符串
实现回调函数
可以说,没有指针,C语言能做的工作会大打折扣。
第二章:字符指针——最简单的指针
2.1 字符指针的定义
字符指针就是指向字符类型数据的指针变量,用char*表示。它是最基础的指针类型之一,也是理解其他复杂指针的基石。
c
char ch = 'A'; char *p = &ch; // p 指向 ch,p 里存放的是 ch 的地址
2.2 字符指针的两种核心用法
用法一:指向单个字符
c
char ch = 'w'; char *pc = &ch; // pc 是指针变量,指向 ch *pc = 'W'; // 通过指针修改 ch 的值 printf("%c\n", ch); // 输出 W关键点:普通变量不会自动变成指针,必须用&取地址。
c
char ch = 'A'; char *p1 = &ch; // ✅ 正确,必须用 & char *p2 = ch; // ❌ 错误!ch 的值是 'A'(ASCII 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 指针的移动
指针变量本身的值(它所存储的地址)是可以改变的,这让我们可以遍历字符串中的每一个字符。
c
const char *p = "abc"; printf("%s\n", p); // 输出 abc printf("%s\n", p+1); // 输出 bc(从 b 开始输出) printf("%s\n", p+2); // 输出 c(从 c 开始输出)注意:这里并没有修改字符串的内容,只是让 p 指向了不同的位置。字符串 "abc" 本身仍然完好无损地躺在内存中。
2.4 const 关键字的位置含义——一个常见的困惑点
很多初学者对const放在*的左边还是右边感到困惑。记住这个规律:
c
const 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 = "abc" | char 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的格式控制符决定了它怎么处理传入的参数:
| 格式符 | 参数类型要求 | 工作原理 |
|---|---|---|
%s | char* | 从传入的地址开始,逐个字节读取,直到遇到\0停止 |
%p | void* | 直接把地址值以十六进制形式打印出来 |
%c | char(值) | 打印单个字符 |
%d | int(值) | 打印整数值 |
c
const 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* | 0x100 | 0x101(1字节) |
&arr[0] | 首元素的地址(同上) | char* | 0x100 | 0x101(1字节) |
&arr | 整个数组的地址 | char(*)[10] | 0x100 | 0x10A(10字节) |
4.2 为什么要用 &arr?
这是理解数组指针的关键问题。
核心原因:数组名在表达式中会自动退化为指向首元素的指针。如果你想要“指向整个数组的指针”,就必须用&来“阻止”退化,并提升类型。
c
char 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("arr+1 = %p\n", arr+1); // 0x101 printf("&arr = %p\n", &arr); // 0x100 printf("&arr+1 = %p\n", &arr+1); // 0x10A return 0; }4.4 普通变量 vs 数组:取地址的区别
重要澄清:只有数组才有“退化”,普通变量没有退化。
| 情况 | 代码 | 原因 |
|---|---|---|
| 普通变量 | char *p = &ch; | 变量不会退化,必须用&取地址 |
| 一维数组(退化) | char *p = arr; | 数组名退化为指针,不用& |
| 一维数组(取整个数组) | char (*p)[10] = &arr; | 要用&,类型才是char(*)[10] |
c
char ch = 'A'; char *p1 = &ch; // ✅ 普通变量,必须用 & char *p2 = ch; // ❌ 错误!ch 的值是 'A',不是地址 char arr[10]; char *p3 = arr; // ✅ 数组名退化,不需要 & char (*p4)[10] = &arr; // ✅ 取整个数组的地址,需要 &
第五章:指针数组——存放指针的数组
5.1 什么是指针数组
指针数组本质上是一个数组,只不过数组里存放的不是整数或字符,而是指针(地址)。
c
int *p[5]; // p 是一个数组,有5个元素,每个元素是 int* 类型 char *arr[3]; // arr 是一个数组,有3个元素,每个元素是 char* 类型
5.2 指针数组的核心作用
指针数组最大的优势是:让多个长度不同的字符串可以像普通数组一样被统一管理。
c
char *fruits[] = {"apple", "banana", "cherry", "durian"}; // 这四个字符串长度分别为5、6、6、6(+1的\0) // 但每个指针只占8字节,总共32字节 // 如果使用二维数组 char[4][10],需要40字节,浪费空间5.3 内存布局详解
c
char *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 什么是数组指针
数组指针本质上是一个指针,只不过它指向的是一个数组整体,而不是单个元素。
c
int (*p)[5]; // p 是一个指针,指向一个包含5个int元素的数组 char (*ptr)[10]; // ptr 是一个指针,指向一个包含10个char元素的数组
6.2 为什么需要数组指针?
数组指针的核心价值在于:能够以“整个数组”为单位进行移动或操作。
c
int arr[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}}; int (*p)[4] = arr; // p 指向第一行 // p 是一个指针,指向一个长度为4的int数组 // p+1 会跳过一整行(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:函数参数传递二维数组(保留列信息)
c
void 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:动态分配二维数组
c
int (*matrix)[5] = malloc(10 * sizeof(*matrix)); // 10行,每行5个int
6.4 指针运算演示
c
#include <stdio.h> int main() { int arr[10] = {0}; // 普通指针 int *p1 = arr; printf("p1 = %p\n", p1); // 0x100 printf("p1+1 = %p\n", p1+1); // 0x104(移动4字节) // 数组指针 int (*p2)[10] = &arr; printf("p2 = %p\n", p2); // 0x100 printf("p2+1 = %p\n", p2+1); // 0x128(移动40字节 = 10*4) return 0; }第七章:深入理解“退化”——C语言最隐蔽的特性
7.1 什么是退化?
退化(Decay)是C语言中一个非常重要的概念:数组名在大多数情况下会自动转换为指向其首元素的指针。
7.2 什么时候不退化?
只有两种情况下数组名不会退化:
c
int arr[10]; // 情况1:作为 sizeof 的操作数 size_t s = sizeof(arr); // s = 40(10 * 4),不是 8(指针大小) // 情况2:作为 & 的操作数 int (*p)[10] = &arr; // &arr 的类型是 int(*)[10],不是 int*
7.3 什么时候会退化?
几乎所有其他情况:
c
int arr[10]; int *p = arr; // ✅ arr 退化 arr[0]; // ✅ arr 退化(等价于 *(arr+0)) 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 为什么二维数组名可以直接赋值给数组指针?
c
int arr[3][5]; int (*ptr)[5] = arr; // ✅ 可以,不需要 &
原因:类型匹配!
arr的类型是int[3][5]在表达式中,
arr退化为int(*)[5](指向有5个int的数组的指针)ptr的类型正好是int(*)[5]
对比一维数组:
c
int 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 大小对比
c
printf("sizeof(p1) = %lu\n", sizeof(p1)); // 40 (5 * 8) printf("sizeof(p2) = %lu\n", sizeof(p2)); // 8 (指针大小)8.3 用途对比
| 场景 | 使用指针数组 | 使用数组指针 |
|---|---|---|
| 存储多个字符串 | ✅ 最佳选择 | ❌ 不合适 |
| 函数传递二维数组 | ❌ 不合适 | ✅ 最佳选择 |
| 字符串排序 | ✅ 高效 | ❌ 不合适 |
| 动态分配二维数组 | ❌ 需要多次分配 | ✅ 一次分配 |
第九章:常见错误与调试技巧
9.1 常见错误清单
错误1:混淆普通变量和数组的取地址
c
char ch = 'A'; char *p1 = ch; // ❌ 错误!普通变量不会退化 char *p2 = &ch; // ✅ 正确 char arr[10]; char *p3 = arr; // ✅ 数组名退化,正确 char (*p4)[10] = arr; // ❌ 类型不匹配 char (*p5)[10] = &arr; // ✅ 正确
错误2:修改字符串常量
c
char *p = "hello"; p[0] = 'H'; // ❌ 危险!
错误3:返回局部数组的地址
c
char* getString() { char arr[] = "hello"; return arr; // ❌ 返回后 arr 被销毁 }9.2 调试技巧
技巧1:使用 %p 打印地址
c
printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr);技巧2:使用 sizeof 区分数组和指针
c
int 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指针。如果你有任何疑问,欢迎继续探讨!