編譯階段的目標是生成計算機可識別的機器碼(machine instruction code)。而編碼階段和預處理階段的主要工作還是「文字編輯」,生成的是人類可識別的原始碼(source code)。編譯階段的特殊性在於,它看到的都是 c++ 語法實體,比如 typedef、using、template、struct/class 這些關鍵字定義的型別,而不是執行階段的變數。所以,這時的程式設計思維方式與平常大不相同。
比如說,讓編譯器遞迴計算斐波那契數列,這已經算是乙個比較容易理解的編譯階段數值計算用法了:
template
<
int n>
struct fib // 遞迴計算斐波那契數列
;template
<
>
struct fib<
0>
// 模板特化計算fib<0>
;template
<
>
struct fib<
1>
// 模板特化計算fib<1>
;// 呼叫後輸出2,3,5,8
cout << fib<
2>
::value << endl;
cout << fib<
3>
::value << endl;
cout << fib<
4>
::value << endl;
cout << fib<
5>
::value << endl;
對於編譯器來說,可以在一瞬間得到結果,但你要搞清楚它的執行過程,就得在大腦裡把 c++ 模板特化的過程走一遍。整個過程無法除錯,完全要靠自己去推導,特別「累人」。(你也可以把編譯器想象成是一種特殊的「虛擬機器」,在上面跑的是只有編譯器才能識別、處理的**。)
在 c++11 之前,標準裡沒有規定這樣的東西,但 gcc、vc 等編譯器發現這樣做確實很有用,於是就實現出了自己「編譯指令」,在 gcc 裡是「__ attribute __」,在 vc 裡是「__declspec」。不過因為它們不是標準,所以名字顯得有點「怪異」。到了 c++11,標準委員會終於認識到了「編譯指令」的好處,於是就把「民間」用法公升級為「官方版本」,起了個正式的名字叫「屬性」。你可以把它理解為給變數、函式、類等「貼」上乙個編譯階段的「標籤」,方便編譯器識別處理。「屬性」沒有新增關鍵字,而是用兩對方括號的形式「[[…]]」,方括號的中間就是屬性標籤(看著是不是很像一張方方正正的便籤條)。所以,它的用法很簡單,比 gcc、vc 的都要簡潔很多。
[
[noreturn]
]// 屬性標籤
intfunc
(bool flag)
// 函式絕不會返回任何值
[
[deprecated
("deadline:2020-12-31")]
]// c++14 or later
intold_func()
;
於是,任何用到這個函式的程式都會在編譯時看到這個標籤,報出一條警告:
warning: 『int
old_func
()』 is deprecated: deadline:
2020-12
-31[-wdeprecated-declarations]
當然,程式還是能夠正常編譯的,但這種強制的警告形式會「提醒」使用者舊介面已經被廢棄了,應該盡快遷移到新介面。顯然,這種形式要比毫無約束力的文件或者注釋要好得多。目前的 c++17 和 c++20 又增加了五六個新屬性,比如 fallthrough、likely,但我覺得,標準委員會的態度還是太「保守」了,在實際的開發中,這些真的是不夠用。好在「屬性」也支援非標準擴充套件,允許以類似名字空間的方式使用編譯器自己的一些「非官方」屬性,比如,gcc 的屬性都在「gnu::」裡。下面我就列出幾個比較有用的(全部屬性可參考gcc 文件)。
deprecated:與 c++14 相同,但可以用在 c++11 裡。
unused:用於變數、型別、函式等,表示雖然暫時不用,但最好保留著,因為將來可能會用。
constructor:函式會在 main() 函式之前執行,效果有點像是全域性物件的建構函式。
destructor:函式會在 main() 函式結束之後執行,有點像是全域性物件的析構函式。
always_inline:要求編譯器強制內聯函式,作用比 inline 關鍵字更強。
hot:標記「熱點」函式,要求編譯器更積極地優化。
以上用法說明 。
以unused為例,在沒有這個屬性的時候,如果有暫時用不到的變數,我們只能用「(void) var;」的方式假裝用一下,來「騙」過編譯器,屬於「不得已而為之」的做法。那麼現在,我們就可以用「unused」屬性來清楚地告訴編譯器:這個變數我暫時不用,請不要過度緊張,不要發出警告來煩我:
[
[gnu::unused]
]// 宣告下面的變數暫不使用,不是錯誤
int nouse;
「屬性」像是給編譯器的乙個「提示」「告知」,無法進行計算,還算不上是程式設計,而接下來要講的「靜態斷言」,就有點編譯階段寫程式的味道了。
assert()用來斷言乙個表示式必定為真。比如說,數字必須是正數,指標必須非空、函式必須返回 true:
assert
(i >0&&
"i must be greater than zero");
assert
(p !=
nullptr);
assert
(!str.
empty()
);
當程式(也就是 cpu)執行到 assert 語句時,就會計算表示式的值,如果是 false,就會輸出錯誤訊息,然後呼叫 abort() 終止程式的執行。注意,assert 雖然是乙個巨集,但在預處理階段不生效,而是在執行階段才起作用,所以又叫「動態斷言」。
模擬一下 assert,它是編譯階段裡檢測各種條件的「斷言」,編譯器看到 static_assert 也會計算表示式的值,如果值是 false,就會報錯,導致編譯失敗。不過它是乙個專門的關鍵字,而不是巨集。因為它只在編譯時生效,執行階段看不見,所以是「靜態」的。
比如說,這節課剛開始時的斐波拉契數列計算函式,可以用靜態斷言來保證模板引數必須大於等於零:
template
<
int n>
struct fib
;
再比如說,要想保證我們的程式只在 64 位系統上執行,可以用靜態斷言在編譯階段檢查 long 的大小,必須是 8 個位元組(當然,你也可以換個思路用預處理程式設計來實現)。
static_assert
(sizeof
(long
)>=8,
"must run on x64");
static_assert
(sizeof
(int)==
4,"int must be 32bit"
);
注意,static_assert 執行在編譯階段,只能看到編譯時的常數和型別,看不到執行時的變數、指標、記憶體資料等,是「靜態」的,所以不要簡單地把 assert 的習慣搬過來用。比如,下面的**想檢查空指標,由於變數只能在執行階段出現,而在編譯階段不存在,所以靜態斷言無法處理。就像編譯器那樣去執行,看看斷言的表示式是不是能夠在編譯階段算出結果。
char
* p =
nullptr
;static_assert
(p ==
nullptr
,"some error.");
// 錯誤用法
不過這句話說起來容易做起來難,計算數字還好說,在泛型程式設計的時候,怎麼檢查模板型別呢?比如說,斷言是整數而不是浮點數、斷言是指標而不是引用、斷言型別可拷貝可移動……這些檢查條件表面上看好像是「不言自明」的,但要把它們用 c++ 語言給精確地表述出來,可就沒那麼簡單了。所以,想要更好地發揮靜態斷言的威力,還要配合標準庫里的「type_traits」,它提供了對應這些概念的各種編譯期「函式」。
// 假設t是乙個模板引數,即template
static_assert
( is_integral
::value,
"int");
static_assert
( is_pointer
::value,
"ptr");
static_assert
( is_default_constructible
::value,
"constructible"
);
「static_assert」裡的表示式樣子很奇怪,既有模板符號「<>」,又有作用域符號「::」,與執行階段的普通表示式大相徑庭,初次見到這樣的**一定會嚇一跳。這也是沒有辦法的事情。因為 c++ 本來不是為編譯階段程式設計所設計的。受語言的限制,編譯階段程式設計就只能「魔改」那些傳統的語法要素了:把類當成函式,把模板引數當成函式引數,把「::」當成 return 返回值。說起來,倒是和「函式式程式設計」很神似,只是它執行在編譯階段。由於「type_traits」已經初步涉及模板元程式設計的領域,不太好一下子解釋清楚,所以,在這裡就不再深入介紹了。
「屬性」相當於編譯階段的「標籤」,用來標記變數、函式或者類,讓編譯器發出或者不發出警告,還能夠手工指定**的優化方式。
官方屬性很少,常用的只有「deprecated」。我們也可以使用非官方的屬性,需要加上名字空間限定。
static_assert 是「靜態斷言」,在編譯階段計算常數和型別,如果斷言失敗就會導致編譯錯誤。它也是邁向模板元程式設計的第一步。
和執行階段的「動態斷言」一樣,static_assert 可以在編譯階段定義各種前置條件,充分利用 c++ 靜態型別語言的優勢,讓編譯器執行各種檢查,避免把隱患帶到執行階段。
靜態屬性和靜態方法2
潛規則 為什麼呢?回顧下 this指標是類的乙個自動生成 自動隱藏的私有成員,它存在於類的非靜態成員函式中,指向被呼叫函式所在的物件的位址。當乙個物件被建立時,該物件的 this指標就自動指向物件資料的首位址。我們從乙個比較有特色的例子來體會 this指標的工作原理 特色例子.txt。include...
php static靜態屬性和靜態方法
一 靜態屬性 靜態屬性不可以由物件通過 操作符來訪問。靜態屬性只能被初始化為文字或常量,不能使用表示式。所以可以把靜態屬性初始化為整數或陣列,但不能初始化為另乙個變數或函式返回值,也不能指向乙個物件。如何訪問靜態屬性呢?在類內部 類名 靜態屬性 或者 self 靜態屬性 在類外部 類名 靜態屬性 或...
靜態庫 a編譯和靜態庫 a合併
第一步 生成test.o目標檔案,使用gcc c test.c o test.o命令。第二步 使用ar將test.o打包成libtest.a靜態庫,使用ar rcs o libtest.a test.o命令 第三步 生成libtest.a靜態庫後,可以使用命令ar t libtest.a檢視libt...