对象使用过程中背后调用了哪些方法
代码示例1
class Test
{
public:
Test(int a = 10) :ma(a)
{ cout << "Test(int)" << endl; }
~Test()
{ cout << "~Test()" << endl; }
Test(const Test &t) :ma(t.ma)
{ cout << "Test(const Test&)" << endl; }
Test& operator=(const Test &t)
{
cout << "operator=" << endl;
ma = t.ma;
return *this;
}
private:
int ma;
};
很多人可能认为:Test t4=Test(20);是临时对象先构造,然后拿临时对象拷贝构造t4,然后语句结束,临时对象析构。
到底是不是这样?
我们来运行看看
t1是调用普通的构造函数。t2和t3都是调用拷贝构造函数。
t4是调用普通的构造函数。并没有向我们刚才说的那样。
int main()
{
Test t1;//调用构造函数
Test t2(t1);//调用拷贝构造函数
Test t3 = t1;//调用拷贝构造函数,因为t3还没有生成
//Test(20) 显示生成临时对象,是没有名字的,所以其生存周期:所在的语句
//语句结束,临时对象就析构了
/*
C++编译器对于对象构造的优化:用临时对象生成新对象的时候,临时对象
就不产生了,直接构造新对象就可以了
*/
Test t4 = Test(20);//和Test t4(20);没有区别的!
cout << "--------------" << endl;
return 0;
}
我们接下去再看
int main()
{
Test t1;//调用构造函数
Test t2(t1);//调用拷贝构造函数
Test t3 = t1;//调用拷贝构造函数,因为t3还没有生成
Test t4 = Test(20);//和Test t4(20);没有区别的!
cout << "--------------" << endl;
//t4.operator=(t2)
t4 = t2;//调用赋值函数,因为t4原本已存在
//Test(30)显式生成临时对象
//t4原本已存在,所以不是构造,这个临时对象肯定要构造生成的
//临时对象生成后,给t4赋值
//出语句后,临时对象析构
//t4.operator=(const Test &t)
t4 = Test(30);
cout << "--------------" << endl;
return 0;
}
我们接下去再看
int main()
{
Test t1;
Test t2(t1);
Test t3 = t1;
Test t4 = Test(20);
cout << "--------------" << endl;
t4 = t2;//t4调用赋值重载函数
t4 = Test(30);
//显式生成临时对象,赋值给t4,出语句后,临时对象析构
t4 = (Test)30;//把30强转成Test类型int->Test(int)
//把其他类型转成类类型的时候,编译器就看这个类类型
//有没有合适的构造函数 把整型转成Test,就看这个类类型有没有
//带int类型参数的构造函数 ,有,就可以显式生成临时对象,然后
//赋值给t4 出语句后,临时对象析构
//隐式生成临时对象,然后赋值给t4,出语句后,临时对象析构
t4 = 30;
//把整型转成Test,Test(30) int->Test(int) char*->Test(char*)
cout << "--------------" << endl;
return 0;
}
我们接下去再看
int main()
{
Test t1;
Test t2(t1);
Test t3 = t1;
Test t4 = Test(20);
cout << "--------------" << endl;
t4 = t2;
t4 = Test(30);
t4 = (Test)30;
t4 = 30
cout << "--------------" << endl;
Test *p = &Test(40);//指针指向临时对象,这个临时对象肯定是要生成的
//然后p指向这个临时对象的地址
//出语句后,临时对象析构
//此时p指向的是一个已经析构的临时对象,p相当于野指针了
const Test &ref = Test(50);//引用一个临时对象,这个临时对象也是要生成的
//出语句后,临时对象不析构,因为引用相当于是别名,临时对象出语句析构是因为没有名字
//用引用变量引用临时对象是安全的,临时对象就是有名字了,临时对象的生存周期就变成引用变量的
//生存周期了。引用变量是这个函数的局部变量,return完,这个临时对象才析构
cout << "--------------" << endl;
return 0;
}
代码示例2
class Test
{
public:
//因为a,b有默认值所以构造有3种方式:
//Test() Test(10) Test(10, 10)
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;
}
void operator=(const Test &src)//赋值函数
{
ma = src.ma;
mb = src.mb;
cout << "operator=" << endl;
}
private:
int ma;
int mb;
};
//对象的构造顺序标识:1,2,3...14
Test t1(10, 10);//1.Test(int, int)
int main()
{
Test t2(20, 20);//3.Test(int, int)
Test t3 = t2;//4.Test(const Test&)
//第一次运行到它才初始化的,static Test t4(30, 30);
static Test t4 = Test(30, 30);//5.Test(int, int)
t2 = Test(40, 40);//6.Test(int, int) operator= 出语句调用 ~Test()
//(50,50)是逗号表达式,(表达式1,表达式2,表达式n)
//(50,50)的最后的结果是最后一个表达式n的结果 50
//(50, 50) = (Test)50; Test(int)
t2 = (Test)(50, 50);//7.Test(int,int) operator= 出语句调用~Test()
t2 = 60;//Test(int) 8.Test(int,int) operator=出语句调用~Test()
Test* p1 = new Test(70, 70);//9. Test(int,int) 要调用delete才析构对象
Test* p2 = new Test[2];//10. Test(int,int) Test(int,int) 要调用delete才析构对象
Test* p3 = &Test(80, 80);//11. Test(int,int) 出语句调用~Test()
const Test& p4 = Test(90, 90);//12. Test(int,int)
delete p1;//13.~Test()
delete[]p2;//14. ~Test() ~Test()
}
Test t5(100, 100);//2.Test(int, int)
代码示例3
class Test
{
public:
//有默认值,可以有2种构造方式:Test() Test(20)
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)
{
cout << "operator=" << endl;
ma = t.ma;
}
int getData()const { return ma; }
private:
int ma;
};
这个方法接收一个Tesl类型的对象,然后把Test对象的值ma取出来赋给val,然后拿val值来构造函数里的局部对象,然后把这个局部对象返回回去。
在这里不能返回指针或者引用,因为返回指针或者引用,要保证函数结束,这个对象还存在,如果对象不存在了,指针间接地访问这个对象的内存就非法访问了。所以不能返回局部对象或者临时对象的地址或者引用。
不能返回局部的或者临时对象的指针或引用
下面这个对象的地址就可以在局部函数中返回了:
因为这个对象是在数据段,程序运行开始,内存就有了,第一次运行到它的时候,构造这个对象,整个程序运行结束,它才进行对象的析构。
我们继续分析下面这个代码:
当我们运行时,打印的结果是什么样的?
#include <iostream>
using namespace std;
class Test
{
public:
//有默认值,可以有2种构造方式:Test() Test(20)
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)
{
cout << "operator=" << endl;
ma = t.ma;
}
int getData()const { return ma; }
private:
int ma;
};
Test GetObject(Test t)
{
int val = t.getData();
Test tmp(val);
return tmp;
}
int main()
{
Test t1;//1、调用带整型参数的构造函数
Test t2;//2、调用带整型参数的构造函数
t2 = GetObject(t1);//函数调用,实参传递给形参,是初始化还是赋值?
//当然是初始化,对象初始化是调用构造函数,赋值是两个对象都存在 调用左边对象的=重载
//t1是已经构造好的Test对象,而形参是t是正在定义的Test对象
//3、调用Test(const Test&) 拿t1拷贝构造形参t
//4、调用Test(int)的构造,构造tmp对象 然后return tmp;tmp是不能直接给t2赋值的
//因为tmp和t2是两个不同函数栈帧上的对象,是不能直接进行赋值的 GetObject函数完成调用时
//tmp对象作为局部对象就析构了 ,为了把返回值带出来, 在return tmp;这里,首先要在main函数栈帧
//上构建一个临时对象,目的就是把tmp对象带出来,
//5、调用 Test(const Test&),tmp拷贝构造main函数栈帧上的临时对象
//6、出 GetObject作用域,tmp析构
//7、形参t对象析构
//8、operator =,把main函数刚才构建的临时对象赋值给t2,临时对象没名字,出了语句就要析构
//9、把main函数刚才构建的临时对象析构
//10、main函数结束,t2析构
//11、t1析构
return 0;
}
总结三条对象优化的规则
- 函数参数传递过程中,对象优先按引用传递,这样可以省去一个形参t的拷贝构造调用,形参没有构建新的对象,出作用域也不用析构了,所以不要按值传!
- 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
- 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
我们看下面代码示例
我们该如何对上面代码优化呢?
1、函数参数传递过程中,对象优先按引用传递,这样可以省去一个形参t的拷贝构造调用,形参没有构建新的对象,出作用域也就不用析构了,所以不要按值传哦!
没有t1的拷贝构造,形参t没有新的对象,出作用域也不用析构。
省去了形参t的拷贝构造和形参t的析构
2、函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
t1和t2都是调用带整型参数的构造函数。
然后实参t1到形参t是传引用,没有对象产生。
return Test(val); 返回的是一个临时对象,我们想的是调用一个带整型参数的构造函数生成临时对象,语句中的临时对象出不了GetObject这个函数的,GetObject函数一结束,栈帧回退,这个临时对象就析构了。所以,为了把这个临时对象带出去,只能在main函数的栈帧上用这个临时对象拷贝构造一个新的临时对象。
但是用一个临时对象拷贝构造一个新对象,C++编译器都会去优化,临时对象就不产生了,而是用产生临时对象的方式直接构造新对象。
所以,return Test(val); 时这个临时对象是不产生的。
是直接在main函数的栈帧上构造这个临时对象就可以了。
所以出GetObject函数作用域,不用进行局部对象的析构了。因为这个临时对象没有产生。
然后就是拿main函数栈帧的这个临时对象对t2进行赋值,出语句后,main函数栈帧上的临时对象析构。
然后就是t2析构,t1析构。
这次优化,少了tmp的构造和析构。
3、接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
1、调用普通构造函数构造t1
2、是给t2初始化的过程。先处理GetObject函数调用。实参到形参没有产生对象。
3、return Test(val);只产生main函数栈帧上的临时对象
4、然后用这个临时对象给t2初始化!用这个临时对象拷贝构造同类型的新对象t2。C++编译器会进行优化,这个main函数栈帧上的临时对象都不产生了,直接构造t2对象。
也就是return Test(val);直接构造t2对象了
Test t2= GetObject(t1);在汇编上,除了把t1的地址传进去,还把t2的地址也传进去了,也压到函数栈帧上,所以return Test(val);就可以取到t2的地址,就知道在哪块内存上构造一个名为t2的对象。
然后析构t2
然后析构t1
Test GetObject(Test &t)
{
int val = t.getData();
return Test(val);
}
int main()
{
Test t1;
Test t2= GetObject(t1);
return 0;
}