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,则删除

基础对象)

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

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

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

相关推荐
gma9992 分钟前
brpc 与 Etcd 二次封装
数据库·c++·rpc·etcd
ö Constancy6 分钟前
设计LRU缓存
c++·算法·缓存
p-knowledge13 分钟前
建造者模式(Builder Pattern)
java·开发语言·建造者模式
网络安全(king)18 分钟前
【Python】【持续项目】Python-安全项目搜集
开发语言·python·安全
工业甲酰苯胺19 分钟前
Python脚本消费多个Kafka topic
开发语言·python·kafka
麻花201330 分钟前
C#之WPF的C1FlexGrid空间的行加载事件和列事件变更处理动态加载的枚举值
开发语言·c#·wpf
_黎明43 分钟前
【Swift】字符串和字符
开发语言·ios·swift
C++忠实粉丝1 小时前
计算机网络socket编程(2)_UDP网络编程实现网络字典
linux·网络·c++·网络协议·计算机网络·udp
聊无生1 小时前
JavaSrcipt 函数高级
开发语言·前端·javascript
Mongxin_Chan1 小时前
【Cpp】指针与引用
c++·算法