拷贝构造函数

传统写法

以实现一个简单的my::string字符串类为例
my::string类的实现部分大致为

namespace my
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		string();						//构造函数
		string(const char* str = "");	//带参构造函数
		~string();						//析构函数
		string(const string& s);		//拷贝构造函数
		string& operator=(string tmp);	//赋值运算符重载
		//此处省略大量成员函数

	private:
		char* _str = nullptr;			//数据存储
		size_t _size = 0;				//size
		size_t _capacity = 0;			//开辟空间大小
	};
}
//此处实现的string不涉及内存池/SSO优化

按照传统写法,我们需要手动开辟一块新的空间,进行手动拷贝,然后手动进行赋值。

string::string(const string& s)
{
	_str = new char[s._capacity + 1];
	memcpy(_str, s._str, s._size + 1);
	_size = s._size;
	_capacity = s._capacity;
}

虽然代码逻辑不算复杂,但是这样写起来确实不够”优雅“,那么是否有更加简洁的写法呢?

现代简洁写法

在上面的传统写法中,我们相当于是手动构造了一个新的string对象,然后再依次将旧的对象赋值到新的对象中,那么是否有办法可以让程序自动完成构造新变量这一步呢?
注意到,我们在前面已经实现了string(const char* str = "")的方法,这意味着我们只需要一个字符串即可构造一个新对象。
构造新对象后,怎么让它的数据保存到当前调用方法的对象呢?
不禁让人想到了swap函数,它能够将两个变量的内部数据互换,因此我们可以利用它来进行交换数据。
于是得出以下代码:

void string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string::string(const string& s)
{
	string tmp(s._str);
	swap(tmp);
}

在简洁写法中,我们使用带参构造,传入s._str字符串构造出对象tmp,顺利解决了传统写法中的手动构造问题。
使用手动实现的swap函数,在里面使用std::swap将数据一一交换,此时,调用方法的对象已经成功获得了利用tmp构造的目标对象的数据,而调用方法原有的空数据被交换给了tmp,随后string(const string& s)方法返回,tmp被调用析构函数释放了空间。
至此,我们就“优雅”地完成了拷贝构造
(可能大家会疑惑,为了一个拷贝构造专门实现一个swap函数,看起来并不划算,为什么说代码更简洁了呢?别急,下面来看看赋值运算符重载)

赋值运算符重载

传统写法

话不多说,直接展示传统写法

string& string::operator=(const string& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);
		delete[] _str;
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

依然如同上文的传统拷贝构造函数一样,存在相同的两个问题。
因此,我们可以迅速反应过来,将代码按照同样的思路进行优化。

现代简洁写法

按照上文相同思路优化的代码如下

void string::swap(string& s)//上文实现过的swap
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string& string::operator=(const string& s)
{
	if (this != &s) {
		string tmp(s);
		swap(tmp);
	}
	return *this;
}

(上文实现过的swap函数在此处再次被调用,因此也解答了上文括号里提出的问题)
相信到这一步,一切都没有什么问题。
但是,我们能否更加偷懒,省去构造函数这行代码呢?
可以的!
我们都知道,在一个函数调用时,函数参数如果不是引用,那么就会拷贝一个新对象,参考下面的例子

class A
{
public:
	int _data = 0;
};
void Test(A a)//a对象是用test拷贝构造的新对象
{
	a._data = 1;//修改a与test无关,因此test里面_data还是0
}
int main()
{
	A test;
	Test(test);//使用test对象传参
	return 0;
}

从上面的例子注意到,在函数参数列表位置是可以进行变量构造的,因此就有了更加简洁的写法:
将引用直接改为构造,省略一行代码

string& string::operator=(string tmp)//直接在这里调用拷贝构造
{
	swap(tmp);
	return *this;
}

在函数参数列表中就完成了拷贝构造,因此tmp已经是一个全新的对象,我们在代码块中就只需要交换两个对象,然后返回结果即可。
这样,我们就成功写出了"简洁"而又"优雅"的代码!

(内容如有错漏请在评论区or私信留言,看到后一定会仔细检查与修改!阿里嘎多!)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐