本文系统讲解 C++ 中的指针,包括基础语法、指针与引用的区别、指针算术与数组、函数指针与成员指针、void* 与类型转换、现代 C++ 的智能指针与所有权模型、常见陷阱与最佳实践。配套可运行的小示例与构建指令。
目录
- 指针基础与语法
- 指针与引用的区别
- 指针算术与数组
- 函数指针与成员指针
void*与类型转换- 智能指针与所有权模型
- 内存安全:常见问题与防护
- 示例与构建
- 最佳实践与 FAQ
- 高级主题补充
- 成员指针高级用法
- 指针与多态、对象布局
- 严格别名(Strict Aliasing)与类型别名规则
- 对齐与
alignas - 原子指针与并发
- 自定义分配器与指针(fancy pointer)
- 更多 FAQ
指针基础与语法
- 声明与取地址/解引用:
int* p = &x;指向x的地址;*p解引用得到所指对象。nullptr表示空指针(C++11);避免使用NULL/。
const与指针:const int* p:指向常量(不能通过p修改目标),p可变。int* const p:指针自身为常量(不可改指向),目标可变。const int* const p:指针与目标均不可变。
- 指针大小:与平台地址宽度相关,通常 64 位系统为 8 字节。
- 对齐与未定义行为:解引用未对齐或悬挂指针是 UB;务必确保指针有效且指向适当类型。
示例:
int x = 42;
int* p = &x; // 指向 x
*p = 43; // 修改 x
int* q = nullptr; // 空指针,禁止解引用
指针与引用的区别
- 引用(
T&)必须绑定有效对象,不能为空;语法上更安全,常用于参数传递与返回。 - 指针(
T*)可为空、可重新指向、可进行算术;表达更灵活,但需自行保证有效性与生命周期。 - 常量引用用于只读视图;指针更适合表达可选/可能缺失的对象或数组遍历。
对比示例:
void inc_by_ref(int& v) { ++v; }
void inc_by_ptr(int* v) { if (v) ++*v; }
指针算术与数组
- 指针算术:
p + n跳过n个元素(按元素大小步进);仅在同一数组范围内有效。 - 数组与指针退化:函数参数中的数组会退化为指向首元素的指针
T*。 - 动态数组:
new T[n]/delete[] p;建议使用 `std::vector
` 替代,避免手工管理。
示例:
int a[3] = {1,2,3};
int* p = a; // 指向首元素
int* e = a + 3; // 尾后位置(不可解引用)
for (; p != e; ++p) { /* 遍历 */ }
函数指针与成员指针
- 函数指针:指向函数的地址,可用于回调。
- 声明:
int (*fp)(int,int) = &add;;调用:fp(1,2)。
- 声明:
- 可调用包装:
std::function<R(Args...)>(类型擦除),灵活但有开销;可直接用模板参数或auto持有可调用对象以避免开销。 - 成员指针:指向成员变量/成员函数,需要实例与
.*/->*运算符。
示例:
int add(int a, int b) { return a + b; }
int (*fp)(int,int) = &add; // 函数指针
int r = fp(1,2);
struct S {
int v{0};
int twice() const { return v*2; }
};
int S::* mp = &S::v; // 成员变量指针
int (S::* mf)() const = &S::twice; // 成员函数指针
S s{10};
int val = s.*mp; // 访问成员
int t = (s.*mf)(); // 调用成员函数
void* 与类型转换
void*:无类型指针,不能直接解引用;常用于与 C 接口交互或通用存储,使用时需转换回正确类型。- 类型转换:
static_cast<T*>:安全的编译期可检查转换(如基类指针到派生不可用)。reinterpret_cast<T*>:位级重解释,极其危险,跨类型别名可能 UB。const_cast<T*>:移除/添加const/volatile限定;对原本常量对象移除const后修改是 UB。- C 风格转换
(T*):等价于一组转换的混合,风险大,建议避免。
智能指针与所有权模型
- `std::unique_ptr
`:独占所有权,移动转移;支持定制删除器。 - `std::shared_ptr
`:共享所有权,引用计数;避免形成循环引用(使用 `std::weak_ptr` 打破)。 - `std::weak_ptr
`:弱引用,不增加计数,用于观测 `shared_ptr` 管理对象的生存期。 - `enable_shared_from_this
`:在类内部安全地获取自身的 `shared_ptr`。 - 删除器:用于资源非
delete释放场景(如文件句柄、fclose),可与unique_ptr结合。
示例(定制删除器与循环引用):
#include
<memory>
#include
<cstdio>
struct FileCloser { void operator()(FILE* f) const { if (f) std::fclose(f); } };
using FilePtr = std::unique_ptr<FILE, FileCloser>;
struct Node {
int value{};
std::shared_ptr
<Node> next; // 小心:可能形成环
std::weak_ptr
<Node> prev; // 用弱引用打破环
};
FilePtr open_file(const char* path) {
return FilePtr(std::fopen(path, "r"));
}
内存安全:常见问题与防护
- 悬挂/野指针:指向已释放或未初始化的地址;避免手动
new/delete,使用 RAII。 - 二次删除:同一资源被删除两次;
unique_ptr自动防护。 - 越界访问:指针算术越界或解引用尾后位置;严格边界检查。
- 未初始化:未赋值指针不可用;初始化为
nullptr。 - 异常下泄漏:构造后抛异常导致泄漏;使用智能指针/容器确保异常安全。
示例与构建
smart_ptr_demo.cpp
#include
<iostream>
#include
<memory>
struct Widget {
Widget(int id) : id(id) { std::cout << "Widget " << id << " constructed\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
int id;
};
int main() {
// unique_ptr:独占所有权
auto up = std::make_unique
<Widget>(1);
// 转移所有权
auto up2 = std::move(up);
std::cout << "up is " << (up ? "not null" : "null") << "\n";
// shared_ptr:共享所有权
auto sp1 = std::make_shared
<Widget>(2);
{
auto sp2 = sp1; // 计数+1
std::cout << "use_count=" << sp1.use_count() << "\n";
}
std::cout << "use_count=" << sp1.use_count() << "\n";
// weak_ptr:观测,不拥有
std::weak_ptr
<Widget> wp = sp1;
if (auto locked = wp.lock()) {
std::cout << "locked id=" << locked->id << "\n";
}
return 0;
}
构建与运行(macOS zsh):
clang++ -std=gnu++20 smart_ptr_demo.cpp -o smart_ptr_demo
./smart_ptr_demo
最佳实践与 FAQ
- 尽量使用引用表达必然存在的对象,使用指针表达可缺失或可重绑定的对象。
- 手工
new/delete能不用就不用;使用std::make_unique/std::make_shared。 - 明确所有权:接口设计时区分“拥有”与“不拥有”的指针;输入参数倾向
const T&或T*+ 空检查。 - 不返回指向局部变量的指针/引用;优先返回值或智能指针。
- 小心转换:避免
reinterpret_cast与 C 风格转换;优先static_cast并保持类型语义一致。 - 避免共享所有权泛滥:能用
unique_ptr就不要用shared_ptr;确需共享再使用,并设计好生命周期与断环策略。
高级用法补充
成员指针高级用法
- 指向成员变量类型:
T Class::*;指向成员函数类型:Ret (Class::*)(Args...) [cv] [noexcept]。 - 结合模板与别名:
template<class C, class M> using member_ptr_t = M C::*; struct S { int v; int f(double) const { return (int)v; } }; member_ptr_t<S,int> mp = &S::v; int (S::* mf)(double) const = &S::f; S s{3}; int a = s.*mp; // 访问成员 int b = (s.*mf)(1.0); // 调用成员函数 - 绑定与包装:使用
std::invoke(mf, s, args...)或std::bind_front/std::bind将成员函数包装为可调用对象。
指针与多态、对象布局
- 多态通过虚表(vtable)实现:含虚函数的类对象首部通常含指向虚表的指针(实现相关)。
- 基类指针指向派生对象时,解引用按动态类型分派虚函数;非虚成员静态绑定。
- 对象切片:以值方式赋给基类对象会丢失派生部分;用于多态时应使用指针/引用。
- 基类需要虚析构,确保通过基类指针删除派生对象时正确析构。
示例:
struct Base { virtual ~Base() = default; virtual int id() const { return 0; } };
struct Der : Base { int id() const override { return 1; } };
Base* p = new Der{}; // 多态
int x = p->id(); // 动态分派,得到 1
delete p; // 虚析构,安全
严格别名(Strict Aliasing)与类型别名规则
- 编译器假设不同不相关类型的指针不别名同一内存(允许更多优化)。
- 违规别名导致未定义行为:用
reinterpret_cast将int*当作double*解引用等。 - 允许别名的例外:通过
char*/std::byte*访问任何对象的字节表示;同一类型或相关类型(如signed/unsigned对应);std::memcpy是安全的按字节拷贝方式。 - 建议:避免跨类型直接解引用,使用序列化(按字节拷贝)或类型安全转换。
对齐与 alignas
- 指针指向对象需满足该类型的对齐要求;解引用未对齐指针可能 UB 或性能下降。
- 使用
alignas(N)指定类型或变量的对齐;使用std::aligned_alloc(C++17 起在 C 库)或对齐分配器确保对齐。
示例:
struct alignas(32) Vec4 { float x,y,z,w; };
static_assert(alignof(Vec4) == 32);
原子指针与并发
- 使用
std::atomic<T*>管理跨线程的指针读写,选择合适的内存序(memory_order)。 - 锁自由结构中常以原子指针实现栈/队列;注意 ABA 问题(用标记指针或
std::atomic<std::shared_ptr<T>>缓解)。
示例:
#include
<atomic>
struct Node { int v; Node* next; };
std::atomic<Node*> head{nullptr};
// push/pop 需使用 compare_exchange_weak/strong 并设计好内存序
自定义分配器与指针
- 标准容器支持自定义分配器;对象生命周期依赖分配器策略。
std::allocator_traits为分配/释放、构造/销毁提供统一接口;指针类型可能是“近似指针”(fancy pointer),非原生T*。- 若分配器使用 fancy pointer,需通过
allocator_traits::pointer与to_address获取原生地址。
示例:
#include
<memory>
template<class T>
T* raw(T* p) { return std::to_address(p); }
更多 FAQ
- 何时使用裸指针?
- 表示非拥有关系(观察者)或与 C 接口交互时;确保生命周期在其他实体中管理。
- 能否用
shared_ptr做所有事?- 不建议。共享所有权增加复杂度与开销;优先
unique_ptr,仅在确需共享时使用。
- 不建议。共享所有权增加复杂度与开销;优先
- 指针参数还是引用参数?
- 必须非空且不重绑定:用引用;可选或可重绑定:用指针。
- 如何避免循环引用?
- 拥有链中使用
weak_ptr打断;设计上优先树状所有权结构。
- 拥有链中使用
- 是否需要
delete自定义数组元素?- 若使用
new[],需delete[];但更推荐std::vector或智能指针管理的动态数组。
- 若使用
正文完


