C++基础整理(3)之智能指针

C++基础整理(3)

注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构



提示:本文为 C++ 中常用的 智能指针 的用法和举例


一、智能指针 in C++

   C++的智能指针拥有一种用于自动管理动态分配内存的机制,它能够在适当的时候自动释放所关联的对象的内存,从而防止忘记手动释放可能带来的内存泄漏。C++11 标准库提供了三种智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr

简要原理:智能指针本质上是一个自定义的类,它接收一个普通指针作为构造函数的参数。在智能指针的析构函数中,它会负责释放该普通指针。由于智能指针的实例是开辟到栈区上使用的,所以当它们所在的函数或作用域结束时,会自动触发其析构函数,从而确保所管理的资源得到释放。

1、unique_ptr

unique_ptr 是一种独占所有权的智能指针,同一时间只能有一个 unique_ptr 指向某个对象(某块内存)。当 unique_ptr 被销毁(例如离开其作用域)时,它所指向的对象也会被自动删除/释放。
unique_ptr的内部实现通常包含一个原始指针和一个删除器(deleter),原始指针指向实际管理的对象,而删除器则是一个函数对象或函数指针用于在 unique_ptr 被销毁时释放所指向的对象(具体不深入,感兴趣可看源码)。注意unique_ptr 禁止复制/赋值操作(即禁止拷贝构造),只允许通过移动语义操作来实现。

创建方式:
(1)可以直接使用智能指针的构造函数来创建一个unique_ptr。
(2)使用库自带的std::make_unique函数(C++14以上)。

// 使用智能指针的构造函数来创建 unique_ptr 
	std::unique_ptr<MyClass> uptr(new MyClass(arg1, arg2, ...));  
//使用自带函数来创建 unique_ptr
	std::unique_ptr<MyClass> uptr = std::make_unique<MyClass>(arg1, arg2, ...);  

代码示例:

class MyClass {  
public:  
    MyClass(int value) : m_value(value) {}  
    ~MyClass() { std::cout << "MyClass destroyed" << std::endl; }  
    void printValue() const { std::cout << "Value: " << m_value << std::endl; }  
private:  
    int m_value;  
};  
  
int main() {  
	std::unique_ptr<MyClass> ptr(new MyClass(10)); 
    // 也能使用自带std::make_unique 创建 unique_ptr(但得C++14 起)  
    std::unique_ptr<MyClass> ptr_2 = std::make_unique<MyClass>(10);  
    ptr->printValue(); // 输出: Value: 10  
  
    // ptr 离开作用域时,MyClass 对象会被自动删除  
    // 输出: MyClass destroyed  
  
    return 0;  
}

2、关于unique_ptr 的拷贝禁止

std::unique_ptr 的一个重要特性是它不支持复制(copy)操作,即你不能创建一个新的 unique_ptr2 来复制一个已存在的 unique_ptr1的值。这是因为 std::unique_ptr 强调独占所有权的概念:一个时刻只能有一个 unique_ptr 拥有某个对象的所有权。如果允许复制,那么可能会导致多个 std::unique_ptr 实例同时认为自己拥有同一个对象,进而在它们各自的析构函数中尝试删除同一个对象,这会导致双重释放(double free)问题,是未定义行为(undefined behavior)。

举个例子来说明 std::unique_ptr 不支持复制的特性:

int main() {  
    std::unique_ptr<int> ptr1(new int(5)); // 创建一个指向 int 的 unique_ptr  
    std::unique_ptr<int> ptr2 = ptr1; // 错误:尝试复制 unique_ptr  
    return 0;  
}

上面的代码在尝试将 ptr1 复制给 ptr2 时会编译失败,因为 std::unique_ptr 没有定义复制构造函数或复制赋值运算符

如果你确实需要将一个 unique_ptr1 的“复制”给另一个 unique_ptr2,应该使用移动语义move)来操作,移动操作可以将所有权从一个 unique_ptr1 转移到另一个 unique_ptr2 且原始 unique_ptr1 将不再拥有任何对象,并会变为 null。移动(发生所有权转移)是通过 std::move 函数来完成的。(关于移动构造,这里先不讲)

下面是一个 unique_ptr 使用移动的例子:

int main() {  
    std::unique_ptr<int> ptr1(new int(5)); // 创建一个指向 int 的 unique_ptr  
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 将 ptr1 的所有权移动到 ptr2  
    // 此时 ptr1 不再拥有任何对象,ptr2 拥有对象的所有权  
    return 0;  
}

在这个例子中,std::move 函数被用来将 ptr1 的所有权移动给 ptr2。移动操作之后,ptr1 不再指向任何对象,而 ptr2 则拥有了原来 ptr1 所指向的对象的所有权。注意,移动操作之后,原 unique_ptr(在这个例子中是 ptr1)会变成空(null)。总结来说,unique_ptr 不支持复制是为了保证对象所有权的独占性和防止双重释放问题。当你需要将所有权从一个 std::unique_ptr 转移到另一个时,应该使用移动(move)操作而不是复制操作。

2、 shared_ptr

shared_ptr (共享指针)是一个共享所有权的智能指针,允许多个 shared_ptr 实例共享同一个对象(指向同一块内存)。每个 shared_ptr 维护一个引用计数,当最后一个指向对象的 shared_ptr 被销毁或重置时,对象才会被删除(即释放内存)
shared_ptr 的内部实现使用引用计数来跟踪指向同一对象的指针数量。当一个新的智能指针指向某个对象时,引用计数会增加;当智能指针不再指向该对象或被销毁时,引用计数会减少。当引用计数归0时,意味着没有智能指针再指向该对象,此时智能指针会自动释放该对象所占用的内存。

创建方式:
(1)可以直接使用智能指针的构造函数来创建一个shared_ptr。
(2)使用库自带的std::make_shared函数(C++14以上)。

// 使用智能指针的构造函数来创建 shared_ptr 
	std::shared_ptr<MyClass> sptr(new MyClass(arg1, arg2, ...)); 
// 使用自带函数来创建 shared_ptr 
    std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>(arg1, arg2, ...);

示例:

class MyClass {  
public:  
    MyClass(int value) : m_value(value) {}  
    ~MyClass() { std::cout << "MyClass destroyed" << std::endl; }  
    void printValue() const { std::cout << "Value: " << m_value << std::endl; }  
private:  
    int m_value;  
};  
  
int main() {  
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10);//也可以是std::shared_ptr<MyClass> ptr1(new MyClass(10));   
    std::shared_ptr<MyClass> ptr2 = ptr1; // ptr2 共享 ptr1 的所有权  
  
    ptr1->printValue(); // 输出: Value: 10  
    ptr2->printValue(); // 输出: Value: 10  
  
    // 当 ptr1 和 ptr2 都离开作用域时,MyClass 对象才会被删除  
    // 输出: MyClass destroyed  
  
    return 0;  
}

3、关于shared_ptr的线程安全性

关于 std::shared_ptr 的线程安全性,有以下几点需要注意:

(1)shared_ptr内部的引用计数是原子的操作,所以是线程安全的:
这意味着多个线程可以同时修改 shared_ptr 的引用计数,而不会导致数据竞争(data race)。这是通过原子操作实现的,原子操作保证了在任何时候只有一个线程可以修改引用计数。

(2)同一个shared_ptr被多线程读,线程安全:
读取 shared_ptr 的值(即它所指向的对象)通常被认为是线程安全的,因为读取操作不会修改 shared_ptr 的状态。但是,需要注意的是,如果读取操作之后紧接着使用对象(例如调用对象的方法),那么对对象的操作是否线程安全则取决于对象本身的线程安全性。

(3)同一个shared_ptr被多线程写,不是线程安全:
这里的“写”通常指的是修改 shared_ptr 指向的值。多个线程尝试同时修改同一个 shared_ptr 的值会导致数据竞争,这是不安全的。因此,应该避免多个线程同时修改同一个 shared_ptr。

(4)共享引用计数的不同的shared_ptr被多线程写,是线程安全:
当多个线程同时操作不同的 shared_ptr 实例,而这些 shared_ptr 实例共享同一个引用计数时,操作是线程安全的。因为引用计数的修改是原子的,所以即使多个线程同时增加或减少引用计数,也不会导致数据损坏或不一致。

4、 weak_ptr

std::weak_ptr 是一种不控制对象生命周期的智能指针,它是对 std::shared_ptr 所管理对象的一个弱引用,不会增加对象的引用计数。weak_ptr不能直接访问它所指向的对象,只能观察shared_ptr的引用计数情况,以防止出现死锁状况,以解决两个 shared_ptr 之间的循环引用问题(如果两个shared_ptr相互引用,它们的引用计数将永远无法降至0,从而导致资源无法被释放。)。
当最后一个 shared_ptr 不再指向对象时,无论有多少个 weak_ptr 指向对象,对象都会被删除。即使某块内存同时被shared_ptr和weak_ptr所引用,只要所有shared_ptr都被析构了,无论是否还有weak_ptr引用该内存,该内存最终都会被释放。
因此,weak_ptr并不保证它所指向的内存始终有效,在使用weak_ptr之前,必须检查它是否已变为空指针

代码示例(解决循环引用):

class B;//先声明一下
class A {  
public:  
    std::shared_ptr<B> b_ptr;  
    ~A() { std::cout << "A destroyed" << std::endl; }  
};  
  
class B {  
public:  
    std::weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用  
    ~B() { std::cout << "B destroyed" << std::endl; }  
};  
  
int main() {  
    {  
        std::shared_ptr<A> a = std::make_shared<A>();  
        std::shared_ptr<B> b = std::make_shared<B>();  
        a->b_ptr = b;  
        b->a_ptr = a;  
        // a 和 b 离开作用域时,由于 a_ptr 是 weak_ptr,不会造成循环引用,A 和 B 都会被正确删除  
    }  
    // 输出: B destroyed  
    // 输出: A destroyed  
  
    return 0;  
}

推荐优先使用 std::make_unique 和 std::make_shared 来创建智能指针,它们比直接使用智能指针的构造函数更安全、更高效。(make_shared有能够对共享对象只分配一次内存的优点,而显式构造函数初始化(使用new)至少得通过两次分配内存,可能会导致内存溢出问题。)

总结

  • 35
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值