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

class Test{
private:
    int ma;
public:
//    explicit Test(int a = 10):ma(a) { cout<<"Test(int)"<<endl;}
    Test(int a = 10):ma(a) { cout<<"Test(int)"<<endl;}
    Test(const Test& t):ma(t.ma){cout<<"Test(const Test&)"<<endl;}
    Test& operator = (const Test& t){
        cout<<"Test::operator = ()"<<endl;
        ma = t.ma;
        return *this;
    }
    ~Test(){
        cout<<"~Test()"<<endl;
    }
};

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<<"-----------------------"<<endl;
    t4 = (Test)20; //这里是把int强转为Tset类型;编译器会看有没有合适的构造函数,
    //发现Test有一个带整型参数的构造函数,所以这里生成了临时对象;
    //所以这里是:先调用构造函数生成临时对象,然后调用拷贝赋值运算符,然后析构函数释放临时对象;
    //即便构造函数前面加了explict这里还是对的,因为这里是显式的强制类型转换;
    //explict关键字是抑制构造函数的隐式类型转换;
    cout<<"----------"<<endl;
    t4 = 20; //隐式类型转换,int转Test,编译器发现Test有一个带整型参数的构造函数,
    //所以这里隐式生成了临时对象,然后调用拷贝赋值运算符,之后析构函数释放临时对象;
    //但是如果Test按个带整型参数的构造函数前面加了explict,那么这里就错误了,
    //因此explict构造函数限制了隐式的类型转换,这里的隐式类型转换就是错误的;
    cout<<"============================"<<endl;
    //    Test* p = &Test(40); //错误:指针指向临时对象,因为临时对象没有名字,
生命周期就是所在的语句,语句结束后临时对象就析构了,那么p就变成了一个野指针,所以报错;
//    Test& ref = Test(50); //错误:普通左值引用指向临时对象
    const Test& ret = Test(50); 
//常引用可以绑定到临时对象上,临时对象的生命周期就跟常引用的生命周期一样了;
    Test&& rRef = Test(50); //正确:右值引用指向临时对象,fRef类型是右值引用,但是它本身是一个左值;

    return 0;
}

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

class Test{
public:
    Test(int a= 5,int b = 5):ma(a),mb(b){cout<<"Test(int,int)"<<endl;}
    //因为函数参数压栈是从右到左,所以函数默认值也要从右到左给;
    ~Test() {cout<<"~Test()"<<endl;}
    Test(const Test& src):ma(src.ma),mb(src.mb){cout<<"Test(const Test&)"<<endl;}
    Test& operator = (const Test& src   ){
        cout<<"operator = (Test&)"<<endl;
        ma = src.ma;
        mb = src.mb;
    }
private:
    int ma;
    int mb;
};
Test t1(10,10); //在main函数之前,编译器先对全局变量初始化,所以这里调用构造函数;
static Test t0;
int main(){
    cout<<"1-----------"<<endl;
    Test t2(20,20); //构造函数
    Test t3 = t2; //拷贝构造函数(因为是定义并初始化t3对象)
    static Test t4  = Test(20,30);  //构造函数
//静态局部变量在运行的时候内存已经存在了,但静态局部变量的初始化是第一次运行到它的时候才进行的
// 局部静态变量在编译器第一次遇见时候进行初始化,然后生命周期存在于程序结束;
    t2 = Test(40,40); //左边t2已经存在,所以调用构造函数生成临时对象,t2调用拷贝赋值运算符,
//临时对象作参数;当本行语句结束临时对象生命周期结束调用析构函数
    t2 = (Test)(50,50);
//注意逗号,表达式(50,50)的结果是右边元素,所以结果是50,相当于是t2 = (Test)50;
//将50显示强制类型转换为Test,所以调用构造函数生成临时对象,t2调用拷贝赋值运算符
//临时对象当参数;当本行语句结束时临时对象生命周期结束调用析构函数
    t2  = 60; //60隐式生成临时对象,调用拷贝赋值运算符,临时对象生命周期结束调用析构函数
    Test* p1 = new Test(70,70); 
//new运算符调用operator new分配内存,然后构造函数初始化new出来的内存上的对象
    Test* p2 = new Test[2]; //二次构造函数调用
//    Test* p3 = &Test(80,80); 错误;指针指向临时对象,临时对象析构后,指针就变成野指针了;
//    Test& p4 = Test(90,90); 错误,普通引用不能指向临时对象
    const Test p4 = Test(90,90); //常引用可以引用临时对象
    delete p1; //析构函数
    delete  [] p2; //二次析构函数
    return 0;
}
Test t5(100,100); //注意:这个虽然在main函数下面,但是也是全局变量;所以main函数运行之前会初始化这个全局变量,因此调用构造函数;
//mian函数之前结束前,析构之前main函数中的局部对象,析构全局对象

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<typename Ty>
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<Ty>(val));
通过类型完美转发,如果val是左值引用类型那么返回左值,匹配左值引用参数的construct函数;
如果val是右值引用类型那么返回右值,匹配右值引用参数的construcct函数;
总结:std::forward:类型完美转发,能够识别左值和右值类型; */
std::move:移动语义,将左值强转为右值;
    _last++;
}
template<typename Ty>
void construct(T p, Ty&& val) { 
//指针还是元素的T类型;Ty是给引用变量用的
    new (p) T(std::forward<Ty>(val));
}

补充其他知识点:

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

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

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

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值