【C++】智能指针详解

🔥个人主页🔥:孤寂大仙V

🌈收录专栏🌈:C++从小白到高手

🌹往期回顾🌹:C++异常

🔖流水不争,争的是滔滔不


一、智能指针简介

智能指针是C++标准库中的一个重要概念,主要用于管理动态分配内存的对象。与传统指针不同,智能指针能够自动管理内存的分配和释放,从而减少内存泄漏和其他内存相关错误的风险。C++中主要有三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

二、为什么要用智能指针

像前面抛异常的捕获try、catch就非常容易造成内存泄漏。

cpp 复制代码
#include <iostream>
#include <stdexcept>

void causeMemoryLeak() {
    int* ptr = new int(42); // 动态分配内存

    // 模拟抛出异常
    throw std::runtime_error("Something went wrong!");

    // 此处如果异常未抛出,应该释放内存
    delete ptr;
}

int main() {
    try {
        causeMemoryLeak();
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
        // 这里 ptr 的内存没有被释放,造成内存泄漏
    }

    return 0;
}

前面的C++异常中聊过,当throw执行时,throw后面的语句将不再被执行。所以当这里抛出runtime_error异常后,导致delete per被跳过。造成了内存泄漏。虽然我们可以通过对代码进行优化,以防止出现这种情况。但是如果用智能指针来对资源进行管理就会非常的方便。

C++98提供了std::auto_ptr

C++11提供了std::unique_ptrstd::shared_ptrstd::weak_ptr

三、 RAII和智能指针

RAII是Resource Acquisition Is Initialization(资源获取即初始化)的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

cpp 复制代码
#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource acquired." << std::endl; }
    ~Resource() { std::cout << "Resource released." << std::endl; }
};

void useResource() {
    std::unique_ptr<Resource> resPtr(new Resource()); // RAII
    // 可以在这里使用 resPtr
    
    // 当函数结束,resPtr 超出作用域时,Resource 的析构函数会被调用
}

int main() {
    try {
        useResource();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }
    return 0; // 此时 Resource 的内存已经被释放,无需手动 delete
}

auto_ptr

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象 ,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。

cpp 复制代码
int main()
{
	auto_ptr<int> p1(new int(1));
	auto_ptr<int> p2(p1);
	return 0;
}


p2拷贝p1,p1的资源管理权给了p2。p1悬空,此时p1是空指针。如果这时访问p1程序就挂了。所以auto_ptr是一个非常糟糕的设计。

auto_ptr的简单模拟实现

cpp 复制代码
namespace hbx
{
	template<class T>
	class auto_ptr
	{
	public:
		//构造
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//拷贝构造
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr=nullptr;//指针置为空,管理权转换
		}
		//赋值重载
		auto_ptr<T>& operator=(const auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				if (_ptr)
				{
					delete _ptr;//释放被赋值对象的资源,这个对象是已经存在的被new出来的。
				}
				_ptr = ap._ptr;
				ap._ptr == nullptr;//赋完值把赋值对象的指针置为空
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
			}
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

}

unique_ptr

unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用他。不允许左值赋值操作,可以通过move使左值转化为右值。移动后,移动的对象的指针也悬空,要谨慎使用。

cpp 复制代码
int main()
{
	unique_ptr<Date> u1(new Date(1, 1, 1));
	unique_ptr<Date> u2(move(u1));//不支持拷贝,支持移动。但是也要谨慎使用u1照样悬空。
	return 0;
}

跟auto_ptr主要区别是,用unique_ptr那么程序员是知道移动对象是悬空的情况下使用。

unique_ptr的简单模拟实现

cpp 复制代码
namespace hbx
{
	template<class T>
	class unique_ptr
	{
	public:
		//构造
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		//析构
		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
			}
		}
		//拷贝构造
		unique_ptr(const unique_ptr<T>& up) = delete;
		//复制重载
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		//移动构造
		unique_ptr(unique_ptr<T>&& up)
			:_ptr(up._ptr)
		{
			up._ptr = nullptr;
		}
		//移动赋值
		unique_ptr<T>& operator=(unique_ptr<T>&& up)
		{
			if (_ptr)
			{
				delete _ptr;
			}
			_ptr = up.ptr;
			up.ptr = nullptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

int main()
{
	hbx::unique_ptr<Date> u1(new Date(1, 1, 1));
	hbx::unique_ptr<Date> u2(move(u1));
	return 0;
}

shared_ptr

shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

shared_ptr当复制或则拷贝的时候,引用计数+1,当智能指针析构的时候引用计数-1。如果引用计数为0,那么这块内存就没有资源了就释放它。

cpp 复制代码
int main()
{
	shared_ptr<Date> sp1(new Date(1, 1, 1));
	shared_ptr<Date> sp2(sp1);
	//use_count():引用计数
	cout << sp2.use_count() << endl;

	shared_ptr<Date> sp3(new Date(2, 2, 2));
	cout << sp3.use_count() << endl;
}

shared_ptr的简单模拟实现

cpp 复制代码
namespace hbx
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		//析构
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
		}
		//拷贝构造
		shared_ptr(const shared_ptr<T>& ps)
			:_ptr(ps._ptr)
			, _pcount(ps._pcount)
		{
			++(*_pcount);
		}
		//赋值重载
		shared_ptr<T>& operator=(const shared_ptr<T>& ps)
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}

			_ptr = ps._ptr;
			_pcount = ps._pcount;
			++(*_pcount);
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int num) : data(num) {
        std::cout << "MyClass constructor called" << std::endl;
    }
    void printData() const {
        std::cout << "Data: " << data << std::endl;
    }
private:
    int data;
};

int main() {
    // 使用make_shared直接初始化资源对象的值来构造shared_ptr
    std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(10);
    ptr->printData();
    return 0;
}

shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int num) : data(num) {}
    void printData() const 
    {
        std::cout << "Data: " << data << std::endl;
    }
private:
    int data;
};

int main() {
    // shared_ptr使用示例
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(20);
    if (ptr1) 
    {
        ptr1->printData();
    } 
    else 
    {
        std::cout << "ptr1 is empty" << std::endl;
    }

    std::shared_ptr<MyClass> ptr2;
    if (ptr2) 
    {
        ptr2->printData();
    } 
    else 
    {
        std::cout << "ptr2 is empty" << std::endl;
    }

    // unique_ptr使用示例
    std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(30);
    if (uptr1)
    {
        uptr1->printData();
    }
     else 
    {
        std::cout << "uptr1 is empty" << std::endl;
    }

    std::unique_ptr<MyClass> uptr2;
    if (uptr2) 
    {
        uptr2->printData();
    } 
    else 
    {
        std::cout << "uptr2 is empty" << std::endl;
    }

    return 0;
}

shared_ptr 和 unique_ptr 都得构造函数都使用explicit 修饰,防止普通指针隐式类型转换成智能指针对象。

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int num) : data(num) {}
    void printData() const {
        std::cout << "Data: " << data << std::endl;
    }
private:
    int data;
};

int main() {
    // shared_ptr使用示例
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(20);
    if (ptr1) {
        ptr1->printData();
    } else {
        std::cout << "ptr1 is empty" << std::endl;
    }

    std::shared_ptr<MyClass> ptr2;
    if (ptr2) {
        ptr2->printData();
    } else {
        std::cout << "ptr2 is empty" << std::endl;
    }

    // unique_ptr使用示例
    std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(30);
    if (uptr1) {
        uptr1->printData();
    } else {
        std::cout << "uptr1 is empty" << std::endl;
    }

    std::unique_ptr<MyClass> uptr2;
    if (uptr2) {
        uptr2->printData();
    } else {
        std::cout << "uptr2 is empty" << std::endl;
    }

    return 0;
}

shared_ptr的循环引用与weak_ptr

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的⼀个循环引用导致内存泄漏的问题

hared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
  4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
    • 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
    • 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
cpp 复制代码
struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
	// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
	/*std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;*/
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	// 循环引用 -- 内存泄露
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	// weak_ptr不支持管理资源,不支持RAII
	// weak_ptr是专⻔绑定shared_ptr,不增加他的引用计数,作为⼀些场景的辅助管理
	//std::weak_ptr<ListNode> wp(new ListNode);
	return 0;
}

weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果它绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的weak_ptr支持expired检查指向的资源是否过期use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。


当weak_ptr的expired函数返回值为 0(在 C++ 中,0 通常代表false)时,这意味着weak_ptr所关联shared_ptr仍然有效,即对应的对象尚未被销毁。打印出来引用计数是2,weak_pt不增加计数但是不代表不指向计数。

weak_ptr所关联的share_ptr过期了

调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

四、删除器

智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃 。智能指针支持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。

因为new[]经常使用,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,管理new []的资源。

cpp 复制代码
unique_ptr<Date[]> up1(newDate[5]);
shared_ptr<Date[]> sp1(new Date[5]); 

定制删除器还有lambda版本和仿函数版本

在shared_ptr下建议使用lambda版本

cpp 复制代码
class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}
int main()
{
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date[]> sp2(new Date[10]);

//lambda版本
 bit::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
 //仿函数版本
 std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);

//文件操作

//lambda版本
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr);});
//仿函数版本
std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
 }

在unique_ptr下建议使用仿函数版本

cpp 复制代码
class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

std::unique_ptr<Date> up1(new Date);
std::unique_ptr<Date[]> up2(new Date[10]);

//lambda版本
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);

//仿函数版本
std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
相关推荐
Tanecious.1 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐1 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
战族狼魂1 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
Tttian6222 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
xyliiiiiL2 小时前
ZGC初步了解
java·jvm·算法
杉之3 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
Merokes3 小时前
关于Gstreamer+MPP硬件加速推流问题:视频输入video0被占用
c++·音视频·rk3588
hycccccch3 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
独好紫罗兰4 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法
天天向上杰4 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal