1423 字
7 分钟
C++ 指针:从语法到内存直觉
这篇文章整理自学习笔记与 The Cherno 的讲解思路:不只背语法,而是建立“内存层面的直觉”。
1. 指针:内存的导航仪
1.1 指针的本质
可以把指针理解成:
- 一个“地址”(通常你会看到它像一个整数一样打印出来)
- 加上一个“类型解释规则”(告诉编译器:解引用时按什么类型读写)
一句话直觉指针的值是地址;指针的类型决定了解引用
*时读/写多少字节,以及如何解释这段比特。
int value = 42;int* ptr = &value; // ptr 存的是 value 的地址
*ptr = 7; // 解引用:去这个地址写入 7// value == 7TIP
sizeof(ptr)永远是“指针本身大小”(32 位常见 4 字节,64 位常见 8 字节);sizeof(*ptr)才是“指向的数据类型大小”。
1.2 两个核心操作符:& 和 *
&(取地址):拿到变量的内存地址*(解引用):沿着地址去读/写那块内存
int a = 5;
int* p = &a; // p 指向 a*p = 2; // 修改 a
// a == 21.3 指针可以“改指向”
int a = 5;int b = 8;
int* ptr = &a;*ptr = 2; // a = 2
ptr = &b; // ptr 改为指向 b*ptr = 1; // b = 12. 引用:变量的替身(通常是指针的语法糖)
2.1 引用是什么
引用(reference)更像“别名”:你给某个变量起了另一个名字。
int a = 10;int& ref = a; // ref 是 a 的别名
ref = 20; // 等价于 a = 20引用的关键约束:
- 必须初始化(必须绑定到一个已存在对象)
- 一旦绑定,不再“改指向”另一个对象(
ref = b是把 b 的值赋给 ref 绑定的对象)
2.2 引用 vs 指针(什么时候用哪个)
| 维度 | 指针 (pointer) | 引用 (reference) |
|---|---|---|
| 是否可为空 | 可以为 nullptr | 不能(语义上必须引用有效对象) |
| 是否可改指向 | 可以 | 不行(绑定后不再改指向) |
| 调用/使用语法 | 需要 *ptr | 像普通变量一样用 |
| 常见用途 | 可选参数、可空语义、所有权表达 | 传参(尤其是 const 引用)、别名 |
经验法则能用引用表达“必须存在”,就优先用引用;需要表达“可能为空/可选”,再用指针。
3. 内存管理:栈 vs 堆
3.1 栈(Stack)
- 分配/释放非常快(通常就是移动栈指针)
- 生命周期跟作用域走:离开
{}就销毁
void foo() { int x = 123; // x 在栈上} // 作用域结束,x 自动销毁3.2 堆(Heap)与 new/delete
- 堆上分配更慢,但生命周期不受作用域影响
- 你必须手动释放,否则会内存泄漏
int* p = new int(42); // 分配并初始化一个 int
delete p; // 释放p = nullptr; // 习惯性置空,避免悬空指针动态数组要用 new[] / delete[] 配对:
// 需要 <cstring>char* buffer = new char[8];memset(buffer, 0, 8);
delete[] buffer;buffer = nullptr;WARNING
new对应delete,new[]对应delete[]。配错是未定义行为(UB)。
IMPORTANT现代 C++ 更推荐用 RAII/智能指针来管理堆资源(比如
std::unique_ptr),尽量避免裸new/delete。
4. 参数传递:值 / 指针 / 引用
这里是理解“性能 + 行为”的关键。
4.1 传值(Pass by Value)
void Increment(int value) { value++;}
int main() { int a = 1; Increment(a); // a 仍然是 1}4.2 传指针(Pass by Pointer)
void Increment(int* value) { (*value)++;}
int main() { int a = 1; Increment(&a); // a 变为 2}4.3 传引用(Pass by Reference)
void Increment(int& value) { value++;}
int main() { int a = 1; Increment(a); // a 变为 2}最佳实践传大对象时,优先用
const T&:既不复制,又不允许函数改动入参。
5. const 与指针:两种“const”别搞反
int a = 1;int b = 2;
const int* p1 = &a; // 指向 const:不能通过 p1 改 a,但 p1 可以改指向// *p1 = 3; // errorp1 = &b; // ok
int* const p2 = &a; // const 指针:p2 不能改指向,但可以改 a*p2 = 3; // ok// p2 = &b; // error记忆法:从右往左读。
6. 常见误区与避坑
6.1 指针大小 vs 数据大小
sizeof(ptr):指针变量本身sizeof(*ptr):指向的类型大小
6.2 不要返回局部变量的指针/引用
int* Bad() { int x = 123; return &x; // 错:x 离开作用域就销毁,返回的是悬空指针}CAUTION这类 bug 往往“偶尔才炸”,因为你指向的是一块已经被复用的栈内存。
6.3 强转指针类型很危险
把 int* 强转成 double* 再解引用,可能会出现越界读取/未对齐访问等问题。
6.4 优先用智能指针表达所有权
如果你的指针“拥有”一块资源(需要负责释放),优先用智能指针把所有权写进类型里:
#include <memory>
struct Entity { int x{};};
int main() { auto e = std::make_unique<Entity>(); // 离开作用域自动释放}7. 调试建议:把问题拉回“地址 + 生命周期”
排查指针问题时,永远先问三件事:
- 这个地址来自哪里?(谁分配的/谁拥有它)
- 指向的对象现在还活着吗?(是否已经离开作用域/已 delete)
- 这个类型解释对吗?(是否配套
new[]/delete[],是否越界)
如果你用的是 Visual Studio:
- 监视窗口看指针值(地址)
- 内存窗口用地址直接观察那段内存
总结
- 指针 = 地址 + 类型解释;引用 = 更严格的别名语义
- 栈快且自动管理生命周期;堆需要你管理,建议用 RAII/智能指针
- 参数传递优先
const T&,需要“可空/可选”才用指针
参考
- C++ 声明(declarations):https://en.cppreference.com/w/cpp/language/declarations
std::unique_ptr:https://en.cppreference.com/w/cpp/memory/unique_ptr