🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》
《Linux系统学习》
《算法日记》
⛺️技术的杠杆,撬动整个世界!

指针是C语言的灵魂,也是许多初学者的噩梦。它赋予了程序员直接操作内存的强大能力,但同时也带来了内存泄漏和野指针的风险。本文系统地梳理了指针的核心概念、运算规则以及与数组的纠葛。
第一部分:指针的基础(内存与类型)
1. 内存与地址
-
内存单元 :内存被划分为一个个内存单元,每个单元的大小是 1字节(Byte)。
-
编址 :为了有效访问内存,每个单元都有一个编号,这个编号就是地址 ,在C语言中也被称为指针。
-
地址空间:
- 32位机器 :有32根地址线,能寻址 2 32 2^{32} 232 个字节(4GB),地址长度为 4字节。
- 64位机器 :有64根地址线,地址长度为 8字节。
2. 指针变量
指针变量是用来存放地址的变量。
c
int a = 10;
int *p = &a; // p是整型指针,存放a的地址
3. 指针类型的意义
既然所有指针在同一平台下的大小是固定的(4或8字节),为什么还需要区分 int*、char* 等类型?主要有两个原因:
-
解引用的权限(访问步长):
int*解引用时访问 4 个字节。char*解引用时访问 1 个字节。- 这决定了你通过指针能操作多大的内存区域。
-
指针运算的步长:
int* p; p+1会跳过 4 个字节。char* p; p+1会跳过 1 个字节。type* p; p+n跳过 n × s i z e o f ( t y p e ) n \times sizeof(type) n×sizeof(type) 个字节。
4. 特殊指针:void*
void* 是无具体类型的指针,可以接收任意类型的地址。
- 限制 :不能直接进行解引用操作(
*p)和指针运算(p++),因为不知道步长。 - 用途 :常用于函数参数(如
memcpy),实现泛型编程。
第二部分:指针运算与关键字
1. const 与指针的"恩怨纠葛"
const 修饰指针时,位置不同,含义截然不同:
| 代码定义 | 记忆口诀 | 含义 |
|---|---|---|
const int *p; |
const在*左边 |
锁内容 :指针指向的内容不能改(*p不可变),指针指向可以改。 |
int const *p; |
const在*左边 |
同上。 |
int * const p; |
const在*右边 |
锁指向 :指针的指向不能改(p不可变),指针指向的内容可以改。 |
const int * const p; |
两边都有 | 全锁:指向和内容都不能改。 |
2. 指针运算
-
指针 +/- 整数:根据类型步长向前或向后移动。
-
指针 - 指针:
- 前提:两个指针必须指向同一块连续空间(如同一个数组)。
- 结果:得到两个指针之间的元素个数(不是字节数)。
- 应用:可以用来模拟实现
strlen函数。
-
指针的关系运算:指针可以比较大小(地址的高低)。
3. 野指针
野指针是指向位置不可知(随机、不正确、受限)的指针。
-
成因:
- 指针未初始化(默认为随机值)。
- 指针越界访问(数组越界)。
- 指针指向的空间已释放(释放后未置空)。
-
规避:
- 初始化时明确赋值,暂不确定指向则赋值为
NULL。 - 使用
assert断言来防错(#include <assert.h>)。 - 释放内存后立即将指针置为
NULL。
- 初始化时明确赋值,暂不确定指向则赋值为
第三部分:指针与数组的深度关系
1. 数组名的本质
通常情况下,数组名就是数组首元素的地址 。
即:arr 等同于 &arr[0]。
唯二的特例:
sizeof(数组名):计算的是整个数组的大小(单位字节)。&数组名:取出的是整个数组的地址。
易错点辨析:
虽然
arr和&arr打印出来的地址值是一样的,但它们的含义不同:
arr + 1:跳过一个元素(4字节)。&arr + 1:跳过整个数组(40字节)。
2. 数组传参
当数组作为参数传递给函数时,数组名会退化为指针(指向首元素)。
- 后果 :在函数内部无法通过
sizeof(arr)计算数组大小(结果永远是指针的大小4/8字节)。 - 对策:必须在传参时同时传递数组的元素个数。
c
// 错误写法
void test(int arr[]) {
int sz = sizeof(arr) / sizeof(arr[0]); // 结果错误,永远是1或2
}
// 正确写法
void bubble_sort(int arr[], int sz) { ... }
第四部分:进阶指针概念
1. 二级指针
指针变量也是变量,它自己在内存中也有地址。存放指针变量地址的指针,就是二级指针。
c
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针
*pp访问的是p。**pp访问的是a。
2. 指针数组
指针数组本质是数组 ,只是数组中存放的元素是指针。
-
定义 :
int* arr[3];(存放3个整型指针的数组)。 -
应用:可以使用指针数组来模拟二维数组。
cint arr1[] = {1,2,3}; int arr2[] = {4,5,6}; int* parr[2] = {arr1, arr2}; // parr[i][j] 即可访问对应元素
【C语言面试宝典】死磕指针:9个最容易挂掉的底层考点
在C语言的面试中,指针是绝对的重灾区。很多同学觉得"懂了",但一做题就错。这些都是面试官最爱问的"细节杀手"。
考点一:指针的大小(架构决定命运)
面试提问:
"
int *和char *哪个占用的内存更大?"
标准回答:
一样大!
指针变量是用来存放地址的。地址的长度只与**硬件架构(地址总线宽度)**有关,与指针指向的数据类型无关。
- 32位环境(x86) :所有类型的指针(
int*,char*,struct*,void*)统统是 4字节。 - 64位环境(x64) :所有类型的指针统统是 8字节。
避坑指南:
千万不要因为 double 占8字节,char 占1字节,就觉得 double* 比 char* 大。在同一台机器同一个编译环境下,sizeof(p) 永远是固定的。
考点二:指针类型的意义(步长与权限)
面试提问:
"既然所有指针大小都一样,为什么还要区分
int*、char*?为什么不直接弄个通用指针?"
核心考点:
这是考察你对指针运算底层逻辑的理解。类型决定了两件事:
-
解引用的权限(视野大小):
int *p; *p一次访问 4个字节。char *p; *p一次访问 1个字节。- 面试题场景 :给你一个
int n = 0x11223344;,让你用char*指针把它改成0x11223300。这就需要利用char*只能访问低地址1个字节的特性。
-
指针运算的步长(跳跃距离):
int *p; p+1地址增加 4。char *p; p+1地址增加 1。- 公式 :
type *p; p+n的实际地址变化是n * sizeof(type)。
考点三:const 与指针的"罗生门"
面试提问:
"请解释
const int *p和int * const p的区别。"
秒杀口诀:看 const 在 * 的哪一边
-
左定值 (
const *):const int * p;或int const * p;const在*左边,修饰的是*p(目标内容)。- 后果 :指针指向可以改 (
p = &bOK),但不能通过指针改内容 (*p = 20Error)。 - 场景:函数参数不想被修改时(如
strlen(const char* str))。
-
右定向 (
* const):int * const p;const在*右边,修饰的是p(指针本身)。- 后果 :指针指向不能改 (
p = &bError),但可以通过指针改内容 (*p = 20OK)。 - 场景:类似于引用的效果,指针一旦绑定就不能换人。
-
双重锁:
const int * const p;- 指哪里不能改,内容也不能改。
考点四:指针减指针(由地址变计数)
面试提问:
"两个指针相减,结果代表什么?是字节数吗?"
标准回答:
不是字节数!
指针 - 指针 的结果是两个指针之间的 元素个数。
前提条件:
两个指针必须指向同一块连续空间(通常是同一个数组)。
代码实战(手写 strlen):
c
int my_strlen(char *s) {
char *start = s;
while(*s != '\0') {
s++;
}
return s - start; // 此时 s 指向末尾,start 指向开头,相减即为字符个数
}
考点五:野指针(内存杀手)
面试提问:
"什么是野指针?如何避免?"
定义:
指向位置不可知(随机、非法、已回收)的指针。
三大成因(必背):
- 未初始化 :
int *p;(局部变量不初始化是随机值,乱指)。 - 越界访问 :数组大小为10,你访问
arr[10]或arr[12]。 - 指"亡"灵 :空间被
free释放了,或者是函数返回了局部变量的地址,但指针还在用。
规避法则(工程规范):
- 指针初始化(不知道指谁就指
NULL)。 - 小心数组越界。
- 指针指向的空间释放后,立即置为
NULL。 - 使用
assert断言拦截非法指针。
考点六:数组名的"人格分裂"
面试提问:
"数组名
arr到底代表什么?是数组首元素的地址,还是整个数组?"
标准回答(黄金法则):
绝大多数情况下,数组名就是数组首元素的地址 (即 arr 等同于 &arr[0])。
唯二的特例(必须死记):
遇到以下两种情况时,数组名代表整个数组:
sizeof(数组名):计算的是整个数组的总大小(字节数)。&数组名:取出的是整个数组的地址。
考点七:arr 与 &arr 的步长差异(高频笔试题)
面试提问:
"
arr和&arr打印出来的地址是一样的,那它们有什么区别?arr+1和&arr+1的结果一样吗?"
核心解析:
这是考察你对指针类型 和步长的理解。虽然数值一样,但它们的"跨度"完全不同。
-
arr:类型是int*(假设是int数组),指向首元素。arr + 1:跳过一个元素(4字节)。
-
&arr:类型是int(*)[N](数组指针),指向整个数组。&arr + 1:跳过整个数组! (如果是int arr[10],一下跳过 40 字节)。
代码实战(心中要有图):
c
int arr[10] = {0};
// 假设 arr 的地址是 0x0012FF40
printf("%p\n", arr + 1);
// 结果:0x0012FF44 (加了4)
printf("%p\n", &arr + 1);
// 结果:0x0012FF68 (加了40 -> 0x28)
// 面试官经常让你算这个地址是多少!
考点八:数组传参的"降维打击"
面试提问:
"我在函数里求数组参数的
sizeof,为什么结果总是4或者8?"
代码场景:
c
void test(int arr[]) {
printf("%d\n", sizeof(arr)); // 输出 4 (32位系统)
}
int main() {
int arr[10] = {0};
test(arr);
}
核心原理:
数组传参,传的不是整个数组,而是首元素的地址!
为了节省内存和提高效率,C语言在函数传参时,数组名会**退化(Decay)**为指向首元素的指针。
- 后果 :在函数内部,
arr不再是数组,而是一个普通的指针变量。 - 面试坑点 :不要在函数内部计算数组大小!必须在外部算好,作为一个整数参数
size传进去。
考点九:指针数组 vs 二级指针
面试提问 1:指针数组
"如何用一维数组模拟一个二维数组?"
解析:
使用指针数组 (int* arr[3])。
- 数组的每个元素都是一个指针。
- 每个指针指向一个独立的一维数组。
- 内存布局 :这三个一维数组在内存中不一定是连续的 ,但可以通过
arr[i][j]的方式访问,效果和二维数组一样。
面试提问 2:二级指针
"什么时候需要使用二级指针(
int **pp)?"
解析:
二级指针主要用于存放一级指针的地址。面试中常见的应用场景:
- 修改指针的指向:如果你想在函数内部修改一个外部指针变量(比如链表节点的插入删除),你需要传这个指针的地址(即二级指针)。
- 指针数组传参 :当函数参数是
char *arr[](如main函数的argv)时,它在函数接收时就是char **argv。
总结:面试避坑指南
指针的学习是一个从"晕头转向"到"豁然开朗"的过程。理解指针的关键在于:
- 分清类型:关注指针指向什么类型,这决定了步长和访问权限。
- 分清层级:是一级指针、二级指针,还是数组指针。
- 内存视角:时刻牢记指针里面存的是地址,通过地址去操作内存。
做指针相关的笔试题时,请默念以下"三步心法":
- 看名字 :有没有
sizeof或&?没有就是首元素地址。 - 看类型 :是指针还是数组?是
int*还是int(*)[10]? - 算步长:加1到底是加4个字节,还是加整个数组的大小?
只要搞清楚 "数组名什么时候降维" 和 "&arr 的步长是多少",这一章的面试题基本难不倒你。