1423 字
7 分钟
C++ 指针:从语法到内存直觉

这篇文章整理自学习笔记与 The Cherno 的讲解思路:不只背语法,而是建立“内存层面的直觉”。

1. 指针:内存的导航仪#

1.1 指针的本质#

可以把指针理解成:

  • 一个“地址”(通常你会看到它像一个整数一样打印出来)
  • 加上一个“类型解释规则”(告诉编译器:解引用时按什么类型读写)
一句话直觉

指针的值是地址;指针的类型决定了解引用 * 时读/写多少字节,以及如何解释这段比特。

pointer-intuition.cpp
int value = 42;
int* ptr = &value; // ptr 存的是 value 的地址
*ptr = 7; // 解引用:去这个地址写入 7
// value == 7
TIP

sizeof(ptr) 永远是“指针本身大小”(32 位常见 4 字节,64 位常见 8 字节); sizeof(*ptr) 才是“指向的数据类型大小”。

1.2 两个核心操作符:& 和 *#

  • &(取地址):拿到变量的内存地址
  • *(解引用):沿着地址去读/写那块内存
address-and-deref.cpp
int a = 5;
int* p = &a; // p 指向 a
*p = 2; // 修改 a
// a == 2

1.3 指针可以“改指向”#

repoint.cpp
int a = 5;
int b = 8;
int* ptr = &a;
*ptr = 2; // a = 2
ptr = &b; // ptr 改为指向 b
*ptr = 1; // b = 1

2. 引用:变量的替身(通常是指针的语法糖)#

2.1 引用是什么#

引用(reference)更像“别名”:你给某个变量起了另一个名字。

reference-alias.cpp
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)#

  • 分配/释放非常快(通常就是移动栈指针)
  • 生命周期跟作用域走:离开 {} 就销毁
stack.cpp
void foo() {
int x = 123; // x 在栈上
} // 作用域结束,x 自动销毁

3.2 堆(Heap)与 new/delete#

  • 堆上分配更慢,但生命周期不受作用域影响
  • 你必须手动释放,否则会内存泄漏
heap-single.cpp
int* p = new int(42); // 分配并初始化一个 int
delete p; // 释放
p = nullptr; // 习惯性置空,避免悬空指针

动态数组要用 new[] / delete[] 配对:

heap-array.cpp
// 需要 <cstring>
char* buffer = new char[8];
memset(buffer, 0, 8);
delete[] buffer;
buffer = nullptr;
WARNING

new 对应 deletenew[] 对应 delete[]。配错是未定义行为(UB)。

IMPORTANT

现代 C++ 更推荐用 RAII/智能指针来管理堆资源(比如 std::unique_ptr),尽量避免裸 new/delete

4. 参数传递:值 / 指针 / 引用#

这里是理解“性能 + 行为”的关键。

4.1 传值(Pass by Value)#

pass-by-value.cpp
void Increment(int value) {
value++;
}
int main() {
int a = 1;
Increment(a);
// a 仍然是 1
}

4.2 传指针(Pass by Pointer)#

pass-by-pointer.cpp
void Increment(int* value) {
(*value)++;
}
int main() {
int a = 1;
Increment(&a);
// a 变为 2
}

4.3 传引用(Pass by Reference)#

pass-by-reference.cpp
void Increment(int& value) {
value++;
}
int main() {
int a = 1;
Increment(a);
// a 变为 2
}
最佳实践

传大对象时,优先用 const T&:既不复制,又不允许函数改动入参。

5. const 与指针:两种“const”别搞反#

const-pointer.cpp
int a = 1;
int b = 2;
const int* p1 = &a; // 指向 const:不能通过 p1 改 a,但 p1 可以改指向
// *p1 = 3; // error
p1 = &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 不要返回局部变量的指针/引用#

dangling-return.cpp
int* Bad() {
int x = 123;
return &x; // 错:x 离开作用域就销毁,返回的是悬空指针
}
CAUTION

这类 bug 往往“偶尔才炸”,因为你指向的是一块已经被复用的栈内存。

6.3 强转指针类型很危险#

int* 强转成 double* 再解引用,可能会出现越界读取/未对齐访问等问题。

6.4 优先用智能指针表达所有权#

如果你的指针“拥有”一块资源(需要负责释放),优先用智能指针把所有权写进类型里:

smart-pointers.cpp
#include <memory>
struct Entity {
int x{};
};
int main() {
auto e = std::make_unique<Entity>();
// 离开作用域自动释放
}

7. 调试建议:把问题拉回“地址 + 生命周期”#

排查指针问题时,永远先问三件事:

  1. 这个地址来自哪里?(谁分配的/谁拥有它)
  2. 指向的对象现在还活着吗?(是否已经离开作用域/已 delete)
  3. 这个类型解释对吗?(是否配套 new[]/delete[],是否越界)

如果你用的是 Visual Studio:

  • 监视窗口看指针值(地址)
  • 内存窗口用地址直接观察那段内存

总结#

  • 指针 = 地址 + 类型解释;引用 = 更严格的别名语义
  • 栈快且自动管理生命周期;堆需要你管理,建议用 RAII/智能指针
  • 参数传递优先 const T&,需要“可空/可选”才用指针

参考#

C++ 指针:从语法到内存直觉
https://fuwari.vercel.app/posts/指针/
作者
某不知名的根号三
发布于
2026-01-26
许可协议
CC BY-NC-SA 4.0