RAII智能指针学习记录部分

发布时间 2023-06-02 10:34:05作者: 阳光中的影子

RAII:资源的有效期与持有资源的对象的声明周期严格绑定,即由对象的构造函数完成资源的分配,析构函数完成资源的释放。

RAII具有异常安全,当发生异常时自动调用已创建对象的析构函数。

struct C {
    C();                // 默认构造函数

    C(C const &c);                // 拷贝构造函数
    C(C &&c);            // 移动构造函数 (C++11 引入)

    C &operator=(C const &c);    // 拷贝赋值函数
    C &operator=(C &&c);        // 移动赋值函数 (C++11 引入)

    ~C();                // 解构函数
}; 

 四个构造函数及三五法则:

#include<string>
class ccpp{
public:
    //编译器默认生成的拷贝构造
    ccpp(ccpp const &other):name(other.name),age(other.age){}
    //编译器默认生成的移动构造
    ccpp(ccpp &&other):name(std::move(other.name)), age(std::move(other.age)){}
    //编译器默认生成的拷贝赋值,赋值是用等号
    ccpp &operator=(ccpp const &other){//重载等号运算符
        name=other.name;
        age=other.age;
        return *this;
    }
    //编译器默认生成的移动赋值
    ccpp &operator=(ccpp &&other){
        name=std::move(other.name);
        age=std::move(other.age);
        return *this;
    }
private:
    std::string name;
    int age;
};

这里想要提一下拷贝构造函数和拷贝赋值函数的不同点。拷贝构造函数是在类尚未初始化的时候,将另一个已经存在的类实例拷贝进来以初始化当前类实例,而拷贝赋值函数是当前类实例已经被初始化了,将当前的类实例销毁,同时将另一个已存在的类实例拷贝进来。所以前者应该比后者更高效,因为执行更少的操作。赋值构造≈析构+拷贝构造。

首先是三五法则中的如果自定义了析构函数一定要自定义或者删除拷贝构造或拷贝赋值函数,如果不自己构造或者删除编译器会生成默认的相应函数

#include<iostream>
#include<string>
struct vector{
    size_t m_size;
    int *m_data;
    
    vector(size_t n):m_size(n){
        m_data=(int*) malloc(n*sizeof(int));
    }
    
    ~vector(){
        free(m_data);
    }
    size_t size(){
        return m_size;
    }
    int &operator[](size_t index){
        return m_data[index];
    }
    void resize(size_t size){
        m_size=size;
        m_data=(int*)realloc(m_data,size);
    }
};
int main(){
    vector v1(2);
    vector v2=v1;
    return 0;
}

上面的代码中实现了一个vector的结构体,其中自定义了析构函数来释放分配的内存,没有定义或者删除拷贝构造函数和拷贝赋值函数,当执行语句vector v2=v1的时候,编译器会对v1进行浅拷贝(因为传递的是引用),退出main()函数时,v1和v2被析构会分别执行两次析构函数,但是由于是浅拷贝,v2和v1中变量在内存中的位置是一样的,析构一次后再对已经析构过的地址析构,堆空间中已经没有内存和指针相对应了,就会出现double free的错误。如果类的数据成员中没有指针,这样浅拷贝是可行的。

解决方法有三个,其一是删除拷贝赋值和拷贝构造函数,其二是删除析构函数,在构造函数中使用智能指针管理内存, 其三是使用智能指针管理vector结构体的对象

 

 

此外,如果自定义了析构函数、拷贝构造数和拷贝赋值函数,那么移动构造函数和移动赋值函数可能会被阻止,使得效率降低,因此如果注重效率或者想要移动语义,最好同时定义五个函数。

 

智能指针:c++中规定除了对智能指针的拷贝,都默认深拷贝

unique_ptr

std::unique_ptr<C> p=std::make_unique<C>();

unique_ptr删除了类的拷贝函数,解决重复释放的问题。使用如下的方式将会报错,因为传值需要调用拷贝构造函数,而这个智能指针的拷贝构造函数是delete的

 如果想要在func中调用智能指针管理的vector类的对象的成员函数,有三种方法,1.使用p.get()获取对象的原始指针,并将原始指针传递给func函数,2.将对象p的权限转移给全局变量,3.权限转移给全局变量后,原来的对象p就会失效,如果还想对p做一些事情,可以提前保存p的原始指针,然后在移交给的全局变量失效前p的原始指针是可以使用的。这部分具体可以见这篇文章

shared_ptr

shared_ptr通过引用计数的方式解决重复释放的问题,初始化一个shared_ptr时,计数器初始化为1,拷贝一次计数器增加1,析构一次计数器减1,当计数器减为0时自动销毁其所指向的对象。

shared_ptr的优缺点:使用方便,可以减少使用者出错的概率,但可能存在循环引用的问题,例如下面的代码:

#include <memory>
struct C {
    std::shared_ptr<C> m_child;
    std::shared_ptr<C> m_parent;
}; 
int main() {
    auto parent = std::make_shared<C>();
    auto child = std::make_shared<C>();
    // 建立相互引用
    parent->m_child = child;
    child->m_parent = parent;
parent
= nullptr; // parent 不会被释放,child 还指向它 child = nullptr; // child 不会被释放,parent 还指向它 return 0; // 完了,直到main函数退出,这两块内存都没有被释放。 }

 

c++中weak_ptr可以解决unique_ptr循环引用的问题

弱引用weak_ptr可以打破交叉引用,因为弱引用不会让计数器加1,即不改变引用计数。当使用弱引用指针访问成员函数时,需要使用olck()函数,提升为强引用。在使用时,把不需要具备所属权的对象改为weak_ptr即可

使用weak_ptr解决上述shared_ptr中循环引用的方案如下:

1.对于上述案例,可以规定子对象从属于父对象,父对象拥有子对象,所以父对象对子对象的引用是shared_ptr,子对象对父对象的引用可以是weak_ptr。

struct C {
    std::shared_ptr<C> m_child;
    std::weak_ptr<C> m_parent;  //只需要把结构体中子对象对父对象的引用更改为弱引用即可
}; 

weak_p.expired() //进行失效检测,失效会返回true,lock()返回nullptr

shared_ptr所管理的对象的生命周期取决于所有引用中最长寿的那个;

unique_ptr所管理的对象的生命周期长度就是他所属的唯一一个引用的寿命。

原始指针相当于unique_ptr的弱引用,二者常一起使用,shared_ptr和weak_ptr一起使用

 2.使用unique_ptr和原始指针管理对象

#include <memory>
struct C { 
    std::unique_ptr<C> m_child;
    C *m_parent;
};
int main() {
    auto parent = std::make_unique<C>();
    auto child = std::make_unique<C>();
    // 建立相互引用:
    parent->m_child = std::move(child);  // 移交 child 的所属权给 parent
    child->m_parent = parent.get();

    parent = nullptr;  // parent 会被释放。因为 child 指向他的是原始指针
    // 此时 child 也已经被释放了,因为 child 完全隶属于 parent
    return 0;
}