前言
在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); //此時test返回值臨時對象并不會析構,
//因為這里把臨時對象直接初始化了p2;
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);//直接返回臨時對象,可以減少
}
其實,只要以值得形式返回對象都會調用多一次拷貝構造函數,所以我們盡量避免這種情況,用合適的方式解決它。
到此這篇關于C++中臨時對象的常見產生情況及其解決的方案的文章就介紹到這了,更多相關C++ 臨時對象內容請搜索html5模板網以前的文章希望大家以后多多支持html5模板網!