前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C/C++开发基础——智能指针

C/C++开发基础——智能指针

作者头像
Coder-Z
发布2023-10-06 14:50:48
2270
发布2023-10-06 14:50:48
举报

一,智能指针

1.智能指针简介

智能指针是用法和行为类似于指针的类对象。

智能指针的底层对原始指针做了一定的封装。

智能指针除了像指针一样可以存储变量的地址,还提供了其他功能,比如可以管理动态内存分配,对引用进行计数等。

当智能指针所指向的变量离开了作用域或被重置时,智能指针会自动释放该变量所占用的堆内存资源。

至于为什么要引入智能指针,可以参考下面这段代码:

代码语言:javascript
复制
void func_1()
{
       Sample* obj_one = new Sample();
       if(something_wrong())
              throw exception();
       delete obj_one;
}

每次调用该函数的时候,都需要在堆中申请一段内存,然后在函数的最后释放该内存。但是当函数运行期间出现异常的时候,delete将不被执行,此时申请到的内存得不到释放,会发生内存泄露。智能指针由于是类对象,该类对象可以在析构的时候自动释放智能指针所指向的内存。因此,如果此时使用智能指针代替原始指针,可以不用手动调用"delete/delete []",智能指针指向的堆内存会自动被释放。

上述代码可以改写为:

代码语言:javascript
复制
void func_2()
{
       auto obj_two = make_unique<Sample>();
       if(something_wrong())
              throw exception();
}

当unique_ptr实例离开作用域时(代码执行到了函数末尾,或者函数抛出异常),就会在其析构函数中自动释放obj_two对象所占有的内存资源。

标准库中提供了相应的类模板,它们可以将任何数据类型封装成智能指针,使用它们时,需要引入<memory>头文件。

智能指针常用的类模板有:

std::unique_ptr<T>

std::shared_ptr<T>

std::weak_ptr<T>

由上述的类模板可以生成三种类型的智能指针实例。这三种智能指针实例的区别在于,管理原始指针的方式不一样。

shared_ptr允许多个指针指向同一个变量。

unique_ptr则独占所指向的变量。

weak_ptr则指向shared_ptr所管理的变量。

2.智能指针的基础用法

1.智能指针的初始化

智能指针是基于类模板生成的,因此,要初始化一个智能指针,就必须声明指针所指向的数据类型,不然智能指针里面包含的原始指针是个空指针。

初始化方式一,在智能指针构造函数中new一个新对象。

代码语言:javascript
复制
struct C{
    int a;
    int b;
};
std::shared_ptr<C> p1(new C);
std::unique_ptr<int> p2(new int(40));

初始化方式二,采用make_shared函数(C++11标准)、make_unique函数(C++14标准)。

代码语言:javascript
复制
std::shared_ptr<int> p3 = std::make_shared<int>(15);
std::unique_ptr<int> p4 = std::make_unique<int>(10);

智能指针在初始化时,还可以用于指向动态分配的数组。

代码样例,创建长度为10的整型数组:

代码语言:javascript
复制
//方式一
auto Array_1 = make_unique<int[]>(10);
//方式二
std::unique_ptr<int[]> Array_2(new int[10]);
//类型+[],表示初始化指向数组的智能指针
//后面的具体用法和数组类似
Array_1[0] = 1;
Array_2[0] = 2;

注意,初始化weak_ptr需要用到shared_ptr。

代码样例:

代码语言:javascript
复制
auto sh_p = make_shared<int>(40);
weak_ptr<int> wp1(sh_p); 
weak_ptr<int> wp2 = sh_p;

2.智能指针的解引用

智能指针的解引用操作与原始指针类似,可以调用"*"或"->"对智能指针进行解引用,访问分配到的堆内存地址。

但是weak_ptr不提供指针的解引用操作,即无法调用"*"或"->"获得weak_ptr所指向的变量。

完整C++代码实现:

代码语言:javascript
复制
#include <iostream>
#include <memory>
int main() {
    struct C {
        int a=1;
        int b=2;
    };
    std::shared_ptr<C> p1(new C);
    std::unique_ptr<int> p2(new int(40));
    std::shared_ptr<int> p3 = std::make_shared<int>(15);
    std::unique_ptr<int> p4 = std::make_unique<int>(10);
    std::weak_ptr<int> p5 = p3;
    std::cout << p1->a << std::endl;
    std::cout << p1->b << std::endl;
    std::cout << *p2 << std::endl;
    std::cout << *p3 << std::endl;
    std::cout << *p4 << std::endl;
    std::cout << *p5.lock() << std::endl;
    return 0;
}

运行结果:

代码语言:javascript
复制
1
2
40
15
10
15

3.unique_ptr智能指针

常用的成员函数:

get(): 返回指向变量的原始指针。

reset(): 重置智能指针,使它所持有的资源为空。

swap(): 交换两个智能指针所管理的资源。

release(): 返回指向变量的原始指针,并释放所有权。

用法说明:

reset()让unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原先占有的内存资源将被释放。

由于一个初始化后的unique_ptr独占了它所指向的变量,因此unique_ptr不支持普通的拷贝或赋值操作。

虽然不能拷贝或赋值unique_ptr,但可以通过调用release()/reset()函数将指针的所有权转移给另一个unique_ptr。

4.shared_ptr智能指针

常用的成员函数:

get(): 返回指向变量的原始指针。

reset(): 重置智能指针,使它所持有的资源为空。

swap(): 交换两个智能指针所管理的资源。

use_count(): 返回智能指针所指向变量的被引用数量。

unique(): 检查所指向的变量是否仅由当前shared_ptr的实例管理。

用法说明:

shared_ptr允许多个指针指向同一块堆内存。

shared_ptr提供了引用计数,监视当前变量正在被多少个指针实例所引用。

由于shared_ptr存在引用计数,仅在最后一个引用被销毁或重置时,该智能指针才会释放持有的内存资源。。

shared_ptr可被以下函数强制转换:

const_pointer_cast()

dynamic_pointer_cast()

static_pointer_cast()

reinterpret_pointer_cast() (C++17标准引入)

如图所示,指针p1、p2指向同一块内存地址。

5.weak_ptr智能指针

常用的成员函数:

reset(): 重置智能指针,使它所持有的资源为空。

swap(): 交换两个智能指针所管理的资源。

expired(): 检查weak_ptr所指向的资源是否有效,返回true的时候,垃圾回收进程就会清除该指针所指向的内存资源。

use_count(): 返回智能指针所指向shared_ptr的数量。

lock(): 获取weak_ptr所指向的shared_ptr实例。

用法说明:

weak_ptr不占有内存资源,但是可以指向由shared_ptr管理的内存资源。

当weak_ptr指向shared_ptr时,是弱共享shared_ptr,并不会使shared_ptr的引用计数增加。

weak_ptr的出现可以帮助开发者解决智能指针使用期间发生的"循环引用"问题。

完整C++代码实现:

代码语言:javascript
复制
#include <iostream>
#include <memory>
using namespace std;
void Check(weak_ptr<int>& wp) {
    shared_ptr<int> sp = wp.lock(); //获得shared_ptr<int>实例
    if (sp != nullptr)
        cout << "still " << *sp << endl;
    else
        cout << "pointer is invalid." << endl;
}
int main() {
    shared_ptr<int> sp1(new int(40));
    shared_ptr<int> sp2 = sp1;
    weak_ptr<int> wp = sp1; //wp指向shared_ptr<int>实例
    cout << *sp1 << endl;
    cout << *sp2 << endl;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    Check(wp);
    sp1.reset();
    /*
    cout << sp1 << endl;  
    返回:00000000
    */
    cout << *sp2 << endl;
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;
    cout << sp2.unique() << endl;
    Check(wp);
    sp2.reset();
    cout << sp2.use_count() << endl;
    Check(wp);
    return 0;
}

运行结果:

代码语言:javascript
复制
40
40
2
2
still 40
40
0
1
1
still 40
0
pointer is invalid.

6.智能指针的复制和移动

unique_ptr不支持复制、赋值等操作,它只能被移动,而移动操作经常借助std::move函数来实现。

std::move可以把一个智能指针所占有的资源转移给另一个智能指针。

shared_ptr包含一个显式的构造函数,可用于将右值unique_ptr转换为shared_ptr。转换成功以后,shared_ptr将接管unique_ptr所占有的所有资源。因此,如果unique_ptr为右值(可以粗略理解为,位于赋值符号的右边)的时候,可以将其赋值给shared_ptr。

复制操作会让shared_ptr所指向变量的引用计数加1,而移动操作不会让shared_ptr所指向变量的引用计数加1。

1.unique_ptr转shared_ptr代码样例:

代码语言:javascript
复制
std::shared_ptr<int> p1 = std::make_unique<int>(66);

完整C++代码实现:

代码语言:javascript
复制
#include <iostream>
#include <memory>
int main() {
    std::shared_ptr<int> p1 = std::make_unique<int>(66);
    std::cout << *p1 << std::endl;
    std::cout << p1.use_count() << std::endl;
    return 0;
}

运行结果:

代码语言:javascript
复制
66
1

2.移动操作代码样例:

代码语言:javascript
复制
//p1指向该int对象
shared_ptr<int> p1(new int(40));

//移动构造p2
//p1指向的内存为空,p2指向该int对象
shared_ptr<int> p2(std::move(p1));

//p2指向的内存为空,p3指向该int对象
shared_ptr<int> p3;
p3 = std::move(p2);

//整个过程中,int对象的引用计数保持在1

完整C++代码实现:

代码语言:javascript
复制
#include <iostream>
#include <memory>
int main() {
    std::shared_ptr<int> p1(new int(40));
    std::shared_ptr<int> p2(std::move(p1));
    std::shared_ptr<int> p3;
    p3 = std::move(p2);
    std::cout << p3.use_count() << std::endl;
    std::shared_ptr<int> p4;
    std::shared_ptr<int> p5;
    p4 = p3;
    p5 = p3;
    std::cout << p3.use_count() << std::endl;
    p3.reset();
    p3 = std::make_unique<int>(10);
    std::cout << p3.use_count() << std::endl;
    return 0;
}

运行结果:

代码语言:javascript
复制
1
3
1

7.关于new/delete的补充说明

显式地调用new和delete不仅会增加代码的复杂度,还会引发内存泄露等风险。

如果必须要使用new/delete,可以考虑以下措施来避免:

1.尽可能使用栈内存

栈内存不会造成内存泄露,且资源一旦超出栈的使用范围就会被销毁。

2.使用make functions在堆上分配资源

例如,使用std::make_unique<T>或std::make_shared<T>来实例化资源,然后将它包装成一个资源管理对象去管理资源以及智能指针。

3.尽量使用容器(标准库中的容器,Boost中的容器等)

容器会对其元素进行存储空间的管理,这些官方容器都实现了自己的内存管理逻辑,避免内存出问题。

至于为何要采用上述规则,后面会新增动态内存分配和右值引用等相关章节来详细说明。

二,参考阅读

《C++ Primer》

《C++代码整洁之道》

《C++高级编程》

《C++新经典》

https://www.apiref.com/cpp-zh/cpp/memory.html

https://learn.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-03-13 08:00:00,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员与背包客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档