编程语言
首页 > 编程语言> > 【从零学C++11(中)】移动语义、右值引用、std::move()、完美转发等新特性

【从零学C++11(中)】移动语义、右值引用、std::move()、完美转发等新特性

作者:互联网

C++11


8. 默认函数控制

C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数、&和const&的重载、移动构造、移动拷贝构造等函数。

如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成

显式缺省函数

C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数

class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	// 显式缺省构造函数,由编译器生成
	A() = default;
	
	// 可以选择在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	A& operator=(const A& a);
private:
	int _a;
};

A& A::operator=(const A& a) = default;		//类外定义

int main(){
	A a1(10);
	A a2;
	a2 = a1;
	return 0;
}

删除默认函数

如果能想要限制某些默认函数的生成:

class A{
public:
	A(int a)
	 : _a(a)
	{}
	
	 // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	A(const A&) = delete;
	A& operator(const A&) = delete;
private:
	int _a;
};

int main(){
	A a1(10);
	A a2(a1);
	// 编译失败,因为该类没有拷贝构造函数
	
	A a3(10);	
	a3 = a2;	
	// 编译失败,因为该类没有赋值运算符重载
	
	return 0;
}

9. 右值引用【★】

移动语义

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:

class String{
public:
	String(char* str = ""){
		if (nullptr == str)
			str = "";
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}

	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	String& operator=(const String& s){
		if (this != &s){
			char* pTemp = new char[strlen(s._str) + 1];
			strcpy(pTemp, s._str);
			delete[] _str;
			_str = pTemp;
		}
		return *this;
	}

	~String(){
		if (_str) delete[] _str;
	}
private:
	char* _str;
};

假设现在有一个函数,返回值为一个String类型的对象:

String GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;		//此时不是返回栈上对象strTemp,而是拷贝构造一个临时对象返回
}
int main(){
	String s2(GetString("world"));	
	/* 用GetString返回的临时对象构造s2
		s2构造完成后临时对象将被销毁,因为临时对象临时对象不能直接返回
		因此编译器需要拷贝构造一份临时对象,然后将strTemp销毁
	*/
	return 0;
}

上述代码看起来没有什么问题,但是有一个不太尽人意的地方:GetString函数返回的临时对象,将s2拷贝构造成功之后,立马被销毁了(临时对象的空间被释放),再没有其他作用;
s2在拷贝构造时,又需要分配空间,一个刚释放一个又申请,有点多此一举。
那能否将GetString返回的临时对象的空间直接交给s2呢?这样s2也不需要重新开辟空间了,代码的效率会明显提高。

String(String&& s)		//两个 &
	: _str(s._str)
	{
		 s._str = nullptr; 
	} 

C++11中的右值

右值引用,顾名思义就是对右值的引用。C++11中,右值由两个概念组成:纯右值将亡值

右值引用

右值引用书写格式:

类型&& 引用变量名字 = 实体;

右值引用最长常见的一个使用地方就是:与移动语义结合,减少无必要资源的开辟来提高代码的运行效率

改造一下刚才的例子代码演示

String&& GetString(char* pStr){
	String strTemp(pStr);
	return strTemp;
}

int main(){
	String s1("hello");
	String s2(GetString("world"));
	return 0;
}

右值引用另一个比较常见的地方是:给一个匿名对象取别名,延长匿名对象的声明周期。

String GetString(char* pStr){
	return String(pStr);
}
int main(){
	String&& s = GetString("hello");
	return 0;
}

【注】:

  1. 与引用一样,右值引用在定义时必须初始化
  2. 通常情况下,右值引用不能引用左值
int main(){
	int a = 10;
	int&& ra; // 编译失败,没有进行初始化
	int&& ra = a; // 编译失败,a是一个左值
	
	const int&& ra = 10;	// ra是匿名常量10的别名
	return 0;
}

std::move()

C++11中,std::move()函数位于<utility> 头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。

注意:被转化的左值,其生命周期并没有随着左右值的转化而改变,即std::move转化的左值变量left_value不会被销毁。

// 移动构造函数
class String{
	String(String&& s) 
	 : _str(s._str){
		s._str = nullptr;
	}
};
int main(){
	String s1("hello world");
	String s2(move(s1));
	String s3(s2);
	return 0;
}

move()更多的是用在生命周期即将结束的对象上。

【注】:为了保证移动语义的传递,程序员在编写移动构造函数时,最好使std::move转移拥有资源的成员为右值。

注意点

  1. 如果将移动构造函数声明为常右值引用或者返回右值的函数声明为常量,都会导致移动语义无法实现。
String(const String&&);
const Person GetTempPerson();
  1. C++11中,无参构造函数 / 拷贝构造函数 / 移动构造函数实际上有3个版本:
Object();
Object(const T&);
Object(T &&);
  1. C++11中默认成员函数
    默认情况下,编译器会为程序员隐式生成一个(如果没有用到则不会生成)移动构造函数。如果程序员声明了自定义的构造函数、移动构造、拷贝构造函数、赋值运算符重载、移动赋值、析构函数,编译器都不会再为程序员生成默认版本。编译器生成的默认移动构造函数实际和默认的拷贝构造函数类似,都是按照位拷贝(即浅拷贝)来进行的。因此,在类中涉及到资源管理时,程序员最好自己定义移动构造函数。其他类有无移动构造都无关紧要。但在C++11中,拷贝构造/移动构造/赋值/移动赋值函数必须同时提供,或者同时不提供,程序才能保证类同时具有拷贝和移动语义。

完美转发

完美转发是指:在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x){
	// ......
}
template<typename T>
void PerfectForward(T t){
	Fun(t);
}

PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美:

C++11通过forward函数来实现完美转发, 比如:

void Fun(int &x) { cout << "lvalue ref" << endl; }
void Fun(int &&x) { cout << "rvalue ref" << endl; }
void Fun(const int &x) { cout << "const lvalue ref" << endl; }
void Fun(const int &&x) { cout << "const rvalue ref" << endl; }

template<typename T>
void PerfectForward(T &&t) { Fun(std::forward<T>(t)); }

int main(){
	PerfectForward(10); // rvalue ref
	
	int a;
	PerfectForward(a); // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref
	const int b = 8;
	PerfectForward(b); // const lvalue ref
	PerfectForward(std::move(b)); // const rvalue ref
	return 0;
}

感谢您阅读至此,感兴趣的看官们可以移步上篇与下篇,继续了解C++11剩余新特性~

【从零学C++11(上)】列表初始化decltype关键字、委派构造等新特性
https://blog.csdn.net/qq_42351880/article/details/100140163

【从零学C++11(下)】lambda表达式、线程库原子操作库等新特性
https://blog.csdn.net/qq_42351880/article/details/100144882

标签:11,std,String,零学,右值,C++,str,构造函数,函数
来源: https://blog.csdn.net/qq_42351880/article/details/100144856