C++ 杂记03 指针(二) 智能指针

news/2024/12/26 13:40:38/文章来源:https://www.cnblogs.com/sirenxie/p/18632642

C++中,智能指针与普通指针不同,是包含指针的一种类模板,用于管理动态分配的内存。智能指针的行为类似于常规指针,但是能够自动地释放所指向的对象,避免内存的泄露。智能指针通过对被引用对象进行计数的方式,或者其他机制,限制被引用的次数,避免形成循环引用。

相较于常规指针,在使用完以后,通常需要使用free或者delete释放指针内容,智能指针会自动释放其所管理的内存,防止程序员忘记释放动态分配的内存而导致内存泄漏。在发生异常的情况下,智能指针可以确保资源被正确地释放,提高程序的健壮性。例如,在函数执行过程中抛出异常,智能指针可以在栈展开时释放资源。

std::unique_ptr

std::unique_ptr是一种独占式智能指针,它所管理的对象只能有一个unique_ptr指向它。当unique_ptr被销毁时,它所指向的对象也会被销毁。unique_ptr不能被复制,但可以被移动(例如,通过返回函数或传递给函数)。

  • 独占所有权unique_ptr所指向的对象在同一时刻只能有一个unique_ptr拥有其所有权,当该unique_ptr被销毁时,它所管理的对象也会被自动销毁,从而确保资源不会泄露。
  • 轻量级unique_ptr对象本身通常只占用一个指针的空间,不会带来过多的额外开销,其大小通常与一个原生指针相当,这使得它在性能敏感的场景中非常高效。
  • 不可复制unique_ptr不支持普通的复制语义,因为复制会导致多个unique_ptr指向同一个对象,从而破坏独占所有权的语义。但它支持移动语义,可以通过移动操作将所有权从一个unique_ptr转移到另一个unique_ptr
#include<iostream>
#include<memory>
using namespace std;class Resource {
public:Resource(int val = 0) : value(val) {std::cout << "Resource constructed: " << value << std::endl;}void setValue(int val){value = val;}void showValue(){cout<<"This object value is "<<value<<endl;}~Resource() {std::cout << "Resource destroyed: " << value << std::endl;}int value;
};void example01()
{//unique_prt的基本使用// 1. 创建unique_ptr的几种方式std::unique_ptr<int> p1(new int(10));                    // 直接构造auto p2 = std::make_unique<int>(20);                     // 推荐方式(C++14)std::unique_ptr<int[]> arr = std::make_unique<int[]>(5); // 数组形式std::unique_ptr<Resource[]> arr_class(new Resource[3]); //在有默认构造函数情况下,且不需要参数的情况下,可以由编译器给初值std::unique_ptr<Resource[]> arr_Resource(new Resource[3]{Resource(1), Resource(2), Resource(3)}); //在给定参数的情况下,相当于是使用数组初始化的方式构造类的对象std::unique_ptr<Resource[]> arr_Resource2(new Resource[3]{{4},{5},{6}}); //在给定参数的情况下,给出初值,再调用类的构造函数,构造出类数组元素的对象//解引用操作与普通一致cout <<"p1指向的内容是: "<<*p1<<endl;//数组操作与普通数组一致,但是无法使用加减操作来寻址arr[2] = 100;//cout << "arr数组的3号元素值为 : "<<*(arr + 2)<<endl;  报错cout <<"arr数组3号元素指向的内容是: "<<arr[2]<<endl;//调用智能指针访问类数组的对象修改值//(arr_Resource + 2)->showValue(); 使用->调用方式会失败cout<<"修改前后的对象值: "<<endl;arr_Resource[2].showValue();arr_Resource[2].setValue(100);arr_Resource[2].showValue();//获取智能指针的原始指针cout<<"通过原始指针访问值: "<<endl;Resource* p_resource = arr_Resource.get();(p_resource+2)->showValue();//unique_ptr只能转移,不能复制std::unique_ptr<Resource[]> p2_resource = std::move(arr_Resource);cout<<"获取控制权后的智能指针访问数组: "<<endl;p2_resource[2].showValue();if(arr_Resource != nullptr){arr_Resource[2].showValue();}else{cout<<"arr_Resource指针已经被置空"<<endl;}}
int main()
{example01();return 0;
}

程序的输出为:

Resource constructed: 0
Resource constructed: 0
Resource constructed: 0
Resource constructed: 1
Resource constructed: 2
Resource constructed: 3
Resource constructed: 4
Resource constructed: 5
Resource constructed: 6
p1指向的内容是: 10
arr数组3号元素指向的内容是: 100
修改前后的对象值: 
This object value is 3
This object value is 100
通过原始指针访问值: 
This object value is 100
获取控制权后的智能指针访问数组: 
This object value is 100
arr_Resource指针已经被置空
Resource destroyed: 100
Resource destroyed: 2
Resource destroyed: 1
Resource destroyed: 6
Resource destroyed: 5
Resource destroyed: 4
Resource destroyed: 0
Resource destroyed: 0
Resource destroyed: 0

不建议使用unique_ptr指向一个已经存在的变量,这是因为一旦超出了变量的作用域,变量会被释放,造成指针空置而报错或者重复释放。

void example02()
{int data = 100; std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量std::unique_ptr<int> p2(&data); //另一个unique_ptr指针cout<<"使用p1访问内存变量: data is "<<*p1<<endl;cout<<"使用p2访问内存变量: data is "<<*p2<<endl;
}
使用p1访问内存变量: data is 100
使用p2访问内存变量: data is 100
munmap_chunk(): invalid pointer
Aborted (core dumped)

因为智能指针中,已经包含了对象自动释放机制,避免内存泄漏的情况,所以重复释放也会造成程序报错,类似于普通指针的重复释放:

void example03()
{// int data = 100; int data2 = 101;// std::unique_ptr<int> p1(&data); //使用unique_ptr指向一个已经存在的变量// std::unique_ptr<int> p2(&data); //另一个unique_ptr指针// cout<<"使用p1访问内存变量: data is "<<*p1<<endl;// cout<<"使用p2访问内存变量: data is "<<*p2<<endl;int* p3 = &data2;cout<<"使用普通指针p3访问内存变量: data2 is "<<*p3<<endl;int* p4 = &data2;cout<<"使用普通指针p4访问内存变量: data2 is "<<*p4<<endl;delete p3;
}
使用普通指针p3访问内存变量: data2 is 101
使用普通指针p4访问内存变量: data2 is 101
munmap_chunk(): invalid pointer
Aborted (core dumped)

unique_str的独占性

前面说到过,unique_str管理的对象只能保证被一个智能指针所管理,但是这个独占性还需要进一步理解。unique_ptr的"独占性"主要体现在所有权(ownership)而不是访问权限(accessibility)上。通过几个例子来说明:

#include <memory>
#include <iostream>int main() {// 1. 错误示范:两个unique_ptr试图拥有同一块内存int* raw = new int(42);std::unique_ptr<int> ptr1(raw);std::unique_ptr<int> ptr2(raw);  // 严重错误!会导致双重释放// 2. 错误示范:复制unique_ptrstd::unique_ptr<int> ptr3 = std::make_unique<int>(42);// std::unique_ptr<int> ptr4 = ptr3;  // 编译错误!unique_ptr不能被复制// 3. 合法但危险:普通指针访问unique_ptr管理的内存std::unique_ptr<int> ptr5 = std::make_unique<int>(42);int* dangerous_ptr = ptr5.get();  // 获取原始指针*dangerous_ptr = 100;             // 可以访问和修改数据// 当ptr5被销毁时,dangerous_ptr变成悬空指针!return 0;
} // ptr1和ptr2都会尝试删除同一块内存,导致未定义行为

unique_ptr具有匹配的内存释放机制,不需要类似普通指针借助delete关键字:

void ownership_example() {auto ptr = std::make_unique<int>(42);int* raw_ptr = ptr.get();  // raw_ptr只是借用访问权// ptr负责资源的释放,而raw_ptr不应该尝试删除// delete raw_ptr;  // 严重错误!
} // ptr自动释放资源
  1. unique_ptr的"独占性"是指资源所有权的独占,而不是访问权限的独占
  2. 只有拥有所有权的unique_ptr才能(也必须)负责资源的释放
  3. 其他指针可以访问同一资源,但:
    • 不能删除资源
    • 必须确保访问时资源仍然有效
    • 需要注意生命周期管理
  4. 使用get()获取原始指针时要特别小心,确保不会在unique_ptr释放后继续使用

建议:

  • 优先使用unique_ptr来管理资源
  • 谨慎使用get()
  • 明确资源的所有权
  • 如果需要共享所有权,考虑使用shared_ptr

std::shared_ptr

  1. 基本用法

在unique_ptr的基础上,其他两种指针相对比较好理解。std::shared_ptr是一种共享所有权的智能指针,允许多个shared_ptr实例共同拥有同一个对象。对象会在最后一个拥有它的shared_ptr被销毁时自动释放。

shared_ptr的声明和使用方式与unique_ptr类似:

void example03()
{//shared_ptr初始化方式与unique_prt还是比较一致的std::shared_ptr<Resource> p1(new Resource(100));p1->showValue();p1->setValue(99);p1->showValue();cout<<"当前p1管理的对应引用为: "<<p1.use_count()<<endl;std::shared_ptr<Resource> p2(p1); //使用p1初始化p2cout<<"当前p1管理的对应引用为: "<<p2.use_count()<<endl;std::shared_ptr<Resource> p3 = std::move(p1); //通过移动构造,初始化p3,p1成为空指针cout<<"当前p2管理的对应引用为: "<<p2.use_count()<<endl; 
}

被shared_ptr管理的内存数据,每多一次引用,就会增加一次引用次数。但是每释放一次智能指针,引用次数会减少。

Resource constructed: 100
This object value is 100
This object value is 99
当前p1管理的对应引用为: 1
当前p1管理的对应引用为: 2
当前p2管理的对应引用为: 2
Resource destroyed: 99
  1. 循环引用情况

使用shared_ptr如果不是很注意,会造成循环引用的情况,内存无法被真正释放,造成内存泄漏。下面对几种循环引用的情况进行说明,典型的循环引用案例包括菱形继承、相互引用这些。

  1. 直接循环引用
// A.h
class B; // 前向声明
class A {B* b_ptr; // 使用指针
};// B.h
class A; // 前向声明
class B {A* a_ptr; // 使用指针
};auto a = std::make_shared<A>();
auto b = std::make_shared<B>();a->ptr_b = b; // A holds a shared pointer to B
b->ptr_a = a; // B holds a shared pointer to A

每当一个新的 std::shared_ptr 被创建或复制时,引用计数器会增加。当一个 std::shared_ptr 被销毁或重新赋值时,引用计数器会减少。如果引用计数器变为零,则删除所管理的对象及其控制块。

而在直接循环引用中,上面代码的计数过程如下:

(1) 创建智能指针a时,A的引用次数加1,从0到1;

(2) 创建智能指针b时,B的引用次数加1,从0到1;

(3) a->ptr_b = b; 使得,b又被a的ptr复制了一次,B的引用次数加1,从1到2;

(4) b->ptr_a = a; 使得,a又被b的ptr复制了一次,A的引用次数加1,从1到2;

当我们离开 main 函数时,会发生以下事情:

局部变量 a 被销毁,这将导致 a 的引用计数减1。但是,由于 b 中的 ptr_a 仍然持有对 a 的引用,所以 a 的引用计数从2变为1,而不是0。同样地,局部变量 b 被销毁,这将导致 b 的引用计数减1。但是,由于 a 中的 ptr_b 仍然持有对 b 的引用,所以 b 的引用计数从2变为1,而不是0。因此,a 和 b 的引用计数都保持为1,而不是降为0。结果是,这两个对象永远不会被销毁,因为它们的引用计数始终大于0,从而导致内存泄漏。

  1. 父子关系中的循环引用

类的继承和包含关系也是经常被混淆的类设计关系,在使用shared_ptr不注意的话,也容易造成循环引用。

class Child;
class Parent {
public:std::shared_ptr<Child> child;~Parent() { std::cout << "Parent destroyed\n"; }
};class Child {
public:std::shared_ptr<Parent> parent;~Child() { std::cout << "Child destroyed\n"; }
};void parent_child_cycle() {auto parent = std::make_shared<Parent>();auto child = std::make_shared<Child>();// 建立双向引用parent->child = child;child->parent = parent;
} // 内存泄漏!
  1. 复杂对象关系中的循环引用
class Component;
class Object {
public:std::string name;std::shared_ptr<Component> component;Object(const std::string& n) : name(n) {std::cout << "Object " << name << " created\n";}~Object() {std::cout << "Object " << name << " destroyed\n";}
};class Component {
public:std::string name;std::shared_ptr<Object> owner;Component(const std::string& n) : name(n) {std::cout << "Component " << name << " created\n";}~Component() {std::cout << "Component " << name << " destroyed\n";}
};void complex_cycle() {auto obj = std::make_shared<Object>("MainObject");auto comp = std::make_shared<Component>("MainComponent");obj->component = comp;comp->owner = obj;
} // 内存泄漏!
  1. 使用weak_ptr解决循环引用
class SafeNode {
public:std::shared_ptr<SafeNode> next;std::weak_ptr<SafeNode> weak_next;  // 使用weak_ptr~SafeNode() {std::cout << "SafeNode destroyed\n";}
};void safe_cycle() {auto node1 = std::make_shared<SafeNode>();auto node2 = std::make_shared<SafeNode>();// 一个方向使用shared_ptr,另一个方向使用weak_ptrnode1->next = node2;node2->weak_next = node1;  // 不会增加引用计数// 使用weak_ptr时需要检查if (auto shared = node2->weak_next.lock()) {std::cout << "Node1 still exists\n";}
} // 正确释放内存

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/859280.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电脑永久免费云存储空间,什么是云存储

在当今全球化的背景下,远程连接技术的重要性愈发凸显。它使得分布在世界各地的团队成员能够实时协作,共享资源,共同推进项目的进展。无论是跨国公司的远程办公,还是国际学术研究中的数据共享,远程连接都发挥着关键的作用,促进了信息的流通和知识的传播。这次给大家介绍什…

这 30 款 IDEA 宝贝插件,顶级优秀!

这 30 款 IDEA 宝贝插件,顶级优秀!头上一片天空 Java知音 2024年12月26日 10:05 河北1、Translation源码英文翻译插件源码中很多注解都是英文,有时候看着有点费劲。这款翻译插件基本上与Idea一体化,从集成度和方便程度来说,可以吊打其他的第三方翻译软件了。不需要你切换窗…

龙哥量化:通达信文华技术指标-双均线策略叠加分时均线,量化策略思路详细分析

如果您需要代写技术指标公式, 请联系我。 龙哥QQ:591438821 龙哥微信:Long622889 也可以把您的通达信,文华技术指标改成TB交易开拓者、金字塔、文华8的自动交易量化策略 开始分享一些细致化的思路和写法,我常用的是TB交易开拓者。对量化感兴趣的朋友可以多交流 这篇介绍重…

MDS100-16-ASEMI电机专用整流模块MDS100-16

MDS100-16-ASEMI电机专用整流模块MDS100-16编辑:ll MDS100-16-ASEMI电机专用整流模块MDS100-16 型号:MDS100-16 品牌:ASEMI 封装:M18 正向电流:100A 反向电压:1600V 引脚数量:5 芯片个数:6 芯片尺寸:50MIL 漏电流:>10ua 恢复时间:>2000ns 浪涌电流:920A 芯片…

Nginx使用手册

由于格式和图片解析问题,为了更好的体验可前往 阅读原文Nginx(发音为 "engine-x")是一个高性能、开源的HTTP和反向代理服务器,也可以作为电子邮件(IMAP/POP3)代理服务器、以及通用的TCP/UDP代理服务器。它由俄罗斯的程序员Igor Sysoev创建于2002年,其目的是解…

在线性坐标系中绘制三角函数图象

本文记述了用 Matplotlib 在线性坐标系中绘制三角函数图象的例子。 代码主体内容如下: ...def main():fig, axs = plt.subplots(1, 3, figsize=(14,4.5)) #1axs[0] = configure_axes(axs[0], Trigonometric Function\t\t\t + r$sine$, 2*np.pi, 1, np.pi, np.pi/2…

美团后端暑期一面,本来收到感谢信,但又复活了!

今天来分享的是一位读者的美团暑期实习一面面经,主要是一些常规八股,难度还是有的,部分题目确实不太好回答。这位同学回答的不是很好,本来是收到感谢信了。结果,过几天又收到复活赛邀请,复活赛倒是打赢了,已oc。 1、线程池的参数/*** 用给定的初始参数创建一个新的Threa…

Makefile文件中,两个$的变量变量$$Xxx 与一个$的变量 $Xxx的区别

使用来引用 Makefile 中的变量。使用$$来引用 shell 中的变量,以确保在传递给 shell 时保留单个符号。原文地址:Makefile文件中,两个$的变量变量$$Xxx 与一个$的变量 $Xxx的区别Makefile 中的变量引用 在 Makefile 中,$ 符号用于变量替换,但它的使用方式有一些细微的区别:…

【日记】各位圣诞节快乐呀!(566 字)

正文不知道为什么最近总是做噩梦。昨天晚上梦到我一枪射死鱼儿,然后兄长用一瓶 4 块钱 1L 的冰红茶将我敲死,最后全人类死于小行星撞地球。有一颗小行星刚好降落在我家附近的山上,然后散射出了无数激光,把我家切割成一块一块的。也没塌,不知道哪个巫师用了魔法,把周围的房…

LDA主题模型——Python实现(三)

img { display: block; margin-left: auto; margin-right: auto } table { margin-left: auto; margin-right: auto } LDA假设每个文档都是多个主题的混合,每个主题又是多个词语的混合。它通过识别文档中的词语分布来推断出文档的主题结构。LDA的一个简单比喻是冰淇淋店:每个…

工具大全-dirsearch探测Web目录

目录扫描工具dirsearch非常详细使用方法dirsearch介绍dirsearch是一款开源的、基于Python开发的命令行工具,主要用于对Web服务器进行目录和文件的扫描,以发现潜在的安全漏洞。 dirsearch下载地址: https://github.com/maurosoria/dirsearch 运行环境:必须安装python3为什么…