您當前的位置:首頁 > 收藏

令人頭疼的C++複雜的型別轉換,我們如何來攻克?我來讓你頭腦清醒

作者:由 C語言程式設計俱樂部 發表于 收藏時間:2019-09-07

令人頭疼的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++的小夥伴可以進入關注小編的專欄一起探討學習喲~