C++智能指针

写在前面

C++中的对象生命周期
生命周期:程序执行时,对象存在的一段时间

  • 全局对象:生命周期从程序开始启动时分配,在程序执行结束时销毁
  • 局部自动对象:生命周期从进入其所在的程序块时开始,在离开程序块时销毁。(自动对象:只存在于块执行期间的对象)
  • 局部static对象:生命周期从第一次使用前进行分配,在程序结束时销毁,该局部静态的生命周期贯穿于函数执行结束后的一段时间。
    说明:如果局部静态对象定义时,未初始化,它将执行值初始化,内置类型的局部静态对象初始化为0
    比较经典的一个例子是:在函数fun中定义了一个静态局部变量,只有在第一次调用时初始化,之后再进行调用时,使用之前的结果,并不进行初始化
#include<bits/stdc++.h>

using namespace std;

int fun(){
    static int a = 1;
    ++a;
    return a;
}

int main(){
    cout << fun() << endl;
    cout << fun() << endl;
    cout << fun() << endl;
    return 0;
}

程序运行结果:

2
3
4

说明:的确在后面每一次进行调用fun函数时,局部静态变量 a 都是在上一次的基础上进行操作的。

  • 动态分配对象:生命周期与在哪里被创建无关,只有被手动释放时才会销毁。
    动态对象是否能正确的释放关乎程序的正确性,这里用智能指针来管理动态对象,以确保动态分配对象能够正确的释放,程序安全的执行

动态内存

  • 动态内存的分配:new ,返回一个指向该对象的指针

  • 动态内存的释放:delete,销毁该对象,释放与之关联的内存

  • 动态内存存在的问题:内存泄漏、引用非法内存的指针
    内存泄漏:程序运行结束后,忘记释放内存空间。(例如:对于动态分配的内存,如果在对该指针指向的内存释放前,程序发生了异常,且异常未捕获,那么该内存就永远不会释放了,但是使用智能指针,就会释放掉该内存。)
    引用非法指针:目前依然有指针指向该内存空间,但是该内存空间已经释放

  • 说明:动态内存在分配时的初始化方式:
    默认情况下,是默认初始化的,如果该对象是内置类型或者组合类型,那么该对象的值是未定义的;如果该对象是类类型,则用类的构造函数进行默认初始化
    直接初始化、列表初始化

  • 说明:动态内存的释放
    delete可以释放指针指向的是由new动态分配的内存或者空指针,若释放不是由new动态分配的指针所指向的内存或者相同指针释放多次其行为是未定义的。
    还有一点:动态内存必须主动释放,否则离开其指针所在的作用域后,该指针被销毁,但是该指针指向的内存不会被释放。
    在这里插入图片描述

为了解决动态内存在使用时,存在的问题,确保程序更安全的使用动态内存,就有了智能指针的概念

智能指针

  • 智能指针的作用:管理动态对象
  • 类型:共享指针、独占指针、弱指针
    共享指针:shared_ptr,允许多个指针指向同一对象
    独占指针:unique_ptr,独占指向的对象
    弱指针:weak_ptr,指向shared_ptr所管理的对象
  • 智能指针存在于memory库中,智能指针也是模板,类似于vector,在定义时需指名其指针指向的类型

shared_ptr

可以有多个shared_ptr指向同一对象

make_shared

int *pnum = new int(10); //申请一个整数空间,整数的值为10==>pnum指向值为10的整型指针
shared_ptr<int> pnum1 = make_shared<int>(10);//pnum1指向值为10的shared_ptr
shared_ptr<int> pnum3(new int(10));//正确
shared_ptr<int> pnum4 = new int(10);//错误,必须使用直接初始化方式
auto pnum2 = make_shared<int>(10);//同上一行代码

说明:make_shared类似容器中的emplace。
emplace_back ,emplace , emplace_front分别对应push_back, insert, push_front
区别在于调用emplace时,是将参数传递给该元素类型的构造函数

解释:shared_ptr pnum4 = new int(10);为什么是错误的?
智能指针的构造函数是explicit的,不支持隐式转化。对于只传一个参数的构造函数,或者除第一个参数外其他参数在定义构造函数时都赋了默认值,如果构造函数不声明成explicit,可以用等号和括号进行初始化(string s(“hello”); 或者 string s1 = “hello”; 都正确),等号初始化的形式会进行隐式转化成括号的形式。参考:更多explicit细节
在这里插入图片描述

shared_ptr关联一个计数器,称为引用计数,该计数器统计他所指向对象的引用计数

auto p = make_shared<int>(10);
p = pnum1;

说明:当将pnum1赋值给p时,p的原指向对象的引用计数会递减1, pnum1指向对象的引用计数会递增1,当一个shared_ptr所指向的对象引用计数递减为0时,它就会自动释放自己所管理的对象. 在该例子中p指向的对象已经没有引用者,会自动释放,是通过该对象的析构函数来实现的。

智能指针使用时,应当注意的问题:
1.不能使用内置指针来初始化一个智能指针
2.不delete 通过get()方法返回的指针
3.不使用get()方法初始化或者reset一个智能指针,如果使用了,当最后一个对应的智能指针销毁后,你的指针就无效了
4.如果使用智能指针管理不是new分配的内存,要定义一个删除器

说明:内置指针 普通指针 智能指针
内置指针:指向哪些内置类型变量的指针,内置类型:int,float,string
普通指针:普通类型(非内置类型)的指针 myclass1* ptr1, myclass2* ptr2…这些就是普通指针。

unique_ptr

只能有一个unique_ptr指向给定的对象

  • 没有make_shared函数返回一个unique_ptr
  • 只能用直接初始化的形式(括号的形式)来初始化一个独占指针
  • 不支持对unique_ptr拷贝、赋值,因为它是独占嘛!!但有一个例外:当独占指针即将被销毁时可以进行拷贝操作,也就是从函数返回一个unique_ptr是可以的
  • 支持将unique_ptr的所有权从一个独占指针转化到另一个指针,通过reset或者release方法
unique_ptr<int> p(new int(10));
unique_ptr<int> p1(p.release());//所有权从p转向p1,p置为空指针
unique_ptr<int> p2(new int(100));
p1.reset(p2.release());//所有权从p2转向了p1,reset释放了p1原来指向的内存,p2置为空指针

weak_ptr

不控制所指向对象的生存周期的弱指针,指向一个shared_ptr所指向的内存,但不增加shared_ptr的引用计数,一旦shared_ptr指向的内存空间被销毁,weak_ptr随之被销毁。

  • 由于weak_ptr指向的对象可能不存在,必须使用lock函数来获得该共享对象的shared_ptr,如果调用lock函数结果为空,说明共享指针指向的对象不存在,否则返回其共享指针
  • 作用:解决shared_ptr中的循环引用的问题

参考链接
举例:
出现循环引用的问题:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    std::shared_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 2
    }
    std::cout << wpp.use_count() << std::endl;  // 1
    std::cout << wpc.use_count() << std::endl;  // 1
    return 0;
}

说明:当智能指针指向的空间计数减为0之后,析构时,该空间才会会释放。而上述程序造成了一个僵局,析构对象时,先析构c(c是后创建的,处于栈的顶部),但发现,c所指向的空间还在被p使用,于是其计数减减后为1;同样,释放p时,p所指向的空间还在被c使用,所以c的计数减减后是1,也不释放。于是p在等待着c释放空间,c也在等待着p释放空间,谁也不释放,将造成了程序执行结束后,动态分配的空间无法释放,内存泄漏的结果。

正确的写法:将其中一个类的成员变量改成weak_ptr:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    return 0;
}

说明:在p、c离开其作用域时,先执行c的析构函数,引用计数-1将其指向的空间释放掉,同时p所指向的空间的引用计数减1,然后再执行p的析构函数,引用计数-1,释放其指向的空间。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章