前言
在C++中很容易就写出一些代码,这些代码的特点就是偷偷的给你产生了一些临时对象,导致临时对象会调用拷贝构造函数,赋值运算符,析构函数,假如该对象还有继承的话,也会调用父类的拷贝构造函数,赋值运算赋函数等。这些临时对象所调用的函数,都是不必要的开销,也就是说,我本意不想你给我调用这些函数的,但你编译器却给我偷偷的调用了,就是由于我程序员写代码产生临时对象而产生的。
所以临时对象产生的话题也应运而生,这篇文章主要是探讨常见的临时对象产生的情况,及其如何避免和解决这种临时对象产生的方式。
1. 以值传递的方式给函数传参
这种是最常见的产生岭师对象的方式了。
以值传递的方式给函数传参这种方式会直接调用对象的拷贝构造函数,生成一个临时对象传参给函数。当临时对象销毁时候,也是函数形参销毁,也是函数执行完后,就会调用该临时对象的析构函数。此时,无论是调用拷贝构造函数和析构函数,都是额外的开销。
(验证是否调用拷贝构造函数和析构函数,可以在书写拷贝构造函数和析构函数验证) (验证是否为临时对象可以通过再函数内部修改形参的值,在函数外部打印看看是否修改成功)
验证临时对象的而外开销(1)
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "无参构造函数!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有参构造函数!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷贝构造函数!" << endl;
}
~Person()
{
cout << "析构函数!" << endl;
}
int fun(Person p)
{
p.m_age = 20;
return p.m_age;
}
int m_age;
};
int main()
{
Person p(10);
p.fun(p);
return 0;
}
先来预测一下调用函数的次数:也就是我们本意想调用的方式: 会执行一次 Person的有参构造函数; 会执行一次Person的析构函数;
于此同时我们看看,编译结果实际情况: 和我们预期并不一样!!! 多了一次拷贝构造函数和一次析构函数。这两个函数并不是我们希望要得,或者说,这个多余函数开销是不必要的;
产生的原因也很好理解:
由于 fun成员函数里面的形参是Person p ,这样会导致在调用这个fun函数时候,会传递过去的是实参的复制品,临时对象,并不是外面main函数的实参,这里可以在fun函数里修改一样形参就可以发现,外面的实参没发生改变。
所以产生的临时对象给形参传参时候,在我们看来类似 Person p = p ;实际上是Person p = temp ;而这句 Person p = temp ;就会发生拷贝构造函数啦,于此同时 fun函数调用结束后,p的声明周期也就结束,所以还会多调用析构函数。
解决方案
如何避免这种临时对象的产生呢?
只要把值传递的方式修改为引用传递的方式即可。这样既不会调用拷贝构造函数,也不会调用多一次临时对象的析构函数。减少额外不必要的开销。
所以我们在函数形参设计时候,能够用引用就用引用的方式,因为这样可以减少对象的复制操作,减少而外的开销。
代码不验证啦,因为比较简单,可以自行验证,修改 fun函数里形参为 Person& p;即可。
2. 类型转换成临时对象 / 隐式类型转换保证函数调用成功
这种方式就是并且把类型转化前的对象当作了形参传递给构造函数,生成临时对象临时对象结束后就会调用析构函数。
验证临时对象的而外开销(2)
代码依旧是上一个代码,只是在main函数做了不一样的动作
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "无参构造函数!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有参构造函数!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷贝构造函数!" << endl;
}
~Person()
{
cout << "析构函数!" << endl;
}
int fun(Person p)
{
p.m_age = 20;
return p.m_age;
}
int m_age;
};
int main()
{
Person p;
p = 1000;
return 0;
}
首先预测一下该代码执行的结果:
首先 调用一次无参构造函数,一次析构函数。
其次看看编译器运行的结果: 为啥会多出一个有参构造函数呢和析构函数呢?
其实是由于 p = 1000;这句引起的,这里p的类型为 Person,而 1000为 int 类型,很明显类型不一致。 编译器其实偷偷的进行了类型转换,如何转换呢?看编译器的调用都可以发现,其实就是创建一个临时对象,这个临时对象调用了有参构造函数,并且把 这个1000作为形参,传入有参构造函数,当这个函数调用结束后,对象也就销毁了,所以临时对象会调用析构函数。
解决方案
其实很简单的: 只要把单参数构造函数的复制(复制)语句,改为初始化语句就行。 那什么是复制语句和初始化语句呢? 两者的区别就是 一个是创建对象同时赋值对象,也就是说创建时候就马上初始化,这就是初始化; 一个是创建对象时候不赋值对象,而是等对象创建好,过后使用再赋值对象,这就是赋值语句啦;
那么我们只需要把:
Person p;
p = 1000;
修改为:
Person p = 1000;
这样就不会有多一次的有参构造和析构的开销了。
3. 函数返回对象时候
在函数返回对象时候,会创建一个临时对象接收这个对象;从而调用了拷贝构造函数,和析构函数。 当你调用函数,没有接收返回值时候,就会调用析构函数,因为都没有人接收返回值了,自然而然析构了。当你调用时候,有接收返回值时候,这个时候,并不会多调用一次析构函数,而是直接把临时对象返回值,给了接受返回值的变量来接收。
验证临时对象的而外开销(3)
代码:
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "无参构造函数!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有参构造函数!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷贝构造函数!" << endl;
}
~Person()
{
cout << "析构函数!" << endl;
}
int fun(Person p)
{
p.m_age = 20;
return p.m_age;
}
int m_age;
};
Person test(Person & p)
{
Person p1;
p1.m_age = p.m_age;
return p1;
}
int main()
{
Person p;
test(p);
return 0;
}
看看执行结果: 其实很好理解:就是以值的方式返回时候,就会多调用一次拷贝构造和析构函数; 结果中的第一个析构时test函数里p1对象的析构,第二个析构时 返回值时候临时对象的析构;第三个析构时main函数里p对象的析构;
请注意我的test函数在调用时候,我并没有给返回值,此时;当我以返回只接受时候,就会有不一样结果:不一样的地方就是,少了一次析构函数,其实少的这次析构函数时test函数里返回值产生的临时对象,因为,当你有对象接收返回值时候,就会直接把test函数里返回值临时对象给初始化接收返回值对象;
即,我修改main函数的代码:
int main()
{
Person p;
Person p2 = test(p);
return 0;
}
可以说时编译器优化手段吧。本来说 p2对象因该也是需要调用多一次拷贝构造函数的,但是由于有临时对象的初始化,所以p2对象就直接接管临时对象了。所以上面结果最后的析构函数,其实时p2对象的析构,并不是临时对象的析构。
解决方案
其实也很简单的解决办法:有两种:
-
当我们在接收函数返回的对象时候,可以用右值引用接收,因为该函数返回值是一个临时变量,用一个右值引用接收它,使得它的生命周期得以延续,这样就少调用一次析构函数的开销。(当然普通的对象接收也是可以) -
当我们在设计函数里的return 语句中,不是返回创建好的对象,而是返回我们临时创建的对象,即使用retturn 类类型(形参) ; 这个时候,就可以直接避免 return 对象 ;返回时候又要调用多一次构造函数。 这两种行为就可以避免了构造函数和析构函数的产生。
但是,右值引用我还没有写到这文章,所以先不讲右值引用的方案,讲第二种方案: 也就是设计函数返回语句 return时候,不要直接返回对象,而是返回临时对象,这个临时对象。
把这个代码修改:
Person test(Person & p)
{
Person p1;
p1.m_age = p.m_age;
return p1;
}
修改为:
Person test(Person &p)
{
return Person(p.m_age);
}
其实,只要以值得形式返回对象都会调用多一次拷贝构造函数,所以我们尽量避免这种情况,用合适的方式解决它。
|