目录
产生临时对象的情况和解决方案
1.以传值的方式给函数传递参数
执行以下案例:
传值时的赋值操作
#include <iostream>
using namespace std;
class CTempValue
{
public:
int val1;
int val2;
public:
CTempValue(int v1 = 0, int v2 = 0);
CTempValue(const CTempValue& t) : val1(t.val1), val2(t.val2)
{
cout << "调用了拷贝构造函数" << endl;
}
virtual ~CTempValue()
{
cout << "调用了析构函数" << endl;
}
public:
int Add(CTempValue tobj);
};
CTempValue::CTempValue(int v1, int v2) : val1(v1), val2(v2)
{
cout << "调用了构造函数!" << endl;
cout << "val1=" << v1 << endl;
cout << "val2=" << v2 << endl;
}
int CTempValue::Add(CTempValue tobj)
{
int tmp = tobj.val1 + tobj.val2;
tobj.val1 = 1000;
return tmp;
}
int main(void)
{
CTempValue tm(10, 20);
int Sun = tm.Add(tm);
cout << "Sun = " << Sun << endl;
cout << "tm.val1 = " << tm.val1 << endl;
}
运行结果:
结果中,调用了拷贝构造函数,其调用原因是:调用Add成员函数时把对象tm传递给了Add成员函数,此时,系统会调用拷贝构造函数创建一个新的副本tobj(成员函数Add的形参),用于在函数体Add内使用,因为这是一个副本,所以val1 = 1000的值并不会影响外界tm对象的val1值.
所以不难看出,"int CTempValue::Add(CTempValue tobj)"这行代码中的形参tobj是一个局部对象(局部变量),从程序功能的角度来讲,函数体内需要临时使用它一下,来完成一个程序的功能,求和运算,但他确实又是一个局部变量,只能在Add函数体内使用,所以从严格意义上讲,它又不能称为一个临时对象,因为真正的临时对象往往指的是真实存在,但又感觉不到的对象.
此过程中,代码生成tobj对象,调用了CTempValue类的拷贝构造函数,有了复制的动作,所以会影响到程序执行效率.
代码优化方式:
在定义和实现中,把对象修改为引用即可,修改如下.
类 CTempValue 的 Add 成员函数的声明代码为:
int Add(CTempValue& tobj);
类 CTempValue 的 Add 成员函数的声明实现为:
int CTempValue::Add(CTempValue& tobj)
{
int tmp = tobj.val1 + tobj.val2;
tobj.val1 = 1000; //此时的修改会对外界产生影响了
return tmp;
}
运行结果:
结果可以发现,少了一次调用拷贝构造函数和析构函数,提示了效率,而且引用修改val1影响了函数外部val1的值.
2.类型转换生成的临时对象/隐式类型转换以保证函数调用成功
下文的案例中的临时对象是真实存在的,但从程序代码的角度不能直接看到它.
接着上面案例:
在main主函数中写这两行
CTempValue sum;
sum = 1000; //这里产生了一个临时对象
运行结果:
第一行代码" CTempValue sum;" 执行的是:
而" sum = 1000;"产生的结果是:
从这4行运行结果可以看出,系统在执行这行语句时,调用了一次CTempValue类的构造函数和析构函数,这说明系统肯定产生了一个对象,但这个对象在哪里,通过代码完全看不到,所以这个对象是一个真正的临时对象.
产生原因:
把1000赋值给sum,sum本身是一个CempValue类型的对象,1000是一个int类型的数字,这里编译器帮助我们以1000位参数调用了CTempValue的构造函数创建了一个临时对象,因为CTempValue构造函数的两个参数都有默认值,所以这里的数字顶替了第一个参数,而第二个参数系统就用了默认值,所以从1000是可以成功创建出CtempValue对象的.
为了方便进一步观察,往CTempValue类中添加public修饰的赋值运算符的定义代码:
CTempValue& operator = (const CTempValue& tmpv)
{
val1 = tmpv.val1;
val2 = tmpv.val2;
cout << "调用了拷贝赋值运算符!" << endl;
return *this;
}
加完后运行的结果:
多了一句"调用了拷贝赋值运算符!".
"sum = 1000"这行代码做的内容:
(1)用1000这个数字创建了一个类型为CTempValue的临时对象.
(2)调用拷贝赋值运算符,把这个临时对象里面的各个成员值赋给了sum对象.
(3)销毁这个刚刚创建的CTempValue临时对象.
因为产生了临时对象.所以起代码优化的方式如下:
//只需要把这两行代码写成一行即可
CTempValue sum = 1000;
运行结果:
这次运行系统没有生成临时对象
原因讨论:
针对"CTempValue sum = 1000;“这行代码,这里的”="是定义是初始化,不是赋值,这行代码的工作过程可以这样理解:在定义了sum对象,系统就为sum对象创建了预留空间,然后用1000调用构造函数来构造临时对象的时候,这种构造是为sum对象创建的预留空间里进行的,所以并没有真的产生临时对象.
3.函数返回对象的时候
同案例2,临时对象雀氏存在,但从程序代码的校对又无法直接看到它.
继续刚才的案例添加.在当前文件中添加一个普通的全局函数:
CTempValue Double(CTempValue& ts)
{
CTempValue tmpm; //这里会消耗一次构造和一次析构函数的调用
tmpm.val1 = ts.val1 * 2;
tmpm.val2 = ts.val2 * 2;
return tmpm;
}
main主函数中添加一下测试代码
CTempValue ts1(10, 20);
Double(ts1); //这里方便区分效果,先不接受函数Double返回的结果
执行结果:
(三个析构分别是,Double中的tmpm,临时对象的和main中ts1)
可以设置断点,观察每一步所调用的函数
return tmpm;这行代码执行后,结果会多出一下两行输出结果:
通过这两行可以看出,这里肯定生成了一个临时对象.
那么这个临时对象是产生的原因是什么呢?
(1)“调用了拷贝构造函数”,认为是系统生成了一个临时CTempValue对象导致,同时,系统还把tmpm对象信息赋值给临时对象了,因为tmpm对象的生命周期马上结束,在销毁之前,系统要把tmpm的信息复制到临时对象中去.
(2)“调用了析构函数”,认为是Double函数里面的tmpm对象销毁时系统调用了CTempValue类的析构函数所致.(当前函数的生命周期结束,系统自动释放的)
继续跟程序运行,当程序流程从Double函数返回,可以看到程序有输出了一行结果信息:
这里的析构其实就是临时对象的释放.
刚刚的代码便于区分,没有接收Double函数的返回结果,现在修改一下main中的代码,用te3来接Double函数返回的值:
CTempValue ts1(10, 20);
CTempValue ts3 = Double(ts1);
执行结果:
虽然这次也调用了三次析构,但是跟上次则不同
(三个析构分别是,Double中的tmpm,main中ts1和main中的ts3)
通过断点也可以看出来,return tmpm;这行代码执行结束,流程同样是从Double返回,结果跟上次少了一个析构函数:
原因就是这里函数Double返回的临时对象实际是被直接构造到ts3里面去了,就相当于临时对象被ts3接管了或者说ts3就是这个临时对象(这是编译器内部的优化手段和措施)
也可以使用右值引用来接,因为临时对象就是一种右值;
CTempValue ts1(10, 20);
CTempValue&& ts3 = Double(ts1);
这样执行的结果也是一样的.
代码优化方式:
改造一下Double函数(只是当前函数使用此方法,否则只能产生临时方法去返回).
CTempValue Double(CTempValue& ts)
{
//CTempValue tmpm; //这里会消耗一次构造和一次析构函数的调用
//tmpm.val1 = ts.val1 * 2;
//tmpm.val2 = ts.val2 * 2;
//return tmpm;
return CTempValue(ts.val1 * 2, ts.val2 * 2);
}
运行结果:
省去了一次"拷贝构造函数"和"析构函数";