

🔥个人主页:代码不加冰(欢迎来访) 🎬作者简介:java后端学习者 ❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb, ✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,还有两天就要开始考C语言了,其实没看多少,这里总结一下指针的知识点吧,我感觉对于初学者,最不容易搞懂的就是指针了,让我们一起来看看吧。
摘要:
本文是一篇关于C语言指针知识点的总结文章,主要面向初学者和即将参加C语言考试的学生。文章通过生动的比喻(如将内存比作快递柜)和清晰的示例代码,详细讲解了指针的核心概念、语法规则以及常见应用场景。内容包括:指针的基本概念(地址、解引用)、指针与数组的关系、字符串指针与字符数组的区别、二级指针的作用、函数指针的使用、动态内存分配注意事项,以及二维数组指针的解析。文章特别强调了期末易错点,如指针类型转换、const修饰符、指针数组与数组指针的区别等,并提供了实用的记忆口诀和思维导图。最后通过典型例题解析帮助读者巩固知识,旨在帮助读者攻克指针难关,顺利通过考试。
一、 关于指针
很多人学指针卡壳,是因为把“地址”想得太抽象。我们可以把内存想象成一整条街的快递柜:
-
每个柜子(内存单元)都有一个唯一的编号(地址)
-
指针变量就是一张写了“柜子编号”的纸条。拿着这张纸条,你就能找到对应的柜子,取出里面的东西(数据)
核心语法:
c
int a = 10; // 普通柜子,里面放了数字10
int *p = &a; // 纸条p,上面写着柜子a的编号
printf("%d", *p); // *p 是“顺着纸条去找那个柜子”,输出10
画重点:
-
&是“取地址”运算符,作用是获取门牌号。
-
*是“解引用”运算符(间接访问),作用是根据门牌号进屋拿东西。
-
在定义变量时(如 int *p),这里的*只是告诉编译器:这是一个指针变量,用来存放地址。
重点!
当 * 出现在变量定义/声明中时,它表示这个变量是指针类型。
int *p; // p 是一个指向 int 的指针
char *q; // q 是一个指向 char 的指针
double **pp; // pp 是一个指向指针的指针(二级指针)
关键理解:这里的 * 不是"操作",而是"类型标记"!
int *p, q; // p 是 int*(指针),q 是 int(普通变量)!不是两个指针!
如果你想定义两个指针,必须每个都加 *:
int *p, *q; // p 和 q 都是 int*
二、 指针的身份
指针是有类型的(int *,char *,double *)。类型决定了两个关键属性:
解引用时访问几个字节:int *访问4个字节,char *访问1个字节。
指针加减整数时的步长:p+1跳过sizeof(类型)个字节。
c
int a = 0x12345678;
int *p = &a;
char *q = (char *)&a;
printf("%x", *p); // 输出 12345678(读4个字节)
printf("%x", *q); // 输出 78(小端存储,只读1个字节)
printf("%d", p+1 – p); // 输出 1,但地址值相差4
期末考点:p+1到底加几,答案是加 sizeof(指针指向的类型)。
三、 数组与指针的关系
数组名在绝大多数情况下(注意是绝大多数)会被隐式转换为指向数组首元素的指针。
c
int arr[5] = {1,2,3,4,5};
int *p = arr; // 等价于 int *p = &arr[0];
| arr | 指向首元素a[0]的指针 | int * |
| &arr[0] | 指向首元素a[0]的指针 | int * |
| &arr | 指向整个数组的指针 | int (*)[5] |
易错点:&arr + 1 跳过了整个数组(5个int),而 arr + 1 只跳过了1个int。
c
int arr[5] = {1,2,3,4,5};
int *p = (int *)(&arr + 1);
printf("%d", *(p – 1)); // 输出 5
// 解析:&arr+1 指向数组尾部的下一个位置,再减1就指向了最后一个元素。
四、 字符串指针与字符数组
内存模型对比:
arr (字符数组):
栈上 (或静态区) 开辟了6个字节的空间,把"hello\\0"复制进来
┌────┬────┬────┬────┬────┬────┐
│ 'h'│ 'e'│ 'l'│ 'l'│ 'o'│'\\0'│ ← 这块内存属于 arr,是变量
└────┴────┴────┴────┴────┴────┘
地址:1000
ptr (字符串指针):
栈上只开辟了1个指针的空间 (4/8字节),指向常量区的字符串
┌──────┐
│ ptr │──────► 常量区: "hello\\0" (只读)
└──────┘
地址:2000 地址:3000
核心区别:
arr 是数组,它拥有自己的内存空间,存储的是字符串的副本。
ptr 是指针,它只存了一个地址,指向常量区的字符串原文。
-
arr[] 是在栈上开辟的空间,内容可以随意读写。
-
ptr 指向常量区(也叫只读数据段),操作系统禁止修改这块内存。强行修改会导致程序崩溃。
这是期末挂科率最高的地方,一定要分清指向常量区的指针和栈上的数组。
给未初始化的指针赋值
c
char *ptr;
strcpy(ptr, "hello"); // ❌ 危险!ptr是野指针,不知道指向哪里
// 改正:分配空间
char *ptr = (char*)malloc(6);
strcpy(ptr, "hello"); // ✅ 安全
混淆 = 和 strcpy
c
char arr[10];
arr = "hello"; // ❌ 编译错误!数组名不能赋值
char *ptr;
ptr = "hello"; // ✅ 正确:让ptr指向常量区
char arr[10];
strcpy(arr, "hello"); // ✅ 正确:复制内容到数组
高频题:
c
char *getString() {
char str[] = "Hello"; // 局部数组,在栈上
return str; // 返回了局部变量的地址
} // 函数结束,str被销毁,这块内存变成“垃圾”
// 调用者拿到的是一个野指针!
改正方案:使用static修饰,或者用malloc动态分配内存。
五、 二级指针
一级指针改值,二级指针改一级指针的指向。
二级指针就是一个普通的变量,它存储的是另一个指针变量的地址。
换句话说:
-
一级指针:存的是"普通变量"的地址
-
二级指针:存的是"一级指针变量"的地址
经典场景:在函数内部修改外部指针的指向。
变量名 地址 存储的内容
┌─────┐
│ a │ 1000 │ 10 ← 普通变量
└─────┘
┌─────┐
│ p │ 2000 │ 1000 ← 一级指针:存a的地址
└─────┘
┌─────┐
│ pp │ 3000 │ 2000 ← 二级指针:存p的地址
└─────┘
c
void swap(int **pp, int **qq) {
int *tmp = *pp;
*pp = *qq;
*qq = tmp;
}
int main() {
int a = 1, b = 2;
int *p = &a, *q = &b;
swap(&p, &q); // 交换的是p和q指针本身指向的变量
// 现在 p 指向 b,q 指向 a
return 0;
}
六、 函数指针
函数名就是函数的入口地址。函数指针常用于回调函数(比如qsort排序)。
c
// 定义一个函数指针类型,指向 参数为(int,int),返回值为int 的函数
int (*pFunc)(int, int) = NULL;
// 指向加法函数
int add(int a, int b) { return a + b; }
pFunc = add;
printf("%d", pFunc(3, 5)); // 输出 8
// 变态写法(了解即可):void (*signal(int, void(*)(int)))(int);
// 这是signal函数的声明,它接受一个int和一个函数指针,返回一个函数指针。
七、 动态内存分配
| 静态分配 | int a[100]; | 编译时确定大小 | 数组大小固定,浪费或不够用 |
| 自动分配(栈) | int arr[n];(VLA) | 函数执行时分配 | 函数结束自动释放,大小仍受限 |
| 动态分配(堆) | malloc(100 * sizeof(int)) | 运行时决定大小 | 需要手动管理,灵活但容易出错 |
使用malloc/calloc在堆上申请内存,用完必须free。
c
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) { // 必须检查是否申请成功
exit(1);
}
// 使用 p…
free(p); // 释放内存
p = NULL; // 必须置空,防止野指针
三大致命伤:
内存泄漏:忘了free,程序越跑越卡。
野指针:free之后没有置NULL,继续使用。
悬挂指针:返回了局部变量的地址(前面字符串部分已讲)。
八、 期末易错点大合集
1. sizeof 和 strlen 的区别
c
char arr[] = "abc";
char *p = "abc";
printf("%d", sizeof(arr)); // 4 (包含'\\0')
printf("%d", strlen(arr)); // 3 (不包含'\\0')
printf("%d", sizeof(p)); // 4/8 (指针本身的大小,取决于系统是32位还是64位)
printf("%d", strlen(p)); // 3
2. 指针与const的三种方式
c
const int *p; // 指向的内容不能变(p可以变),即 *p 不可改
int const *p; // 同上(写法不同)
int * const p; // 指针本身不能变(p不能变),即 p 不可改
const int * const p; // 都不能变
口诀:const在*左边修饰指向的内容,在*右边修饰指针变量本身。
3. 指针数组 vs 数组指针
-
指针数组:int *p[5]; —— [ ] 优先级高,p先和[ ]结合,所以p是数组,数组里存的是int*指针。
-
数组指针:int (*p)[5]; —— ( ) 优先级高,p先和*结合,所以p是指针,指向一个长度为5的int数组。
补充:二维数组指针:
二级指针(int **p)和二维数组的行指针(int (*p)[N])是完全不同的两码事,它们在内存布局上有着本质的区别,绝对不能混用。
| 二维数组 a[i][j] | 计算偏移量(编译期已知步长) | 真正的解引用(取数据) | 编译器帮你算好地址,只解引用一次 |
| 二级指针 p[i][j] | 第一次解引用(取出一级指针) | 第二次解引用(取出最终数据) | 两次间接访问,跳转两次内存 |
通俗版理解:
-
二维数组:你手里有张地图,告诉你从起点走 i 行 j 列,一次性找到那个格子。
-
二级指针:你先去信箱 A 取出一张新纸条(第一次解引用),再拿着新纸条去信箱 B 取东西(第二次解引用)。
首先,我们要明白:C语言的内存是线性的,没有真正的“二维”。
c
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
这个数组在内存中,就是连续排列的12个int: 1 2 3 4 5 6 7 8 9 10 11 12
所谓的“二维”,只是编译器帮我们做的逻辑划分:3行,每行4个。
核心:a 和 &a[0]
这是所有混乱的根源,我们必须分清楚两个概念:
| a | 二维数组的数组名,代表首行的地址(第0行的地址) | int (*)[4](行指针,指向含4个int的一整行) |
| &a[0] | 取第0行的地址 | int (*)[4](和上面的类型完全一样) |
| a[0] | 第0行的数组名,代表该行首元素的地址 | int *(列指针,指向一个int) |
| &a[0][0] | 取第0行第0列元素的地址 | int *(列指针) |
最重要的记忆点:
a 是“行指针”,它指向的是“一整行”。 a[0] 是“列指针”,它指向的是“一个具体的int元素”。
虽然 a 和 a[0] 打印出来的地址数值是相同的,但它们的类型完全不同,导致 +1 跳过的距离不同!
我们来写代码验证,这是期末笔试最常考的点:
c
#include <stdio.h>
int main() {
int a[3][4] = {0};
printf("a = %p, a+1 = %p, 跨度 = %d 字节\\n", a, a+1, (char*)(a+1) – (char*)a);
printf("a[0] = %p, a[0]+1= %p, 跨度 = %d 字节\\n", a[0], a[0]+1, (char*)(a[0]+1) – (char*)a[0]);
return 0;
}
运行结果(假设地址从1000开始):
text
a = 1000, a+1 = 1016, 跨度 = 16 字节 // 跳过了4个int(一行)
a[0] = 1000, a[0]+1= 1004, 跨度 = 4 字节 // 跳过了1个int(一个元素)
结论:
-
a + 1 跳到了第1行(跳了16个字节)
-
a[0] + 1 跳到了第0行第1列(跳了4个字节)
假设我们要访问 a[i][j](第 i 行第 j 列),有以下等价写法:
方式1:最简单直观(最推荐)
c
a[i][j]
方式2:把行指针拆开(理解本质)
c
*(a[i] + j)
// 先取第i行的数组名 a[i](即该行首元素地址),再偏移j个元素,最后解引用
方式3:完全用指针运算
c
*(*(a + i) + j)
// 1. a + i:从第0行地址移动到第i行地址(行指针)
// 2. *(a + i):对行地址解引用,得到第i行的首元素地址(降级为列指针)
// 3. *(a + i) + j:在该行内偏移j个元素
// 4. 最外层 *:取出该元素的值
重点理解 *(a+i) 发生了什么: a 是 int (*)[4] 类型,a+i 还是 int (*)[4] 类型。 对它解引用 *(a+i),就变成了 int * 类型(行指针 → 列指针的降级)。
二维数组做函数参数(期末必考)
如果你想把二维数组传给函数,有且只有三种正确写法:
写法1:用 int a[][4](行数省略,列数必须写)
c
void printArray(int arr[][4], int rows) {
for(int i=0; i<rows; i++) {
for(int j=0; j<4; j++) {
printf("%d ", arr[i][j]);
}
printf("\\n");
}
}
// 调用:printArray(a, 3);
写法2:用行指针 int (*p)[4]
c
void printArray(int (*p)[4], int rows) {
// 用法一模一样
for(int i=0; i<rows; i++) {
for(int j=0; j<4; j++) {
printf("%d ", p[i][j]); // 甚至可以 p[i][j]
}
}
}
错误示范(初学者常犯):
c
void printArray(int **arr) { … } // ❌ 这是二级指针,不能接收二维数组!
printArray(a); // 编译警告/错误
因为 int ** 和 int (*)[4] 是完全不同的类型,内存布局不一样
指针数组 vs 数组指针(复习巩固)
既然讲到二维数组,这两个概念必须再提一次,因为它们经常一起考:
-
指针数组:int *p[3];
-
它是一个数组,里面有3个元素,每个元素都是 int* 指针。
-
常用来存放多个字符串:char *str[3] = {"hello", "world", "c"};
-
-
数组指针(行指针):int (*p)[4];
-
它是一个指针,指向一个有4个int元素的数组。
-
正好可以用来指向二维数组的每一行:p = a;(让p指向a的第0行)
-
记忆口诀:[] 优先级高于 *,所以 int *p[3] 先结合 [] 是数组;加括号 (*p) 就先结合 * 是指针。
综合实战:用行指针遍历二维数组
c
#include <stdio.h>
int main() {
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 定义一个行指针,指向含4个int的数组
int (*row)[4] = a; // row 指向第0行
// 用行指针遍历
for(int i=0; i<3; i++) {
for(int j=0; j<4; j++) {
// 三种等价写法:
// printf("%d ", row[i][j]);
// printf("%d ", *(*(row+i)+j));
printf("%d ", *(row[i] + j)); // 最直观的指针写法
}
printf("\\n");
}
// 如果想让 row 指向第2行?
row = &a[2]; // 或者 row = a + 2;
printf("第2行第1个元素:%d\\n", (*row)[0]); // 输出 9
// 注意:row 是指向一整行的指针,所以 *row 才得到该行的首元素地址
return 0;
}
总结:
如果你现在还是很晕,请记住下面这句话,反复念三遍:
二维数组名 a 是“行地址”,相当于 int (*)[N]。 每一行的数组名 a[i] 是“列地址”,相当于 int *。 对“行地址”解引用 *,就得到“列地址”。
思维导图:
text
a (行地址, 类型 int (*)[4])
│
├─ a + 0 → 指向第0行
│ │
│ ├─ *(a+0) 即 a[0] (列地址, 类型 int *)
│ │ ├─ a[0] + 0 → &a[0][0]
│ │ ├─ a[0] + 1 → &a[0][1]
│ │ └─ *(a[0]+1) → a[0][1]
│
├─ a + 1 → 指向第1行
│ └─ *(a+1) 即 a[1] (列地址)
│
└─ a + 2 → 指向第2行
└─ *(a+2) 即 a[2] (列地址)
九、一个例子搞懂:
题目:下面程序运行后输出什么
c
int a[5] = {1, 2, 3, 4, 5};
int *p = (int *)(&a + 1);
printf("%d, %d", *(a + 1), *(p – 1));
答案:2, 5
解析:
*(a+1):a是首元素地址,+1指向第二个元素,解引用得2。
&a+1:取整个数组的地址,+1跳过整个数组(5个int),指向数组末尾。
强制转为int *赋给p,此时p指向数组末尾。
*(p-1):指针向前移动一个int,指向最后一个元素,得5。
总结:
不要把指针想得太玄乎。它本身就是一个存着整数的变量,只不过这个整数比较特殊——它是内存的编号。只要记住:
-
传参想改变外部变量,必须传地址(一级指针传值,二级指针传址)。
-
返回值不要返回局部变量的地址。
-
动态内存申请了就要释放,释放了就置空。
-
数组名当指针用,但别混淆&arr和arr。
祝大家期末考神附体,指针题目全部拿下!如果有疑问,欢迎评论区留言讨论。


