日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者 | 櫻雨樓

責編 | 胡雪蕊

出品 | CSDN(ID:CSDNnews)

1.引言

不同的數據在計算機內存中的存儲方式不同,導致了“類型”這一抽象概念的出現。

對于一個變量而言,其必須要回答三個問題:

1. 在哪可以訪問到這個變量的起點?

2. 從起點向后需要讀取多少內存?

3. 應該如何解析讀取到的二進制數據?

上述的三個問題中,問題 1,由內存地址回答,問題 2 和 3,均由類型回答。

由此可見,類型與內存地址共同構成了一個變量的完整組分。之所以不能對 void 取值,也是由于無法回答問題 2 和 3 導致。

進一步的,我們可以得到一條十分重要的結論:對于兩個不同類型的變量,由于其對問題 2 和 3 的答案不同,故如果將這樣的兩個變量直接進行運算,在絕大多數情況下都將無法產生有價值的計算結果。

故在幾乎所有的編程語言中都有一條重要的規定:不同類型的兩個變量無法直接進行運算。

雖然不同類型的兩個變量無法進行運算,但顯然,我們可將其中的一個變量通過類型轉換,轉為與另一個變量類型一致,此時就滿足“同類型變量才能進行運算”這一規定了。

同時,由于某些類型轉換是“理所應當”的,而另一些不是,故由此又派生出兩個概念:隱式類型轉換與顯式類型轉換。

隱式類型轉換指不通過專門的類型轉換操作,而是通過其它規定或代碼上下文隱式發生的類型轉換,而顯式類型轉換則通過專門的類型轉換操作進行轉換,顯式類型轉換具有強制性,其將不受任何類型轉換以外的因素影響,故顯式類型轉換又稱為強制類型轉換 。

在 C++ 中,類型轉換是一個非常復雜的話題。本文將先從隱式類型轉換入手,逐步討論各類 C++ 的類型轉換話題。

2.類型提升與算術類型轉換

算術類型轉換專指 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, ptr_t 等其它特殊類型

算術類型轉換是一類不完全明確的,且與底層密切相關的隱式類型轉換。

其遵循以下幾條主要原則:

1. 對于同類算術類型,如short 與 int,float 與 double,占用較小內存的類型將轉換成另一類型。如 short+ int將被轉換為 int + int。此種類型轉換稱為類型提升。

2. 整形將轉為浮點型。如 int+ double 將被轉換為 double + double。

3. 僅當無符號類型占用的內存小于有符號類型時,無符號類型才發生類型提升從而轉為有符號類型,否則,有符號類型將轉為無符號類型。這是一個非常需要注意的點。

參考以下代碼:

int main
{
unsigned short a = 1;
unsigned b = 1;
cout << (a > -1) << " " << (b > -1) << endl; // 1 0!
}

上述代碼中,-1 作為 int 直接量而存在,由于變量 a 是unsigned short 類型,故其將被轉為 int,值仍為 1。

但由于變量 b 的類型是與 int 同級的 unsigned 類型,故此時 -1 將被轉為 unsigned 類型,這明顯不是我們需要的結果。

由此可見,當有符號類型與無符號類型(如 size_t)發生混用時,一定要小心可能會發生的隱式類型轉換。

3.轉換構造函數

3.1 定義轉換構造函數

C++ 中,如果一個構造函數滿足以下所有條件,則其成為一個轉換構造函數:

1. 至多有一個不含默認值的形參。這主要包括以下幾種情況:

  • 構造函數只有一個形參

  • 構造函數有不止一個形參,但只有第一形參無默認值

  • 構造函數有不止一個形參,但全部形參均有默認值

2. 第一形參的類型不為類本身或其附加類型(否則此構造函數將成為拷貝構造函數或移動構造函數)

如果一個類定義了某種轉換構造函數,則被定義的類型將可以通過任何類型轉換方式轉為當前類類型。這常見于以下幾種情況:

1. 賦值時發生的隱式類型轉換

2. 實參傳遞時發生的隱式類型轉換

3. 基于 static_cast 的顯式類型轉換

參考以下代碼:

c++
struct A { A (int) {} }; // 轉換構造函數

void test(A) {}

int main
{
A _ = 0; // 賦值時發生的隱式類型轉換
test(0); // 實參傳遞時發生的隱式類型轉換
}

上述代碼中,我們為類A 定義了從 int 到 A 的轉換構造函數。則此時,我們既可以將一個 int 直接賦值給類型為 A 的變量,也可以直接將一個 int 作為實參傳給類型為 A 的形參。這兩種情況發生時,都隱式地通過轉換構造函數構造了類 A 的一個實例。

3.2 阻止基于轉換構造函數進行的隱式類型轉換

由上文可知,當定義了一個轉換構造函數后,就打通了某個其它類型向類類型進行轉換的通道。此時,如果我們希望禁用基于轉換構造函數進行的隱式類型轉換,則需要在轉換構造函數前追加 explicit 聲明。

當一個轉換構造函數被聲明為 explicit 后,其具有以下性質:

1. 禁止一切場合下的基于轉換構造函數的隱式類型轉換

2. 不影響被轉換類型到類類型的強制類型轉換

3. 不影響對轉換構造函數的正常調用

參考以下代碼:

struct A { explicit A (int) {} }; // explicit轉換構造函數

void test(A) {}

int main
{
A _ = 0; // Error!禁止賦值時發生的隱式類型轉換!
test(0); // Error!禁止實參傳遞時發生的隱式類型轉換!
static_cast<A>(0); // explicit不影響強制類型轉換
A(0); // explicit不影響對轉換構造函數的正常調用
}

上述代碼中,我們將類A 的轉換構造函數聲明為 explicit,則此時 int 將不能通過賦值或實參傳遞的方式隱式的轉換為 A。但顯然,explicit 只是禁用了轉換構造函數的隱式類型轉換功能,其構造函數功能以及顯式類型轉換功能并不受影響。
 

4.類型轉換運算符

轉換構造函數定義了其它類型向類類型的轉換方案,類型轉換運算符則定義了與之相反的過程:其用于定義類類型向其它類型的轉換方案。當類定義了某種類型的類型轉換運算符后,類類型將可以向被定義類型發生類型轉換。

參考以下代碼:

struct A { operator int const { return 0; } }; // 定義A -> int進行類型轉換的方案

void test(int) {}

int main
{
test(A); // 發生了A -> int的隱式類型轉換
}

與轉換構造函數類似,如果希望禁用隱式類型轉換,則需要對類型轉換運算符追加 explicit 聲明。同樣的,explicit 不影響強制類型轉換。

參考以下代碼:

struct A { explicit operator int const { return 0; } }; // explicit類型轉換運算符

void test(int) {}

int main
{
test(A); // Error!禁止A -> int的隱式類型轉換
test(static_cast<int>(A)); // explicit不影響強制類型轉換
}

對于類型轉換運算符與explicit 還有一條額外規定:operator bool 在條件表達式(這主要包括:if、while、for、 ?: 的條件部分)或邏輯表達式中發生的隱式類型轉換將不受 explicit 影響。

參考以下代碼:

struct A { explicit operator bool const { return true; } }; // explicit類型轉換運算符

int main
{
if (A) {} // 即使operator bool被聲明為explicit,其在if中也能發生隱式類型轉換
}

5.繼承類到基類的類型轉換5.1 靜態類型與動態類型

C++ 的繼承機制決定了這樣的抽象模型:繼承類 = 基類部分 + 繼承類部分。這意味著每一個繼承類都含有其所有基類(如果基類不止一個)的數據各一份。也就是說,對于一個繼承類對象,對其基類部分進行操作顯然是可行的,這主要包括:

1. 得到基類部分的數據

2. 將類型轉換為基類類型(以丟失某些信息為代價)

也就是說,我們可以將一個繼承類對象直接賦值給一個基類類型的變量,顯然,這樣的賦值建立在隱式類型轉換之上,稱為繼承類到基類的類型轉換,或稱為向上類型轉換。

根據附加類型的不同,向上類型轉換分為以下幾種情況:


 

struct A {};

struct B: A {};

int main

{

A a1 = B; // 值向上轉換

A *a2 = new B; // 指針向上轉換

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 這三個關鍵字。

按照常規的解讀,這三個關鍵詞用于限定類的用戶的訪問權,需要注意的是:“類的用戶”不僅指類實例,也指繼承此類的類。

說明如下:

  • public:當用于訪問說明符時,表示對類的一切用戶可見;用于繼承時,表示繼承時不修改基類的一切訪問說明符

  • protected:當用于訪問說明符時,表示僅對類的繼承用戶可見,對類的實例用戶不可見;用于繼承時,表示將基類的一切 public 訪問說明符在繼承類中修改為 protected

  • private:當用于訪問說明符時,表示對一切類的用戶均不可見;用于繼承時,表示將基類的一切 public 和 protected 訪問說明符在繼承類中修改為 private

上述描述中,“將基類的xxx訪問說明符在繼承類中修改為xxx”是一個很奇怪且魔幻的描述,我們不禁要思考,為什么 C++ 會給出這樣的三種繼承模式?又為什么要“伴隨著繼承修改訪問說明符”呢?

如果我們從向上類型轉換這一角度思考,就能得出答案:

  • public:不阻止任何用戶進行向上類型轉換

  • protected:阻止類的實例用戶進行向上類型轉換

  • private:阻止一切用戶進行向上類型轉換

由此我們可知,“修改訪問說明符”是一種訪問說明符在繼承時的作用的較為直觀的理解,而其真正意義是阻止向上類型轉換。

參考以下代碼:


 

struct A {};

struct B: A {}; // 不阻止任何B類的用戶向A進行類型轉換

struct C: protected A {}; // 阻止C類的實例用戶向A進行類型轉換

struct D: private A {}; // 阻止D類的一切用戶向A進行類型轉換

struct E: B { void test { static_cast<A *>(this); } }; // B類的繼承類用戶可以向A進行類型轉換

struct F: C { void test { static_cast<A *>(this); } }; // C類的繼承類用戶可以向A進行類型轉換

struct E: D { void test { static_cast<A *>(this); } }; // Error!D類的繼承類用戶不可以向A進行類型轉換

int main

{

static_cast<A *>(new B); // B類的實例用戶可以向A進行類型轉換

static_cast<A *>(new C); // Error!C類的實例用戶不可以向A進行類型轉換

static_cast<A *>(new D); // Error!D類的實例用戶不可以向A進行類型轉換

}

上述代碼中,類 B、C、D 分別以三種不同的訪問說明符繼承自類 A,同時,我們分別為類B、C、D 各定義了一個繼承類用戶和一個實例用戶。

由此可見,public 繼承將不阻止類的任何用戶進行向上類型轉換,而 private 繼承將阻止類的一切用戶進行向上類型轉換,protected 繼承只阻止類的實例用戶進行向上類型轉換,但不阻止類的繼承類用戶進行向上類型轉換。

5.3 多重繼承與向上類型轉換

對于多重繼承,其向上類型轉換對于同一繼承層的多個基類是全面進行的。

參考以下代碼:

struct A { int i; };
struct B { int i; };
struct C: A, B { int i; };
struct D: A, B {};

int main
{
C.i; // 訪問C::i
D.i; // Error!存在二義性!
}

對于類 C,由于其自身定義了變量 i,故訪問 C 類的i變量時并未發生向上類型轉換。而對于類 D,由于其自身沒有定義變量 i,故訪問D 類的i變量時需要在其各個基類中分別進行查找。由于編譯器發現D -> A -> i 與 D -> B -> i 這兩種查找路線都是可行的,故此時編譯器判定此查找存在二義性。
 

6.其它隱式類型轉換

C++ 中還定義了一些特殊的類型轉換,以下列舉出一些常見的情況:

1. 0 轉換為空指針

int main
{
int *p = 0;
}

2. 數組退化為指針

int main
{
int a[10];
int *p = a;
}

3. 空指針或數字 0 轉為 false,其它指針或數字轉為 true

int main
{
if(ptr) {}
if (2) {}
}

4. T轉換為 void

int main
{
void *p = new int;
}

5. 非 const 轉換為 const

int main
{
int *a;
const int * const b = a;
}

作者簡介:櫻雨樓,畢業于生物信息學專業,是一枚Python/C++/Perl開發,自稱R語言黑粉,Github勾搭:https://github.com/yingyulou

【END】

分享到:
標簽:轉換 類型
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定