前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++】C++11 右值引用和移动语义

【C++】C++11 右值引用和移动语义

作者头像
野猪佩奇`
发布2023-04-27 19:59:29
6740
发布2023-04-27 19:59:29
举报
文章被收录于专栏:C/C++ 后台开发学习路线

一、左值与左值引用

在 C++11 之前,我们把数据分为常量和变量,而在 C++11 之后,我们将数据分为左值和右值;

左值是一个表示数据的表达式 (如变量名或解引用的指针),我们可以对左值取地址,也可以对左值赋值 (const 左值不能赋值);左值既可以出现在赋值符号的左边,也可以出现在赋值符号的右边;如下:

代码语言:javascript
复制
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

左值引用就是对左值的引用,给左值取别名;如下:

代码语言:javascript
复制
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;  
int& pvalue = *p;

二、右值与右值引用

右值也是一个表示数据的表达式,如字面常量、表达式返回值、函数返回值等等,右值可以出现在赋值符号的右边,但不能出现出现在赋值符号的左边,右值不能取地址;如下:

代码语言:javascript
复制
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);

// 右值不能被赋值,所以这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

注意事项:

  • 为什么函数返回值是右值:当函数返回的是一个局部变量时,因为局部变量出了函数生命周期就会结束,所以返回时会将该变量拷贝到寄存器中,然后返回这个寄存器中的内容,而寄存器中的变量是临时变量,临时变量具有常性,属于右值。
  • 为什么右值不能取地址:在 C++中,右值则是一个临时使用的、不可寻址的内存值;右值没有独立的内存空间,它只是存储在寄存器或其他临时内存空间中的一个值;我们也不能把右值放入内存中,因为右值没有确定的内存位置,所以右值不能取地址。

右值引用就是对右值的引用,给右值取别名;如下:

代码语言:javascript
复制
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意:虽然右值不能取地址,但是给右值取别名后,会导致右值被存储到特定位置,拥有独立的内存空间,所以可以取到该位置的地址;换句话来说,虽然右值引用引用的是右值,但右值引用本身是一个左值。如下:

image-20230417150931929
image-20230417150931929

所以如果我们不希望改变右值引用,我们就需要将右值引用定义为 const 右值引用。

三、左值引用和右值引用的比较

左值引用不能直接引用左值,但是 const 左值引用可以引用右值,因为 const 左值引用也是只读的,而权限可以平移:

代码语言:javascript
复制
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;
	//int& ra2 = 10;  // 编译失败,因为10是右值

	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用也不可以直接引用左值,但是右值引用可以引用 move 后的左值

代码语言:javascript
复制
int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	int a = 10;
	//int&& r2 = a;

	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);

	return 0;
}

四、右值引用的使用场景和意义

1、左值引用的短板

我们之前接触到的引用其实都是 (const) 左值引用,左值引用主要有两个作用:

  • 修改实参的值;
  • 引用做参数/做函数返回值可以减少拷贝。

我们可以把函数形参定义为实参的引用,这样函数在传参时实参就不用拷贝构造形参了,从而提高程序的效率,特别是对于需要深拷贝的的自定义类型来说;如下:

代码语言:javascript
复制
//左值引用的使用场景
//做参数--减少一次拷贝构造
template <class T>
void func1(const T& x) {
	//...
}

int main() {
	vector<int> v(10, 0);
	//const左值引用可以接受左值,也可以接收右值
	func1(v);
	func1(vector<int>(10, 1));

	return 0;
}

左值引用做返回值的效果也一样,当返回的对象出了函数作用域还存在时,直接使用引用返回可以减少一次拷贝构造;如下:

代码语言:javascript
复制
//左值引用的使用场景
//做返回值--减少一次拷贝构造
template <class T>
const T& func1(const T& x) {
	//...

	return x;  //出这个函数x仍然存在
}

int main() {
	vector<int> v(10, 0);
	//const左值引用可以接受左值,也可以接收右值
	func1(v);
	func1(vector<int>(10, 1));

	return 0;
}

但是当函数返回对象是一个局部变量时,就不能使用左值引用返回,而只能传值返回了,因为局部对象出了函数作用域就不存在了,此时引用的就是一个野指针;如下:

代码语言:javascript
复制
//左值引用的短板--不能解决局部对象的返回值问题
template <class T>
T func1(const T& x) {
	T tmp;
	//...

	return tmp;  //出这个函数tmp会自动销毁
}

这种情况下下编译器会使用这个局部对象拷贝构造一个临时对象,然后再返回这个临时对象,也就是说,会比引用返回多一次拷贝构造;当局部对象是一个需要进行深拷贝的自动类型时,比如 vector<vector<int>>,拷贝构造的代价就很大了。而右值引用的提出就是为了补足左值引用存在的这些短板的。

注:当局部对象较小时会将其拷贝到寄存器中,让寄存器充当临时对象;当局部对象较大寄存器放不下时,会直接将其压入到本函数栈帧和调用本函数的函数栈帧之间。

2、移动构造和移动赋值

为了更好的查看左值引用和右值引用对拷贝构造的优化,这里我们给出自己实现的一个简易版的 string 类,类中如果要调用拷贝构造/赋值重载进行深拷贝我们就打印一下;如下:

代码语言:javascript
复制
#pragma once
#include <string.h>
using std::cout;
using std::endl;

namespace thj {
	class string {
	public:
		typedef char* iterator;
		iterator begin() { return _str; }
		iterator end() { return _str + _size; }

		//构造
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str) -- 构造" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			_size = s._size;
			_capacity = s._capacity;
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			if (this == &s)
				return *this;
			delete[] _str;
			_size = s._size;
			_capacity = s._capacity;
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
			return *this;
		}

		// //移动构造
		//string(string&& s)
		//	:_str(nullptr)
		//	, _size(0)
		//	, _capacity(0)
		//{
		//	cout << "string(string&& s) -- 移动语义" << endl;
		//	swap(s);
		//}

		// //移动赋值
		//string& operator=(string&& s)
		//{
		//	cout << "string& operator=(string&& s) -- 移动语义" << endl;
		//	swap(s);
		//	return *this;
		//}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

现在,假设我们要实现一个 to_string 函数,如下:

代码语言:javascript
复制
thj::string to_string(int value)
{
	bool flag = true;
	if (value < 0)
	{
		flag = false;
		value = 0 - value;
	}
	thj::string str;
	while (value > 0)
	{
		int x = value % 10;
		value /= 10;
		str += ('0' + x);
	}
	if (flag == false)
	{
		str += '-';
	}
	std::reverse(str.begin(), str.end());
	return str;
}

可以看到,由于 to_string 函数返回的 str 是一个局部对象,所以这里我们只能使用传值返回,而传值返回就需要进行深拷贝:

其实这里程序的执行结果和我们预想的并不一样,正常情况下应该是 str 先拷贝构造一个临时对象,然后由这个临时对象来拷贝构造 s,所以应该是两个拷贝构造 (多出来一次构造是 to_string 函数内部构造 str);但是我们发现这里只有一次拷贝构造。这其实是因为当遇到连续构造的场景时编译器会进行优化,直接使用 str 来拷贝构造得到 s,而不再创建临时对象,关于编译器的优化场景我们在 类和对象下 部分已经详细讲解过,这里就不再赘述。

但是优化只适用于少数场景,大部分情况下还是会拷贝构造产生临时对象,比如:

image-20230417172902529
image-20230417172902529

而且就算编译器进行了优化,这里还是会有一次拷贝构造,那么我们能不能想办法将 str 的资源直接转移给 s,中间不发生拷贝构造呢?这时右值引用就可以发挥作用了。

C++11 中的右值广义的来说一共分为两种:

  1. 纯右值:内置类型表达式的值;
  2. 将亡值:自定义类型表达式的值;所谓的将亡值就是指生命周期马上就要结束的值,一般来说匿名对象、临时对象、move 后的自定义类型都可以看做是将亡值。

注:上面我们说右值不能取地址其实是右值的严格定义,但其实将亡值也是可以被当作右值看待的,而将亡值有独立的内存空间,可以取地址;所以对于是否是右值我们要灵活看待。

既然将亡值的生命周期马上就要结束了,那么在拷贝构造中我们就可以直接将将亡值的资源拿过来给我自己使用,这样我就不用再去一个一个 new 节点了,将亡值也不用去一个一个释放节点了,两全其美;如下:

代码语言:javascript
复制
// s1.swap(s2)
void swap(string& s)
{
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}		

//移动构造
string(string&& s)
    :_str(nullptr)
     , _size(0)
     , _capacity(0)
{
    cout << "string(string&& s) -- 移动语义" << endl;
    swap(s);
}

如上,我们重载了一个右值引用版本的构造函数 – 移动构造,这样当实参类型为右值的对象需要进行拷贝构造时就会调用此函数;在函数中,我们直接拿走将亡值的资源,从而使得深拷贝变为了浅拷贝,显著提高了程序的效率。

此时我们再调用 to_string 函数与之前的结果进行比对:

image-20230417175844204
image-20230417175844204

和上面一样,本来这里 str 会先拷贝构造一个临时对象,由于临时对象属于右值,所以会直接调用移动拷贝来构造 s;但是这里编译器进行了优化,直接将 str 识别为右值,让它来移动构造 s,所以通过移动构造 (右值引用) 我们成功将深拷贝变为了浅拷贝。

image-20230417180831708
image-20230417180831708

注意:只有当实参为右值时才会匹配 移动构造构造函数进行优化,当实参为左值时编译器在匹配参数还是会匹配形参为 const T& 的拷贝构造函数;因为编译器不知道我们是否还会对左值进行操作,所以它不敢拿走左值的资源来构造新的对象。

image-20230417180524387
image-20230417180524387

移动赋值和移动构造同理,只是移动赋值中将亡值还需要释放掉我之前的资源,不过这个过程是自动的:

代码语言:javascript
复制
//移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}
image-20230417181353214
image-20230417181353214

注:网上有的人说右值引用延长了变量的生命周期,这种说法其实是不准确的;因为右值引用只是将该变量的资源转移给另外一个变量,让它的资源能够不随着该变量的销毁而被释放,而该变量本身的生命周期是没有变的。

总结 – 左值引用和右值引用减少拷贝的原理

  • 左值引用让形参成为实参的别名,直接减少拷贝
  • 右值引用通过实现移动构造和移动赋值,将将亡值的资源进行转移,间接减少拷贝。(浅拷贝的类不需要进行资源转移,所以也就没有移动赋值和移动拷贝)

3、STL 容器的变化

C++11 设计出右值引用之后,为 STL 的所有容器都提供了移动构造和移动赋值,包括容器适配器:

image-20230417200230977
image-20230417200230977
image-20230417200255757
image-20230417200255757
image-20230417200429974
image-20230417200429974

同时,还提供了右值版本的插入接口

image-20230417200523738
image-20230417200523738
image-20230417200608944
image-20230417200608944
image-20230417200632843
image-20230417200632843
image-20230417200923691
image-20230417200923691

所以,以后如果我们要向容器中插入需要深拷贝的自定义类型的元素时,我们尽量构造匿名对象进行插入,这样调用的就是右值插入接口,元素会调用移动拷贝函数完成浅拷贝,从而提高程序效率。

我们也可以让我们自己实现的 list 类支持右值版本的插入接口;部分代码如下:

代码语言:javascript
复制
#pragma once

#include <iostream>
#include <assert.h>
#include <algorithm>

namespace thj {
	template<class T>
	struct list_node  //list的节点
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		//普通构造
		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}

		//移动构造
		list_node(T&& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(x))
		{}
	};

    //list的迭代器
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		//...
	};

	//list 类
	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		void empty_initialize() {  //初始化 -- 哨兵位头结点
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;  //空间换时间,用于标记节点个数
		}

		list() {
			empty_initialize();
		}

		void push_back(const T& x) {
			insert(end(), x);  //复用
		}
		
        //尾插右值版本
		void push_back(T&& x) {
			insert(end(), move(x));  //复用
		}

		void push_front(const T& x) {
			insert(begin(), x);  //复用
		}
        
        //头插右值版本
        void push_front(T&& x) {
			insert(begin(), move(x));  //复用
		}

		iterator insert(iterator pos, const T& x) {
			node* newnode = new node(x);
			node* cur = pos._pnode;
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			cur->_prev = newnode;
			newnode->_next = cur;

			++_size;
			return iterator(pos);
		}

        //插入右值版本
		iterator insert(iterator pos, T&& x) {
			node* newnode = new node(move(x));
			node* cur = pos._pnode;
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			cur->_prev = newnode;
			newnode->_next = cur;

			++_size;
			return iterator(pos);
		}

	private:
		node* _head;
		size_t _size;
	};
}
image-20230417205832234
image-20230417205832234

list 的主要改动以及右值插入的执行逻辑如下:

image-20230417210430066
image-20230417210430066

这里关键的点需要注意:

  1. 右值函数的形参的类型都是 T&& x,而不是 const T&& x,这是因为最终在 thj::string 类中我们需要将 x 的资源转移给别人,这就要求 x 必须是可修改的 (能够交换 _str _size _capacity 三个指针);同时,右值引用 x 之所以能够被修改是因为给右值取别名后,右值会被存储起来。
  2. 右值引用虽然引用的是右值,但是右值引用本身是左值,所以当我们继续往下一层传时,我们需要将 x 重新 move 为右值,否则下一层调用时就会调用参数为左值引用的函数。

这两点其实在文章开始将右值引用的时候我们就提到过,不过还是要结合具体实例大家才能够真正的理解并且记住它。


五、万能引用与完美转发

1、万能引用

我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来选择调用参数为左值引用的构造/插入接口还是参数为右值引用的构造/插入接口。那么,我们能不能让函数能够根据实参的类型自动实例化出对应不同的函数呢?万能引用可以实现这个功能。

万能引用是一个函数模板,且函数的形参类型为右值引用;对于这样的函数模板,编译器能够自动根据实参的类型 – 左值/ const 左值/ 右值/ const 右值,自动推演实例化出不同的形参类型分别为 左值引用/ const 左值引用/ 右值引用/ const 右值引用 的函数;如下:

代码语言:javascript
复制
//万能引用
template<typename T>
void PerfectForward(T&& t)
{
	//fun(t);
}

int main()
{
	int a;
	const int b = 8;
	PerfectForward(a);	//左值
	PerfectForward(b);	//const 左值
	PerfectForward(10);	//右值
	PerfectForward(std::move(b));  //const 右值

	return 0;
}
image-20230417215648729
image-20230417215648729

可以看到,不管实参为什么类型,模板函数都能正确接受并实例化为对应的引用类型,所以我们把形参为右值引用的函数模板叫做万能引用。其中,当实参为左值或 const 左值时,T&& 会被实例化为 T& 或 const T&,我们称其为引用折叠,即将 && 折叠为 &。

2、完美转发

我们上面讲解了万能引用,但是万能引用存在一个很大的问题:万能引用实例化后函数的形参的属性全部都是左值 – 如果实参为左值/ const 左值,则实例化函数的形参是左值/ const 左值;如果实参是右值/ const 右值,虽然实例化函数的形参是右值引用/ const 右值引用,但是右值引用本身是左值;所以就会出现下面这种情况:

代码语言:javascript
复制
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

//万能引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	int a;
	const int b = 8;
	PerfectForward(a);	//左值
	PerfectForward(b);	//const 左值
	PerfectForward(10);	//右值
	PerfectForward(std::move(b));  //const 右值

	return 0;
}
image-20230417221628088
image-20230417221628088

同时,这里我们也不能简单的将 t move 后传递给 Fun 函数,因为这样会让 t 全部变为右值,又满足不了实参为左值的情况。为了在传参的过程中能够保留对象原生类型属性,C++11 又设计出了完美转发 – forward。如下:

image-20230417222217290
image-20230417222217290
代码语言:javascript
复制
Func(forward<T>(t));
image-20230417222310020
image-20230417222310020

总结:C++11 的右值引用之旅

  • 旅程一:为了弥补左值引用局部对象返回会发生拷贝构造的问题,C++11 设计出了右值引用;右值引用可以通过移动构造和移动赋值实现资源转移,将深拷贝转化为浅拷贝,从而提高程序效率,这是 C++11 中非常重要的一个设计; 同时,C++11 还为 STL 中的容器都提供了右值版本的插入接口,但由于右值引用本身是左值,所以往下一层传递时不能保证其仍然是右值,所以C++11 又设计出了 move,但盲目的对左值进行 move 会导致错误。
  • 旅程二:为了让模板函数能同时接受 (const) 左值和 (const) 右值并正确实例化为对应的引用类型,C++11 又设计出了万能引用,附带的又引出了引用折叠这个概念;但是这样奇怪的设计让许多学习 C++11 的人苦不堪言。
  • 旅程三:万能引用的设计又带来了新的问题 – 不管是左值引用还是右值引用,其本身都是左值,所以往下一层传递时又要面对类型丢失的问题,但是这里使用之前的 move 已经不能解决问题了,所以 C++11 又又又设计出了完美转发,来保证传参的过程中对象原生类型属性能够保持不变。

大家不难看出,C++11 右值引用的设计是不断使用新坑来填旧坑的过程,并不是说这些东西设计的不好,只是 C++11 中这些奇怪的设计让 C++ 的学习成本变得很高,因为万能引用、引用折叠、完美转发这些语法的设计和之前的 C++ 可以说是大不相同,它们使得 C++ 越来越像一门新的语言;个人觉得,万能引用和完美转发这两个东西可以去掉,只留下右值引用和 move 即可。


六、新增默认成员函数

在 C++11 之前,C++ 的类一共有六个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

由于 C++11 设计出了右值引用,所以 C++11 为每个类新增的两个默认成员函数,即移动构造函数和移动赋值运算符重载;它们的生成规则如下:

  • 移动构造:如果析构函数 、拷贝构造、赋值重载这三个默认成员函数你都没有实现,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会完成浅拷贝,对于自定义类型,如果其实现了移动构造,就调用它的移动构造,如果没有实现就调用它的拷贝构造
  • 移动赋值:移动赋值和移动构造的规则一样,如果析构函数 、拷贝构造、赋值重载这三个默认成员函数你都没有实现,那么编译器会自动生成一个默认移动赋值运算符重载。默认生成的移动赋值函数,对于内置类型成员会完成浅拷贝,对于自定义类型,如果其实现了移动赋值,就调用它的移动赋值,如果没有实现就调用它的赋值重载
  • 如果你显式定义了移动构造或者移动赋值,那么编译器不会自动生成拷贝构造和拷贝赋值。

简单来说,如果你什么都没有实现,或者只实现了一个构造函数,那么编译器会自动生成移动拷贝和移动赋值;自动生成的对于内置类型完成值拷贝,对于自定义类型看自定义类型是否实现了移动构造或移动赋值,实现了就调用自定义类型的移动构造或移动赋值,没有实现就调用自定义类型拷贝构造和赋值重载。


七、成员变量的缺省值

由于 C++98 构造函数默认的初始化列表对内置类型不处理,所以 C++11 允许在类定义时给成员变量初始缺省值,这些缺省值会在构造函数的初始化列表用来初始化成员变量,如下:

image-20230418001714847
image-20230418001714847
image-20230418002137884
image-20230418002137884

八、default 和 delete

强制生成默认函数的关键字default

由于默认移动构造和移动赋值函数的生成条件十分苛刻,所以 C++11 提供了 default 关键字,它可以显示指定生成某个默认成员函数;比如我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成;如下:

代码语言:javascript
复制
class Person {
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}

	//显式声明了拷贝构造
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}

	//此时我们可以通过default关键字来让编译器默认生成移动构造
	Person(Person&& p) = default;

private:
	thj::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}
image-20230418002725895
image-20230418002725895

禁止生成默认函数的关键字delete:

假如我们要设计一个类,它不允许被拷贝,传统的做法是将拷贝构造函数定义为私有函数:

代码语言:javascript
复制
class A {
public:
	A() {
		_ptr = new int[10]{ 0 };
	}
    
    ~A() {
		delete[] _ptr;
	}

private:
	//将拷贝构造定义为私有,防止在类外进行拷贝
	A(const A& a)
		: _ptr(a._ptr)
	{}

	int* _ptr;
};
image-20230418003250244
image-20230418003250244

但是上面这种做法只防止了在类外进行拷贝,而在类内我们仍然可以调用拷贝构造函数完成拷贝,此时编译器在编译时不会发生错误,只有运行起来对同一块空间析构两次时才会报错;如下:

代码语言:javascript
复制
class A {
public:
	A() {
		_ptr = new int[10]{ 0 };
	}
    
    ~A() {
		delete[] _ptr;
	}
	
    //在类内进行拷贝
	void func() {
		A tmp(*this);
		//...
	}

private:
	//将拷贝构造定义为私有,防止在类外进行拷贝
	A(const A& a)
		: _ptr(a._ptr)
	{}

	int* _ptr;
};
image-20230418003959733
image-20230418003959733

那么我们如何才能让一个类既不能在外部被拷贝,也不能在内部被拷贝呢?其实我们可以只给出拷贝构造函数的声明,且声明为私有;这样,只要调用了拷贝构造函数,那么在链接时一定会发生错误:

代码语言:javascript
复制
private:
	A(const A& a);
image-20230418004337813
image-20230418004337813

上面是 C++98 中防止一个类被拷贝的做法,C++11 中提供了一种更为便捷的方法 – 在函数声明加上 =delete 即可delete 关键字可以阻止函数的自动生成,我们称被 =delete 修饰的函数为删除函数;如下:

代码语言:javascript
复制
class A {
public:
	A() {
		_ptr = new int[10]{ 0 };
	}

	~A() {
		delete[] _ptr;
	}

	void func() {
		A tmp(*this);
		//...
	}

	A(const A& a) = delete;

private:
	int* _ptr;
};
image-20230418004800727
image-20230418004800727

注意:default 关键字都只能针对默认成员函数使用;而 delete 关键字既可以对默认成员函数使用,也可以对非默认成员函数和普通函数使用


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-04-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、左值与左值引用
  • 二、右值与右值引用
  • 三、左值引用和右值引用的比较
  • 四、右值引用的使用场景和意义
    • 1、左值引用的短板
      • 2、移动构造和移动赋值
        • 3、STL 容器的变化
        • 五、万能引用与完美转发
          • 1、万能引用
            • 2、完美转发
            • 六、新增默认成员函数
            • 七、成员变量的缺省值
            • 八、default 和 delete
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档