#include <iostream>
using namespace std;
class Object
{
public:
Object(int i = 1) { n = i; cout << "Object::Object()" << endl; }
Object(const Object& a)
{
n = a.n;
cout << "Object::Object(const Object&)" << endl;
}
~Object() { cout << "Object::~Object()" << endl; }
void inc() { ++n; }
int val() const { return n; }
private:
int n;
};
void foo(Object a)
{
cout << "enter foo, before inc(): inner a = " << a.val() << endl;
a.inc();
cout << "enter foo, after inc(): inner a = " << a.val() << endl;
}
int main()
{
Object a; ①
cout << "before call foo : outer a = " << a.val() << endl;
foo(a); ②
cout << "after call foo : outer a = " << a.val() << endl; ③
return 0;
}
输出为:
Object::Object() ④
before call foo : outer a = 1
Object::Object(const Object&) ⑤
enter foo, before inc(): inner a = 1 ⑥
enter foo, after inc(): inner a = 2 ⑦
Object::~Object() ⑧
after call foo : outer a = 1 ⑨
Object::~Object()
可以看到,④处的输出为①处对象a的构造,而⑤处的输出则是②处foo(a)。调用开始时通过构造函数生成对象a的复制品,紧跟着在函数体内检查复制品的值。输出与外部原对象的值相同(因为是通过拷贝构造函数),然后复制品调用inc()函数将值加1。再次打印出⑦处的输出,复制品的值已经变成了 2。foo函数执行后需要销毁复制品a,即⑧处的输出。foo函数执行后程序又回到main函数中继续执行,重新打印原对象a的值,发现其值保持不变(⑨ 处的输出)。
重新审视foo函数的设计,既然它在函数体内修改了a。其原意应该是想修改main函数的对象 a,而非复制品。因为对复制品的修改在函数执行后被"丢失",那么这时不应该传入Object a,而是传入Object& a。这样函数体内对a的修改,就是对原对象的修改。foo函数执行后其修改仍然保持而不会丢失,这应该是设计者的初衷。
如果相反,在foo函数体内并没有修改a。即只对a执行"读"操作,这时传入const Object& a是完全胜任的。而且还不会生成复制品对象,也就不会调用构造函数/析构函数。
综上所述,当函数需要修改传入参数时,如果函数声明中传入参数为对象,那么这种设计达不到预期目的。即是错误的,这时应该用应用传入参数。当函数不会修改传入参数时,如果函数声明中传入参数为对象,则这种设计能够达到程序的目的。但是因为会生成不必要的复制品对象,从而引入了不必要的构造/析构操作。这种设计是不合理和低效的,应该用常量引用传入参数。
下面这个简单的小程序用来验证在构造函数中重复赋值对性能的影响,为了放大绝对值的差距,将循环次数设置为100 000:
#include <iostream>
#include <windows.h>
using namespace std;
class Val
{
public:
Val(double v = 1.0)
{
for(int i = 0; i < 1000; i++)
d[i] = v + i;
}
void Init(double v = 1.0)
{
for(int i = 0; i < 1000; i++)
d[i] = v + i;
}
private:
double d[1000];
};
class Object
{
public:
Object(double d) : v(d) {} ①
/*Object(double d) ②
{
v.Init(d);
}*/
private:
Val v;
};
int main()
{
unsigned long i, nCount;
nCount = GetTickCount();
for(i = 0; i < 100000; i++)
{
Object obj(5.0);
}
nCount = GetTickCount() - nCount;
cout << "time used : " << nCount << "ms" << endl;
return 0;
}
类Object中包含一个成员变量,即类Val的对象。类Val中含一个double数组,数组长度为1 000。Object在调用构造函数时就知道应为v赋的值,但有两种方式,一种方式是如①处那样通过初始化列表对v成员进行初始化;另一种方式是如②处那样在构造函数体内为v赋值。两种方式的性能差别到底有多大呢?测试机器(VC6 release版本,Windows XP sp2,CPU为Intel 1.6 GHz内存为1GB)中测试结果是前者(①)耗时406毫秒,而后者(②)却耗时735毫秒,如图2-1所示。即如果改为前者,可以将性能提高 44.76%。
图2-1 两种方式的性能对比
从图中可以直观地感受到将变量在初始化列表中正确初始化,而不是放置在构造函数的函数体内。从而对性能的影响相当大,因此在写构造函数时应该引起足够的警觉和关注。