一、c++11智能指针详解之shared_ptr代码实现:基本功能

c++11智能指针之shared_ptr实现:基本功能

接下来开个坑,用c++11实现shared_ptr
背景介绍:c++方向的硕士狗,实验室没什么c++项目,都是自学,在网上看到说智能指针这块挺重要的,自己也看了不少资料,觉得不如自己动手实战,从零到有实现一个shared_ptr。

规划:首先用c++11实现一个基本的智能指针,包括构造析构、拷贝移动等,然后往里面添加功能,如自定义删除器,支持数组管理的接口,以及目前我认为比较重要的多线程安全等等。最后采用c++17和20重构代码,看有没有什么需要改进的。

在代码实现中,我会一块附上如何调用这些接口,方便自己和大家学习

笔者也是刚入门c++,可能会有很多错误,欢迎大家指正,也欢迎大家一起学习,有任何问题可以留言评论

另外我有个问题想问大家,因为本身实验室没有什么c++项目,大家有没有什么比较好的可以写在简历上的c++项目推荐,除了webserver(听说这个人手一个)


本节内容将介绍智能指针之shared_ptr基本框架和功能



一、shared_ptr基本原理

  1. 作用:多个智能指针可以指向相同的对象。
  2. 实现方法:RAII,资源获取即初始化。
  3. 原理:使用技术机制表明资源被几个指针共享,调用拷贝构造和拷贝赋值时,计数加1;释放一个智能指针时,计数减1。当计数等于0,资源会被释放。

这部分网上讲解的很多,我就不多赘述了。

二、基本功能

1.构造、析构、拷贝、移动

首先写一个基本框架,对于一个初学者,一些问题首先是要搞明白:

  1. 为什么要用模板?
    智能指针类通常使用模板来实现,是因为它们需要能够管理不同类型的指针。模板允许你定义一个通用的类T,它可以用来管理任意类型的指针。例如,shared_ptr 是一个模板类,它可以用来管理任意类型的指针。你可以使用 shared_ptr<int> 来管理 int 类型的指针,也可以使用 shared_ptr<string> 来管理 string 类型的指针。

  2. 为什么要将引用数定义为指针?
    这是因为在智能指针类中,引用数需要在多个智能指针对象之间共享。当你将一个智能指针对象赋值给另一个智能指针对象时,它们会共享同一个引用计数。为了实现这种共享,智能指针将引用数定义为指针,这样就可以让不同的智能指针通过自己管理的引用数指针去获取同一个引用数变量。这样,当其中一个智能指针对象被销毁时,它会递减引用计数,而不会直接删除所管理的资源。

  3. 为什么拷贝赋值和移动赋值要有返回值(当前对象的引用),而拷贝构造和移动构造没有?
    这是因为考虑到链式赋值,如

a = b = c; 

如果拷贝赋值运算符和移动赋值运算符没有返回值,那么这样的链式赋值就无法实现。当然,如果你不需要链式赋值,你也可以不加返回值。

  1. 为什么拷贝构造和拷贝赋值的参数设置为const,移动构造和移动赋值则没有?
    这是因为拷贝构造函数的参数通常是 const 的,是因为它不应该修改传入的对象。拷贝构造函数的目的是根据传入的对象创建一个新的对象,它不应该改变传入的对象的状态。拷贝赋值同理。
    但是移动构造和移动赋值是将传入对象的资源“夺走”给本对象,资源拷贝后还需要将传入对象置空。==注意:==只是将传入对象的指针置空,而不是删除,不然这一块资源就没了,本对象也就没法实现“夺走”。

shared_ptr基本框架如下:

template<typename T>
class mySharedSp {
private:
	int* m_count; 
	T* m_ptr; 
public:
	mySharedSp(T* p = NULL);  //构造。类内声明,需要指定默认参数,类外实现不需要再次指定
	mySharedSp(const mySharedSp<T>& other); //拷贝构造
	mySharedSp<T>& operator = (const mySharedSp<T>& other); //拷贝赋值
	mySharedSp(mySharedSp<T>&& other); //移动构造函数
	mySharedSp<T>& operator = (mySharedSp<T>&& other); //移动赋值,注意不能将参数设置为const
	~mySharedSp(); //析构
};

2. 代码实现

1)构造

使用初始化列表的方式

//构造  
//类内声明,需要指定默认参数,类外实现不需要再次指定
//采用初始化列表的方式。new返回指针,构造了一个肯定引用计数量就初始化为1。
template<typename T>
mySharedSp<T>::mySharedSp(T* p) : m_ptr(p), m_count(new int(1)){
	cout << "调用构造函数" << endl;
}

如何调用:

int main(){
	mysharedSp<int> p1(new int(0));  
	
	return 0;
}

2)拷贝构造

//拷贝构造
template<typename T>
mySharedSp<T>::mySharedSp(const mySharedSp<T>& other) : m_ptr(other.m_ptr), m_count(other.m_count) {
	++(*m_count);
	cout << "调用拷贝构造函数" << endl;
}

链接: 为什么是 ++(*m_count); 而不是 (*m_count)++;

如何调用:

int main(){
	mysharedSp<int> p1(new int(0));  
	mysharedSp<int> p2(p1); 
	
	return 0;
}

3)拷贝赋值

//拷贝赋值
template<typename T>
mySharedSp<T>& mySharedSp<T>::operator = (const mySharedSp<T>& other) {
	++(*other.m_count);
	--(*m_count);
	if (*m_count == 0) {
		delete m_ptr;
		m_ptr = NULL; //只delete不置空会产生悬空指针
		delete m_count;
		m_count = NULL; 
	}
	m_ptr = other.m_ptr;
	m_count = other.m_count;
	
	cout << "调用拷贝赋值函数" << endl;
	
	return *this;
}

这里首先在减少本对象使用计数之前使other的使用计数加1,从而防止自身赋值而导致的提早释放内存。

这里提供另一个版本的拷贝赋值,更加直观:

template<typename T>
mySharedSp<T>& mySharedSp<T>::operator = (const mySharedSp<T>& other) {
	if (this != &other) { //如果不是自赋值,则拷贝
		--(*m_count);
		if ((*m_count) == 0) {
			delete m_ptr;
			m_ptr = NULL;
			delete m_count;
			m_count = NULL;
		}
		++(*other.m_count); //确保当前对象和other对象的引用数相同并都是++的
		m_ptr = other.m_ptr; //然后再拷贝
		m_count = other.m_count;
	}
	cout << "调用拷贝赋值函数" << endl;

	return *this; //如果是自赋值,直接返回 *this
}

如何调用:

int main(){
	mysharedPtr<int> p1(new int(0));  
	p1 = p1;  //自赋值
 	mysharedPtr<int> p3(new int(1));  
 	p3 = p1;  
	
	return 0;
}

补充:

为什么检查自赋值的时候,是if (this != &other)而不是if (*this != other)

前者是比较两个对象的地址是否相同,后者是比较两个对象的值是否相同,只有地址相等才能认为是同一个对象,值相等不能作为判断自赋值的依据。

4)移动构造

原理:采用浅拷贝的原理,将原对象的资源指针拷贝给新对象的资源指针,然后删除原对象的资源指针,避免了资源的拷贝。

参数为右值引用,需要调用时,需要把other指针转换成右值。

如 mySharedSp p(move(new int(10)))。

std::move函数将左值转换成右值。

//移动构造函数
template<typename T>
mySharedSp<T>::mySharedSp(mySharedSp<T>&& other) :m_ptr(other.m_ptr), m_count(other.m_count){
	other.m_ptr = NULL;
	other.m_count = NULL;
	
	out << "调用移动构造函数" << endl; //引用数不变化
}

如何调用:

int main(){
	mySharedSp<int> p(move(new int(10)));  
	
	return 0;
}

为什么要使用右值,而不是使用左值?

  • 移动构造函数的参数必须是右值引用,因为它的目的是接管源对象中的资源,而不是复制它们。右值通常表示临时对象或即将销毁的对象,它们不再需要它们所拥有的资源。因此,将这些资源转移到新对象中是安全的。
  • 如果移动构造函数的参数是左值引用,则可能会导致问题。左值通常表示持久性对象,它们在移动构造区数调用后仍然存在。如果移动构造函数接管了左值对象中的资源,则该左值对象将不再能够访问这些资源,这可能会导致未定义行为。
    因此,移动构造函数的参数必须是右值引用,以确保只有当源对象不再需要其资源时才能调用移动构造函数。

5)移动赋值

不能将参数设置为const,因为转移资源后会对临时对象指针进行置空操作

//移动赋值
template<typename T>
mySharedSp<T>& mySharedSp<T>::operator = (mySharedSp<T>&& other) {
	if (this != &other) {
		--(*m_count);
		if ((*m_count) == 0) {
			delete m_ptr;
			m_ptr = NULL; //只delete不置空会产生悬空指针
			delete m_count;
			m_count = NULL;
		}
		m_ptr = other.m_ptr;
		m_count = other.m_count;
		m_size = other.m_size;

		other.m_ptr = NULL; //不能delete,因为这时候 m_ptr 和 other.m_ptr 都指向同一块资源
		other.m_count = NULL;

		cout << "调用移动赋值函数" << endl;
	}
	return *this; //如果是自赋值,直接返回当前对象
}

如何调用:

int main(){
	mysharedPtr<int> p1(new int(0));  
	p1 = move(p1);  //自赋值
 	mysharedPtr<int> p3(new int(1));  
 	p3 = move(p1);  
	
	return 0;
}

6)析构

//析构
template<typename T>
mySharedSp<T>::~mySharedSp() {
	--(*m_count);
	if ((*m_count) == 0) {
		delete m_ptr;
		m_ptr = NULL;
		delete m_count;
		m_count = NULL;
		cout << "调用析构函数,现在已无指针指向" << endl;
	}
	else {
		cout << "调用析构函数,仍有指针指向" << endl;
	}
}

析构就不用调用了,离开智能指针的作用域会自动析构的。


总结

本节是智能指针基本功能,下一节会介绍一些常用接口,比如解引用、成员访问、智能指针的比较操作等。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值