详解C++对象优化-右值引用-移动语义-完美转发

class Test{
private:
    int ma;
public:
//    explicit Test(int a = 10):ma(a) { cout<<"Test(int)"<

1.对象背后调用哪些方法?

看看上面这个类的定义,在main函数中,对象背后调用了什么方法?

int main()
{
    Test t1; //构造函数
    Test t2(t1);//拷贝构造函数
    Test t3 = t2; //因为定义对象了,所以调用拷贝构造函数
    Test t5 = Test(60);//原理等同于: t4
    Test t4 = Test(20);  //Test(20)就是显式生成临时对象,临时对象没有名字,
    所以生存周期就是所在的语句,语句结束之后,临时对象就析构了;
    //通常我们认为:调用构造函数创建临时对象,然后t4调用拷贝构造函数通过临时对象构造自己,
    然后析构函数释放临时对象,但C++编译器在这里一般都有优化;
    //注意:一般C++编译器都有这个优化:《用临时对象拷贝构造一个新对象,那么临时对象就不产生了,直接构造新对象》
    //所以这里没有临时对象了,直接调用构造函数构造t4;跟Test t4(20)没有区别;
    //类似于:string str1 = string("hello"); 等价于 string str1("hello");
    t4 = t2 ;//调用拷贝赋值运算符:t4.operator = (const Test&) ;
    t4 = Test(30); //这里t4已经产生了,不是定义;所以这里是调用构造函数生成临时对象,
    //然后t4调用拷贝赋值运算符,临时对象被当成函数参数传进去,之后临时对象析构;
    cout<<"-----------------------"<

再来看另外一个例子,体会对象背后调用了哪些方法!

class Test{
public:
    Test(int a= 5,int b = 5):ma(a),mb(b){cout<<"Test(int,int)"<

2.函数调用过程中对象背后调用的方法:

看看下面代码背后调用了哪些函数:

class Test {
public:
	Test(int data = 10) :ma(data) { cout << "Test(int)" << endl; }
	~Test() { cout << "~Test()" << endl; }
	Test(const Test& t) :ma(t.ma) { cout << "Test(const Test&)" << endl; }
	void operator = (const Test& t) {
		ma = t.ma;
		cout << "Test::operator = " << endl;
	}
	int getData()const {
		return ma;
	}
private:
	int ma;
};
Test GetObject(Test t) { 
//不能返回local object局部对象的指针或者引用,因为函数结束后局部变量就析构了
//实参给形参是值传递,所以调用3:Test(const Test&)拷贝构造函数
	int val = t.getData();
	Test tmp(val); //4:Test(int)构造函数
	return tmp; //5.Test(const Test&)拷贝构造
	//6.~Test()析构函数析构t
	//7~Test()析构函数析构tmp
//tmp是函数内局部对象,要想返回到main函数,
//会在main函数上开辟一块临时空间,然后调用拷贝构造函数拷贝tmp到main函数栈内存上
	//<函数参数压栈原理和返回值传递原理:看程序员自我修养-装载链接与库>
}
int main() {
	Test t1; //1:Test(int)构造函数
	Test t2;//2:Test(int)构造函数
	t2 = GetObject(t1); //8:t2调用拷贝赋值运算符,参数是临时对象
	//9.析构函数:临时对象
	//10.t2析构函数
	//11.t1析构函数
	return 0;

3.总结3条对象优化原则:

针对上面代码,发现编译器在背后调用了大量的函数,如何优化呢?3条原则

1.函数参数优先用引用传递:pass by reference (加const否看函数内部是否更改参数)

2.函数返回对象时候,应该优先return 临时对象,而不要返回一个定义过的对象;

3.接受《返回值是对象的函数》优先按初始化的方式接受,不要按赋值的方式接受;

通过3条原则优化上面代码如下:

Test GetObject(const Test &t) {
//函数参数按引用传递:减少了t的拷贝构造函数和析构函数调用
	int val = t.getData();
	return  Test(val); //函数返回:直接return 临时对象
//不需要在这里构造临时对象了,直接构造mian函数栈上的临时对象
}

int main1() {
	//优化:1.函数参数优先用引用传递:pass by reference (const看函数内部更改变量否选择加不加)
	//2.函数返回对象时候,应该优先return 临时对象,而不要返回一个定义过的对象;
	//3.接受<返回值是对象的函数>,优先按初始化的方式接受,不要按赋值的方式接受;
	/*注:《用临时对象拷贝构造一个新对象,C++会进行优化,不产生临时对象了,直接用产生临时对象的方式去构造新对象;》 */
	Test t1; //1:Test(int)构造函数
	Test t2;//2:Test(int)构造函数
	t2 = GetObject(t1); 
//8:产生man函数中的临时对象《3.构造函数》,t2调用4.拷贝赋值运算符,参数是临时对象;
	//5.析构函数:临时对象
	//6.t2析构函数
	//7.t1析构函数
	return 0;
}
//mian函数中:接受返回值是临时对象的函数,优先用初始化的方式接受,而不是赋值的方式;
int main() {

	Test t1; //1:Test(int)构造函数
	Test t2 = GetObject(t1);//2构造函数
	//用临时对象拷贝构造一个新对象,临时对象不产生了,直接用产生临时对象的方式去构造新对象
	//2个析构函数
	return 0;
}

4:带右值引用的拷贝构造函数和拷贝赋值运算符(移动构造函数和移动赋值运算符)

首先了解一下一下右值和右值引用:

	int a = 10;//a是左值:有内存,有名字  ; 10是右值:临时对象,没名字;
	int& b = a; //左值引用绑定左值
	const int& d = 10; 
	/*常左值引用可以绑定到右值上,实际是右值生成了一个临时对象,
	d引用到哪个临时对象上了,此时临时对象的生命周期跟引用d绑定了;d只能读不能写*/
//	int&& c = a; //错误;无法将左值绑定到右值引用上面;右值引用只能绑定右值;
	int&& c = 10; /*c是右值引用类型,<它本身是左值>,它绑定到右值10上面;
				 实际上这里也是10生成临时变量,c绑定到临时变量上;但是c可读可写; 
				 注:右值引用类型的变量本身是左值;
				  */

给自定义String类添加带右值引用的拷贝构造函数和拷贝赋值运算符:

/*类定义中增加带右值引用的拷贝构造函数和拷贝赋值运算符;
那么当临时对象进行拷贝或者赋值时候就调用这两个版本了;*/
CMyString::CMyString(CMyString&& str) 
//右值引用类型参数:临时对象调用的右值引用的拷贝构造函数
{
	cout << "CMyString(CMyString&&)" << endl;
	m_data = str.m_data;
	str.m_data = nullptr; 
}
CMyString& CMyString::operator=(CMyString&& str) 
//右值引用类型参数:临时对象调用的右值引用的拷贝赋值运算符
{
	cout << "operator = (CMyString&&)" << endl;
	if (this == &str) {
		return *this;
	}
	delete[] m_data;
	m_data = str.m_data;
	str.m_data = nullptr;
	return *this;
}

5.通过模板实现右值引用函数:

对自定义vector中的push_back实现右值引用版本

template
void push_back(Ty&& val) { 
/* Ty&& 就是万能引用: push_back可以接受左值也可以接受右值;
 引用不是对象,通常不能定义引用的引用,但是通过模板类型参数就可以定义引用的引用;
1.<右值引用的特殊类型推断原则>:如果将一个左值传递给函数的右值引用参数,且此右值引用参数指向模板类型参数(如:Ty&&),编译器会推断模板类型参数为实参的左值引用类型;
2.<引用折叠>:如果通过模板类型参数创建了引用的引用,那么会发生引用折叠;即除了右值引用的右值引用会折叠为右值引用,其他情况引用折叠的结果都是左值引用; 参考C++Primer;
所以这里如果传递左值,类型推导Ty类型就是CMyString& ,那么函数形参CMySting& && 引用折叠为CMyString&;如果传递右值,类型推导Ty类型就是CMySting&&,那么函数形参CMyString&& &&引用折叠为CMyString&&;*/
    if (full()) {
        expand();
    }
/*_allocator.construct(_last, val);
但是在construct这里不管val是左值引用类型还是右值引用类型,val变量它本身是左值;
所以这里匹配的是construct的左值引用版本,怎么解决呢?通过完美转发std::forward即可
_allocator.construct(_last, std::forward(val));
通过类型完美转发,如果val是左值引用类型那么返回左值,匹配左值引用参数的construct函数;
如果val是右值引用类型那么返回右值,匹配右值引用参数的construcct函数;
总结:std::forward:类型完美转发,能够识别左值和右值类型; */
std::move:移动语义,将左值强转为右值;
    _last++;
}
template
void construct(T p, Ty&& val) { 
//指针还是元素的T类型;Ty是给引用变量用的
    new (p) T(std::forward(val));
}

补充其他知识点:

普通全局变量:在本文件中可以无限制使用,其他文件中通过extern关键字声明后也可以使用;

全局静态变量:即全局静态变量只能给本文件使用,其他文件不能使用;即在普通全局变量上取消了extern关键字声明,

局部静态变量:作用域在定义它的那个函数内,编译器在编译阶段为其分配地址,但是执行到它才进行初始化,且只初始化一次,编译器通过一个标志判断是否已经对局部静态变量进行初始化了;

三者主要区别就在于作用域不同,声明周期从程序运行开始到结束;

你可能感兴趣的:(C++,后端,c++)