C++指针从入门到崩溃?一篇文章彻底讲透
1. 为什么指针是C++的“劝退之王”?
很多初学者在学完基础语法后,一碰到指针就开始怀疑人生:指针到底是什么?它和变量有什么区别?为什么用不好就会段错误、内存泄漏、悬空指针?
其实指针并没有想象中那么可怕。它本质上就是一个保存内存地址的变量。你之所以觉得难,是因为它把“值”和“地址”两个概念叠加在了一起,再加上内存管理、引用、智能指针等一堆衍生概念,很容易让人绕晕。
这篇文章会从最底层的“内存地址”讲起,一步步带你理解指针的本质、常见用法、经典陷阱,以及现代C++中如何用智能指针优雅地替代裸指针。读完你会发现:指针不是玄学,而是C++最强大的工具之一。
2. 先搞懂内存地址:指针的地基
在理解指针之前,必须先理解一个核心概念:程序运行时,变量是存放在内存中的,而内存中的每一个字节都有一个编号,这个编号就是内存地址。
你可以把内存想象成一排带编号的储物柜。每个柜子能放一定大小的数据,柜子的编号就是地址。当你声明一个变量时,编译器会为你分配一个或多个柜子,并把变量名和这些柜子的编号关联起来。
2.1 取地址运算符 & 与解引用运算符 *
在C++中,有两个运算符是理解指针的关键:
- &(取地址运算符):获取变量在内存中的起始地址。
- *(解引用运算符):根据地址访问该地址上存储的数据。
2.2 第一个指针程序
#include <iostream>
int main() {
int age = 25; // 声明一个普通变量
int* ptr = &age; // 声明一个指针,保存 age 的地址
std::cout << "age 的值:" << age << std::endl;
std::cout << "age 的地址:" << &age << std::endl;
std::cout << "ptr 保存的地址:" << ptr << std::endl;
std::cout << "通过 ptr 解引用得到:" << *ptr << std::endl;
*ptr = 30; // 通过指针修改 age 的值
std::cout << "修改后 age 的值:" << age << std::endl;
return 0;
}
运行这段代码,你会发现 ptr 保存的地址和 &age 完全一致,而 *ptr 取出的值就是 age 的值。这就是指针最核心的机制:通过地址间接访问变量。
2.3 指针的声明语法
指针的声明语法是 类型* 变量名,其中类型表示“这个指针指向什么类型的数据”。例如:
int* p:指向int的指针double* p:指向double的指针char* p:指向char的指针void* p:无类型指针,可以指向任意类型,但不能直接解引用
3. 指针的核心操作:赋值、解引用与算术
掌握了基础语法后,接下来要理解指针的三种核心操作:赋值、解引用和指针算术。
3.1 指针的赋值
指针只能保存“同类型变量的地址”,否则编译器会报错(除非使用 void* 或强制类型转换)。例如:
int a = 10;
double b = 3.14;
int* p1 = &a; // 正确:p1 指向 int
// int* p2 = &b; // 错误:&b 是 double*,不能赋给 int*
void* p3 = &b; // 正确:void* 可以保存任意地址
// *p3 = 1.0; // 错误:void* 不能直接解引用
3.2 解引用与空指针
解引用一个指针前,必须确保它指向有效的内存。如果指针是 nullptr(空指针),解引用会导致程序崩溃(段错误)。
int* p = nullptr;
// *p = 42; // 危险!解引用空指针会导致崩溃
if (p != nullptr) {
*p = 42;
} else {
std::cout << "指针为空,不能解引用" << std::endl;
}
3.3 指针算术:数组与指针的亲密关系
指针支持加减运算,但加减的单位是“指向类型的大小”,而不是1个字节。这一点在遍历数组时非常有用:
#include <iostream>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // 数组名会退化为指向首元素的指针
for (int i = 0; i < 5; ++i) {
std::cout << "arr[" << i << "] = " << *(p + i) << std::endl;
}
// 指针也可以自增遍历
int* q = arr;
while (q != arr + 5) {
std::cout << *q << " ";
++q;
}
std::cout << std::endl;
return 0;
}
注意:p + 1 并不是地址加1,而是加 sizeof(int) 个字节(通常是4字节),这样 *(p + 1) 才能正确访问下一个 int 元素。
4. 指针与函数:传值、传址与引用
指针最常见的用途之一,就是作为函数参数,实现“在函数内部修改外部变量”的效果。
4.1 传值 vs 传址
#include <iostream>
void swap_by_value(int a, int b) {
int tmp = a;
a = b;
b = tmp;
// 这里修改的是副本,不影响外部变量
}
void swap_by_pointer(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
// 通过地址修改外部变量
}
void swap_by_reference(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
// 引用是别名,直接修改外部变量
}
int main() {
int x = 1, y = 2;
swap_by_value(x, y);
std::cout << "传值后:x=" << x << ", y=" << y << std::endl; // 1, 2
swap_by_pointer(&x, &y);
std::cout << "传址后:x=" << x << ", y=" << y << std::endl; // 2, 1
swap_by_reference(x, y);
std::cout << "传引用后:x=" << x << ", y=" << y << std::endl; // 1, 2
return 0;
}
4.2 指针与引用的区别
| 对比项 | 指针 | 引用 |
|---|---|---|
| 是否可以为空 | 可以,nullptr | 不可以,必须初始化 |
| 是否可以重新指向 | 可以,随时改变指向 | 不可以,绑定后不能改变 |
| 是否需要解引用 | 需要 * | 不需要,直接使用 |
| 内存占用 | 占用一个指针大小的空间 | 通常不额外占用空间(编译器优化) |
| 适用场景 | 动态内存、数组遍历、可选参数 | 函数参数、运算符重载、避免拷贝 |
4.3 函数指针:把函数当作参数传递
函数指针是指向函数的指针,可以用来实现回调机制:
#include <iostream>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// 函数指针作为参数
int compute(int x, int y, int (*func)(int, int)) {
return func(x, y);
}
int main() {
int (*fp)(int, int) = add; // 声明并初始化函数指针
std::cout << "add(3, 4) = " << fp(3, 4) << std::endl;
std::cout << "compute(3, 4, add) = " << compute(3, 4, add) << std::endl;
std::cout << "compute(3, 4, multiply) = " << compute(3, 4, multiply) << std::endl;
return 0;
}
5. 动态内存:new 与 delete 的正确姿势
指针最强大的能力,是配合 new 和 delete 在堆上动态分配内存。堆内存的生命周期由程序员控制,这也是内存泄漏和悬空指针的“重灾区”。
5.1 基本用法
#include <iostream>
int main() {
// 分配单个变量
int* p = new int(42);
std::cout << "*p = " << *p << std::endl;
delete p; // 释放内存
p = nullptr; // 置空,避免悬空指针
// 分配数组
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i * i;
}
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr; // 释放数组要用 delete[]
return 0;
}
5.2 经典陷阱:内存泄漏与悬空指针
- 内存泄漏:
new了却没有delete,导致内存无法回收。程序长期运行会耗尽内存。 - 悬空指针:
delete之后没有把指针置空,指针仍然保存着已释放的地址。再次解引用是未定义行为。 - 重复释放:对同一个指针
delete两次,会导致程序崩溃。
int* p = new int(10);
delete p;
// p 现在是悬空指针
// delete p; // 错误!重复释放
p = nullptr; // 正确做法:置空
delete p; // 对 nullptr 执行 delete 是安全的
5.3 用智能指针告别手动管理
现代C++强烈推荐使用智能指针替代裸指针,让内存管理自动化:
#include <iostream>
#include <memory>
int main() {
// unique_ptr:独占所有权,不能拷贝
std::unique_ptr<int> uptr = std::make_unique<int>(100);
std::cout << "*uptr = " << *uptr << std::endl;
// shared_ptr:共享所有权,引用计数
std::shared_ptr<int> sptr1 = std::make_shared<int>(200);
std::shared_ptr<int> sptr2 = sptr1;
std::cout << "use_count = " << sptr1.use_count() << std::endl;
// weak_ptr:弱引用,不增加引用计数,用于打破循环引用
std::weak_ptr<int> wptr = sptr1;
if (auto sp = wptr.lock()) {
std::cout << "weak_ptr 有效,*sp = " << *sp << std::endl;
}
return 0;
// 离开作用域时,智能指针自动释放内存
}
6. 指针进阶:多级指针、const 与 void*
掌握了基础之后,再来看几个容易让人“崩溃”的进阶概念。
6.1 多级指针:指向指针的指针
二级指针保存的是“一级指针的地址”,常用于在函数中修改指针本身:
#include <iostream>
void allocate(int** pp) {
*pp = new int(99); // 修改外部指针的指向
}
int main() {
int* p = nullptr;
allocate(&p); // 传入指针的地址
std::cout << "*p = " << *p << std::endl;
delete p;
return 0;
}
6.2 const 与指针的三种组合
| 写法 | 含义 | 能否修改指向 | 能否修改值 |
|---|---|---|---|
const int* p | 指向 const int 的指针 | 可以 | 不可以 |
int* const p | const 指针,指向 int | 不可以 | 可以 |
const int* const p | const 指针,指向 const int | 不可以 | 不可以 |
int a = 10, b = 20;
const int* p1 = &a; // 指向常量的指针
// *p1 = 30; // 错误:不能通过 p1 修改值
p1 = &b; // 正确:可以改变指向
int* const p2 = &a; // 常量指针
*p2 = 30; // 正确:可以修改值
// p2 = &b; // 错误:不能改变指向
const int* const p3 = &a; // 双重 const
// *p3 = 30; // 错误
// p3 = &b; // 错误
6.3 void* 与类型转换
void* 可以保存任意类型的地址,但使用前必须转换回具体类型:
int a = 42;
void* vp = &a;
// 必须转换回 int* 才能解引用
int* ip
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)