重温C和C++之智能指针

简介: C++进阶之智能指针

导读

《C++之指针扫盲》一文中我们对指针进行了讲解,虽然原始指针是几乎无所不能,的确是一把利器,但就是这样的一把利器让多少人既爱又恨,一不小心就杀敌一千,自损八百,无论你是
多么的严谨,总是很难从根本上避免内存泄漏。

有没有好的方式去用好这把利刃而又不伤手呢?带着手套不就行了么。。。

RAII

在C程序中有一条行规是:

谁开发谁保护,谁污染谁治理

所以我们在很多库的API中经常发现一些传递二级指针的alloc函数和一些对应的xxx_free函数,这就是遵循谁开发谁保护,谁污染谁治理的原则。

在进入智能指针话题之前我们先来了解下RAII

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

堆指针在C/C++实在是太灵活,但是每次使用完毕后都需要程序员手动地去释放它,但是程序员们往往会忘记释放它,又或者是命名写了释放的代码,但是因为各种执行的异常情况,导致到释放资源的代码根本没有执行到,特别是引入了异常机制后的C++更是如此。
因此为了解决这些问题,c++之父给出了解决问题的方案:
使用RAII,它充分的利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。

一句话总结起来就是在构造函数中去申请资源,在析构函数中去释放资源。

以下展示了一个简单的RAII的例子:

class Student{
public:

    Student():name(new string("张三")){

    }

    ~Student(){
        // 声明周期结束自动释放指针
        delete name;
        name = nullptr;
    }

public:
    const string *name;
};

int main() {
    Student student;
//    delete student.name; //  不需要手动释放内部的指针
    return 0;
}

咋一看,这RAII确实是个好东西呀,既然有了RAII还需要智能指针干嘛呢?难道C++造物主也要扛KPI???

智能指针

C++11推出了三种智能指针,它们分别是unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr废弃掉。既然auto_ptr已经废弃掉了,这里就不再讨论了,
感兴趣的童鞋可以自行查阅auto_ptr的相关隐患来了解下它为什么会被替代掉。

智能指针,既然是智能的,为什么需要三种呢?一种万能的不就可以了吗?存在即合理,unique_ptr、shared_ptr和weak_ptr三种智能指针在不同的场合区分使用更能提升我们程序的强壮性。

当需要使用智能指针的时候包含头文件<memory>即可。

1、unique_ptr

std::unique_ptr是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,也就是拷贝构造函数,赋值运算符这些相对于std::unique_ptr来说是不可用的了。
如下例子可以验证这点:

int main() {
    unique_ptr<int> p1(new int(1));
    unique_ptr<int> p2 = p1; // 错误,unique_ptr不允许共享内部的原始指针
    unique_ptr<int> p3(new int(3));
    p3 = p1;               //错误,unique_ptr不允许共享内部的原始指针
    unique_ptr<int> p4 = std::move(p1);// 可以,但是此时p1是不可用的,p1中的指针已经转移到了p4
    std::cout << "p1:" << *p1 << std::endl; // 获取不到值
    return 0;
}

注意,在C++11 没有提供std::make_unique,它是在C++14之后才提供的。

2、shared_ptr

显然std::unique_ptr智能指针的独占性在在一些场合是无法满足开发者们的需求的,此时就需要shared_ptr登场了,shared_ptr是基于引用计数的一种智能指针,
shared_ptr内部维护着一个原始指针和一个引用计数的指针,当shared_ptr发生析构销毁的时候,shared_ptr会将引用计数减去1,如果引用计数不为0则不会销毁原始指针,直到引用计数
为0才会销毁这个原始指针。

相对于std::unique_ptr来说,shared_ptr的拷贝构造函数和赋值运算符是可以正常使用的,同时在C++11中就提供了make_shared函数,至于std::make_unique为什么到了C++14才提供,
俺也不知道,俺也不敢问呀....

shared_ptr可以通过use_count获取内部的原始指针的引用计数。

shared_ptr可以通过get函数获取到内部的原始指针,但是这是一件很疯狂的事情,意味着你需要对你自己做的事情负责....

int main() {
    shared_ptr<int> p1 = make_shared<int>(1);
    shared_ptr<int> p2 = p1; // 可以
    shared_ptr<int> p3(p1);
    std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 3
    std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 3
    std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 3
    p1.reset(); // p1重置
    std::cout << "#######################" << std::endl;
    std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 0
    std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 2
    std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 2
    std::cout << "*p1:" << *p1<< std::endl; // 无法获取到值
    return 0;
}

当然shared_ptr除了上面例子中用到的成员函数,还有其他的成员函数,大家需要多动手敲敲才能实践出真知。

与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

int main() {
    unique_ptr<int[]> up(new int[10]); // 正确
    shared_ptr<int[]> sharedPtr1(new int[10]); // 错误
    shared_ptr<int> sharedPtr(new int[10], [](int *p) {
        std::cout << "释放指针" << std::endl;
        delete[] p;
    });  // 正确,要许自定义指针释放函数
    return 0;
}

3、weak_ptr

有了shared_ptrunique_ptr看来和内存泄漏说再见真是指日可待呀。

我们看看以下的例子,为什么他们的构造函数没有被调用?

main.cpp
using namespace std;
class A;
class B;
class A{
public:
    A(){

    }
    ~A(){
        std::cout << "A被析构" << std::endl;
    }
public:
    shared_ptr<B> ptrB;
};

class B{
public:
    B(){

    }
    ~B(){
        std::cout << "B被析构" << std::endl;
    }
public:
    shared_ptr<A> ptrA;
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->ptrB = pb;
    pb->ptrA = pa;
    std::cout << "pa.use_count:" << pa.use_count() << std::endl;
    std::cout << "pb.use_count:" << pb.use_count() << std::endl;
    return 0;
}

很明显,已经发生了循环引用了,所以导致main函数执行完毕之后智能指针papb依然保持着1个引用计数,所以导致A和B都没有执行析构函数。

这是就需要weak_ptr来解决循环引用的问题了,weak_ptr是一种若引用的指针,它一般是搭配shared_ptr使用,但是它不会增加shared_ptr的引用计数,也就是说虽然我拥有你,但是我并限制你。。。

但也就是因为weak_ptr没有强引用,所以有可能在weak_ptr需要使用原始指针的时候,原始指针已经被别人释放掉了,所以在使用weak_ptr获取原始值之前需要使用lock校验一下。

weak_ptr一般不会单独使用,它一般都会使用一个shared_ptr来初始化它。

针对以上程序我们使用weak_ptr修复一下即可:

using namespace std;
class A;
class B;
class A{
public:
    A(){

    }
    ~A(){
        std::cout << "A被析构" << std::endl;
    }
public:
    weak_ptr<B> ptrB;
};

class B{
public:
    B(){

    }
    ~B(){
        std::cout << "B被析构" << std::endl;
    }
public:
    weak_ptr<A> ptrA;
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->ptrB = pb;
    pb->ptrA = pa;
    std::cout << "pa.use_count:" << pa.use_count() << std::endl;
    std::cout << "pb.use_count:" << pb.use_count() << std::endl;
    shared_ptr<B> lock = pa->ptrB.lock(); // 如果已经被释放weak_ptr lock会返回空
    if(nullptr != lock){

    }
    return 0;
}

4、返回自己的智能指针

假如你希望通过智能指针来管理你的资源,在书写一个类的成员函数时你希望不返回原始的指向自己的指针,你希望是返回一个shared_ptr那么请看以下写法是否正确?

class A{
public:
    shared_ptr<A> getPtr(){
        return make_shared<A>();
    }
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<A> ptr = pa->getPtr();
    std::cout << "pa.use_count" << pa.use_count() << std::endl; // 打印1
    std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 打印1
    return 0;
}

上面的写法会有什么问题呢?智能指针pa和智能指针ptr的引用计数都是1,但是他们却引用了同一个对象,这是很危险的,既然智能指针pa和智能指针ptr是相互独立的,并没有实现真正意义上的共享,
一旦他们当中的其中一个被析构,另外一个再去获取对象时就会发生意外。

如果想要在类的成员函数内返回自己的共享智能指针的话需要继承std::enable_shared_from_this<T>

class A:public std::enable_shared_from_this<A>{
public:
    shared_ptr<A> getPtr(){
        return shared_from_this();
    }
};

int main() {
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<A> ptr = pa->getPtr();
    std::cout << "pa.use_count" << pa.use_count() << std::endl; // 正常,打印2
    std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 正常,打印2
    return 0;
}

经过修改后我们发现智能指针pa和智能指针ptr实现了共享,他们的引用计数变成了2。

5、shared_ptr是线程安全的吗
这是一个面试官经常喜欢问的一道面试题。
shared_ptr内部的引用计数是线程安全的,但是shared_ptr的指针不是线程安全的。大家可以想象下为什么shared_ptr引用计数要设计成线程安全的呢?

智能指针使用的建议

1、在使用指针指针时尽量使用标准库提供的初始化方法,不到万不得已,不要使用new的方式产生智能指针。
这是因为 只有当一切构造动作都完成了,析构函数才有可能被调用。如果智能指针构造失败,那new方式传递进去的指针就不会被析构函数释放。

这条在《C++ Primer》一书中有说:

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

2、尽量不要获取智能指针内部的原始指针,在《C++ Primer》一书中有提到:

使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

3、智能指针作为函数参数,能传引用则传递引用,否则使用值传递,虽然之间传递原始指针可能会损耗更加小的内存,但是这样会加大犯错的概率,
不太建议。

4、优先使用unique_ptr指针指针,如果确实需要共享的才使用shared_ptr,如果存在这循环引用的对象则shared_ptr搭配weak_ptr使用。

5、更多关于智能指针的资料,笔者建议童鞋们看看《Effective Modern C++》这本书的第四章内容。

目录
相关文章
|
16天前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
18 1
|
17天前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
20 2
|
19天前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
24天前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
1月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
27 3
|
19天前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
1月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
1月前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
2月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
29 1
|
2月前
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针