C++語言體系設計哲學的一些隨想(未完待續)
對於靜態類型語言,其本質目標在於恰當地操作數據,得到期望的值。具體而言,需要:
(1)定義數據類型
你定義的數據是什麼,是整形還是浮點還是字符。該類型的數據可以包含的值的范圍是什麼。
(2)定義操作的含義
操作是嚴格數據類型相關的。操作表明了對了一個具有特定類型的數據,執行操作後產生什麼樣結果。
===========================================
C++就是一個典型的靜態類型語言。在C++中,無論是"數據類型"還是"操作",都分為內置的和自定義的。
C++的內置數據類型包括:
(1)基本內置類型
整形、浮點、布爾、字符....
(2)STL庫定義的類型
例如常用的iostream、string、迭代器......
此外C++和定義了復合類型機制,包括所有類型的引用、指針、數組,他們可以作為一個完整數據類型的一部分。
順便提一下,頂層/底層const、static、volatile...等修飾符,定義了數據的其他屬性,這些屬性也可以是一個完整數據類型的組成部分。
而自定義類型,最常用的就是class、struct、union定義,還有函數簽名,當然也可以使用復合類型機制定義自己類的引用、指針、數組等。
==============================
重點在於,無論是變量還是常量,必須屬於某一特定的數據類型。因為操作只有基於精確的數據類型,其定義才有了確定的含義(在編譯原理中叫做“語義”)。也就是說,在一個確定的操作集合中(例如C++語言內置的所有操作),只要給一個變量賦於了數據類型,這個變量可以執行的操作也就確定了。定義變量nVal為int類型,那麼nVal就可以參與加減乘除、關系運算、拷貝、轉換為double、傳遞給函數形參、作為數組的下標.........
C++的“操作”,其含義非常廣泛。其實C++語言已經通過成員函數、操作符重載、函數重載、構造函數定義的隱式類型轉換...等機制,表明了 C++作為一個靜態類型語言的本質:屬於特定類型的數據,加上其上的操作。可以這樣理解,任何一個操作,本質就是函數,操作符在C++語言內部也是被當作函數來看待的(這也能解釋C++提供operator操作符重載機制的動機);類的成員函數、友元函數,也是對類本身這個“數據類型”的操作。
更進一步,操作本身也是一種特殊的數據類型。可以定義函數的指針、函數的數組,成員訪問(->*,.*),只是可以被當作數據類型來使用的機會不多,也被語言本身限制了。
C++的內置操作不太好理解,實際上我們常用的語言機制都是“操作”,具體包含了:
(1)各種各樣的操作符
算術操作符、關系操作符、位運算、取地址、單目運算、解引用、數組元素訪問.....
(2)拷貝操作
拷貝初始化、列表初始化(C++ 11)、賦值運算、函數傳參、函數返回值、類型轉換執行的臨時變量拷貝......等其他非引用場景
(3)數據類型轉換
類型轉換也是一種操作。對於普通的操作,執行前需先匹配要操作的數據的類型。現實中,不可能總能保證在代碼裡提供類型嚴格匹配的數據,因此類型轉換也是C++語言非常普遍的操作。
該如何理解這樣的操作呢?舉個例子,例如:
1
2
3
4
5
6
7
int nVal = 42;
double fVal = 3.14;
double fValTwo;
fValTwo = fVal + nVal ; // nVal類型提升為double
上述代碼最後一行的相加操作將執行類型提升。從編譯器的角度看,此時將生成一個匿名的變量,變量的類新和需要匹配的類型(double)相同,之後執行int至double的類型轉換操作,操作結果保存在這個匿名變量中。之後才會執行“+”操作。也就是說,如果選定了操作,那麼就會期待若干數據類型完全匹配的操作數,為了滿足這個條件,系統會執行類型轉換。
對於賦值操作,該操作會期待=右邊操作數的數據類型和左邊完全匹配,此時也會和上述相同,生成匿名變量,執行類型轉換。准備工作完成後,再執行"="操作。
函數的調用也是基於相同的原理,即實參類型和形參類型的匹配。
... ...
C++語言內部定義了異常復雜的類型轉換規則(操作),只不過大多數對使用者是透明的。例如:
整形提升 - char、short、bool會先轉換為int;
類型提升 - 防止精度損失;
類型降低 -有精度損失,常見於拷貝操作。拷貝操作是將源對象嚴格匹配目標對象,因此不會有算術操作裡的“整形提升”。拷貝包括了拷貝初始化、賦值運算、函數調用實參賦給形參
非bool值都可以轉換為bool,相反則轉換為0/1;
任意類新指針都可轉換為void*;
數組在不用於decltype、sizeof、typeid、取地址&的情況下,會自動轉換為指向第一個元素的指針。
非底層const向底層const的轉換 - 指向常量的引用和指針可以綁定到非常量上,和內置類型的提升與降低不同,底層const向非底層const的轉換是非法的;
子類向基類的轉換 - 基類指針/引用可以指向子類,這是多態的基礎。和底層const一樣,相反的轉換是非法的。
... ...
-
PS:關於底層const和繼承體系類型轉換的單向性:
本質而言,一個數據的數據類型,可以執行的操作的集合越小,該數據可以引用/綁定的對象類型越廣。例如:
數據類型A,可以執行operA - operZ 共26個操作。數據類新B,可以執行的操作是A的子集,比如operH-operN。那麼,B的引用/指針可以綁定到A(B的引用/指針可以接受A/A的指針賦值),相反則是非法的。
const int *不能修改指向的int,而int *可以,也就是說,數據類型const int *的操作范圍比int *要小,所以const int *可以綁定到int*指向的對象(本質上是指const int *可以接受int*賦值)。
在繼承體系中,基類的操作范圍肯定是小於子類的,所以 基類指針指向/基類引用 子類的合法的。
造成這一切的原因就在於,對靜態類型語言,編譯器始終“固執”地、“自以為是”地按照其靜態聲明類型,來決定一個操作是否合法,而不去管這個對象實際指向的類型。可以想象,編譯器“自以為是”地認為通過int *可以改變這個int,而不管這個int*實際指向的是const int,如果允許底層const向非底層const轉換,就會帶來沖突。
-
PS:基於該觀點理解重載
函數重載、操作符重載的本質,是用同一個名字定義了多個操作。結果是在編譯階段引入了一個確定具體操作的過程 - 從候選操作中選出最匹配的操作。而上述“類型轉換”操作則是在運行階段進行的。