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关键字声明,
局部静态变量:作用域在定义它的那个函数内,编译器在编译阶段为其分配地址,但是执行到它才进行初始化,且只初始化一次,编译器通过一个标志判断是否已经对局部静态变量进行初始化了;
三者主要区别就在于作用域不同,声明周期从程序运行开始到结束;