令人頭疼的C++複雜的型別轉換,我們如何來攻克?我來讓你頭腦清醒
不同的資料在計算機記憶體中的儲存方式不同,導致了“型別”這一抽象概念的出現。
對於一個變數而言,其必須要回答三個問題:
1。 在哪可以訪問到這個變數的起點?
2。 從起點向後需要讀取多少記憶體?
3。 應該如何解析讀取到的二進位制資料?
上述的三個問題中,問題 1,由記憶體地址回答,問題 2 和 3,均由型別回答。
由此可見,型別與記憶體地址共同構成了一個變數的完整組分。之所以不能對 void 取值,也是由於無法回答問題 2 和 3 導致。
進一步的,我們可以得到一條十分重要的結論:對於兩個不同型別的變數,由於其對問題 2 和 3 的答案不同,故如果將這樣的兩個變數直接進行運算,在絕大多數情況下都將無法產生有價值的計算結果。
故在幾乎所有的程式語言中都有一條重要的規定:不同型別的兩個變數無法直接進行運算。
雖然不同型別的兩個變數無法進行運算,但顯然,我們可將其中的一個變數透過型別轉換,轉為與另一個變數型別一致,此時就滿足“同類型變數才能進行運算”這一規定了。
同時,由於某些型別轉換是“理所應當”的,而另一些不是,故由此又派生出兩個概念:隱式型別轉換與顯式型別轉換。
隱式型別轉換指不透過專門的型別轉換操作,而是透過其它規定或程式碼上下文隱式發生的型別轉換,而顯式型別轉換則透過專門的型別轉換操作進行轉換,顯式型別轉換具有強制性,其將不受任何型別轉換以外的因素影響,故顯式型別轉換又稱為強制型別轉換 。
在 C++ 中,型別轉換是一個非常複雜的話題。本文將先從隱式型別轉換入手,逐步討論各類 C++ 的型別轉換話題。
如果你也想學程式設計 ,如果你也想從零基礎的小白蛻變成優秀的開發人才,可以和小編交流,讓你從此學習不再孤單,進裙更能認識一些志同道合小夥伴!
型別提升與算術型別轉換
算術型別轉換專指 C++ 提供的各種內建算術型別之間的隱式型別轉換。
內建算術型別主要包括以下型別:
1。 bool
2。 char, signed char, unsigned char
3。 short, int, long, long long, unsignedshort, unsigned int, unsigned long, unsigned long long
4。 float, double, long double
5。 size_t, ptrdiff_t, nullptr_t 等其它特殊型別
算術型別轉換是一類不完全明確的,且與底層密切相關的隱式型別轉換。
其遵循以下幾條主要原則:
1。 對於同類算術型別,如short 與 int,float 與 double,佔用較小記憶體的型別將轉換成另一型別。如 short+ int將被轉換為 int + int。此種類型轉換稱為型別提升。
2。 整形將轉為浮點型。如 int+ double 將被轉換為 double + double。
3。 僅當無符號型別佔用的記憶體小於有符號型別時,無符號型別才發生型別提升從而轉為有符號型別,否則,有符號型別將轉為無符號型別。這是一個非常需要注意的點。
參考以下程式碼:
int main
{
unsignedshorta = 1;
unsignedb = 1;
cout<< (a > -1) << “ ”<< (b > -1) << endl; // 1 0!
}
上述程式碼中,-1 作為 int 直接量而存在,由於變數 a 是unsigned short 型別,故其將被轉為 int,值仍為 1。
但由於變數 b 的型別是與 int 同級的 unsigned 型別,故此時 -1 將被轉為 unsigned 型別,這明顯不是我們需要的結果。
由此可見,當有符號型別與無符號型別(如 size_t)發生混用時,一定要小心可能會發生的隱式型別轉換。
轉換建構函式
3.1 定義轉換建構函式
C++ 中,如果一個建構函式滿足以下所有條件,則其成為一個轉換建構函式:
1。 至多有一個不含預設值的形參。這主要包括以下幾種情況:
2。 第一形參的型別不為類本身或其附加型別(否則此建構函式將成為複製建構函式或移動建構函式)
如果一個類定義了某種轉換建構函式,則被定義的型別將可以透過任何型別轉換方式轉為當前類型別。這常見於以下幾種情況:
1。 賦值時發生的隱式型別轉換
2。 實參傳遞時發生的隱式型別轉換
3。 基於 static_cast 的顯式型別轉換
參考以下程式碼:
c++
structA{A (int) {} }; // 轉換建構函式
voidtest(A){}
int main
{
A _ = 0; // 賦值時發生的隱式型別轉換
test(0); // 實參傳遞時發生的隱式型別轉換
}
上述程式碼中,我們為類A 定義了從 int 到 A 的轉換建構函式。則此時,我們既可以將一個 int 直接賦值給型別為 A 的變數,也可以直接將一個 int 作為實參傳給型別為 A 的形參。這兩種情況發生時,都隱式地透過轉換建構函式構造了類 A 的一個例項。
3.2 阻止基於轉換建構函式進行的隱式型別轉換
由上文可知,當定義了一個轉換建構函式後,就打通了某個其它型別向類型別進行轉換的通道。此時,如果我們希望禁用基於轉換建構函式進行的隱式型別轉換,則需要在轉換建構函式前追加 explicit 宣告。
當一個轉換建構函式被宣告為 explicit 後,其具有以下性質:
1。 禁止一切場合下的基於轉換建構函式的隱式型別轉換
2。 不影響被轉換型別到類型別的強制型別轉換
3。 不影響對轉換建構函式的正常呼叫
參考以下程式碼:
structA{explicitA(int){} }; // explicit轉換建構函式
voidtest(A){}
int main
{
A _ = 0; // Error!禁止賦值時發生的隱式型別轉換!
test(0); // Error!禁止實參傳遞時發生的隱式型別轉換!
static_cast(0); // explicit不影響強制型別轉換
A(0); // explicit不影響對轉換建構函式的正常呼叫
}
上述程式碼中,我們將類A 的轉換建構函式宣告為 explicit,則此時 int 將不能透過賦值或實參傳遞的方式隱式的轉換為 A。但顯然,explicit 只是禁用了轉換建構函式的隱式型別轉換功能,其建構函式功能以及顯式型別轉換功能並不受影響。
型別轉換運算子
轉換建構函式定義了其它型別向類型別的轉換方案,型別轉換運算子則定義了與之相反的過程:其用於定義類型別向其它型別的轉換方案。當類定義了某種型別的型別轉換運算子後,類型別將可以向被定義型別發生型別轉換。
參考以下程式碼:
structA{operatorintconst{ return0; } }; // 定義A -> int進行型別轉換的方案
voidtest(int){}
int main
{
test(A); // 發生了A -> int的隱式型別轉換
}
與轉換建構函式類似,如果希望禁用隱式型別轉換,則需要對型別轉換運算子追加 explicit 宣告。同樣的,explicit 不影響強制型別轉換。
參考以下程式碼:
structA{explicitoperatorintconst{ return0; } }; // explicit型別轉換運算子
voidtest(int){}
int main
{
test(A); // Error!禁止A -> int的隱式型別轉換
test(static_cast(A)); // explicit不影響強制型別轉換
}
對於型別轉換運算子與explicit 還有一條額外規定:operator bool 在條件表示式(這主要包括:if、while、for、 ?: 的條件部分)或邏輯表示式中發生的隱式型別轉換將不受 explicit 影響。
參考以下程式碼:
structA{explicitoperatorboolconst{ returntrue; } }; // explicit型別轉換運算子
int main
{
if(A) {} // 即使operator bool被宣告為explicit,其在if中也能發生隱式型別轉換
}
繼承類到基類的型別轉換
5.1 靜態型別與動態型別
C++ 的繼承機制決定了這樣的抽象模型:繼承類 = 基類部分 + 繼承類部分。這意味著每一個繼承類都含有其所有基類(如果基類不止一個)的資料各一份。也就是說,對於一個繼承類物件,對其基類部分進行操作顯然是可行的,這主要包括:
1。 得到基類部分的資料
2。 將型別轉換為基類型別(以丟失某些資訊為代價)
也就是說,我們可以將一個繼承類物件直接賦值給一個基類型別的變數,顯然,這樣的賦值建立在隱式型別轉換之上,稱為繼承類到基類的型別轉換,或稱為向上型別轉換。
根據附加型別的不同,向上型別轉換分為以下幾種情況:
structA{};
structB:A {};
int main
{
A a1 = B; // 值向上轉換
A *a2 = newB; // 指標向上轉換
A &a3 = a1; // 左值引用向上轉換
A &&a4 = B; // 右值引用向上轉換
}
上述程式碼中,變數a1 的型別是 A,這是一個非指標或引用變數,故變數的記憶體大小就是 A類物件的大小。如果對基類型別變數使用繼承類物件賦值,則將強行去除繼承類物件的繼承類部分,而將基類部分賦值給變數。
故對於 a1 而言,其得到的應該是一個 B 類物件的 A 類部分。即:如果發生向上型別轉換的型別是類本身,則將以丟失繼承類物件的繼承類部分為代價進行向上型別轉換。
事實上,此賦值操作呼叫了 A 類的合成複製賦值運算子,而非基於隱式型別轉換。C++ 對於類的某些成員函式的合成操作是一個非常複雜的話題,且涉及大量與本文無關的內容,故本文不再詳述。
對於變數 a2-4,其型別都是 A 的指標或引用(也是指標),而非 A 的本體。由於指標本身並不與型別直接掛鉤,故理論上,此類變數中真正存放的值可以是一個非 A 型別的資料。
由此,我們引出“靜態型別”與“動態型別”的概念。
C++ 中,一個變數宣告的型別稱為靜態型別,而其實際儲存的資料的型別稱為動態型別。
在絕大多數情況下,靜態型別與動態型別都是必須一致的,如果不一致,將發生隱式型別轉換或引發編譯錯誤。當且僅當使用基類的指標或引用儲存繼承類物件時,變數的靜態型別與動態型別將不一致。
此時,雖然看上去發生了向上型別轉換,但實際上並未發生,此過程稱為動態繫結。
一個變數的靜態型別,決定了由此變數能夠訪問到的成員名稱。當靜態型別是基類指標或引用時,即使變數存放的是繼承類物件,也只能夠訪問到基類中宣告的成員名稱。
即:如果發生向上型別轉換的型別是類的指標或引用,則將以丟失繼承類部分的成員名稱為代價進行向上型別轉換。
但由於虛擬函式的存在,訪問成員名稱所得到的實際成員函式將不一定與靜態型別保持一致,此性質是 C++ 多型的核心。虛擬函式相關話題與本文無關,這裡不再詳述。
5.2 阻止向上型別轉換
讓我們重新思考這樣一個問題:為什麼繼承類可以訪問基類的成員?
不難發現,“繼承類可以訪問基類成員”這一性質並不是天經地義的,因為繼承類中並沒有“複製貼上”一個基類,而只有繼承類本身的部分,故原則上繼承類雖然繼承了基類,但其本身仍然是沒有能力訪問基類的成員的。
繼承類物件之所以能夠訪問基類成員,是因為在進行這樣的訪問時,繼承類的 this 指標透過向上型別轉換操作轉換成了一個基類型別的指標,然後以基類指標的身份訪問到了基類的成員。
如果希望阻止這種隱式的向上型別轉換呢?
讓我們認真考察 public、protected 與 private 這三個關鍵字。
按照常規的解讀,這三個關鍵詞用於限定類的使用者的訪問權,需要注意的是:“類的使用者”不僅指類例項,也指繼承此類的類。
說明如下:
上述描述中,“將基類的xxx訪問說明符在繼承類中修改為xxx”是一個很奇怪且魔幻的描述,我們不禁要思考,為什麼 C++ 會給出這樣的三種繼承模式?又為什麼要“伴隨著繼承修改訪問說明符”呢?
如果我們從向上型別轉換這一角度思考,就能得出答案:
由此我們可知,“修改訪問說明符”是一種訪問說明符在繼承時的作用的較為直觀的理解,而其真正意義是阻止向上型別轉換。
參考以下程式碼:
structA{};
structB:A {}; // 不阻止任何B類的使用者向A進行型別轉換
structC:protectedA {}; // 阻止C類的例項使用者向A進行型別轉換
structD:privateA {}; // 阻止D類的一切使用者向A進行型別轉換
structE:B { voidtest{ static_cast(this); } }; // B類的繼承類使用者可以向A進行型別轉換
structF:C { voidtest{ static_cast(this); } }; // C類的繼承類使用者可以向A進行型別轉換
structE:D { voidtest{ static_cast(this); } }; // Error!D類的繼承類使用者不可以向A進行型別轉換
int main
{
static_cast(newB); // B類的例項使用者可以向A進行型別轉換
static_cast(newC); // Error!C類的例項使用者不可以向A進行型別轉換
static_cast(newD); // Error!D類的例項使用者不可以向A進行型別轉換
}
上述程式碼中,類 B、C、D 分別以三種不同的訪問說明符繼承自類 A,同時,我們分別為類B、C、D 各定義了一個繼承類使用者和一個例項使用者。
由此可見,public 繼承將不阻止類的任何使用者進行向上型別轉換,而 private 繼承將阻止類的一切使用者進行向上型別轉換,protected 繼承只阻止類的例項使用者進行向上型別轉換,但不阻止類的繼承類使用者進行向上型別轉換。
5
.3 多重繼承與向上型別轉換
對於多重繼承,其向上型別轉換對於同一繼承層的多個基類是全面進行的。
參考以下程式碼:
structA{inti; };
structB{inti; };
structC:A, B { inti; };
structD:A, B {};
intmain
{
C。i; // 訪問C::i
D。i; // Error!存在二義性!
}
對於類 C,由於其自身定義了變數 i,故訪問 C 類的i變數時並未發生向上型別轉換。而對於類 D,由於其自身沒有定義變數 i,故訪問D 類的i變數時需要在其各個基類中分別進行查詢。由於編譯器發現D -> A -> i 與 D -> B -> i 這兩種查詢路線都是可行的,故此時編譯器判定此查詢存在二義性。
其它隱式型別轉換
C++ 中還定義了一些特殊的型別轉換,以下列舉出一些常見的情況:
1. 0 轉換為空指標
int main
{
int*p = 0;
}
2. 陣列退化為指標
intmain
{
inta[ 10];
int*p = a;
}
3. 空指標或數字 0 轉為 false,其它指標或數字轉為 true
intmain
{
if( nullptr) {}
if( 2) {}
}
4. T轉換為 void
intmain
{
void*p = newint;
}
5. 非 const 轉換為 const
intmain
{
int*a;
constint* constb = a;
}
想要了解、學習C/C++的小夥伴可以進入關注小編的專欄一起探討學習喲~