C++八股——智能指针

发布于:2025-05-14 ⋅ 阅读:(18) ⋅ 点赞:(0)

1. 背景

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏悬空指针等问题。

动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。

  • C++ 98中产生第一个智能指针auto_ptr
  • C++ 11起提供三个主要的智能指针,位于<memory>头文件中:
    • std::unique_ptr
    • std::shared_ptr
    • std::weak_ptr

关键特性对比

类型 所有权 复制语义 线程安全 性能开销
unique_ptr 独占 移动转移 单线程安全
shared_ptr 共享 允许复制 引用计数原子化 较高
weak_ptr 无所有权 允许复制 依赖关联shared

2. 原理与使用

智能指针的基本原理是利用RAII,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源(不需要显式地释放资源)。

如果只是简单地用类封装指针,如:

template<class T>
class Smartptr
{
public:
    Smartptr(T* ptr) : _ptr(ptr) {}
    ~Smartptr() {
        delete _ptr;
    }
    
    T& operator*() {
        return *_ptr;
    }
    
    T* operator->(){
        return _ptr;
    }
private:
    T* _ptr;
};

会出现拷贝问题:用一个智能指针赋值给另一个指针指针时,因为是浅拷贝,会将两个指针指向同一块内存,在程序结束析构智能指针时释放两次空间,导致程序崩溃。

为了解决这个问题,出现以下四类智能指针:

2.1 auto_ptr

原理

管理权转移,被拷贝对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空。保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放。

使用

auto_ptr<int> ap1(new int(1));
auto_ptr<int> ap2(ap1);
// 此时ap1悬空

模拟实现

template<class T>
class auto_ptr 
{
public:
    auto_ptr(T* ptr) : _ptr(ptr) {}
    
    auto_ptr(const auto_ptr<T>& ap) {
        _ptr = ap._ptr;
        ap._ptr = nullptr;
    }
    
    auto_ptr<T>& operator=(const auto_ptr<T>& ap) {
        // 检测是否自己给自己赋值
        if(this != &ap) {
            // 释放当前资源
            if(_ptr) delete _ptr;
            _ptr = ap._ptr;
            ap._ptr = nullptr;
        }
        return *this;
    }
    
    ~auto_ptr() {
        delete _ptr;
    }
    
    T& operator*() {
        return *_ptr;
    }
    
    T* operator->() {
        return _ptr;
    }
private:
    T* _ptr;
};

2.2 unique_ptr

原理

通过删除拷贝构造函数/赋值运算符来防止拷贝

使用

unique_ptr<int> up0 = make_unique<int>(0);
unique_ptr<int> up1(new int(1));
unique_ptr<int> up2(new int(2));
sp1 = sp2; // 报错

在函数返回unique_ptr时不要返回其引用:

auto getUnique() {
    auto ptr = std::make_unique<int>(10);
    return ptr;  // 正确:移动语义转移所有权
}

// auto& getUniqueRef() { ... }  // 错误:返回引用会导致悬空指针

模拟实现

template<class T>
class unique_ptr 
{
public:
    unique_ptr(T* ptr) : _ptr(ptr) {}
    
    unique_ptr(const unique_ptr<T>& up) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;
    
    ~unique_ptr() {
        delete _ptr;
    }
    
    T& operator*() {
        return *_ptr;
    }
    
    T* operator->() {
        return _ptr;
    }
private:
    T* _ptr;
};

2.3 shared_ptr

原理

通过引用计数的方式解决智能指针的拷贝问题:

  • shared_ptr给每个资源都维护了着一份计数用来记录该份资源被几个对象共享;
  • 在对象被销毁时(也就是析构函数调用),引用计数减一;
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,就可以释放该资源;
  • 如果引用计数不是0,就说明还有其他对象在使用该份资源,不能释放该资源。

使用

shared_ptr<int> sp1(new int(1));
cout << sp1.use_count() << endl; // 1
shared_ptr<int> sp2(sp1);
cout << sp2.use_count() << endl; // 2

注意避免混用裸指针与智能指针

int* raw = new int(5);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);  // 双重释放!

模拟实现

template<class T>
class shared_ptr
{
public:
    shared_ptr(T* ptr) : _ptr(ptr), _pcount(new int(1)) {}
   
    ~shared_ptr(){
        Release();
    }

    shared_ptr(const shared_ptr<T>& sp){
        _ptr = sp._ptr;
        _pcount = sp._pcount;  
        ++(*_pcount);
    }

    void Release() {
        if (--(*_pcount) == 0) {
            delete _pcount;
            delete _ptr;
        }
    }

    shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
        //资源地址不一样
        if (_ptr != sp._ptr) {
            Release();
            _pcount = sp._pcount;
            _ptr = sp._ptr;
            ++(*_pcount);
        }

        return *this;
    }

    int use_count() {
        return *_pcount;
    }

    // 像指针一样
    T& operator*() {
        return *_ptr;
    }

    T* operator->() {
        return _ptr;
    }

    T& operator[](size_t pos) {
        return _ptr[pos];
    }
private:
    T* _ptr;
    int* _pcount;
};

为什么引用计数要用指针,而不用成员变量或者静态成员变量?

  1. 若将引用计数作为普通成员变量:不同 shared_ptr 副本之间无法共享计数(成员变量属于对象,而非资源)。

  2. 静态变量(static)的特性:

    • 属于类本身,所有对象共享同一个静态变量。
    • 生命周期与程序一致,无法动态创建或销毁。
    • 全局唯一性,无法为每个资源单独维护计数。

    这与智能指针的资源独立性要求直接冲突:每个 shared_ptr 需要为其管理的资源单独维护引用计数,而静态变量会导致所有资源共享同一个计数器,引发严重错误。

    如果使用静态变量来计数,以下代码会出现错误:

    int main() {
        int* a = new int(10);
        int* b = new int(20);
        
        BadSharedPtr p1(a);  // count=1
        BadSharedPtr p2(b);  // count=2(错误!两个资源共享同一个计数器)
        
        p1.~BadSharedPtr();  // count=1(但 a 未被删除)
        p2.~BadSharedPtr();  // count=0,错误地删除 b,而 a 泄漏!
        return 0;
    }
    // 结果:a 内存泄漏,b 被提前释放,且程序崩溃。
    

循环引用问题

class Node;

class Parent {
public:
    std::shared_ptr<Node> child;  // Parent 持有 Node 的 shared_ptr
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

class Node {
public:
    std::shared_ptr<Parent> parent;  // Node 也持有 Parent 的 shared_ptr(循环引用)
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto parent = std::make_shared<Parent>();  // parent 引用计数 = 1
    auto node = std::make_shared<Node>();      // node 引用计数 = 1

    parent->child = node;    // node 引用计数 +1 → 2
    node->parent = parent;   // parent 引用计数 +1 → 2

    return 0;
    // main 结束时:
    // parent 引用计数 -1 → 1 → Parent 未被销毁!
    // node 引用计数 -1 → 1 → Node 未被销毁!
}

2.4 weak_ptr

原理

观察但不拥有资源,用于解决shared_ptr循环引用问题。

使用

  • 解决shared_ptr循环引用问题:

    #include <memory>
    
    class Node;  // 前置声明
    
    class Parent {
    public:
        std::shared_ptr<Node> child;
        ~Parent() { std::cout << "Parent destroyed\n"; }
    };
    
    class Node {
    public:
        // std::shared_ptr<Parent> parent;  // 循环引用导致内存泄漏
        std::weak_ptr<Parent> parent;      // 改用 weak_ptr 解决
        ~Node() { std::cout << "Node destroyed\n"; }
    };
    
    int main() {
        auto parent = std::make_shared<Parent>(); // parent 引用计数 = 1
        auto node = std::make_shared<Node>();     // node 引用计数 = 1
        
        parent->child = node;   // node 引用计数 +1 → 2
        node->parent = parent;  // weak_ptr 不会增加引用计数
        
        return 0;  // 正确析构
        // main 结束时:
        // parent 引用计数 -1 → 0 → Parent 销毁 → child 销毁 → node 引用计数 -1  → 1
        // node 引用计数 -1 → 0 → Node 销毁!
    }
    
  • 安全访问共享资源:

    #include <memory>
    #include <iostream>
    
    class Data {
    public:
        void process() { std::cout << "Processing data...\n"; }
    };
    
    int main() {
        std::shared_ptr<Data> sharedData = std::make_shared<Data>();
        std::weak_ptr<Data> weakData = sharedData;
    
        // 检查 weak_ptr 是否有效
        if (auto tmp = weakData.lock()) {  // 提升为 shared_ptr
            tmp->process();  // 安全使用
            std::cout << "Use count: " << tmp.use_count() << "\n";  // 输出 2
        } else {
            std::cout << "Data expired\n";
        }
    
        sharedData.reset();  // 释放资源
    
        if (weakData.expired()) {
            std::cout << "Data is no longer available\n";
        }
        return 0;
    }
    

模拟实现

template<class T>
class weak_ptr
{
public:
	weak_ptr() :_ptr(nullptr) {}
	weak_ptr(const shared_ptr<T>& wp) {
		_ptr = wp.get();
	}
 
	weak_ptr<T>& operator=(const shared_ptr<T>& wp) {
		_ptr = wp.get();
		return *this;
	}
 
	T& operator*() {
		return *_ptr;
	}
 
	T* operator->() {
		return _ptr;
	}
private:
	T* _ptr;
};

2.5 定制删除器

定制删除器可以解决:如何正确释放用new或者new []开辟的资源。

template<class U, class D> unique_ptr(U* p, D del);

其中Del参数是一个定制删除器,是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

// unique_ptr 自定义删除器
auto del = [](int* p) { delete[] p; };
std::unique_ptr<int[], decltype(del)> arr(new int[10], del);

参考


网站公告

今日签到

点亮在社区的每一天
去签到