C/C++ 指针完全指南 - 从基础到进阶

指针是 C/C++ 中最强大但也最容易让人困惑的特性。本文系统讲解指针的概念、用法和注意事项,帮助你真正理解指针。


一、指针基础概念

1.1 核心概念

概念 说明
内存块 存储单位,1 字节 = 8 位,是最小可寻址单位
内存首地址 内存块的起始地址,可用数字表示
指针 内存首地址的数值表示
指针变量 存储指针(内存首地址)的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────────────────────────┐
│ 内存与指针概念图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 内存空间 │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │0x00│0x01│0x02│0x03│0x04│0x05│0x06│0x07│0x08│0x09│0x0A│0x0B│... │
│ ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │
│ │ │ │ 20 │ │ │ │'a' │'b' │'c' │'\0'│ │ │ │
│ └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │
│ ▲ ▲ │
│ │ │ │
│ int a = 20 char str[] = "abc" │
│ &a = 0x02 &str[0] = 0x06 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

1.2 指针大小

机器架构 指针大小 说明
32 位 4 字节 可寻址 4GB 内存
64 位 8 字节 可寻址更大内存空间

1.3 类型与内存分配

1
2
3
4
char ch;   // 分配 1 字节
int a; // 分配 4 字节
float f; // 分配 4 字节
double d; // 分配 8 字节

💡 关键:变量定义时即分配内存,内存首地址由编译器分配。


二、* 号与 & 号

2.1 * 号的含义

上下文 含义
定义时 int* p 声明 p 是指针类型
使用时 *p 解引用,获取指针指向内存块的值
等号左边 *p = 向内存块写入值
等号右边 x = *p 从内存块读取值
1
2
3
4
5
int a = 10;
int* p = &a; // p 是指针类型,存储 a 的地址

*p = 20; // 向 p 指向的内存写入 20
int b = *p; // 从 p 指向的内存读取值到 b

2.2 & 号的含义

上下文 含义
&变量 取变量的内存首地址
&指针 取指针变量的地址(二级指针)
1
2
3
int a = 10;
int* p = &a; // p 存储 a 的地址
int** pp = &p; // pp 存储 p 的地址

2.3 优先级

1
2
3
int* p;    // int* 是一个整体类型,p 是变量名
// 等价于
int *p; // 写法不同,意义相同

⚠️ 注意int* 类型的优先级高于 int 类型。


三、指针类型

3.1 指针分类

类型 说明
空指针 值为 NULL (0) 的指针
野指针 指向非法内存的指针
万能指针 void* 类型,可转换为任意指针类型

3.2 空指针与野指针

1
2
3
4
5
6
7
8
9
// 空指针 - 安全
int* p = NULL;
if (p != NULL) {
*p = 10; // 安全检查
}

// 野指针 - 危险
int* p; // 未初始化,值随机
*p = 10; // ⚠️ 可能导致程序崩溃

💡 最佳实践:指针定义时务必初始化为 NULL

3.3 万能指针

1
2
3
4
5
6
7
void* p;           // 万能指针
int a = 10;
p = &a; // 可指向任意类型

// 使用时必须转换
int* ip = (int*)p;
*ip = 20;

3.4 指针运算

1
2
3
4
5
6
7
8
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;

// 指针运算与类型相关
p + 1; // 地址增加 sizeof(int) = 4 字节

char* cp = (char*)arr;
cp + 1; // 地址增加 sizeof(char) = 1 字节

四、数组与指针

4.1 数组名即指针

1
2
3
4
5
int arr[10];
int* p = arr; // ✅ 正确,数组名即首地址

// 等价写法
int* p2 = &arr[0]; // ✅ 正确

4.2 指针与数组的关系

1
2
3
4
5
6
7
8
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;

// 以下写法完全等价
arr[2] // 下标访问
*(arr + 2) // 指针运算访问
p[2] // 指针下标访问
*(p + 2) // 指针运算访问

💡 核心*(p + i)p[i] 是完全等价的。


五、多级指针(套娃)

5.1 多级指针概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────────────┐
│ 多级指针结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 一级指针 │
│ ┌─────────┐ ┌─────────┐ │
│ │ int a │◄─────│ int* p │ │
│ │ value=10│ │ &a │ │
│ └─────────┘ └─────────┘ │
│ │
│ 二级指针 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ int a │◄─────│ int* p │◄─────│ int** q │ │
│ │ value=10│ │ &a │ │ &p │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 三级指针 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ int a │◄─────│ int* p │◄─────│ int** q │◄─────│int*** w │ │
│ │ value=10│ │ &a │ │ &p │ │ &q │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

5.2 多级指针代码

1
2
3
4
5
6
7
8
9
10
int a = 10;
int* p = &a; // 一级指针
int** q = &p; // 二级指针
int*** w = &q; // 三级指针

// 访问 a 的值
a // 直接访问
*p // 一级解引用
**q // 二级解引用
***w // 三级解引用

六、函数传参

6.1 值传递

1
2
3
4
5
6
7
8
// ❌ 错误示例
void print_Array(int a[]) // 实际是 int* a
{
// sizeof(a) 返回指针大小,不是数组大小!
for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
printf("%d,", a[i]);
}
}

⚠️ 陷阱:形参数组退化为指针,sizeof(a) = 指针大小(4 或 8),不是数组大小。

6.2 正确的数组传参

1
2
3
4
5
6
7
8
9
10
11
// ✅ 正确示例
void print_Array(int* array, int length)
{
for (int i = 0; i < length; i++) {
printf("%d,", array[i]);
}
printf("\n");
}

int array[] = {7, 79, 465, 65, -345, -346, 798, 1, 0, 45};
print_Array(array, sizeof(array) / sizeof(array[0]));

6.3 形参数组等价性

写法 编译器理解
void func(int a[100]) void func(int* a)
void func(int a[]) void func(int* a)
void func(int* a) void func(int* a)

💡 结论:形参中的数组只是语法糖,本质上都是指针。


七、指针数组与数组指针

7.1 指针数组

指针数组:是一个数组,每个元素是指针。

1
char* a[10];    // 指针数组,10个 char* 元素

7.2 数组指针

数组指针:是一个指针,指向整个数组。

1
int (*p)[10];   // 数组指针,指向包含10个int的数组

7.3 区分技巧

声明 含义 记忆技巧
int* p[10] 指针数组 [] 优先级高,先是个数组
int (*p)[10] 数组指针 () 优先级高,先是个指针
1
2
3
4
5
6
7
// 指针数组 - 每个元素是char*
char* arr[3] = {"hello", "world", "pointer"};

// 数组指针 - 指向包含3个int的数组
int (*p)[3];
int nums[3] = {1, 2, 3};
p = &nums;

八、指针函数与函数指针

8.1 指针函数

指针函数:返回指针的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 错误示例:返回局部变量地址
int* pointer_Function()
{
int a = 200;
return &a; // ⚠️ 危险!a 在函数结束后被释放
}

// ✅ 正确示例:使用静态变量
int* pointer_Function()
{
static int a = 200; // 静态变量,函数结束后仍存在
return &a;
}

// ✅ 正确示例:动态分配内存
int* pointer_Function()
{
int* p = (int*)malloc(sizeof(int));
*p = 200;
return p; // 需要调用者 free
}

8.2 函数指针

函数指针:指向函数的指针。

1
2
3
4
5
6
7
8
9
10
11
// 函数指针声明
int (*func_ptr)(int, int);

// 函数定义
int add(int a, int b) { return a + b; }

// 函数指针赋值
func_ptr = add;

// 通过函数指针调用
int result = func_ptr(3, 5); // 返回 8

8.3 函数指针数组

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数指针数组
int (*funcs[3])(int, int);

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; }

funcs[0] = add;
funcs[1] = sub;
funcs[2] = mul;

// 调用
int result = funcs[0](3, 5); // add(3, 5) = 8

九、字符串与指针

9.1 字符串结束符

表示 ASCII 说明
0 48 字符 ‘0’
'\0' 0 字符串结束符
1
2
3
4
5
6
7
8
9
10
11
char a[] = {'a', 'b'};
printf("%s", a); // ❌ 乱码,没有结束符

char a[10] = {'a', 'b'};
printf("%s", a); // ✅ 正常,自动补 0

char a[] = {'a', 'b', '\0'};
printf("%s", a); // ✅ 正常

char a[] = "ab"; // ✅ 自动添加 '\0'
printf("%s", a); // 正常

9.2 字符指针的特殊性

1
2
3
4
5
6
7
8
9
char str[] = "hello";
printf("%s\n", str); // 特殊处理,逐字符输出直到 '\0'

// 等价于:
int i = 0;
while (str[i] != '\0') {
printf("%c", str[i]);
i++;
}

9.3 字符常量区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 字符串常量在 data 区域(只读)
printf("%p\n", "aaa"); // 每次打印相同地址
printf("%p\n", "aaa"); // 同一个地址

// 字符数组在栈区(可修改)
char buf[] = "aaa"; // 内容可修改

// 字符指针指向常量区(只读)
char* p1 = "hello"; // ⚠️ 内容不可修改
// p1[0] = 'H'; // ❌ 错误,常量不可修改

// 字符数组内容可修改
char buf[] = "hello"; // ✅ 内容可修改
buf[0] = 'H'; // 正确

十、结构体与指针

10.1 结构体指针的三种操作方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Student {
char name[20];
int age;
float score;
};

struct Student s[3] = {};

// 方式一:下标访问(直接操作内存块)
s[0].age = 15;
strcpy(s[0].name, "mike");
s[0].score = 90.5f;

// 方式二:指针解引用(直接操作内存块)
(*(s + 1)).age = 16;
strcpy((*(s + 1)).name, "john");
(*(s + 1)).score = 85.0f;

// 方式三:箭头操作符(操作内存首地址)
(s + 2)->age = 17;
strcpy((s + 2)->name, "tom");
(s + 2)->score = 88.0f;

10.2 指针访问结构体

1
2
3
4
5
6
7
8
9
10
struct Student* p = s;  // 或 &s[0]

// 下标访问
p[0].age = 15;

// 解引用访问
(*(p + 1)).age = 16;

// 箭头访问
(p + 2)->age = 17;

十一、内存管理

11.1 内存分配时机

1
2
3
4
5
6
7
// 自动分配内存(栈区)
int a; // 定义时分配,作用域结束释放
int* p; // 指针变量本身也分配了内存

// 动态分配内存(堆区)
int* p = (int*)malloc(sizeof(int)); // 手动分配
free(p); // 手动释放

11.2 变量与内存

代码 内存分配
int a; 分配内存首地址 + 内存块
int* p; 分配指针变量的内存首地址和内存块

💡 理解:指针变量也是变量,也有自己的内存地址和内存块,只是它的内存块专门用来存储地址。


十二、常见陷阱

12.1 返回局部变量地址

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错误
int* func() {
int a = 10;
return &a; // a 在函数结束后被销毁
}

// ✅ 正确
int* func() {
static int a = 10; // 静态变量,永久存在
return &a;
}

12.2 数组传参陷阱

1
2
3
void func(int arr[100]) {
printf("%zu", sizeof(arr)); // 输出指针大小(4 或 8),不是 400!
}

12.3 字符串修改陷阱

1
2
3
4
5
char* p = "hello";
p[0] = 'H'; // ❌ 段错误,常量不可修改

char arr[] = "hello";
arr[0] = 'H'; // ✅ 正确

十三、总结

主题 核心要点
基础概念 内存块 + 内存首地址 = 指针
* 号 定义时表示类型,使用时表示解引用
& 号 取地址操作符
数组与指针 数组名即首地址,*(p+i) 等价 p[i]
多级指针 每一级 * 都需要解引用一次
函数传参 数组退化为指针,需传递长度
字符串 自动添加 '\0' 结束符
常量区 字符串字面量只读,需拷贝后修改

💡 学习建议

  • 理解内存模型是关键
  • 多画图,多思考内存布局
  • 注意数组传参的退化问题
  • 警惕野指针和返回局部地址
  • 使用工具(如 Valgrind)检测内存错误

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com