前言
在C++中很容易就寫出一些代碼,這些代碼的特點就是偷偷的給你產(chǎn)生了一些臨時對象,導(dǎo)致臨時對象會調(diào)用拷貝構(gòu)造函數(shù),賦值運算符,析構(gòu)函數(shù),假如該對象還有繼承的話,也會調(diào)用父類的拷貝構(gòu)造函數(shù),賦值運算賦函數(shù)等。這些臨時對象所調(diào)用的函數(shù),都是不必要的開銷,也就是說,我本意不想你給我調(diào)用這些函數(shù)的,但你編譯器卻給我偷偷的調(diào)用了,就是由于我程序員寫代碼產(chǎn)生臨時對象而產(chǎn)生的。
所以臨時對象產(chǎn)生的話題也應(yīng)運而生,這篇文章主要是探討常見的臨時對象產(chǎn)生的情況,及其如何避免和解決這種臨時對象產(chǎn)生的方式。
1. 以值傳遞的方式給函數(shù)傳參
這種是最常見的產(chǎn)生嶺師對象的方式了。
以值傳遞的方式給函數(shù)傳參這種方式會直接調(diào)用對象的拷貝構(gòu)造函數(shù),生成一個臨時對象傳參給函數(shù)。當臨時對象銷毀時候,也是函數(shù)形參銷毀,也是函數(shù)執(zhí)行完后,就會調(diào)用該臨時對象的析構(gòu)函數(shù)。此時,無論是調(diào)用拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù),都是額外的開銷。
(驗證是否調(diào)用拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù),可以在書寫拷貝構(gòu)造函數(shù)和析構(gòu)函數(shù)驗證)
(驗證是否為臨時對象可以通過再函數(shù)內(nèi)部修改形參的值,在函數(shù)外部打印看看是否修改成功)
驗證臨時對象的而外開銷(1)
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "無參構(gòu)造函數(shù)!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有參構(gòu)造函數(shù)!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷貝構(gòu)造函數(shù)!" << endl;
}
~Person()
{
cout << "析構(gòu)函數(shù)!" << endl;
}
int fun(Person p) //普通的成員函數(shù),注意參數(shù)是以值的方式調(diào)用的
{
p.m_age = 20; //這里修改對外界沒有印象
return p.m_age;
}
int m_age;
};
int main()
{
Person p(10);//初始化
p.fun(p);
return 0;
}
先來預(yù)測一下調(diào)用函數(shù)的次數(shù):也就是我們本意想調(diào)用的方式:
會執(zhí)行一次 Person的有參構(gòu)造函數(shù);
會執(zhí)行一次Person的析構(gòu)函數(shù);
于此同時我們看看,編譯結(jié)果實際情況:
和我們預(yù)期并不一樣!!! 多了一次拷貝構(gòu)造函數(shù)和一次析構(gòu)函數(shù)。這兩個函數(shù)并不是我們希望要得,或者說,這個多余函數(shù)開銷是不必要的;
產(chǎn)生的原因也很好理解:
由于 fun成員函數(shù)里面的形參是Person p
,這樣會導(dǎo)致在調(diào)用這個fun函數(shù)時候,會傳遞過去的是實參的復(fù)制品,臨時對象,并不是外面main函數(shù)的實參,這里可以在fun函數(shù)里修改一樣形參就可以發(fā)現(xiàn),外面的實參沒發(fā)生改變。
所以產(chǎn)生的臨時對象給形參傳參時候,在我們看來類似 Person p = p
;實際上是Person p = temp
;而這句 Person p = temp
;就會發(fā)生拷貝構(gòu)造函數(shù)啦,于此同時 fun函數(shù)調(diào)用結(jié)束后,p的聲明周期也就結(jié)束,所以還會多調(diào)用析構(gòu)函數(shù)。
解決方案
如何避免這種臨時對象的產(chǎn)生呢?
只要把值傳遞的方式修改為引用傳遞的方式即可。這樣既不會調(diào)用拷貝構(gòu)造函數(shù),也不會調(diào)用多一次臨時對象的析構(gòu)函數(shù)。減少額外不必要的開銷。
所以我們在函數(shù)形參設(shè)計時候,能夠用引用就用引用的方式,因為這樣可以減少對象的復(fù)制操作,減少而外的開銷。
代碼不驗證啦,因為比較簡單,可以自行驗證,修改 fun函數(shù)里形參為 Person& p;即可。
2. 類型轉(zhuǎn)換成臨時對象 / 隱式類型轉(zhuǎn)換保證函數(shù)調(diào)用成功
這種方式就是并且把類型轉(zhuǎn)化前的對象當作了形參傳遞給構(gòu)造函數(shù),生成臨時對象臨時對象結(jié)束后就會調(diào)用析構(gòu)函數(shù)。
驗證臨時對象的而外開銷(2)
代碼依舊是上一個代碼,只是在main函數(shù)做了不一樣的動作
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "無參構(gòu)造函數(shù)!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有參構(gòu)造函數(shù)!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷貝構(gòu)造函數(shù)!" << endl;
}
~Person()
{
cout << "析構(gòu)函數(shù)!" << endl;
}
int fun(Person p) //普通的成員函數(shù),注意參數(shù)是以值的方式調(diào)用的
{
p.m_age = 20; //這里修改對外界沒有印象
return p.m_age;
}
int m_age;
};
int main()
{
Person p;
p = 1000;
return 0;
}
首先預(yù)測一下該代碼執(zhí)行的結(jié)果:
首先 調(diào)用一次無參構(gòu)造函數(shù),一次析構(gòu)函數(shù)。
其次看看編譯器運行的結(jié)果:
為啥會多出一個有參構(gòu)造函數(shù)呢和析構(gòu)函數(shù)呢?
其實是由于 p = 1000;這句引起的,這里p的類型為 Person,而 1000為 int 類型,很明顯類型不一致。
編譯器其實偷偷的進行了類型轉(zhuǎn)換,如何轉(zhuǎn)換呢?看編譯器的調(diào)用都可以發(fā)現(xiàn),其實就是創(chuàng)建一個臨時對象,這個臨時對象調(diào)用了有參構(gòu)造函數(shù),并且把 這個1000作為形參,傳入有參構(gòu)造函數(shù),當這個函數(shù)調(diào)用結(jié)束后,對象也就銷毀了,所以臨時對象會調(diào)用析構(gòu)函數(shù)。
解決方案
其實很簡單的:
只要把單參數(shù)構(gòu)造函數(shù)的復(fù)制(復(fù)制)語句,改為初始化語句就行。
那什么是復(fù)制語句和初始化語句呢?
兩者的區(qū)別就是
一個是創(chuàng)建對象同時賦值對象,也就是說創(chuàng)建時候就馬上初始化,這就是初始化;
一個是創(chuàng)建對象時候不賦值對象,而是等對象創(chuàng)建好,過后使用再賦值對象,這就是賦值語句啦;
那么我們只需要把:
Person p;
p = 1000;
修改為:
Person p = 1000;
這樣就不會有多一次的有參構(gòu)造和析構(gòu)的開銷了。
3. 函數(shù)返回對象時候
在函數(shù)返回對象時候,會創(chuàng)建一個臨時對象接收這個對象;從而調(diào)用了拷貝構(gòu)造函數(shù),和析構(gòu)函數(shù)。
當你調(diào)用函數(shù),沒有接收返回值時候,就會調(diào)用析構(gòu)函數(shù),因為都沒有人接收返回值了,自然而然析構(gòu)了。當你調(diào)用時候,有接收返回值時候,這個時候,并不會多調(diào)用一次析構(gòu)函數(shù),而是直接把臨時對象返回值,給了接受返回值的變量來接收。
驗證臨時對象的而外開銷(3)
代碼:
# include<iostream>
using namespace std;
class Person{
public:
Preson()
{
cout << "無參構(gòu)造函數(shù)!" << endl;
}
Person(int a)
{
m_age = a;
cout << "有參構(gòu)造函數(shù)!" << endl;
}
Person(const Person &p)
{
m_age = p.m_age;
cout << "拷貝構(gòu)造函數(shù)!" << endl;
}
~Person()
{
cout << "析構(gòu)函數(shù)!" << endl;
}
int fun(Person p) //普通的成員函數(shù),注意參數(shù)是以值的方式調(diào)用的
{
p.m_age = 20; //這里修改對外界沒有印象
return p.m_age;
}
int m_age;
};
Person test(Person & p)
{
Person p1; //這里會調(diào)用無參構(gòu)造函數(shù)和結(jié)束的一次析構(gòu)函數(shù)
p1.m_age = p.m_age;
return p1; //這里會多調(diào)用一次臨時拷貝和析構(gòu)函數(shù)
}
int main()
{
Person p;
test(p);
return 0;
}
看看執(zhí)行結(jié)果:
其實很好理解:就是以值的方式返回時候,就會多調(diào)用一次拷貝構(gòu)造和析構(gòu)函數(shù);
結(jié)果中的第一個析構(gòu)時test函數(shù)里p1對象的析構(gòu),第二個析構(gòu)時 返回值時候臨時對象的析構(gòu);第三個析構(gòu)時main函數(shù)里p對象的析構(gòu);
請注意我的test函數(shù)在調(diào)用時候,我并沒有給返回值,此時;當我以返回只接受時候,就會有不一樣結(jié)果:不一樣的地方就是,少了一次析構(gòu)函數(shù),其實少的這次析構(gòu)函數(shù)時test函數(shù)里返回值產(chǎn)生的臨時對象,因為,當你有對象接收返回值時候,就會直接把test函數(shù)里返回值臨時對象給初始化接收返回值對象;
即,我修改main函數(shù)的代碼:
int main()
{
Person p;
Person p2 = test(p); //此時test返回值臨時對象并不會析構(gòu),
//因為這里把臨時對象直接初始化了p2;
return 0;
}
可以說時編譯器優(yōu)化手段吧。本來說 p2對象因該也是需要調(diào)用多一次拷貝構(gòu)造函數(shù)的,但是由于有臨時對象的初始化,所以p2對象就直接接管臨時對象了。所以上面結(jié)果最后的析構(gòu)函數(shù),其實時p2對象的析構(gòu),并不是臨時對象的析構(gòu)。
解決方案
其實也很簡單的解決辦法:有兩種:
- 當我們在接收函數(shù)返回的對象時候,可以用右值引用接收,因為該函數(shù)返回值是一個臨時變量,用一個右值引用接收它,使得它的生命周期得以延續(xù),這樣就少調(diào)用一次析構(gòu)函數(shù)的開銷。(當然普通的對象接收也是可以)
- 當我們在設(shè)計函數(shù)里的return 語句中,不是返回創(chuàng)建好的對象,而是返回我們臨時創(chuàng)建的對象,即使用
retturn 類類型(形參)
; 這個時候,就可以直接避免return 對象
;返回時候又要調(diào)用多一次構(gòu)造函數(shù)。
這兩種行為就可以避免了構(gòu)造函數(shù)和析構(gòu)函數(shù)的產(chǎn)生。
但是,右值引用我還沒有寫到這文章,所以先不講右值引用的方案,講第二種方案:
也就是設(shè)計函數(shù)返回語句 return時候,不要直接返回對象,而是返回臨時對象,這個臨時對象。
把這個代碼修改:
Person test(Person & p)
{
Person p1; //這里會調(diào)用無參構(gòu)造函數(shù)和結(jié)束的一次析構(gòu)函數(shù)
p1.m_age = p.m_age;
return p1; //這里會多調(diào)用一次臨時拷貝和析構(gòu)函數(shù)
}
修改為:
Person test(Person &p)
{
return Person(p.m_age);//直接返回臨時對象,可以減少
}
其實,只要以值得形式返回對象都會調(diào)用多一次拷貝構(gòu)造函數(shù),所以我們盡量避免這種情況,用合適的方式解決它。
到此這篇關(guān)于C++中臨時對象的常見產(chǎn)生情況及其解決的方案的文章就介紹到這了,更多相關(guān)C++ 臨時對象內(nèi)容請搜索html5模板網(wǎng)以前的文章希望大家以后多多支持html5模板網(wǎng)!