c++11新特性——智能指针

智能指针

智能指针的发展历史

智能指针的发展历史可以追溯到C++语言对内存管理问题的不断探索和改进。以下是智能指针的主要发展阶段:

一、早期尝试与std::auto_ptr

时间背景:C++98/03标准时期。

特点:std::auto_ptr是C++98标准中定义的第一个智能指针模板。它管理通过new获得的地址,当对象过期时,其析构函数将使用delete来释放内存。
缺陷
拷贝或赋值会改变资源的所有权,这导致在STL容器中使用std::auto_ptr存在重大风险,因为容器内的元素必须支持可复制和可赋值。不支持数组对象的管理。

ps:所以并不建议使用此指针,指针悬空非常危险

二、std::unique_ptr的引入

时间背景:C++11标准。
特点:std::unique_ptr对其持有的堆内存具有唯一拥有权,即引用计数永远是1。std::unique_ptr对象销毁时会释放其持有的堆内存。
优势
禁止复制语义,只能通过移动构造来转移所有权,这避免了多个智能指针指向同一个资源的问题。提供了更安全、更明确的内存管理方式。

三、std::shared_ptr与std::weak_ptr的推出

时间背景:C++11标准。

std::shared_ptr特点

  • 实现了引用计数机制,可以自动追踪和管理指向堆上对象的引用计数。
  • 当引用计数变为零时,智能指针会自动释放内存。
  • 适合用来管理那些生命周期不确定的资源,比如在类中管理成员变量的生命周期,或者在函数间传递资源所有权。

std::weak_ptr特点

  • 为解决std::shared_ptr可能产生的循环引用问题而设计下面会解释此问题。
  • 提供了一种不增加引用计数的引用方式,使得std::shared_ptr对象间可以相互查看,但不会阻止它们各自作用域结束时资源的释放。

四、智能指针的进一步发展与完善

C++14及以后:C++14标准中添加了std::make_unique方法,使得创建std::unique_ptr更加安全和便捷。

性能与优化:随着C++标准的发展,智能指针的性能也得到了不断优化,以更好地满足高性能应用的需求。

多线程支持:虽然std::shared_ptr本身不是线程安全的,但可以通过额外的同步机制(如锁)来实现线程安全的智能指针。

auto_ptr

一、基本特性

定义:auto_ptr是一种智能指针(虽然也没那么智能),它是“它所指向的对象”的拥有者。这种拥有具有唯一性,即一个对象只能有一个auto_ptr所指向它,严禁一物二主。
在这里插入图片描述
在这里插入图片描述

原理:auto_ptr的实现原理是RAII(Resource Acquisition Is Initialization),在构造的时候获取资源,在析构的时候释放资源。简单说就是构造的时候加入进去管理,智能指针销毁的时候一起销毁管理的内容

接口:auto_ptr的接口行为与普通指针相似,提供了operator*和operator->来访问其所指向的对象。但它没有定义任何的算术运算(包括++)。
二、使用方式

构造:auto_ptr的构造函数被声明为explicit,因此不能通过隐式转换来构造,只能显式调用构造函数。例如:

std::auto_ptr<int> pa(new int(10));

赋值:当auto_ptr以传值方式被作为参数传递给某函数时,这时对象的原来拥有者(实参)就放弃了对象的拥有权,把它转移到被调用函数中的参数(形参)上。赋值操作同样会转移拥有权。
这种情况下就会造成原指针悬空,不能再使用
例如

std::auto_ptr<int> pa(new int(10));
std::auto_ptr<int> pb;
pb = pa; // 此时pb拥有对象,pa不再拥有

销毁:当auto_ptr指针被摧毁时,它所指向的对象也将被隐式销毁。即使程序中有异常发生,auto_ptr所指向的对象也将被销毁。

三、注意事项

  • 不要共享所有权:不要让两个auto_ptr指向同一个对象,否则会导致重复释放或访问已被释放的内存。

  • 不要指向数组:auto_ptr是通过delete而不是delete[]来释放其所拥有的资源的,因此不能指向数组。

  • 不要作为容器元素:STL容器中的元素经常要支持拷贝和赋值等操作,而auto_ptr在这些过程中会传递所有权,导致source与sink元素之间不等价,因此不要将auto_ptr作为标准容器的元素。

  • 避免异常安全问题:虽然auto_ptr可以在一定程度上解决内存泄漏问题,但在构造函数中申请多个资源时,如果其中一个资源申请失败,则可能导致已申请的资源无法释放。因此,在使用auto_ptr时需要谨慎处理异常安全问题。

ps:正因如此,不建议使用此老版本智能指针,实在是不太智能,很多公司明确规定不能使用,所以了解一下就好了

unique_ptr

概念

unique_ptr是C++11中引入的一种智能指针,用于管理动态分配的内存。它的主要特点是独占所有权,即每个unique_ptr实例拥有并管理所指向对象的唯一所有权。这种独占性确保了内存资源的安全释放,避免了内存泄漏和悬空指针问题。

在这里插入图片描述
在这里插入图片描述

定义

unique_ptr的定义在头文件中,它是一个模板类,可以管理任何类型的动态分配对象。其定义如下:

template< class T, class Deleter = std::default_delete<T> > class unique_ptr;

其中,T是unique_ptr所管理对象的类型,Deleter是一个删除器,用于在unique_ptr销毁时释放对象。默认情况下,使用std::default_delete作为删除器。

删除器是一个函数对象(可以是函数指针、函数对象或lambda表达式),它定义了当unique_ptr销毁时如何释放其所管理的资源。默认情况下,unique_ptr使用std::default_delete<T>作为其删除器,该删除器通过调用delete来释放对象。但在某些情况下,可能需要自定义删除器来执行特定的清理操作,例如释放动态分配的内存(free等)、关闭文件、释放锁等。

语法

unique_ptr的基本语法包括创建、访问、移动、释放等操作。以下是一些常用的语法示例:

创建unique_ptr

std::unique_ptr<int> ptr(new int(10)); // 创建并管理一个int类型的动态分配对象

访问对象

int value = *ptr; // 使用解引用操作符访问对象

移动unique_ptr

std::unique_ptr<int> ptr2 = std::move(ptr); // 将ptr的所有权转移到ptr2,ptr变为空

释放对象

ptr.reset(); // 释放ptr所管理的对象,使ptr变为空

自定义删除器

void my_deleter(int* ptr) {
    // 自定义删除逻辑
    delete ptr;
}

std::unique_ptr<int, decltype(&my_deleter)> ptr_with_deleter(new int(20), &my_deleter); // 使用自定义删除器

使用案例

以下是一个使用unique_ptr管理动态分配内存并避免内存泄漏的示例:

#include <iostream>
#include <memory>
 
void process(std::unique_ptr<int> ptr) {
    // 对ptr所指向的对象进行处理
    std::cout << "Value: " << *ptr << std::endl;
    // 当process函数结束时,ptr会自动销毁并释放所管理的内存
}
 
int main() {
    std::unique_ptr<int> ptr(new int(10)); // 创建unique_ptr并管理动态分配的内存
    process(std::move(ptr)); // 将ptr的所有权转移到process函数中
    // 此时main函数中的ptr已经为空,不再拥有所管理的内存
    return 0;
}

注意事项

  • 独占所有权:unique_ptr的独占所有权意味着它不能被复制,只能被移动。因此,在传递unique_ptr时,需要使用std::move来转移所有权。

  • 避免悬空指针:由于unique_ptr在销毁时会自动释放所管理的内存,因此可以避免悬空指针问题。但是,如果手动调用了reset方法或转移了所有权,需要确保不再访问已释放的内存

  • 自定义删除器:虽然unique_ptr提供了默认的删除器,但在某些情况下,可能需要使用自定义删除器来执行特定的清理操作。

  • 不要与裸指针混用:为了避免内存泄漏和悬空指针问题,建议始终使用unique_ptr来管理动态分配的内存,而不是直接使用裸指针。

  • 注意作用域:unique_ptr的作用域结束时,它会自动销毁所管理的对象。因此,在定义unique_ptr时,需要注意其作用域范围,以确保在适当的时候释放内存。

shared_ptr

概念

std::shared_ptr 是 C++ 标准库中定义的一种智能指针,它通过引用计数机制来管理动态分配的内存。多个 shared_ptr 可以指向同一个对象,当所有的 shared_ptr 都被销毁或者不再指向该对象时,该对象会自动释放。

在这里插入图片描述
在这里插入图片描述

语法结构

#include <memory>

std::shared_ptr<Type> ptr1(new Type());  // 使用原始指针
std::shared_ptr<Type> ptr2 = std::make_shared<Type>();  // 使用 make_shared 创建

常用函数和操作

  • reset(): 重置 shared_ptr,使其不再指向原来的对象,可以选择性地将它指向一个新的对象。
  • get(): 返回指向对象的原始指针。
  • use_count(): 返回当前共享该对象的 shared_ptr 数量。
  • unique(): 如果当前 shared_ptr 是唯一指向对象的指针,返回 true。
  • swap(): 交换两个 shared_ptr。
  • operator*() 和 operator->(): 用于访问 shared_ptr 所管理的对象。

删除器的使用

在 std::shared_ptr 中,删除器(deleter)是用于指定在智能指针销毁时要执行的自定义删除操作。shared_ptr 会在对象不再被任何 shared_ptr 所管理时自动调用删除器来释放资源。通常,shared_ptr 会调用 delete 来销毁对象,但有时你可能需要一个不同的销毁逻辑(比如使用自定义内存管理、释放额外的资源等),这时就可以使用删除器。

1. 使用删除器的基本方式

要在 shared_ptr 中使用删除器,你需要在创建智能指针时传入一个自定义删除器。这个删除器通常是一个函数、函数对象或 Lambda 表达式,用来替代默认的 delete 操作。

示例:使用自定义删除器
下面是一个简单的示例,展示如何在 shared_ptr 中使用自定义删除器:

#include <iostream>
#include <memory>

// 示例类
class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

// 自定义删除器
void custom_deleter(MyClass* ptr) {
    std::cout << "Custom deleter called\n";
    delete ptr;
}

int main() {
    // 创建 shared_ptr,并指定 custom_deleter 作为删除器
    std::shared_ptr<MyClass> ptr(new MyClass(), custom_deleter);

    // 离开作用域时,ptr 会调用 custom_deleter 来释放内存
    return 0;
}

输出

MyClass constructor
Custom deleter called
MyClass destructor

解释
当创建 shared_ptr 时,你通过构造函数传递了一个原始指针 (new MyClass()) 和一个自定义删除器 custom_deleter。
当 shared_ptr 超出作用域时,它会自动调用删除器,而不是默认的 delete,因此调用 custom_deleter 函数。
在 custom_deleter 中,首先输出一条消息,然后使用 delete 删除对象。

2. 使用 Lambda 表达式作为删除器

除了定义普通的删除器函数,你还可以使用 Lambda 表达式来作为删除器,这种方式更加灵活,尤其是当删除逻辑简单或者依赖外部变量时。你可以在创建 shared_ptr 时直接嵌入删除器逻辑。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

int main() {
    // 使用 Lambda 表达式作为删除器
    std::shared_ptr<MyClass> ptr(new MyClass(), [](MyClass* ptr) {
        std::cout << "Lambda deleter called\n";
        delete ptr;
    });

    // 离开作用域时,ptr 会调用 Lambda 删除器
    return 0;
}

输出

MyClass constructor
Lambda deleter called
MyClass destructor

3. 使用函数对象作为删除器

你还可以使用自定义的函数对象(也叫做仿函数)作为删除器。它是一个类,该类实现了 operator(),使得实例可以像函数一样调用。

#include <iostream>
#include <memory>

// 自定义删除器函数对象
struct MyDeleter {
    void operator()(MyClass* ptr) const {
        std::cout << "MyDeleter called\n";
        delete ptr;
    }
};

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor\n"; }
    ~MyClass() { std::cout << "MyClass destructor\n"; }
};

int main() {
    // 使用自定义的函数对象作为删除器
    std::shared_ptr<MyClass> ptr(new MyClass(), MyDeleter());

    // 离开作用域时,ptr 会调用 MyDeleter 来释放内存
    return 0;
}

输出

MyClass constructor
MyDeleter called
MyClass destructor

4. 通过删除器释放资源

删除器不仅限于释放内存,它还可以用于执行其他资源清理工作,比如释放文件句柄、网络连接、数据库连接等。比如:

#include <iostream>
#include <memory>
#include <fstream>

// 自定义删除器:关闭文件流
void file_deleter(std::ofstream* file) {
    if (file->is_open()) {
        std::cout << "Closing file\n";
        file->close();
    }
    delete file;
}

int main() {
    // 创建并管理文件流的 shared_ptr
    std::shared_ptr<std::ofstream> file(new std::ofstream("example.txt"), file_deleter);
    
    // 写入文件
    *file << "Hello, World!" << std::endl;

    // 离开作用域时,文件流会被删除器处理并关闭
    return 0;
}

输出

Closing file

5. 总结

  • 删除器的作用:自定义删除器允许你在 shared_ptr 销毁时执行额外的清理操作,替代默认的 delete 操作。
  • 用法:删除器可以是普通函数、函数对象或 Lambda 表达式。
  • 应用场景:删除器不仅适用于内存释放,还可以用来管理其他资源的释放(如文件、网络连接等)。

使用案例

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructor\n"; }
    ~MyClass() { std::cout << "MyClass Destructor\n"; }
    void say_hello() { std::cout << "Hello from MyClass\n"; }
};

int main() {
    // 使用 make_shared 创建 shared_ptr
    std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();

    // 调用成员函数
    p1->say_hello();
    std::cout << "use_count: " << p1.use_count() << "\n";  // 输出引用计数

    {
        std::shared_ptr<MyClass> p2 = p1;  // p2 和 p1 都指向同一个 MyClass 对象
        std::cout << "use_count: " << p1.use_count() << "\n";  // 输出引用计数
    }  // p2 离开作用域,引用计数减少

    std::cout << "use_count: " << p1.use_count() << "\n";  // 输出引用计数
    // 当 p1 离开作用域时,MyClass 对象会被销毁

    return 0;
}

输出

MyClass Constructor
Hello from MyClass
use_count: 1
use_count: 2
use_count: 1
MyClass Destructor

注意事项(循环引用问题)

循环引用(Cyclic Reference)是指在程序中,两个或多个对象相互持有对方的引用,从而导致它们的生命周期永远无法结束,最终导致内存泄漏。在这种情况下,引用计数机制(如 std::shared_ptr)无法释放内存,因为每个对象的引用计数始终大于零。

循环引用的原因
在 C++ 中,std::shared_ptr 是通过引用计数来管理内存的。当多个 shared_ptr 实例指向同一个对象时,引用计数会增加,每当 shared_ptr 被销毁时,引用计数会减少。当最后一个 shared_ptr 被销毁时,对象的内存才会被释放。

然而,如果两个对象 A 和 B 使用 shared_ptr 相互持有对方的 shared_ptr,那么它们的引用计数就不会减少到零。具体原因是:

对象 A 持有 B 的 shared_ptr,而对象 B 持有 A 的 shared_ptr。
这会导致两个对象的引用计数始终大于 1,因为 A 对 B 的引用计数在 A 存在时永远不会为零,B 对 A 的引用计数也同样不会为零。
因为引用计数永远不会为零,std::shared_ptr 无法检测到这两个对象已经不再被需要,从而导致内存泄漏。

循环引用的示例
假设我们有两个类 A 和 B,它们互相持有对方的 shared_ptr:

#include <iostream>
#include <memory>

class B;  // 前置声明

class A {
public:
    std::shared_ptr<B> b_ptr;  // A 持有 B 的 shared_ptr
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;  // B 持有 A 的 shared_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    // 创建 A 和 B 对象
    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 的引用计数都不会归零
    return 0;
}

输出

(没有输出)—— 内存泄漏

在这个例子中,A 和 B 都互相持有对方的 shared_ptr,这导致它们的引用计数永远不会归零。所以当 main 函数结束时,它们的析构函数不会被调用,内存也不会被释放。

解决循环引用
为了避免循环引用,我们通常会使用 std::weak_ptr 来打破这种引用循环。

std::weak_ptr 是一种不增加引用计数的智能指针,它可以观察对象但不参与引用计数管理。通过 weak_ptr,我们可以避免循环引用带来的内存泄漏问题。

使用 std::weak_ptr 打破循环引用
在上面的例子中,我们可以用 std::weak_ptr 来打破循环引用,使得 B 不再持有 A 的强引用:

#include <iostream>
#include <memory>

class B;  // 前置声明

class A {
public:
    std::shared_ptr<B> b_ptr;  // A 持有 B 的 shared_ptr
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // B 使用 weak_ptr 持有 A
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    // 创建 A 和 B 对象
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    // 互相引用,但 B 使用 weak_ptr
    a->b_ptr = b;
    b->a_ptr = a;

    // 离开作用域时,A 和 B 的引用计数都能归零,内存可以被释放
    return 0;
}

输出

B destroyed
A destroyed

在这个修改后的版本中

  • A 仍然持有对 B 的 shared_ptr(即强引用)。
  • B 通过 weak_ptr 持有对 A 的引用,这样 B 就不会增加 A 的引用计数。
  • 当 main 函数结束时,A 和 B 都可以正常销毁,因为没有循环引用。

其它问题

  • 性能开销: 由于使用了引用计数,shared_ptr 会比原始指针或 unique_ptr 有更高的性能开销。在频繁创建和销毁的情况下,性能可能会受到影响。

  • 线程安全性: shared_ptr 的引用计数操作是线程安全的,但管理的对象本身并不是线程安全的。如果多个线程同时访问相同的对象,必须额外小心同步问题。

  • 避免使用原始指针: 创建 shared_ptr 时,最好避免直接使用原始指针。可以使用 std::make_shared 来创建对象,它不仅提供更安全的内存管理,还能提高性能,因为它在一次内存分配中分配对象和引用计数。

std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();  // 推荐

自定义简易shared_ptr的实现

自定义的智能指针要注意赋值重载函数,要考虑自我赋值和同一个资源的智能指针无意义相互赋值,需要单独考虑处理,提高效率。

同时删除器是在定义构造函数时候再开了一个,而且删除器是用包装器打包的,方便适配仿函数,lambda函数等,默认的是用delete删除的

namespace share
{
	template<class T>
	class shared_ptr
	{
		shared_ptr() = delete;
		void destroy()
		{
			_del(_ptr);
			delete _count;
			cout << "destroy" << endl;
		}
		
	public:
		shared_ptr(T* ptr)
			: _ptr(ptr)
			, _count(new int(1))
		{
			cout << "参数构造" << endl;
		}

		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _count(new int(1))
			, _del(del)
		{
			cout << "模板参数构造" << endl;
		}

		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_count = sp._count;
			_del = sp._del;
			(*_count)++;
			cout << "拷贝构造" << endl;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr == sp._ptr)
			{
				return *this;
			}

			if (--(*_count) == 0)
			{
				destroy();
			}

			_ptr = sp._ptr;
			_count = sp._count;
			_del = sp._del;
			++(*_count);
			cout << "赋值重载" << endl;
		}

		T& operator*() const 
		{
			return *_ptr;
		}

		T* operator->() const
		{
			return _ptr;
		}

		~shared_ptr()
		{
			if (--(*_count) == 0)
				destroy();
		}


	private:
		T* _ptr;
		int* _count;
		function<void(T*)> _del = [](T* ptr) {
			delete ptr;
		};
	};

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值