#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 兩種方式的性能對比
從圖中可以直觀地感受到將變量在初始化列表中正確初始化,而不是放置在構造函數的函數體內。從而對性能的影響相當大,因此在寫構造函數時應該引起足夠的警覺和關注。