跳轉到內容

C++ 程式設計

來自華夏公益教科書,開放的書籍,為一個開放的世界

預處理器

[編輯 | 編輯原始碼]

預處理器是一個獨立的程式,由編譯器呼叫,或者作為編譯器本身的一部分。它執行中間操作,在編譯器嘗試編譯生成的原始碼之前修改原始原始碼和內部編譯器選項。

預處理器解析的指令稱為指令,有兩種形式:預處理器指令和編譯器指令。預處理器指令指示預處理器如何處理原始碼,而編譯器指令指示編譯器如何修改內部編譯器選項。指令用於簡化編寫原始碼(例如,使其更具可移植性),並使原始碼更易於理解。它們也是使用 C++ 標準庫提供的設施(類、函式、模板等)的唯一有效方法。

注意
查閱編譯器/預處理器的文件,瞭解其如何實現預處理階段以及任何標準未涵蓋的附加功能。有關解析主題的深入資訊,您可以閱讀 "編譯器構造" (https://wikibook.tw/wiki/Compiler_Construction)

所有指令在行首都以 '#' 開頭。標準指令為

  • #define
  • #elif
  • #else
  • #endif
  • #error
  • #if
  • #ifdef
  • #ifndef
  • #include
  • #line
  • #pragma
  • #undef

包含標頭檔案(#include)

[編輯 | 編輯原始碼]

#include 指令允許程式設計師將一個檔案的內容包含到另一個檔案中。這通常用於將多個程式部分所需的資訊分離到一個單獨的檔案中,以便可以重複包含這些資訊,而無需將所有原始碼重新鍵入到每個檔案中。

C++ 通常要求您在使用之前宣告要使用的內容。因此,名為標頭檔案的檔案通常包含要使用的宣告,以便編譯器能夠成功編譯原始碼。這在本書的檔案組織部分中有更詳細的說明。標準庫(包含在每個符合標準的 C++ 編譯器中的程式碼庫)和第三方庫使用標頭檔案,以便將必要的宣告包含在您的原始碼中,允許您使用語言本身不包含的功能或資源。

任何原始檔的第一行通常應該類似於以下內容

#include <iostream>
#include "other.h"

以上程式碼行導致檔案iostreamother.h 的內容被包含,以供您的程式使用。通常,這是透過將iostreamother.h 的內容插入到您的程式中來實現的。當在指令中使用尖括號 (<>) 時,預處理器會收到指令,在編譯器特定的位置搜尋指定的檔案。當使用雙引號 (" ") 時,預處理器預計將在一些額外的、通常是使用者定義的位置搜尋標頭檔案,並且如果在這些額外位置中找不到標頭檔案,則返回到標準包含路徑。通常,當使用這種形式時,預處理器還會在包含#include指令的檔案所在的同一目錄中搜索。

Theiostream標頭檔案包含使用稱為的 I/O 機制抽象進行輸入/輸出 (I/O) 的各種宣告。例如,有一個名為std::cout(其中“cout”是“控制檯輸出”的縮寫)的輸出流物件,它用於將文字輸出到標準輸出,這通常會在計算機螢幕上顯示文字。

注意
包含標準庫時,允許編譯器在給定名稱的標頭檔案實際上是否存在為物理檔案,還是僅僅是一個邏輯實體,導致預處理器修改原始碼,並具有與實體存在為物理檔案時相同的最終結果,方面做出例外。查閱您的預處理器/編譯器的文件,瞭解任何特定於供應商的 #include 指令實現以及標準和使用者定義標頭檔案的特定搜尋位置。這會導致可移植性問題和混亂。

下面列出了標準 C++ 標頭檔案列表


標準模板庫

以及

標準 C 庫
  1. a b c d e f g h i j k l m n o p q r s t u v 僅在 C++11 中

C++ 標準庫中的所有內容都包含在std:名稱空間中。

舊的編譯器可能會包含帶有.h字尾的標頭檔案(例如,非標準的<iostream.h>與標準的<iostream>相比)。這些名稱在 C++ 標準化之前很常見,一些編譯器仍然包含這些標頭檔案以確保向後相容性。與使用std:名稱空間相比,這些舊的標頭檔案會汙染全域性名稱空間,並且可能只以有限的方式實現標準。

一些供應商使用 SGI STL 標頭檔案。這是標準模板庫的第一個實現。

非標準但比較常見的 C++ 庫
  1. 基於 stdio.h 中 FILE* 的流。
  2. iostream 的前身。舊的流庫主要為了向後相容性而保留,即使在舊的編譯器中也是如此。
  3. 使用 **char***,而 sstream 使用 string。建議使用標準庫 sstream。


注意
在標頭檔案標準化之前,它們被呈現為分離的檔案,比如 <iostream.h> 等等。這可能仍然是舊的(不符合標準的)編譯器的要求,但更新的編譯器將接受這兩種方法。標準中也沒有要求標頭檔案必須以檔案形式存在。將標準庫引用為獨立檔案的舊方法已經過時了。

#pragma

[edit | edit source]

pragma(實用資訊)指令是標準的一部分,但任何 pragma 指令的含義都取決於所使用的標準軟體實現。

Pragma 指令在源程式中使用。

#pragma token(s)

你應該檢視要使用的 C++ 標準的軟體實現,以獲取支援的令牌列表。

例如,最廣泛使用的預處理器 pragma 指令之一,#pragma once,當放在標頭檔案的開頭時,表示如果預處理器多次包含該檔案,則會跳過該檔案。

注意
還存在另一種方法,通常稱為包含守衛,它提供相同的功能,但使用其他包含指令。

在 GCC 文件中,#pragma once 被描述為一個過時的預處理器指令。

C++ 預處理器包含定義“宏”的功能,這大致意味著能夠將命名宏的使用替換為一個或多個令牌。這在定義簡單常量(儘管在 C++ 中更常使用const來實現這一點)、條件編譯、程式碼生成等方面有各種用途——宏是一個強大的功能,但如果不小心使用,也會導致程式碼難以閱讀和除錯!

注意

宏不僅取決於 C++ 標準或你的操作。它們可能由於使用了外部框架、庫,甚至由於你使用的編譯器和特定作業系統而存在。我們不會在這本書中介紹這些資訊,但你可以在 ( http://predef.sourceforge.net/ ) 的 預定義 C/C++ 編譯器宏 頁面中找到更多資訊,該專案維護著與編譯器和作業系統無關的宏的完整列表。

#define 和 #undef
[edit | edit source]

#define 指令用於定義預處理器用來在編譯原始碼之前操作程式原始碼的值或宏。

#define USER_MAX (1000)

#undef 指令刪除當前的宏定義。

#undef USER_MAX

使用#define更改宏的定義是錯誤的,但使用#undef嘗試取消定義當前未定義的宏名稱不是錯誤。因此,如果你需要覆蓋之前的宏定義,首先要#undef它,然後使用#define設定新的定義。

注意
由於預處理器定義是在編譯器處理原始碼之前進行替換的,因此由#define引入的任何錯誤都難以追蹤。例如,使用與某些現有識別符號相同的數值或宏名稱會導致細微的錯誤,因為預處理器會替換原始碼中的識別符號名稱。

如今,出於這個原因,#define主要用於處理編譯器和平臺差異。例如,一個定義可能包含一個常量,該常量是系統呼叫的適當錯誤程式碼。因此,應該儘量限制#define的使用,除非絕對必要;typedef 語句、常量變數、列舉、模板和 行內函數 通常可以更有效、更安全地完成相同目標。

按照慣例,使用#define定義的值用大寫字母和 "_" 分隔符命名,這使讀者清楚地知道該值是不可改變的,在宏的情況下,該結構需要小心。儘管這樣做不是必須的,但這樣做被認為是極其不好的做法。這允許在閱讀原始碼時輕鬆識別這些值。

嘗試使用 constinline 而不是 #define

\ (行延續)
[edit | edit source]

如果由於某種原因需要將給定語句分成多行,請使用\(反斜槓)符號來“轉義”行尾。例如,

#define MULTIPLELINEMACRO \
        will use what you write here \
        and here etc...

等效於

#define MULTIPLELINEMACRO will use what you write here and here etc...

因為預處理器將以反斜槓(“\”)結尾的行與它們後面的行連線起來。即使在處理指令(如 #define)之前也會發生這種情況,因此它幾乎適用於所有目的,而不僅僅是用於宏定義。反斜槓有時被認為是換行的“轉義”字元,它改變了換行的解釋。

在某些(相當罕見)情況下,宏在跨多行分割時可能更具可讀性。好的現代 C++ 程式碼只會謹慎地使用宏,因此對多行宏定義的需求不會經常出現。

當然有可能過度使用此功能。例如,編寫以下程式碼是完全合法的,但完全站不住腳

int ma\
in//ma/
()/*ma/
in/*/{}

但這是一種對該功能的濫用:雖然轉義的換行符可以出現在令牌的中間,但永遠不應該有任何理由在那裡使用它。不要嘗試編寫看起來像是屬於國際混淆 C 程式碼競賽的程式碼。

警告:使用轉義換行符有時會出現一個“陷阱”:如果反斜槓之後有任何不可見字元,行將不會連線,並且幾乎肯定會在以後產生錯誤訊息,儘管它可能並不明顯是什麼原因導致的。

類函式宏
[edit | edit source]

#define 命令的另一個功能是它可以接受引數,使其作為偽函式建立者相當有用。考慮以下程式碼

#define ABSOLUTE_VALUE( x ) ( ((x) < 0) ? -(x) : (x) )
// ...
int x = -1;
while( ABSOLUTE_VALUE( x ) ) {
// ...
}

注意

通常使用額外的括號來包裝宏引數是一個好主意,它可以避免參數以意外的方式解析。但有一些例外情況需要考慮

  1. 由於逗號運算子的優先順序低於任何其他運算子,這消除了出現問題的可能性,因此不需要額外的括號。
  2. 使用 ## 運算子連線令牌,使用 # 運算子轉換為字串,或者連線相鄰的字串文字時,不能單獨對引數加括號。

請注意,在上面的示例中,變數“x”始終在其自己的括號內。這樣,它將在整體上進行評估,然後再與 0 進行比較或乘以 -1。此外,整個宏都被括號包圍,以防止它受到其他程式碼的汙染。如果你不小心,你就有可能讓編譯器誤解你的程式碼。

宏將文字中使用的每個宏引數的出現替換為宏引數的字面內容,而不會進行任何驗證檢查。編寫不當的宏會導致程式碼無法編譯或產生難以發現的錯誤。由於存在副作用,因此使用上面描述的宏函式被認為是一個非常糟糕的主意。但是,與任何規則一樣,可能存在宏是實現特定目標的最有效方法的情況。

int z = -10;
int y = ABSOLUTE_VALUE( z++ );

如果 ABSOLUTE_VALUE() 是一個真正的函式,“z”現在將具有 -9 的值,但因為它是在宏中作為引數使用的,所以z++ 被擴充套件了 3 次(在這種情況下),因此(在這種情況下)執行了 2 次,將 z 設定為 -8,並將 y 設定為 9。在類似的情況下,很容易編寫具有“未定義行為”的程式碼,這意味著它所做的事情在 C++ 標準看來是完全不可預測的。

// ABSOLUTE_VALUE( z++ ); expanded
( ((z++) < 0 ) ? -(z++) : (z++) );


注意
使用 GCC 編譯器擴充套件稱為“語句表示式”(不是標準 C++),允許在表示式中使用語句,請查閱編譯器手冊以瞭解其他注意事項,這樣就可以只評估它一次

#define ABSOLUTE_VALUE( x ) ( { typeof (x) temp = (x); (temp < 0) ? -temp : temp; } )

使用內聯模板函式可以作為宏的替代方法,消除了宏引數內部副作用的問題。

通常最好避免使用特定於編譯器的擴充套件,除非計劃使用依賴關係。

// An example on how to use a macro correctly

#include <iostream>
 
#define SLICES 8
#define PART(x) ( (x) / SLICES ) // Note the extra parentheses around '''x'''
 
int main() {
   int b = 10, c = 6;
   
   int a = PART(b + c);
   std::cout << a;
   
   return 0;
}

--“a”的結果應該是“2”(將 b + c 傳遞給 PART -> ((b + c) / SLICES) -> 結果是“2”)

注意

可變引數宏
可變引數宏是預處理器的功能,透過該功能,可以宣告宏以接受可變數量的引數(類似於可變引數函式)。

它們目前不是 C++ 程式語言的一部分,儘管許多最新的 C++ 實現都支援可變引數宏作為擴充套件(即:GCC、MS Visual Studio C++),並且預計可變引數宏可能會在以後新增到 C++ 中。

可變引數宏是在 1999 年的 C 程式語言標準 ISO/IEC 9899:1999(C99)修訂版中引入的。

# 和 ##
[edit | edit source]

### 運算子與#define宏一起使用。使用 # 會導致 # 後的第一個引數作為帶引號的字串返回。例如

#define as_string( s ) # s

將使編譯器將

std::cout << as_string( Hello  World! ) << std::endl;

變成

std::cout << "Hello World!" << std::endl;

注意
觀察來自#引數的前導和尾隨空格被刪除,令牌之間的連續空格序列被轉換為單個空格。

使用 #### 之前的部分與之後的部分連線起來;結果必須是格式良好的預處理令牌。例如

#define concatenate( x, y ) x ## y
...
int xy = 10;
...

將使編譯器將

std::cout << concatenate( x, y ) << std::endl;

變成

std::cout << xy << std::endl;

當然,這將顯示10到標準輸出。

不能使用 ## 連線字串文字,但好訊息是這不是問題:只需編寫兩個相鄰的字串文字就足以使預處理器連線它們。

宏的危險
[edit | edit source]

為了說明宏的危險,請考慮這個簡單的宏

#define MAX(a,b) a>b?a:b

以及程式碼

i = MAX(2,3)+5;
j = MAX(3,2)+5;

看一下這個,並考慮執行後的值可能是多少。語句變成了

 
int i = 2>3?2:3+5;
int j = 3>2?3:2+5;

因此,執行後i=8j=3而不是預期的結果i=j=8!這就是為什麼之前建議你使用額外的括號,但即使使用這些括號,道路也充滿危險。警覺的讀者可能會很快意識到,如果a,b包含表示式,則定義必須在宏定義中對每個a,b的使用加括號,如下所示

#define MAX(a,b) ((a)>(b)?(a):(b))

這有效,前提是a,b沒有副作用。實際上,

 
 i = 2;
 j = 3;
 k = MAX(i++, j++);

將導致k=4, i=3j=5。這對於任何期望MAX()像函式一樣工作的任何人來說都是非常令人驚訝的。

那麼正確的解決方案是什麼?解決方案是不使用宏。一個全域性的行內函數,像這樣

inline int max(int a, int b) { return a>b?a:b }

沒有上面提到的任何缺點,但不能與所有型別一起使用。模板(見下文)可以解決這個問題

template<typename T> inline max(const T& a, const T& b) { return a>b?a:b }

實際上,這是 STL 庫中用於 std::max() 的定義(其變體)。該庫包含在所有符合標準的 C++ 編譯器中,因此理想的解決方案是使用它。

std::max(3,4);

使用宏的另一個危險是它們被排除在型別檢查之外。在 MAX 宏的情況下,如果與字串型別變數一起使用,它不會生成編譯錯誤。

MAX("hello","world")

因此,最好使用行內函數,它將進行型別檢查。如果行內函數按上述方式使用,則允許編譯器生成有意義的錯誤訊息。

字串文字連線

[edit | edit source]

預處理器的一個次要功能是將字串連線在一起,“字串文字連線”--將程式碼

std::cout << "Hello " "World!\n";

變成

std::cout << "Hello World!\n";

除了晦澀的使用之外,這在編寫長訊息時最有用,因為正常的 C++ 字串文字不允許在原始碼中跨越多行(即,在其中包含換行符)。對此的例外是 C++11 原始字串文字,它可以包含換行符,但不會解釋任何跳脫字元。使用字串文字連線也有助於將程式行保持在合理的長度內;我們可以編寫

 function_name("This is a very long string literal, which would not fit "
               "onto a single line very nicely -- but with string literal "
               "concatenation, we can split it across multiple lines and "
               "the preprocessor will glue the pieces together");

請注意,這種連線發生在編譯之前;編譯器只看到一個字串文字,並且在執行時沒有做任何工作,即,你的程式不會因為這種字串的連線而執行得更慢。

連線也適用於寬字串文字(以 L 為字首)

 L"this " L"and " L"that"

被預處理器轉換為

 L"this and that".

注意
為了完整起見,請注意 C99 針對此操作與 C++98 具有不同的規則,並且 C++0x 似乎幾乎肯定會匹配 C99 更寬容的規則,這些規則允許將窄字串文字連線到寬字串文字,這是 C++98 中無效的操作。

條件編譯

[edit | edit source]

條件編譯主要用於兩個目的

  • 允許在編譯程式時啟用/停用某些功能
  • 允許以不同方式實現功能,例如在不同平臺上編譯時

它有時也用於臨時“註釋掉”程式碼,儘管使用版本控制系統通常是更有效的方法。

  • 語法:
#if condition
  statement(s)
#elif condition2
  statement(s)
...
#elif condition
  statement(s)
#else
  statement(s)
#endif

#ifdef defined-value
  statement(s)
#else
  statement(s)
#endif

#ifndef defined-value
  statement(s)
#else
  statement(s)
#endif

#if 指令允許對預處理器值進行編譯時條件檢查,例如使用 #define 建立的預處理器值。如果condition 非零,則預處理器將包括所有statement(s),直到輸出中處理的 #else#elif#endif 指令。否則,如果 #if condition 為假,則將按順序檢查所有 #elif 指令,第一個為真的condition 將在其輸出中包含其statement(s)。最後,如果 #if 指令的condition 和所有存在的 #elif 指令都為假,則如果存在,#else 指令的statement(s) 將被包含在輸出中;否則,將不包含任何內容。

在 **#if** 後使用的表示式可以包含布林和整數常量以及算術運算,以及宏名稱。允許的表示式是 C++ 表示式完整範圍的子集(有一個例外),但足以滿足許多目的。**#if** 可用的一個額外運算子是 **defined** 運算子,它可以用來測試是否當前定義了給定名稱的宏。

#ifdef 和 #ifndef
[編輯 | 編輯原始碼]

**#ifdef** 和 **#ifndef** 指令是 '#if defined(defined-value)' 和 '#if !defined(defined-value)' 的簡寫形式。**defined**(identifier) 在預處理器評估的任何表示式中都有效,如果用 #define 定義了名稱為 identifier 的預處理器變數,則返回真(在此上下文中,等效於 1),否則返回假(在此上下文中,等效於 0)。事實上,圓括號是可選的,也可以在沒有圓括號的情況下寫 **defined** identifier

(可能 **#ifndef** 最常見的用法是建立標頭檔案的“包含保護”,以確保標頭檔案可以安全地包含多次。這將在標頭檔案部分進行解釋。)

**#endif** 指令結束 **#if**、**#ifdef**、**#ifndef**、**#elif** 和 **#else** 指令。

  • 示例:
 #if defined(__BSD__) || defined(__LINUX__)
 #include <unistd.h>
 #endif

這可以用來例如提供多個平臺支援,或者為不同的程式版本設定一個通用的原始檔集。另一個使用示例是使用它代替(非標準的)**#pragma once**。

  • 示例:

foo.hpp

 #ifndef FOO_HPP
 #define FOO_HPP
 
  // code here...
 
 #endif // FOO_HPP

bar.hpp

 #include "foo.h"
 
  // code here...

foo.cpp

 #include "foo.hpp"
 #include "bar.hpp"
 
  // code here

當我們編譯 **foo.cpp** 時,由於使用了包含保護,只會包含一份 **foo.hpp**。當預處理器讀取行 #include "foo.hpp" 時,將擴充套件 **foo.hpp** 的內容。由於這是第一次讀取 **foo.hpp**(並且假設不存在宏 **FOO_HPP** 的現有宣告),**FOO_HPP** 尚未宣告,因此程式碼將正常包含。當預處理器在 foo.cpp 中讀取行 #include "bar.hpp" 時,將按常例擴充套件 **bar.hpp** 的內容,並且檔案 **foo.h** 將再次擴充套件。由於之前聲明瞭 **FOO_HPP**,因此不會插入 **foo.hpp** 中的任何程式碼。因此,這可以實現我們的目標 - 避免檔案內容被包含超過一次。

編譯時警告和錯誤

[編輯 | 編輯原始碼]
  • 語法:
 #warning message
 #error message
#error 和 #warning
[編輯 | 編輯原始碼]

**#error** 指令會導致編譯器停止並輸出遇到它時的行號和訊息。**#warning** 指令會導致編譯器輸出遇到它時的行號和訊息的警告。這些指令主要用於除錯。

注意
**#error** 是標準 C++ 的一部分,而 **#warning** 則不是(儘管它得到了廣泛支援)。

  • 示例:
 #if defined(__BSD___)
 #warning Support for BSD is new and may not be stable yet
 #endif
 
 #if defined(__WIN95__)
 #error Windows 95 is not supported
 #endif

原始檔名和行號宏

[編輯 | 編輯原始碼]

可以使用預定義的宏 __FILE__ 和 __LINE__ 來檢索正在執行預處理的當前檔名和行號。行號是在任何轉義換行符被移除之前測量的。可以使用 **#line** 指令覆蓋 __FILE__ 和 __LINE__ 的當前值;在手寫程式碼中這樣做很少是合適的,但對於根據其他輸入檔案建立 C++ 程式碼的程式碼生成器來說很有用,這樣(例如)錯誤訊息將參考原始輸入檔案而不是生成的 C++ 程式碼。

華夏公益教科書