C-Style 指针浅谈
0. ChangeLog
2025.09.09 开始撰写\
2025.09.17 通过第一稿\
2025.09.25 修缮
1. 什么是指针?
程序运行时,所有变量、函数、数据都会被加载到计算机内存中,而每一块内存区域都拥有一个唯一的标识,这个标识就是内存地址(通常用十六进制数表示,如 0x1145141919810 )。
指针的本质,就是专门用于存储内存地址的变量。
2. 指针变量的定义与语法
指针变量的定义需在变量名前添加 * 符号,语法格式为:
// 基础语法:数据类型 *指针变量名;
int *p; // 定义一个指向 int 类型变量的指针 p
char *str; // 定义一个指向 char 类型变量的指针 str
double *d_ptr; // 定义一个指向 double 类型变量的指针 d_ptr
::::warning[注意]{open}
* 仅在定义指针变量时表示 "该变量是指针",在后续使用中(如取值操作)含义不同;
指针变量的类型必须与它所指向的变量类型一致(除非使用 void 指针或强制类型转换),例如 int *p 不能直接指向 double 类型变量,否则会导致内存访问错误。
::::
3. 指针的运算符:&(取地址)与 *(解引用)
指针的使用依赖 & 与 * 两个运算符
& :取地址符,获取变量的内存地址。
例如:
int a = 10; // 声明一个值为 10 的变量 a
int *p = &a; // 声明指针 p,指向变量 a
* :不同于声明变量时,这里的 * 代表获取指针指向的变量。
例如:
int b = *p; // 取出指针 p 指向的变量,并将值赋值给 b
*p = 10; // 修改指针 p 指向的变量为 10
4. 空指针与野指针
指针变量若未正确初始化,可能会指向内存中的随机地址(即 "野指针"),访问野指针会导致程序崩溃或数据损坏;而 "空指针" 则是明确表示 "指针不指向任何有效地址" 的安全状态。
4.1. 空指针:NULL/nullptr
C 语言中通过 NULL 宏来表示空指针,C++11 后可以并且推荐使用 nullptr 表示空指针。
int *p1 = NULL; // C-style 空指针
int *p2 = nullptr; // C++11 空指针
// 空指针不可解引用,否则会触发运行时错误
// cout << *p2; // return value 3221225477
if (p2 != nullptr) { // 判断是否为空指针
*p2 = 10; // 若 p2 为空,此代码不会执行
}
4.2. 野指针
野指针通常由以下场景产生:
指针未初始化:int *p;(p 指向随机地址);
指针指向的变量已被释放(访问 free/delete 后的指针);
数组越界等。
规避野指针的核心原则:
指针定义时立即初始化(若暂时无指向对象,初始化为 nullptr/NULL);
动态内存释放后(delete/free),立即将指针置为 nullptr;
访问指针前,先判断是否为 nullptr 或是否在合法范围内。
5. 指针与数组
在 C/C++ 中,数组名本质是指向数组首元素的常量指针(即不能修改数组名的指向,但可以通过数组名访问元素)。这一特性使得指针与数组的操作可以相互转换,数组退化为指针也就是因为这个。
:::warning[函数中包含数组]{open}
在函数参数中直接包含一个数组会退化成指针,例如 int f(int a[]) 等价于 int f(int *a),避免这点需要用 int f(int (&a)[N])。
:::
指针访问数组元素的两种方式
假设有数组 int a[5] = {1, 2, 3, 4, 5};,数组名 a 可以转换为 &a[0](指向首元素的地址),则访问数组元素有两种方式:
#include <iostream>
using namespace std;
int main() {
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // 等价于 int *p = &a[0],p 指向数组首元素
// 方式1:常规方式
cout << "a[0] = " << a[0] << ", a[2] = " << a[2] << "\n";
// 方式2:指针偏移法
cout << "*p = " << *p << ", *(p + 2) = " << *(p + 2) << "\n";
cout << "*(a + 1) = " << *(a + 1) << "\n";
// 指针自增:指向数组下一个元素
p++;
cout << "*p = " << *p; // 输出:2(此时 p 指向 a[1])
return 0;
}
需要注意的是:指针的算术运算(如 p++、p+2)并非简单的地址数值加 int *p 每次自增,地址会增加 int 类型一般为 sizeof(int) 查看),确保指针始终指向有效元素。
二维数组与指针的关联
二维数组可理解为 "数组的数组",其指针操作更复杂,需区分 "指向一维数组的指针" 与 "指向元素的指针":
int a[2][3] = {{1,2,3}, {4,5,6}};
int (*p)[3] = a; // p 是指向"包含 3 个 int 元素的数组"的指针(即指向 a[0])
cout << **p << "\n"; // 输出:1(*p 是 a[0],**p 是 a[0][0])
cout << *(*p + 1) << "\n"; // 输出:2(*p + 1 指向 a[0][1])
cout << **(p + 1) << "\n"; // 输出:4(p + 1 指向 a[1],**(p+1) 是 a[1][0])
6. 指针与函数
指针在函数中的应用主要有两个场景:通过指针修改函数外部变量的值(解决函数参数 "值传递" 无法修改外部变量的问题),以及函数指针(实现函数的动态调用,如回调函数)。
指针作为函数参数
C/C++ 函数参数默认是 "值传递"(函数内修改不影响外部变量),而通过指针传递变量地址,可实现函数对外部变量的修改:
#include <iostream>
using namespace std;
// 通过指针交换两个变量的值
void swap(int *a, int *b) {
int temp = *a; // 获取指针 a 指向的变量值
*a = *b; // 修改指针 a 指向的变量值
*b = temp; // 修改指针 b 指向的变量值
}
int main() {
int x = 5, y = 10;
cout << "交换前:x = " << x << ", y = " << y << "\n"; // 输出:5, 10
swap(&x, &y); // 传递 x 和 y 的地址
cout << "交换后:x = " << x << ", y = " << y << "\n"; // 输出:10, 5
return 0;
}
::::info[关于不使用指针修改外部变量]{open}
也可以使用传引用的方式。
#include <iostream>
using namespace std;
// 通过引用交换两个变量的值
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 5, y = 10;
cout << "交换前:x = " << x << ", y = " << y << "\n"; // 输出:5, 10
swap(x, y); // 直接传入 x, y
cout << "交换后:x = " << x << ", y = " << y << "\n"; // 输出:10, 5
return 0;
}
::::
指向函数的指针
函数指针是专门存储函数地址的指针,其核心用途是 "动态选择调用的函数"。
// 语法:返回值类型 (*函数指针名)(参数类型列表);
int (*add_ptr)(int, int); // 定义一个指向"返回 int、参数为两个 int"的函数的指针
以下是函数指针的使用示例:
#include <iostream>
using namespace std;
// 加法函数
int add(int a, int b) {
return a + b;
}
// 减法函数
int subtract(int a, int b) {
return a - b;
}
// 通过函数指针调用指定函数
int calculate(int (*func_ptr)(int, int), int a, int b) {
return func_ptr(a, b); // 调用函数指针指向的函数
}
int main() {
int a = 10, b = 3;
int (*op_ptr)(int, int); // 定义函数指针 op_ptr
// 选择加法运算
op_ptr = add;
cout << "a + b = " << calculate(op_ptr, a, b) << "\n"; // 输出:13
// 选择减法运算
op_ptr = subtract;
cout << "a - b = " << calculate(op_ptr, a, b) << "\n"; // 输出:7
return 0;
}
7. 动态内存管理与 const 修饰
在实际开发中,指针的核心价值体现在动态内存管理(灵活分配内存,避免内存浪费)和 const 修饰(限制指针操作,提升代码安全性)。
动态内存管理:new/delete 与 malloc/free
C/C++ 中的内存分为 "栈内存"(自动分配释放,如局部变量)和 "堆内存"(手动分配释放,需通过指针操作)。动态内存管理即通过指针操作堆内存,核心工具是 C++ 的 new/delete(推荐)和 C 语言的 malloc/free。
C++ 风格:new(分配内存)与 delete(释放内存)
new 会自动根据变量类型分配对应大小的堆内存,并返回指向该内存的指针;delete 用于释放 new 分配的内存,避免内存泄漏。
#include <iostream>
using namespace std;
int main() {
// 动态分配单个 int 变量
int *p1 = new int; // 分配 4 字节堆内存,p1 指向该内存
*p1 = 20; // 给动态内存赋值
cout << "动态变量值:" << *p1 << "\n"; // 输出:20
delete p1; // 释放 p1 指向的堆内存
p1 = nullptr; // 释放后将指针置空,避免野指针
// 2. 动态分配数组(需指定数组长度)
int n = 5;
int *a_ptr = new int[n]; // 分配 5 个 int 大小的堆内存(共 20 字节)
for (int i = 0; i < n; i++) {
a_ptr[i] = i + 1; // 给动态数组赋值:1,2,3,4,5
}
for (int i = 0; i < n; i++) {
cout << a_ptr[i] << " "; // 输出:1 2 3 4 5
}
delete[] a_ptr; // 释放动态数组
a_ptr = nullptr; // 置空指针
return 0;
}
C 风格:malloc(分配)与 free(释放)
malloc 需手动指定分配的内存字节数,返回 void* 类型指针(需强制转换为目标类型);free 用于释放 malloc 分配的内存:
#include <iostream>
#include <cstdlib>
using namespace std;
int main() {
// 分配 1 个 int 大小的内存(sizeof(int) 获取 int 字节数)
int *p = (int*)malloc(sizeof(int));
if (p == nullptr) { // malloc 失败时返回 nullptr,需判断
cout << "内存分配失败" << "\n";
return 1;
}
*p = 30;
cout << *p << "\n"; // 输出:30
free(p); // 释放内存
p = nullptr; // 置空指针
return 0;
}
动态内存管理的核心注意事项
new 对应 delete,new[] 对应 delete[],malloc 对应 free,不可混用(如 new 分配的内存用 free 释放,可能会导致内存错误);
动态分配的内存若未手动释放,程序结束前会一直占用堆内存,长期运行会导致内存耗尽;
避免重复释放:同一内存地址不可多次释放(会触发运行时错误),释放后需将指针置空。
const 修饰指针
const 修饰指针时,根据修饰位置的不同,可实现 "指针指向的值不可修改" 或 "指针的指向不可修改",甚至二者皆不可修改,具体规则如下:
const int *p:指向 const int 的指针(值不可改,指向可改)
int* const p:指向 int 的不可改变指向的指针
const int *const p 指向 const int 的 const 指针(都不可改)
#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
// 1. const int *p:指向的值不可修改
const int *p1 = &a;
// *p1 = 30; // 错误:不能修改指向的值
p1 = &b; // 正确:可以修改指针指向
// 2. int *const p:指针指向不可修改
int *const p2 = &a;
*p2 = 30; // 正确:可以修改指向的值
// p2 = &b; // 错误:不能修改指针指向
cout << "a = " << a << "\n"; // 输出:30(被 p2 修改)
// 3. const int *const p:都不可修改
const int *const p3 = &a;
// *p3 = 30; // 错误:不能修改值
// p3 = &b; // 错误:不能修改指向
return 0;
}
const 修饰指针的核心用途是 "明确接口约束"—— 例如在函数参数中使用 const int *p,表示函数不会修改 p 指向的数据。
8. 常见问题与调试技巧
常见问题及原因分析
野指针访问崩溃
指针未初始化、指向的内存已释放但指针未置空、指针越界。
程序运行时触发 Segmentation Fault(段错误)或 return value 3221225477。
内存泄漏
动态分配的内存 new/malloc 未用 delete/free 释放,且指针丢失;
程序运行时间越长,内存占用越高,最终可能因内存耗尽崩溃。
指针类型不匹配
将不同类型的指针直接赋值(如 double a_double = 1.14514; int *p = (int*)&a_double,a_double 是 double 类型);
访问指针时获取的数据错误(因不同类型的内存布局和字节数不同)。
动态数组使用 delete 释放遗漏 []
用 new[] 分配数组后,用 delete(而非 delete[])释放;
数组中除首元素外的其他元素内存未释放,导致内存泄漏。
调试技巧
打印指针地址与值
在关键位置打印指针的地址(cout << p)和指向的值(cout << *p),确认指针指向是否合法、值是否正确;
示例:
if (p != nullptr) {
cout << "指针 p 地址:" << p << ",指向的值:" << *p << "\n";
} else {
cout << "指针 p 为空" << "\n";
}
使用调试工具
有的 IDE 的调试功能支持 "查看指针指向的内存"。在此不多赘述。想看的可以自行 bdfs。
9. The End
给一份豆 皮 包的结语:
至此,我们已围绕 C/C++ 指针完成了从基础概念到实际应用的全面解析。从指针 “存储内存地址的变量” 这一本质出发,我们逐步梳理了其定义语法、核心运算符(& 取地址与 * 解引用),深入探讨了空指针与野指针的安全边界,也揭示了指针与数组(从一维到二维)的底层关联 —— 数组名作为 “指向首元素的常量指针”,让二者的操作得以灵活转换。
在函数场景中,指针不仅解决了值传递无法修改外部变量的问题,更通过函数指针实现了函数的动态调用,为回调函数等高级用法奠定基础;而动态内存管理(new/delete 与 malloc/free)则充分展现了指针的核心价值 —— 灵活操控堆内存,避免资源浪费,搭配 const 修饰的指针约束,更能在灵活中保障代码安全性。
当然,指针的强大也伴随着风险:野指针导致的程序崩溃、动态内存未释放引发的内存泄漏、类型不匹配带来的数据错误,都是开发者需要警惕的 “陷阱”。但正如文中的调试技巧所示,通过打印指针地址与值、善用 IDE 调试工具,这些问题并非不可攻克。
指针是 C/C++ 直接操作内存的 “桥梁”,也是理解内存模型、优化程序性能的关键。掌握指针,不仅能更高效地编写代码(如动态数据结构、系统级编程),更能深入理解编程语言与计算机硬件的交互逻辑。希望本文的解析能成为你攻克指针难点的助力,在后续实践中,不妨多通过代码验证、调试分析加深理解,让指针真正成为你编程工具箱中的 “利器”。
这是本蒟蒻第一次写文章,如有遗漏,请在讨论指出。