C++ Ptr 指针详解

64次阅读
没有评论

本文系统讲解 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_castint* 当作 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::pointerto_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 或智能指针管理的动态数组。

正文完
 1
评论(没有评论)

YanQS's Blog