C++智能指针

目录

内存管理问题

动态内存管理经常会出现两种问题:

① 忘记释放内存,会造成内存泄漏

② 尚有指针引用内存的情况下就释放了它,产生指针悬挂

RALL

RAII是C++中的一个惯用法,即"Resource Acquisition Is Initialization",翻译为"资源获取就初始化"。**在构造函数中申请分配资源,在析构函数中释放资源。**因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。RAII的核心思想是将资源或者状态与对象的生命周期绑定,通过C++的语言机制,实现资源和状态的安全管理,智能指针是RAII最好的例子

智能指针

智能指针本质上是一个对象,里面封装了普通指针,就是RALL技术

智能指针就可以方便我们控制指针对象的生命周期。在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理

unique_ptr 独享指针

unique_ptr没有复制构造函数,不支持普通的拷贝和赋值操作

unique最常见的使用场景,就是替代原始指针 ,为动态申请的资源提供异常安全保证。

只要unique_ptr创建成功,unique_ptr对应的析构函数都能保证被调用,从而保证申请的动态资源能被释放掉。

cpp 复制代码
class MyClass {
public:
    MyClass() 
    ~MyClass()
};

int main() {
    std::unique_ptr<MyClass> ptr1(new MyClass()); // 创建独占指针

    // std::unique_ptr<MyClass> ptr2 = ptr1; // 错误,不能复制
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 移动所有权
	//nuique_ptr是一个模板类,Myclass是指针管理的对象资源是模板类的参数,这两个告诉编译器怎么编译
	//move(ptr1)将ptr1转换成右值,返回一个右值引用,ptr2 将接管 ptr1 所指向的对象的所有权。之后,ptr2 成为唯一拥有该对象的指针。ptr1指空
	
    if (!ptr1) {
        std::cout << "ptr1 is now null\n"; // ptr1 已经失去所有权
    }

    return 0; // 当ptr2超出作用域时,MyClass会被自动销毁
}

shared_ptr 共享指针

允许多个指针指向同一个对象。

当对象的所有权需要共享(share)时,share_ptr可以进行赋值拷贝,每一个shared_ptr的拷贝都指向相同的内存

内部使用计数机制 维护,每使用他一次,内存的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。

cpp 复制代码
class MyClass {
public:
    MyClass() 
    ~MyClass() 
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); // 使用make_shared创建共享指针 
    //初始化智能指针ptr1,引用为1
    {
        std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权
        //ptr1 的引用计数从 1 增加到 2。
		//ptr2 现在也与 ptr1 共享这个计数器,所以它们的引用计数都是 2。
        std::cout << "Reference count: " << ptr2.use_count() << "\n"; 
		//ptr2是在{}定义的,作用域在{}内
    } // ptr2超出作用域,它的析构函数被调用,引用计数减少到 1,失去对资源的引用。
    //ptr1的作用域是整个main函数
    std::cout << "Reference count after ptr2 is out of scope: " << ptr1.use_count() << "\n"; // ptr1仍保持对资源的引用,技术1

    return 0; // 当ptr1超出作用域时,MyClass会被自动销毁
}

weak_ptr
weak_ptr 比较特殊,它主要是为了配合shared_ptr而存在的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。

cpp 复制代码
#include <iostream>
#include <memory>//是 C++ 标准库中的一个头文件,用于提供智能指针的定义和功能。

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // 共享指针指向 B
    ~A()
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 弱指针指向 A
    ~B()
};

int main() {
    // 创建 A 的共享指针
    std::shared_ptr<A> a = std::make_shared<A>();
    
    // 创建 B 的共享指针,并将其保存到 A 的成员中
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;

    // 将 B 中的 weak_ptr 指向 A
    b->a_ptr = a; // 此时 a 和 b 之间建立了弱引用关系

    std::cout << "Reference count of A: " << a.use_count() << "\n"; // 输出引用计数为 1
    std::cout << "Reference count of B: " << b.use_count() << "\n"; // 输出引用计数为 1

    // weak_ptr 不增加引用计数
    if (auto a_locked = b->a_ptr.lock()) { // 尝试从 weak_ptr 获取 shared_ptr
        std::cout << "A is still alive\n";
    } else {
        std::cout << "A is already destroyed\n";
    }
	/*.lock() 方法:

当你调用 lock() 方法时,它会尝试获取一个指向 weak_ptr 管理的对象的 shared_ptr。
如果 weak_ptr 所指向的对象仍然存在(即至少有一个 shared_ptr 指向它,引用计数大于 0),则 lock() 会返回一个有效的 shared_ptr,指向该对象。
如果 weak_ptr 所指向的对象已经被销毁(即引用计数为 0),lock() 会返回一个空的 shared_ptr,可以通过检查返回值来判断对象是否仍然有效。*/
	
    // 当 a 和 b 超出作用域时,A 和 B 的析构函数会被调用
    return 0; 
}

weak_ptr存在的意义到底是什么呢?

weak指针的出现是为了解决shared指针循环引用造成的内存泄漏的问题 。由于shared_ptr是通过引用计数来管理原生指针的,那么最大的问题就是循环引用(因为它们都在互相等待对方先释放,所以造成内存泄漏。),这样必然会导致内存泄露(无法删除)。而weak_ptr不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。

为什么shared_ptr共享指针会出现循环引用问题,以及怎么解决

当两个或多个对象通过shared_ptr相互引用时,它们的引用计数会互相增加,从而导致内存无法被释放。

比如说:

两个类,a和b都被创建,并且它们互相持有对方的shared_ptr。

a的引用计数和b的引用计数都增加到2。

当main结束时,a和b的引用计数不会归零,导致它们永远存在于内存中,造成内存泄漏。

解决:

将其中一个对象的指针改为weak_ptr,以打破循环引用。

a和b的引用计数可以正常减少到0,从而调用它们的析构函数,正确释放内存。

make_shared与显式通过构造函数初始化(new)的shared_ptr区别?

显式通过构造函数初始化:

cpp 复制代码
std::shared_ptr<MyClass> ptr(new MyClass(constructor_args));

这种方式显式地使用new来创建对象并将其传递给shared_ptr。在这种情况下,指针内存 的分配和控制块 的分配是分开的。

使用 new 来创建对象时,会单独为对象分配内存。之后,shared_ptr 的控制块 会进行另一轮内存分配,以存储引用计数和其他管理信息。这意味着至少需要两次内存分配。

new 来创建对象额外开销

性能开销 :

由于显式初始化需要两次内存分配,这会增加性能开销,特别是在需要频繁创建和销毁对象的场景中。

每次内存分配都需要调用操作系统的内存分配器,这会增加开销并导致性能下降。
内存溢出问题 :

如果在高频率分配内存的情况下,由于系统的内存管理策略,可能会导致内存碎片的增加,进而提高内存溢出的风险。频繁的内存分配和释放会导致内存不可用的情况,从而引发 std::bad_alloc 异常。

make_shared:

cpp 复制代码
auto ptr = std::make_shared<MyClass>(constructor_args);

make_shared会为对象的构造分配内存,并返回一个shared_ptr,同时在单个内存分配中管理对象和控制块(用于引用计数等信息)。

make_shared 的优点:

make_shared 的优点:

只进行一次内存分配,性能更优。

降低了内存碎片的可能性,减少了内存溢出的风险。

提供了更好的异常安全性,确保内存管理更加高效。

因此,在实际开发中,推荐使用 make_shared 来创建 shared_ptr,以提高性能和内存管理的安全性。

智能指针的实现?(share)

原理 :智能指针是一个类,这个类的构造函数中传入一个普通指针 ,析构函

数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程

序)结束时会自动被释放

➢ 智能指针(smart pointer)的通用实现技术是使用引用计数。智能指针类

将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指

针指向同一对象。

+1:

每次创建类的新对象时,初始化指针就将引用计数置为 1;当对象作为另

一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计

数;

-1:

对一个对象进行赋值:(如果引用计数为减至 0,则删除对象),并增加右

操作数所指对象的引用计数;(只能转移,不能拷贝,因此赋值运算会使左

操作数-1,右操作数+1)

调用析构函数时,析构函数减少引用计数(如果引用计数减至 0,则删除

基础对象)

注意:为了实现智能指针的效果,必须借助一个计数器,以便随时获知有多

少智能指针绑定在同一个对象上。显而易见,这个计数器不应该是智能指针

这个类的一部分。(指针内存和控制块分开创建)这是因为,如果计数器是智能指针类的一部分,那么每次增减计数器的值,都必须广播到每一个管理着目标对象的智能指针。这样做的代价太大了。

相关推荐
加载中loading...14 分钟前
Linux线程安全(二)条件变量实现线程同步
linux·运维·服务器·c语言·1024程序员节
Wx120不知道取啥名17 分钟前
C语言之长整型有符号数与短整型有符号数转换
c语言·开发语言·单片机·mcu·算法·1024程序员节
Python私教44 分钟前
Flutter颜色和主题
开发语言·javascript·flutter
代码吐槽菌1 小时前
基于SSM的汽车客运站管理系统【附源码】
java·开发语言·数据库·spring boot·后端·汽车
biomooc1 小时前
R语言 | paletteer包:拥有2100多个调色板!
r语言·数据可视化·1024程序员节
Ws_1 小时前
蓝桥杯 python day01 第一题
开发语言·python·蓝桥杯
Hello.Reader1 小时前
FFmpeg 深度教程音视频处理的终极工具
ffmpeg·1024程序员节
zdkdchao1 小时前
jdk,openjdk,oraclejdk
java·开发语言
神雕大侠mu2 小时前
函数式接口与回调函数实践
开发语言·python
Y.O.U..2 小时前
STL学习-容器适配器
开发语言·c++·学习·stl·1024程序员节