文章目錄
- 前言
- 1. 隱式類型轉換
- 1.1 單參數的隱式類型轉換
- 1.2 多參數的隱式類型轉換
- 1.3 explicit關鍵字
- 2. 編譯器的優化
- 2.1 普通構造優化
- 2.2 函數傳參優化
- 2.3 函數返回優化
前言
在類與對象的學習過程中,一定會對隱式類型轉換這個詞不陌生。對于內置類型而言,相似的類型會支持隱式類型轉換,例如int a = 3.1;
在這篇文章我們細細談談類對象中的隱式類型轉換:
- 類對象的隱式類型轉換
- 編譯器的優化
注意:這里的討論,沒有考慮右值。
1. 隱式類型轉換
關于隱式類型轉換產生臨時變量,我在類型轉換細節中有談到,大家可以先閱讀一下。
1.1 單參數的隱式類型轉換
C++11之前,C++98僅僅支持單參數的隱式類型轉換,當然這種轉換也帶來了很多的便利!同時也有潛在危險,需要合理看待!
為了方便測試,我們在VS2022下,給出以下的A
類:
#include<iostream>
using namespace std;
class A
{
public:A(int a = 1):_a(a){cout << "A(int a = 1)" << endl;}~A(){cout << "~A()" << endl;}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;return *this;}//暫時不考慮移動構造和移動賦值int _a;
};
在大多數時候,我們會寫出這樣的代碼, 例1.1.1:
int main()
{A a = 1; //直接以1來賦值這個a類對象return 0;
}
這個過程會發生什么呢?
-
編譯器會先利用
1
來構造一個A
類的tmp對象。 -
再調用
a
的對象的拷貝構造函數。
(但似乎這樣的代價也太大了,所以編譯器會做出優化,我們稍后再談)
當我們在使用STL的時候,也常常會發生這種隱式類型的轉換,例如:
#include<iostream>
#include<string>
#include<vector>
using namespace std;int main()
{vector<string> v;v.push_back("this is a string?");return 0;
}
我們的vector
存放的是string
類對象,而我們傳入的卻是一個char*
類型(字符串常量類型都被解釋為了const char *
)。在不考慮其它因素外,這個時候就應該會發生隱式類型的轉換:將char*
類型構造一個string
對象,再調用push_back
函數
這就是單參數的隱式類型轉換
1.2 多參數的隱式類型轉換
C++11支持了列表初始化:
(C++11我們會在后面的專題談到)
也對類的多參數的情況進行了升級!支持了多參數的隱式類型轉換。給出下面一個例子:
#include<iostream>
using namespace std;
class A
{
public:A(int a = 1, int b = 2):_a(a),_b(b){cout << "A(int a = 1, int b = 2)" << endl;}~A(){cout << "~A()" << endl;}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;return *this;}int _a;int _b;
};
那么支持了多參數的隱式類型轉換之后,我們可以這么寫
例1.2.1:
int main()
{A a = {1, 3}; //支持的return 0;
}
同樣地,我們應該了解到這個語句干了什么?
-
編譯器會先利用
{1, 3}
來構造一個A
類的tmp對象。 -
再調用
a
的對象的拷貝構造函數。
1.3 explicit關鍵字
有些時候,我們其實并不想構造函數支持這種隱式類型轉換,我們就可以采用關鍵字explicit
來對構造函數進行聲明!
語法如下:
class A
{
public:explicit A(int a = 1, int b = 2):_a(a),_b(b){cout << "A(int a = 1, int b = 2)" << endl;}~A(){cout << "~A()" << endl;}int _a;int _b;
};int main()
{//關于這樣的代碼就無法通過編譯了!//A a = {1, 3}; //不支持了return 0;
}
但是這樣真的很方便……所以我們還可以采用另外一個形式,匿名對象
例1.3.1
int main()
{A a = A{ 1, 3 }; //語句一//A a = A({ 1, 3 }); //語句二return 0;
}
說明:
- 語句一可以在支持隱式類型轉換的情況下使用。本質和隱式類型轉換類似,都是構造一個
tmp
類對象。 - 語句二使用的前提是這個
A
類支持initializer_list
的A類構造函數。本質上是{ }
調用了initializer_list
的構造函數,是一個initializer_list
的tmp
對象,然后再初始化A
類。所以,當你的A類不支持這樣的一個構造函數,就無法成功初始化了!
是否需要驗證呢?
#include<iostream>
using namespace std;
class A
{
public:explicit A(int a = 1, int b = 2):_a(a), _b(b){cout << "A(int a = 1, int b = 2)" << endl;}~A(){cout << "~A()" << endl;}A(initializer_list<int> il) //支持列表初始化的構造函數{cout << "A(initializer_list<int> il)" << endl;}int _a;int _b;
};int main()
{A a = A({ 1, 3 }); //注意不要寫成這樣return 0;
}
這樣的調用,不知道是否有說服力呢?
2. 編譯器的優化
在上面的大多數例子中,我并沒有驗證那些我們看起來的步驟。因為:編譯器是會對同一行的連續構造采取優化措施的。
現在,在來考慮這個類,和幾條語句:
2.1 普通構造優化
例2.1.1:
class A
{
public:A(int a = 1, int b = 2):_a(a),_b(b){cout << "A(int a = 1, int b = 2) " << _a << endl; //為了區別每一個構造,這里多給了一個打印}~A(){}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;return *this;}int _a;int _b;
};int main()
{A a0(-1, -1);cout << " ------------------ " << endl;A a1 = a0; //語句一A a2 = { 0, 0 }; //語句二A a3 = A{ 1, 1 }; //語句三return 0;
}
來看運行結果:
(單參數的也是這樣的結果)
在此之前,我們并沒有給出實際的運行結果,因為編譯器會為我們做出優化:
- 語句一沒有優化。a0本身就是一個存在的對象!
- 語句二、三進行了優化,本來我們應該是先普通構造再拷貝構造,但編譯器為我們直接構造!
2.2 函數傳參優化
同樣直接給出示例:
class A
{
public:A(int a = 1, int b = 2):_a(a),_b(b){cout << "A(int a = 1, int b = 2) " << _a << endl; //為了區別每一個構造,這里多給了一個打印}~A(){}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;return *this;}int _a;int _b;
};void func(A a) //注意這里并沒有傳引用 -- 傳引用就不會進行拷貝構造了
{//……
}int main()
{A a0(-1, -1);cout << " ------------------ " << endl;func(a0); //語句一func({ 2, 2 }); //語句二func(A{ 3, 3 }); //語句三return 0;
}
同樣發生了優化!
2.3 函數返回優化
這里的函數返回值優化又有所不同,返回值優化又被稱為:RVO(Return Value Optimization)
給出示例:
class A
{
public:A(int a = 1, int b = 2):_a(a),_b(b){cout << "A(int a = 1, int b = 2) " << _a << endl; //為了區別每一個構造,這里多給了一個打印}~A(){}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;return *this;}int _a;int _b;
};A func()
{A tmp(5,5);//……return tmp;
}int main()
{A a0(-1, -1);cout << " ------------------ " << endl;a0 = func(); //語句一A a1 = func(); //語句二return 0;
}
我們在func中創建一個臨時變量tmp
,想讓這個tmp
完成一些業務,然后返回這個臨時變量。來看運行結果:
語句一:創建一個tmp
對象,然后調用一個賦值運算符重載,十分合理的。但是看到語句二:直接就完成了構造!。沒有在func()中調用tmp的構造函數?還是沒有調用a1的構造函數?
- 首先分析:
A a1 = func();
。首先函數返回的時候是會將返回值拷貝到一個tmp
對象中的,然后再通過這個tmp
對象返回給外面的接收變量,這里本身就有兩個拷貝構造,編譯器發生優化是很情理之中的! - 同時函數
func
中又定義了一個變量,這個變量也會調用一個構造函數的。 - 可是結果告訴我們整個的調用只調用了一次構造函數!
沒錯,這就是編譯器的RVO!
RVO:編譯器優化技術。它可以減少函數返回時創建臨時對象的次數,從而提高程序的運行效率。RVO主要針對未命名的臨時對象,消除了函數返回時創建的臨時對象,避免了不必要的拷貝構造函數調用。
來看這樣一張圖片:
發現了嗎?tmp
這個對象被處理成為了一個指針!這個指針指向的對象就是a1
對象。我們通過對tmp
的操作,在編譯器看來就是對a1
進行操作。所以上述情況下只會調用一次拷貝構造函數!
在有些時候,這樣采用RVO的代碼效率不差同時也更好維護!大家可以自己做性能測試!
RVO并不總是適用,存在一些限制條件,例如:
-
函數拋出異常時,RVO可能不會進行。
-
函數可能返回具有不同變量名的對象時,RVO無法進行。
-
函數有多個出口時,RVO可能不會進行。
完
希望這篇文章能夠幫助到你!