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

news2025/1/11 14:44:04

文章目录

  • 一、左值与左值引用
  • 二、右值与右值引用
  • 三、左值引用和右值引用的比较
  • 四、右值引用的使用场景和意义
    • 1、左值引用的短板
    • 2、移动构造和移动赋值
    • 3、STL 容器的变化
  • 五、万能引用与完美转发
    • 1、万能引用
    • 2、完美转发
  • 六、新增默认成员函数
  • 七、成员变量的缺省值
  • 八、default 和 delete

一、左值与左值引用

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

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

// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

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

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;  
int& pvalue = *p;

二、右值与右值引用

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

// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);

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

注意事项:

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

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

// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

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

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

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

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

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;
	//int& ra2 = 10;  // 编译失败,因为10是右值

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

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

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) 左值引用,左值引用主要有两个作用:

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

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

//左值引用的使用场景
//做参数--减少一次拷贝构造
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;
}

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

//左值引用的使用场景
//做返回值--减少一次拷贝构造
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;
}

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

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

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

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

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

2、移动构造和移动赋值

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

#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 函数,如下:

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

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

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

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

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

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

// 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

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

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

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

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

image-20230417181353214

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

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

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

3、STL 容器的变化

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

image-20230417200255757

image-20230417200429974

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

image-20230417200608944

image-20230417200632843

image-20230417200923691

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

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

#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

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

这里关键的点需要注意:

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

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


五、万能引用与完美转发

1、万能引用

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

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

//万能引用
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

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

2、完美转发

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

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

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

Func(forward<T>(t));

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-20230418002137884


八、default 和 delete

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

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

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

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

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

class A {
public:
	A() {
		_ptr = new int[10]{ 0 };
	}
    
    ~A() {
		delete[] _ptr;
	}

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

	int* _ptr;
};

image-20230418003250244

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

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

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

private:
	A(const A& a);

image-20230418004337813

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

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

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/436450.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Blender建模】newbird从零开始学+新手常见问题处理

目标 第一阶段&#xff1a;在跟着教程下&#xff0c;熟悉如何使用blender 教程地址&#xff1a;https://www.youtube.com/watch?vnIoXOplUvAw 一、移动、旋转、扩展各视角下的物体&#xff0c;熟悉各个窗口 鼠标中键&#xff08;Shift&#xff09;控制视角的方向 ~键快速选择…

Redis --- 入门、数据类型

一、前言 1.1、什么是Redis Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件&#xff0c;它是「Remote Dictionary Service」的首字母缩写&#xff0c;也就是「远程字典服务」。 基于内存存储&#xff0c;读写性能高适合存储热点…

Pytorch基础 - 5. torch.cat() 和 torch.stack()

目录 1. torch.cat(tensors, dim) 2. torch.stack(tensors, dim) 3. 两者不同 torch.cat() 和 torch.stack()常用来进行张量的拼接&#xff0c;在神经网络里经常用到。且前段时间有一个面试官也问到了这个知识点&#xff0c;虽然内容很小很细&#xff0c;但需要了解。 1. t…

Spring(10. 面试问题简析)学习笔记

上一篇&#xff1a;9. Spring 底层原理 文章目录 1. 对Spring的IOC机制的理解2. 对spring的AOP机制的理解3. 了解过cglib动态代理吗&#xff1f;他跟jdk动态代理的区别是什么&#xff1f;4. 能说说Spring中的Bean是线程安全的吗&#xff1f;5. Spring的事务实现原理是什么&…

Leetcode-二叉树

1.中序-后序构建二叉树 106. 从中序与后序遍历序列构造二叉树 - 力扣&#xff08;LeetCode&#xff09; 1. 首先根据后序&#xff08;左右中&#xff09;确定顶点元素&#xff1b; 2. 根据顶点元素划分中序序列&#xff1b; 3. 根据划分中序序列中-左子树的长度&#xff0c;进…

半小时学会HTML5

一、了解几个概念 1、HTML定义 HTML是&#xff08;Hyper Text Markup Language&#xff09;超文本标记语言&#xff0c;超文本包含&#xff1a;文字、图片、音频、视频、动画等。 2、W3C 是什么&#xff1f; W3C 即&#xff08;World Wide Web Consortium&#xff09; 万维…

【性能测试】常见适用场景以及策略

面对日益复杂的业务场景和不同的系统架构&#xff0c;前期的需求分析和准备工作&#xff0c;需要耗费很多的时间。而不同的测试策略&#xff0c;也对我们的测试结果是否符合预期目标至关重要。 这篇博客&#xff0c;聊聊我个人对常见的性能测试策略的理解&#xff0c;以及它们…

RK3399 Android 10 Camera2保存录像时缩略图获取为空

RK3399 Android 10相机录像保存时无法获取缩略预览图 先找到录像点击按钮 //点击快门按钮时可以通过log打印看到停止录像的流程onShutterButtonClick() //这里主要看停止的流程即stop true时会进入onStopVideoRecording方法 public void onShutterButtonClick() {Log.d(TAG…

【HAL库】BMP180气压传感器+STM32,hal库移植

BMP180气压传感器STM32 1 导入.c.h文件&#xff08;不再赘述&#xff0c;详细见LED部分&#xff09;2 Cubemx配置3 修改 .h 文件4 测试 将BMP180从标准库移植到HAL库。模拟IIC。 极简工程代码如下&#xff1a; https://github.com/wyfroom/HAL_BMP180 该份代码硬件配置&#x…

C++——深入探究函数重载

文章目录 概述函数重载函数重载的概念函数重载的细节 C支持函数重载的原理——名字修饰(name Mangling) 概述 本篇博客讲诉的是c函数重载是什么&#xff0c;以及了解其种的一些特征以及重载函数的意义&#xff0c;并且运用linux中的g编译器简单探究一下函数重载底层是如何实现的…

线性表之顺序表(增删查改)详解

&#x1f355;博客主页&#xff1a;️自信不孤单 &#x1f36c;文章专栏&#xff1a;数据结构与算法 &#x1f35a;代码仓库&#xff1a;破浪晓梦 &#x1f36d;欢迎关注&#xff1a;欢迎大家点赞收藏关注 文章目录 &#x1f349;线性表&#x1f352;顺序表1. 概念及结构2. 接口…

数据库JDBC

数据库厂商提供一个程序来完成 API 的转换&#xff0c;对原生 API 封装再提供成JDBC 的形状。 这个程序叫数据库驱动包。 JAVA程序员要想对数据库开发&#xff0c; 就要导入对应的数据库驱动包&#xff0c;才能编写代码。 数据库驱动是让JDBC认识数据库API URL 计算机里的一…

MySQL安装与新用户的创建相关

一、MySQL安装 1. 官网下载mysql的ims包 MySQL :: Download MySQL Installer (Archived Versions) 下载好&#xff0c;双击运行。 2. 根据提示进行安装 这里选择手动安装的选项&#xff1a; 然后选择你安装的MySQL版本&#xff0c;这里是5.7 勾选自定义MySQL安装位置 下一…

教育大数据总体解决方案(7)

考勤查询 创客教室 为体现学校创新教育的成果&#xff0c;丰富学校创新实践活动&#xff0c;加强创新课程普及教育&#xff0c;把机器人创新教育作为学校的教育特色来体现&#xff0c;使学生通过理论与实践相结合的方法&#xff0c;进一步学习掌握机械、电子结构、信息技术、人…

让我们谈谈你对 ThreadLocal 的理解

介绍 ThreadLocal 从 JDK1.2 开始&#xff0c;ThreadLocal 是一个被用来存储线程本地变量的类。在 ThreadLocal 中的变量在线程之间是独立的。当多个线程访问 ThreadLocal 中的变量&#xff0c;它们事实上访问的是自己当前线程在内存中的变量&#xff0c;这能确保这些变量是线…

基于鲸鱼算法的极限学习机(ELM)回归预测-附代码

基于鲸鱼算法的极限学习机(ELM)回归预测 文章目录 基于鲸鱼算法的极限学习机(ELM)回归预测1.极限学习机原理概述2.ELM学习算法3.回归问题数据处理4.基于鲸鱼算法优化的ELM5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;本文利用鲸鱼算法对极限学习机进行优化&#xff0c;并…

kettle——数据清洗(数据表——>转换——>数据表)

目录 1、表输入 ①点击“新建”选项&#xff0c;新建数据库 ②选择“获取SQL” ③选择表a ④注意&#xff1a;字段只显示了5个&#xff0c;而一共有6个字段&#xff0c;money字段需要手动添加 2、转换 ①打开java 控件&#xff0c;设置变量 3、表输出 ①连接表b ②映…

【Redis7】Redis7 复制(重点:复制原理)

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍Redis7 复制。 后续会继续分享Redis7和其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下吧】 上一篇文章&#xff1a;《【Redis7】Redis7 事务&管道&…

Git入门指南(手把手教学)

Git入门指南 一、什么是Git二、Git的安装下载三、git的简单实践1.创建git仓库2.Windows上生成公钥以绑定GitHub仓库3.写一个Helloworld 四、帮助学习的网站 一、什么是Git Git是一种分布式版本控制系统&#xff0c;它是由Linus Torvalds为了管理Linux内核开发而开发的。与中心化…

项目第四天

解决了路变墙 墙变路的问题 void onechange(ExMessage* msg) {if (msg->message WM_LBUTTONDOWN && msg->x > 50 && msg->x < 410 && msg->y > 50 && msg->y < 410){//printf("鼠标位置&#xff1a;x&#…