跳轉到內容

使用 C 和 C++ 的程式語言概念/C 程式設計入門

來自華夏公益教科書

以下是提供對語言基礎知識的介紹的簡單 C 程式的精選。為了更好地實現這個目標,示例保持簡單和簡短。不幸的是,對於如此規模的程式,某些標準變得不那麼重要,因為它們通常應該那樣。其中一個標準是原始碼可移植性,可以透過檢查程式的標準合規性來確保。如果從一開始沒有認真追求這個標準,它就很難實現。

為了更容易解決這個問題,許多編譯器提供了一些幫助。雖然支援的程度及其形式可能有所不同,但值得看一下編譯器開關。對於我們將在本課程中使用的編譯器,下面列出了部分適用的選項。

選項* 含義 在其中有效
-Wall 對可疑的程式設計實踐提供警告,例如缺少函式返回型別、未使用的識別符號、沒有預設值的 switch 語句等等。 兩者
-std 檢查是否符合特定標準。 gcc
-pedantic 檢查程式是否嚴格符合標準,並拒絕使用非標準擴充套件的所有程式。 gcc
-Tc 類似於 -pedantic MS Visual C/C++
*:MS Visual C/C++ 支援的選項也可以用 '/' 作為字首。
示例:編譯器選項的使用。

使用 gcc,提供確保任何草率程式設計都不會被忽視所需的編譯命令。還要確保您的原始碼可以輕鬆移植到其他開發環境。

第一個要求是透過傳遞 -Wall 來滿足的,而第二個要求是透過使用 -pedantic 選項來滿足的。因此,所需的編譯命令如下所示。

gcc -Wall -pedantic [其他選項] 檔名

在 MS Visual C/ C++ 中,可以透過以下命令實現相同的目的

cl [其他選項] /Wall /Tc 檔名

C 預處理器

[編輯 | 編輯原始碼]

C 預處理器是一個簡單的宏處理器——一個 C 到 C 的翻譯器——它在 C 編譯器讀取源程式之前,在概念上處理 C 程式的源文字。通常,預處理器實際上是一個單獨的程式,它讀取原始原始檔並寫出一個新的“預處理”原始檔,然後可以將其用作 C 編譯器的輸入。在其他實現中,單個程式在對原始檔進行單次掃描時執行預處理和編譯。前一種方案的優點是,除了其更模組化的結構之外,還可以使用預處理器為其他程式語言建立翻譯器。

預處理器由特殊的預處理器命令列控制,這些命令列是原始檔以字元 # 開頭的行。

預處理器從原始檔中刪除所有預處理器命令列,並根據命令對原始檔進行額外的轉換。

命令的名稱必須緊跟在 # 字元之後。[1] 唯一非空格字元是 # 的行在 ISO C 中被稱為 *空指令*,其處理方式與空行相同。

定義宏

[編輯 | 編輯原始碼]
#define
#define 預處理器命令使一個名稱成為預處理器定義的宏。一系列標記,稱為宏的 *主體*,與該名稱相關聯。當在程式源文字或某些其他預處理器命令的引數中識別到宏的名稱時,它將被視為對該宏的呼叫;該名稱實際上被一個主體副本替換。如果宏被定義為接受引數,那麼跟隨宏名稱的實際引數將被替換為宏主體中的形式引數。使用的引數傳遞機制類似於按名稱呼叫。但是,不要忘記,文字替換是由預處理器執行的,而不是由編譯器執行的。

此宏指令可以以兩種不同的方式使用。

  • 類似物件的宏定義:#define name sequence-of-tokens?,其中 ? 代表前面實體的出現次數為零或一次。換句話說,宏的主體可以為空。
示例:類似物件的宏定義

#define BOOL char /* 不是 #define BOOL=char */ #define FALSE 0 #define TRUE 1 #define CRAY

請注意,沒有等號。也不需要分號來終止行。標記序列周圍的前導和尾隨空格將被丟棄。

在 ISO C 中,允許重新定義宏,前提是新定義與現有定義在標記上完全相同。只有在發出 undef 指令後,才能使用不同的定義重新定義宏。

  • 定義帶引數的宏:#define name(name1, name2, ..., namen) sequence-of-tokens?。左括號必須緊跟在宏名稱之後,中間沒有空格。這種宏定義將被解釋為一個類似物件的宏,它以左括號開頭。形式引數的名稱必須是識別符號,不能有兩個相同。類似函式的宏可以有空的形式引數列表。這用於模擬沒有引數的函式。
示例:帶引數的宏
#define increment(n) (n + 1)

當遇到類似函式的宏呼叫時,整個宏呼叫將被替換,在引數處理之後,被主體處理後的副本替換。將宏呼叫替換為其主體處理後的副本的整個過程稱為 *宏展開*;主體處理後的副本稱為宏呼叫的展開。因此,對於上述定義,a = 3 * increment(a); 將擴充套件為 a = 3 * (a + 1);。現在,預處理在編譯之前進行,真正的編譯器甚至看不到原始碼中的識別符號 increment。它看到的是宏的展開形式。

示例
#define product(x, y) (x * y)
不幸的是,上面這段看似無害的程式碼是錯誤的。它不會為 result = product(a + 3, b); 生成正確的結果。這行程式碼在預處理後將被轉換為 result = (a + 3 * b);。這並不是我們想要的!
相反,我們必須寫
#define product(x, y) ((x) * (y))
有了這個定義,之前的示例將被擴充套件為 result = ((a + 3) * (b));。請注意,程式設計師看不到多餘的括號。她看到的是程式文字的未預處理形式。
示例:錯誤的宏定義
#define SQUARE(x) x * x
這個定義無法為 a + 3 生成正確的結果。它將生成 a + 3 * a + 3 而不是 (a + 3) * (a + 3)。這可以透過在 x 周圍新增括號來解決。
#define SQUARE(x) (x) * (x)
但是,這個第二個版本無法為 result = (long) SQUARE(a + 3); 生成正確的結果,它被擴充套件為 result = (long) (a + 3) * (a + 3);
在上面的示例中,只有第一個子表示式會被轉換為 long。為了將強制轉換運算子應用於整個表示式,我們應該在宏的整個主體周圍新增括號。因此,宏的正確版本是
#define SQUARE(x) ((x) * (x))
問題到此結束了嗎?不幸的是,沒有!如果我們以以下方式使用宏會怎麼樣?這將被擴充套件為 result = ((x++) * (x++));
result = SQUARE(x++);
此表示式的問題是,由於自增運算子引起的副作用發生了兩次,並且 x 被乘以 x + 1,而不是自身。這 [再次] 表明人們必須避免編寫產生副作用的表示式。

我們在上述示例中遇到的問題可能是由於所用引數傳遞機制的文字性質造成的:按名稱呼叫:每次引數名出現在文字中時,它都會被替換為引數的精確文字;每次替換時,它都會被再次求值。

示例

#define swap(x, y) \ { unsigned long temp = x; x = y; y = temp; }

\ 在行尾用於行延續。對於

if (x < y) swap(x, y); else x = y;

我們得到

if (x < y) { unsigned long temp = x; x = y; y = temp; }; else x = y;

右大括號後的分號是多餘的,會導致編譯時錯誤。解決方法如下所示。

#define FALSE 0 #define swap(x, y) \ do { unsigned long temp = x; x = y; y = temp; } while(FALSE)

擴充套件後,我們將有

if (x < y) do { unsigned long temp = x; x = y; y = temp; } while(FALSE); else x = y;

預定義宏

[edit | edit source]
__LINE__ 當前源程式行的行號,用十進位制整數常量表示
__FILE__ 當前原始檔的名稱,用字串常量表示
__DATE__ 翻譯的日曆日期,用以下形式的字串常量表示:"Mmm dd yyyy"
__TIME__ 翻譯的時間,用以下形式的字串常量表示:"hh:mm:ss"
__STDC__ 十進位制常量,當且僅當編譯器是符合 ISO 標準的實現時
__STDC_VERSION__ 如果實現符合 ISO C 的修正案 1,則此宏被定義,並且值為 199409L

取消定義宏

[edit | edit source]
#undef name
#undef 使預處理器忘記 name 的任何宏定義。取消定義當前未定義的名稱不會導致錯誤。一旦名稱被取消定義,就可以在不出現錯誤的情況下為它賦予一個全新的定義。
示例:取消定義宏。

#define square(x) ((x) * (x)) ... a = square(b); ... ... #undef square ... ... c = square(d++); ...

第一次應用 square 將使用宏定義,而第二次將呼叫函式。

宏與函式

[edit | edit source]

在以下情況下,選擇宏定義而不是函式是有意義的:

  • 效率是首要考慮因素,
  • 宏定義簡單且短小,或者在原始碼中很少使用,以及
  • 引數只計算一次。

宏定義之所以高效,是因為預處理發生在執行時之前。實際上,它甚至在真正的編譯器開始之前就完成了。另一方面,函式呼叫是一個相當昂貴的指令,它在執行時執行。從這個意義上說,宏可以用來模擬函式的內聯。

宏定義應該簡單且短小,因為在原始碼中替換大量文字會導致程式碼膨脹,尤其是在它被頻繁使用時。

第三個要求的理由在前面已經給出。

另一方面,應該記住,預處理器不會對宏引數進行任何型別檢查。

程式碼包含

[edit | edit source]
#include <檔名>
#include "檔名"
#include 預處理器標記
預處理器命令 #include 會導致指定原始檔的所有內容都被處理,就好像這些內容出現在 #include 命令的位置一樣。

通常,"檔名" 形式用於引用程式設計師編寫的標頭檔案,而 <檔名> 形式用於引用標準實現檔案。第三種形式會進行正常的宏擴充套件,擴充套件的結果必須與前兩種形式之一匹配。

條件預處理

[edit | edit source]
#if 常量表達式
程式碼塊1
#elif
程式碼塊2
...
#elif
程式碼塊n
#endif
這些命令一起使用,允許有條件地包含或排除原始碼行進行編譯。當我們需要為不同的架構(例如,Vax 和 Intel)或在不同的模式(例如,除錯模式和生產模式)下生成程式碼時,這很方便。
#ifdef 名稱
#ifndef 名稱
這兩個命令用於測試一個名稱是否被定義為預處理器宏。它們等效於 #if defined(名稱)#if !defined(名稱),分別。

請注意,#if 名稱#ifdef 名稱 不等效。雖然當 名稱 未定義時,它們的工作方式完全相同,但當 名稱 被定義為 0 時,這種對等性就會被打破。在這種情況下,前者將為假,而後者將為真。

defined 名稱
defined(名稱)
運算子 defined 只能在 #if#elif 表示式中使用;它不是預處理器命令,而是預處理器運算子。

發出錯誤訊息

[edit | edit source]
#error 預處理器標記
指令 #error 會生成一個編譯時錯誤訊息,其中包含引數標記,這些標記會進行宏擴充套件。
示例:#error

#if (TABLESIZE > 1000) #error "Table size cannot exceed 1000" #endif

編譯時開關

[edit | edit source]

除了使用 #define 命令之外;我們可以使用編譯時開關來定義宏。

示例:在命令列中定義宏。
gcc –DTABLESIZE=500 prog_name↵
將把程式視為 #define TABLESIZE 500 是原始碼的第一行。
示例:透過編譯器開關進行條件預處理。
gcc –DVAX prog_name↵
上面的命令將定義一個名為 VAX 的宏,並(預處理和)編譯名為 prog_name 的程式。

#ifdef VAX
VAX 相關的程式碼
#endif #ifdef PDP11
PDP11 相關的程式碼
#endif #ifdef CRAY2
CRAY2 相關的程式碼
#endif

透過編譯時開關定義了 VAX,並且原始碼中包含了這些定義,只有 VAX 相關的程式碼會被處理。與其他架構相關的程式碼將被忽略。當我們想要為不同的架構編譯程式時,我們所要做的就是將編譯時開關更改為相關的架構。
新手(或惡意)程式設計師可能會在命令列中定義多個架構宏。
gcc –DVAX –DCRAY2 prog_name↵
在這種情況下,編譯後的程式將包含針對兩個不同架構的程式碼。可以使用以下預處理器命令避免這種情況。

#if defined(VAX)
VAX 相關的程式碼
#elif defined(PDP-11)
PDP11 相關的程式碼
#elif defined(CRAY2)
CRAY2 相關的程式碼
#else #error "目標架構未指定/未知!中止編譯..." #endif

示例:除錯模式編譯
prog_name 中,

... #if defined(DEBUG)
除錯模式程式碼
#endif ...

命令 gcc –DDEBUG prog_name↵ 將生成(物件)程式碼,其中包含一些用於除錯的額外語句。一旦除錯階段結束,就可以使用 gcc prog_name↵ 重新編譯程式,除錯模式程式碼將不會包含在物件程式碼中。這樣做的優點是,您無需從原始碼中刪除除錯模式程式碼!

char 資料型別

[edit | edit source]

問題:編寫一個程式,打印出字元 'a'..'z'、'A'..'Z' 和 '0'..'9' 以及它們的編碼。


Encoding.c

以下預處理器指令將名為 stdio.h 的檔案的內容拉入當前檔案。包含該檔案後,預處理器會解釋其內容。

對於我們使用的每個函式,都需要引入其原型,其位置可以透過在基於 UNIX 的環境中找到的 man 命令來確定:man 函式名 將向您提供有關 函式名 的資訊,包括其宣告所在的標頭檔案。

包含檔案通常包含在不同應用程式之間共享的宣告。常量宣告和函式原型就是例子。在本例中,我們必須包含 stdio.h,以便引入 printf 函式的原型。

一個函式原型包含函式的返回型別、函式名和引數列表。它描述了函式的介面:它詳細說明了呼叫函式時必須提供的引數的數量、順序和型別,以及函式返回的值的型別。

函式原型幫助編譯器確保正確使用特定函式。例如,在 stdio.h 中的宣告中,不能將 int 作為第一個引數傳遞給 printf 函式。

你會經常看到原型和簽名這兩個詞被互換使用。然而,這是不正確的。函式的簽名是其引數型別的列表;它不包括函式的返回型別。

#include <stdio.h>

所有可執行的 C 程式都必須包含一個名為 main 的函式。此函式作為程式的入口點,在載入可執行檔案後,作為初始化程式碼的一部分,從 C 執行時內部呼叫。

將程式從輔助儲存複製到主記憶體以便執行所需的系統軟體稱為載入器。除了將程式載入到記憶體之外,它還可以設定保護位、安排虛擬記憶體將虛擬地址對映到磁碟頁面,等等。

以下 main 函式的形式引數列表由單個關鍵字 void 組成,這意味著它根本不接受任何引數。有時你會看到 C 程式碼中寫著 int main()main(),而不是 int main(void)。在這種情況下,所有這三種方式都具有相同的用途,儘管前兩種應該避免使用。在宣告/定義中沒有返回型別的函式被假定為返回型別為 int 的值。具有空形式引數列表的函式原型是一種舊式宣告,它告訴編譯器函式的存在,該函式可以接受任意數量的任意型別的引數。它還有未經請求的副作用,即從該點開始關閉原型檢查。因此,應該避免此類用法。

int main(void) {

雖然我們將操作字元,但用於儲存字元的變數 ch 被宣告為 int。這樣做是以下原因:在原始語言設計中,沒有符號限定符 (signedunsigned),這給了編譯器實現者自由地將 char 解釋為包含 [0..255] 範圍內值的型別(因為字元只能由非負值索引)或包含 [–128..127] 範圍內值的型別(因為它是在單個位元組中表示的整數型別)。這兩種觀點基本上將 char 的範圍限制為 [0..127]。

一些關於 ASCII 的資訊

由於 C 中沒有異常,大多數處理字元和字串的函式需要將異常情況(例如檔案結束)表示為不太可能的返回值。這意味著,除了合法的字元之外,我們還應該能夠將異常情況編碼為不同的值。ASCII,這意味著一個可以容納 128 + n 個帶符號值的表示,其中 n 是要處理的異常情況的數量。結合上一段的結論,可以看出我們需要一個比型別 char 更大的表示。因此,你經常會看到一個 int 變數用於儲存型別為 char 的值。

遇到一個字元常量時,C 編譯器會用一個整數常量替換它,該常量對應於字元在編碼表中的順序。例如,ASCII 中的 'a' 被替換為 97,它是一個型別為 int 的整數常量。

如果我們使用 char 而不是 int,我們的程式仍然可以正常工作。這是因為我們不必在程式中處理任何異常情況,並且使用的所有 int 常量都在 char 表示的限制範圍內。也就是說,所有縮窄的隱式轉換(就像在 for 迴圈的初始化語句中發生的)都是值保持的。

  int ch;
  for(ch = 'a'; ch <= 'z'; ch++)

printf 函式用於格式化並在標準輸出檔案 stdout 上傳送格式化的輸出,預設情況下是螢幕。它是一個可變引數函式:也就是說,它接受一個可變長度的引數列表。第一個引數被認為是格式控制字串,用於確定引數的型別和數量。這是透過使用以 % 開頭的特殊字元序列來完成的。遇到此類序列時,如果實際引數可以在字元序列暗示的上下文中使用,則會用相應的實際引數替換它。例如,以下行中的 '%c' 意味著相應的引數必須可解釋為一個字元。同樣,'%d' 是十進位制數的佔位符。

請注意,printf 實際上返回一個 int。不將此返回值分配給某個變數意味著它被忽略了。

    printf("Encoding of '%c' is %d\n", ch, ch);
  printf("Press any key to continue\n"); getchar();

  for(ch = 'A'; ch <= 'Z'; ch++)
    printf("Encoding of '%c' is %d\n", ch, ch);
  printf("Press any key to continue\n"); getchar();
  for(ch = '0'; ch <= '9'; ch++)
    printf("Encoding of '%c' is %d\n", ch, ch);

  return(0);
} /* end of int main(void) */

在 Linux 命令列中編譯和執行程式

[編輯 | 編輯原始碼]

假設此程式儲存為 Encoding.c,可以使用以下命令編譯(和連結)它

gcc –o AlphaNumerics Encoding.c↵
GCC(GNU 編譯器集合)顧名思義,是一個針對多種程式語言的編譯器集合。 gcc 是 GCC 的 C/C++ 前端,它執行從 C 原始檔建立可執行檔案所需的預處理、編譯、彙編和連結階段。

gcc 呼叫 GNU C/C++ 編譯器驅動程式,該驅動程式首先獲取 C 預處理器處理 Encoding.c,並將轉換後的原始檔傳遞給編譯器本身。編譯器本身的輸出(一個彙編程式)隨後被傳遞給彙編器。由彙編器彙編的目的碼檔案最終被傳遞給連結器,連結器將它與標準 C 庫連結起來,並將可執行檔案儲存在磁碟檔案中,該檔案的檔名透過 -o 選項提供。請注意,你無需告訴驅動程式連結到標準 C 庫。這是一個特例。對於其他庫,你必須告訴編譯器驅動程式要連結的庫和目標檔案。整個方案基本上建立了一個名為 AlphaNumerics 的新外部命令。在命令列中發出命令

./AlphaNumerics

最終會導致載入器從輔助儲存將程式載入到記憶體並執行它。

使用 Emacs 編譯和執行程式

[編輯 | 編輯原始碼]
emacs &

此命令會將你帶到 Emacs 開發環境中。單擊 檔案→開啟... 並從檔案列表中選擇 Encoding.c。這將在當前視窗中開啟一個新的 C 模式緩衝區。請注意,最底部的第二行顯示 (C Abbrev),這意味著 Emacs 已將你的原始碼識別為 C 程式。接下來,單擊 工具→編譯►→編譯.... 這將提示你輸入編譯(和連結)程式所需的命令。此提示將列印在一個名為迷你緩衝區的區域中,該區域通常是框架的最後一行。擦除預設選擇並寫

gcc –o AlphaNumerics.exe Encoding.c↵

當你按下回車鍵時,你會看到一個 *編譯* 緩衝區彈出,讓你知道編譯過程的進行情況。希望你沒有打錯字,並且一切順利,接下來我們將要執行可執行檔案。為此,單擊 工具→Shell►→Shell。這將在 Shell 模式緩衝區內開啟一個受限制的 shell,從那裡可以執行你的可執行檔案。在該緩衝區中輸入

./AlphaNumerics↵

你會看到與上一節中相同的輸出。

如果你遇到編譯錯誤,單擊 *編譯* 緩衝區中的錯誤行會將你帶到相關的原始碼行。

如果您想返回原始碼並進行一些更改,請單擊 Buffers→Encoding.c。完成更改後,您可以透過單擊 Tools→Compile►→Repeat Compilation 再次編譯原始碼。這將使用上面輸入的命令重新編譯 Encoding.c。但是,如果您可能想要修改命令,請單擊 Tools→Compile►→Compile... 並按照之前的操作進行。

不可移植版本

[編輯 | 編輯原始碼]

假設使用 ASCII,您可能會傾向於用相應的整數值替換程式中的所有字元常量。強烈建議不要這樣做,因為它會使程式不可移植。


ASCII_Encoding.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int ch;

以下行包含嵌入的常量,這些常量會導致程式碼不可移植。如果我們想將程式碼移動到一些使用 EBCDIC 編碼字元的環境中會怎麼樣?因此,應該避免將這種依賴於實現的功能嵌入到程式中,而讓編譯器完成繁重的工作。

另外,由於編譯器會執行相同的操作,因此用整數常量替換字元字面量以加快程式速度的可能動機也毫無根據。

  for(ch = 97; ch <= 122; ch++)
    printf(Encoding of %c is %d\n, ch, ch);
  printf(Press any key to continue\n);
  getchar();

  for(ch = 65; ch <= 90; ch++)
    printf(Encoding of %c is %d\n, ch, ch);
  printf(Press any key to continue\n);
  getchar();

  for(ch = 48; ch <= 57; ch++)
    printf(Encoding of %c is %d\n, ch, ch);

The exit 函式使程式終止,並將其傳遞的值作為執行程式的結果返回。可以透過從 main 函式返回一個整數值來實現相同的效果。按照慣例,值為 0 表示成功終止,而非零值表示異常終止。

  exit(0);
} /* end of int main(void) */

使用 MS Visual C/C++ 編譯和執行程式

[編輯 | 編輯原始碼]

首先,確保您執行 vcvars32.bat,您可以在 MS Visual C/C++ 目錄的 bin 子目錄中找到它。這將設定一些您需要進行命令列工具正確操作的環境變數。

cl /FeAscii_Enc.exe Ascii_Encoding.c↵

gcc 類似,此命令將經歷預處理、編譯、彙編和連結階段。完成成功後,我們可以簡單地透過在命令列中輸入可執行檔名的名稱來執行我們的程式。作業系統 shell 將識別生成的執行檔案作為外部命令,並獲取載入器將 Ascii_Enc.exe 載入到記憶體中並最終執行它。

Ascii_Enc↵

使用 DGJPP-rhide 編譯和執行程式

[編輯 | 編輯原始碼]

啟動一個新的 DOS 框,並輸入以下命令。

rhide Ascii_Encoding.c↵

這將啟動一個基於 DOS 的 IDE,您可以使用它來開發不同程式語言的專案。選擇 Compile→Make 或 Compile→Build All 或 Compile→Compile,然後選擇 Compile→Link。這將編譯原始碼並將生成的的目標模組與 C 執行時連結。您現在可以透過單擊 Run→Run 或透過選擇 File→DOS Shell 並退出到 DOS 來執行可執行檔案,並在提示符下輸入檔名。(如果您可能從 rhide 中看到意外的行為,請確保檔案不在目錄層次結構中太深,並且名稱不包含空格等特殊字元。)如果您選擇第二個選項,您可以在命令提示符下鍵入 exit 返回 rhide。

問題:編寫一個以英語或土耳其語列印問候語的程式。語言應透過編譯時開關選擇。要問候的人員姓名透過命令列引數傳遞給程式。


Greeting.c
#include <stdio.h>

以下行檢查是否已定義名為 TURKISH 的宏。此宏或任何宏的定義可以在檔案內或在命令列提示符處作為編譯器開關進行。在此示例中,檔案或任何包含檔案都沒有這樣的定義。因此,在命令列提示符處沒有這樣的宏定義,將導致控制跳轉到 #else 部分,並且 #else#endif 之間的語句將包含在原始檔中。假設定義是在命令列提示符處完成的,則 #if#else 之間的語句將包含在原始檔中。無論包含程式碼的哪一部分,有一點是肯定的: #if#else 之間的部分或 #else#endif 之間的部分將被包含,而不是兩者都被包含;不會出現重複定義的風險。

請注意變數命名的特殊方式。這是所謂的匈牙利命名法。透過在識別符號名稱之前新增特殊解釋的字元,此方法旨在為其他程式設計師/維護人員提供儘可能多的上下文資訊。無需任何關於識別符號定義的引用(可能在相隔數頁的頁面上,甚至在不可訪問的另一個原始檔中),我們現在只需透過解釋字首即可獲取所需的資訊。例如,szGreeting 表示以零結尾的字串(即 C 風格的字串)。

#if defined(TURKISH)
  char szGreeting[] = "Gunaydin,";
  char szGreetStranger[] = "Her kimsen gunaydin!";
  char szGreetAll[] = "Herkese gunaydin!";
#else
  char szGreeting[] = "Good morning,";
  char szGreetStranger[] = "Good morning, whoever you might be!";
  char szGreetAll[] = "Good morning to you all!";
#endif

C 不允許程式設計師過載函式名。main 函式是一個例外:它有兩種形式。第一個我們已經看到,它不接受任何引數。第二個允許使用者將命令列引數傳遞給應用程式。這些引數以指向字元的指標向量形式傳遞。如果使用者希望將引數解釋為屬於其他資料型別,則應用程式程式碼必須進行一些額外的處理。

在 C 中,沒有標準方法可以告訴陣列的大小。應該使用約定或將大小儲存在另一個變數中。C 中的字串(可以視為字元陣列)是前者的一個例子。在此,使用一個哨兵值(NULL 字元)來表示字串的結尾。在大多數情況下,這種方案是不可能或不可行的。在這種情況下,我們需要將大小(或長度)資訊儲存在單獨的變數中。因此需要第二個變數。

程式名是引數向量的第一個元件。因此,如果引數計數為 1,則表示使用者根本沒有傳遞任何引數。

int main(int argc, char *argv[]) {
  switch (argc) {

執行 break 語句將終止最內層的封閉迴圈(whilefordo while)或 switch 語句。也就是說,它將基本上跳轉到迴圈或 switch 語句之後的下一行。在我們的例子中,控制將被轉移到 return 語句。

從基於 Pascal 的背景遷移到 C 的新手必須注意 switch 語句的本質:與 case 語句不同(每個分支都是相互執行的),switch 允許執行多個分支。如果您不希望這樣,則必須使用 break 語句分隔這些分支,如下所示。如果沒有 break 語句,引數計數為 1 將導致列印所有三個訊息。同樣,引數計數為 2 將列印匿名訊息以及相應的訊息。

    case 1: printf("%s\n", szGreetStranger); break;
    case 2: printf("%s %s\n", szGreeting, argv[1]); break;
    default: printf("%s\n", szGreetAll);
  } /* end of switch (argc) */

  return(0);
} /* end of int main(int, char**) */

使用編譯器開關

[編輯 | 編輯原始碼]

將此 C 程式儲存為 Greeting.c,並在命令列中輸入

gcc Greeting.c –DTURKISH –o Gunaydin↵ # 在 Linux 中

這將生成一個名為 Gunaydin 的可執行檔案。此可執行檔案將不包含 #else#endif 之間的任何語句的程式碼物件。同樣,如果我們使用以下命令編譯程式

gcc Greeting.c –DENGLISH –o GoodMorning↵

我們將獲得一個名為 GoodMorning 的可執行檔案,其中包含 #if 定義和 #else 排除之間的語句。請注意,在命令列中沒有定義任何宏時,將包含英語版本的問候語。

不要將編譯時開關與命令列引數混淆。前者傳遞給預處理器,用於更改要編譯的程式碼,而後者傳遞給正在執行的程式,用於更改其行為。假設我們按照上面所示構建了可執行檔案

./Gunaydin Tevfik↵

將產生以下輸出

Gunaydin, Tevfik

然而

./GoodMorning Tevfik Ugur↵

將產生

早上好,各位!

指標運算

[編輯 | 編輯原始碼]

問題:編寫一個程式來演示指標和地址之間的關係。


Pointer_Arithmetic.c
#include <stdio.h>

int string_length(char *str) {
  int len = 0;

在下面的 for 迴圈中,第三部分對變數 str 進行增量操作,該變數是指向 char 的指標。如果我們沒有意識到地址和指標是兩個不同的東西,我們可能會傾向於認為我們所做的只是簡單地對地址值進行增量操作。但這將是完全錯誤的。儘管出於教學目的,我們可能假設指標是一個地址,但它們並不完全相同。當我們對指標進行增量操作時,其中包含的地址值將根據指標指向的值型別的尺寸進行增量操作。但是,就指向 char 的指標而言,對指標進行增量操作和對地址進行增量操作具有相同的效果。這是因為 char 值儲存在一個位元組中。

定義:指標是一個變數,它包含另一個變數的地址,該變數的內容被解釋為屬於某種型別。[2]

Effect of incrementing a char*

請注意,雖然對物件的控制代碼可以被視為一種“類似”指標的東西,但它們是兩個不同的概念。除了支援繼承等其他差異之外,與指標和地址不同,控制代碼不參與算術運算。

  for(; *str != '\0'; str++) len++;

  return len;
} /* end of int string_length(char *) */

long sum_ints(int *seq) {
  long sum = 0;

下一行是一個示例,展示了指標和地址之間的區別。在這裡,對 seq 進行增量操作將使其中包含的地址值增加 int 的大小。

Effect of incrementing a int*
  for (; *seq > 0; seq++) sum += *seq;

  return sum;
} /* end of long sum_ints(int *) */

int main(void) {

下一行建立並初始化了一個 char 陣列。編譯器計算此陣列的大小。編譯器所做的是基本上計算雙引號之間的字元數,並相應地保留記憶體。請注意,編譯器也會自動附加一個 NULL 字元。但是,如果我們選擇使用聚合初始化,我們需要更加小心。

char string[] = {G, o, k, c , e};

將建立一個包含 5 個字元的陣列,而不是 6 個。在字元陣列的末尾將不會有 NULL 字元。如果這不是我們真正想要的,我們應該在初始化時新增 NULL 字元,如下所示

char string[] = {G, o, k, c, e, ‘\0};

或恢復到以前的方式。在這兩種情況下,陣列都在執行時堆疊中分配。

還要注意使用轉義序列在字元字串中嵌入雙引號。由於它用於標記結尾,因此無法在字串文字中插入“”。解決此問題的辦法是使用“”,它告訴編譯器後面的“”不表示字元字串的結尾,而是應該按字面意思嵌入字串中。

  char string[] = "Kind of \"long\" string";
  int i;
  int sequence[] = {9, 1, 3, 102, 7, 5, 0};

下一個 printf 語句的第三個引數是對返回 int 的函式的呼叫。這個呼叫更有趣的地方在於其引數的傳遞方式。雖然我們似乎正在傳遞一個數組,但實際上在幕後傳遞的是陣列第一個元素的地址;也就是說,&string[0]。無論陣列大小如何,都會這樣做。這種方案的優點是

  1. 我們只需要傳遞一個指標,而不是整個陣列。隨著陣列大小的增加,我們在記憶體方面節省得更多。
  2. 現在我們傳遞了一個指標,避免了複製整個陣列。這意味著既節省了時間又節省了記憶體。
  3. 我們對陣列所做的更改是永久性的;呼叫者會看到被呼叫者所做的更改。這是因為傳遞的是值傳遞的指標,而不是陣列。因此,雖然我們不能更改陣列第一個元素的地址,但我們可以修改陣列的內容。

缺點是,如果希望陣列在呼叫之間保持不變,則需要製作陣列的本地副本。

  printf("Length of \"%s\": %d\n", string, string_length(string));
  printf("Sum of ints ( ");
  for (i = 0; sequence[i] != 0; i++)
    printf("%d ", sequence[i]);
  printf("): %ld\n", sum_ints(sequence));

  return(0);
} /* end of int main(void) */

位操作

[編輯 | 編輯原始碼]

C 最初是系統程式語言,它為逐位操作資料提供幫助。這包括按位運算以及定義帶有位域的結構的功能。

毫不奇怪,語言的這方面被用於機器相關的應用程式,例如即時系統、系統程式[例如裝置驅動程式],在這些應用程式中,執行速度是最重要的。鑑於當今編譯器所做的複雜最佳化以及位操作的不可移植性,在存在替代方案的情況下,應該避免在通用程式設計中使用它們。

按位運算

[編輯 | 編輯原始碼]

問題:編寫函式來提取 float 引數的指數和尾數。

Extract_Fields.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

#define TOO_FEW_ARGUMENTS 1
#define TOO_MANY_ARGUMENTS 2

C 的規範沒有標準化(除了型別 char 之外)整數型別的尺寸。語言規範給出的唯一保證是以下關係

sizeof(short)sizeof(int)sizeof(long)sizeof(long long)

在大多數機器上,int 佔用四個位元組,而在某些機器上,它佔用兩個位元組。C 系統程式設計方向的體現是,這種差異是由於 int 的大小被認為等於底層體系結構的字長。因此,如果我們認為四個位元組用於表示 int 值,我們可能會偶爾看到我們的程式產生無意義的輸出。這是因為用四個位元組表示的 int 值在用兩個位元組表示時可能會導致溢位。

下面的 #if-#else 指令用於規避此問題。 UINT_MAX(在 limits.h 中定義)儲存最大的無符號 int 值。在 int 值儲存在兩個位元組的機器上,它將等於 65535。否則,它將是其他值。因此,如果 UINT_MAX 恰好是 65535,我們可以說 int 用兩個位元組表示。如果不是,則用四個位元組表示。

#if UINT_MAX==65535
typedef unsigned long BIG_INT;
#else
typedef unsigned int BIG_INT;
#endif

char *usage = USAGE: Extract_Fields float_number;

下一個函式提取特定 float 變數的指數部分。這是透過隔離指數部分並將結果值向右移位來實現的。我們使用按位與運算子(二進位制 &)來隔離數字並向右移位(>>)這個隔離的指數(以某種方式調整數字的右對齊)。

請注意,對負整數進行右移的結果(即最高有效位為 1 的位模式的數字)是未定義的。換句話說,行為取決於實現。在某些實現中,符號位會保留,因此右移實際上是對數字進行符號擴充套件,而在其他實現中,此位將被 0 替換。[3]

BIG_INT exponent(float num) {
  float *fnump = &num;
  BIG_INT *lnump = (BIG_INT *) fnump;

此處的記憶體部分影像在圖 3 中給出。如果,例如,num 的值為 -1.5,它將透過 fnump 被解釋為 -1.5。也就是說,*fnump 將為 -1.5。但是,如果透過 lnump 檢視,它將被解釋為包含 3,217,031,168!這種差異是由於看待同一事物的不同方式造成的:*fnumpnum 看作符合 IEEE 754/854 標準的 float 值,而 *lnump 將相同四個位元組的記憶體看作一個整數(型別為 unsigned longunsigned int,具體取決於執行程式的機器)使用二進位制補碼錶示法編碼。

(在 此處 插入 圖表)

問題
*lnump 加 1 後,*fnump 的值是多少?

以下行首先使用位掩碼隔離指數部分,然後將其右移,以便指數位佔據最低有效位。

(在 此處 插入 圖表)

  return((*lnump & 0x7F800000) >> 23);
} /* end of BIG_INT exponent(float) */

下一個函式提取數字的尾數部分。它透過簡單地遮蔽數字的符號和指數部分來實現這一點。這是透過返回表示式中的按位與運算子完成的。請注意,我們不需要移位數字,因為它的尾數由表示形式的最低 23 位組成。

BIG_INT mantissa(float num) {
  float *fnump = &num;
  BIG_INT *lnump = (BIG_INT*) fnump;

  return(*lnump & 0x007FFFFF);
} /* end of BIG_INT long mantissa(float) */

以下函式試圖理解命令列引數。此類引數的數量為 1 表示使用者沒有傳遞任何數字。我們顯示一條適當的訊息並退出程式。如果引數計數為 2,則第二個引數將被視為我們將提取其元件的數字。否則,使用者傳遞了超出我們處理能力的引數;我們只是通知她並退出程式。

在失敗的情況下,建議使用非零值退出。這在程式透過 shell 指令碼協同執行時可能非常有用。如果一個程式依賴於另一個程式的成功完成,指令碼需要有一種可靠的方法來檢查先前程式的結果。使用非描述性值的程式在這種情況下的幫助不大。我們必須確保當我們返回零時,它確實意味著執行成功。否則,就存在問題,這可以透過不同的退出程式碼進一步說明。

strtod 函式用於將一個可能包含 -/+ 和 e/E 的數字字串轉換為型別為 double 的浮點數。雖然很容易弄清楚第一個引數的功能,即指向要轉換的字串的指標,但第二個引數則不然。從 strtod 返回後,第二個引數將儲存指向輸入字串中已轉換部分之後字元的指標。由於這一點,我們可以使用 strtod 及其朋友處理字串的其餘部分。

strtod 的朋友
宣告 描述 錯誤情況
long strtol(const char* str, char **ptr, int base): 將字元字串轉換為 long int 返回 0L。
unsigned long strtoul(const char* str, char **ptr, int base): 將字串轉換為 unsigned long int 返回 0L。
double atof(const char* str): str 轉換為浮點數,並將轉換結果作為 double 返回。 返回 0.0。
int atoi(const char* str): str 轉換為整數,並將轉換結果作為 int 返回。 返回 0。
unsigned long atol(const char* str): str 轉換為整數,並將轉換結果作為 unsigned long 返回。 返回 0L。
char* strtok(char* str, const char* set): 使用 set 中的字元作為分隔符來標記 str。將 NULL 傳遞給第一個引數告訴此函式從上次停止的地方繼續。
Parse.c

#include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char *name, *midterm, *final, *assignment; // 假設 argv[1] 是 “Selin Yardimci: 80, 90, 100”。 name = strtok(argv[1], ":"); // // printf("Name: %s\n", name); midterm = strtok(NULL, ","); // // printf("Midterm: %lu\t", strtoul(midterm, NULL, 10)); final = strtok(NULL, ","); // // printf("Final: %lu\t", strtoul(final, NULL, 10)); assignment = strtok(NULL, "\0"); // // printf("Assignment: %lu\n", strtoul(assignment, NULL, 10)); return(0); } /* int main(int, char**) 的結尾 */

gcc -o Parse.exe Parse.c↵
#需要雙引號將字串視為單個引數。它不是引數字串的一部分,將在傳遞給 main 之前被剝離!
Parse "Selin Yardimci: 80, 90, 100"↵
Name: Selin Yardimci
Midterm: 80 Final: 90 Assignment: 100
float number(int argc, char *argv[]) {
  switch (argc) {
    case 1: 
      printf(Too few arguments. %s\n, usage);
      exit(TOO_FEW_ARGUMENTS);
    case 2:
      return((float) strtod(argv[1], NULL));
    default:
      printf(Too many arguments. %s\n, usage);
      exit(TOO_MANY_ARGUMENTS):
  } /* end of switch (argc) */
} /* end of float number(int, char **) */

觀察main函式的簡潔性。我們只是說明程式的功能,並沒有深入研究它是如何實現的。閱讀main函式,程式碼維護者可以輕鬆地弄清楚它聲稱要做什麼。如果她需要更多細節,則需要檢查函式體。這些函式將根據程式的複雜性提供實現的完整細節或將此提供推遲到另一個函式。在複雜的程式中,這種延遲可以擴充套件到多個級別。

無論手頭的問題有多簡單或多複雜,無論我們使用什麼正規化,我們都首先回答“是什麼”的問題,然後(可能按程度)繼續回答“如何”的問題。換句話說,我們首先分析問題,併為解決方案設計一個方案,然後提供實現。[4] 我們的程式碼應該反映這一點:它應該首先公開對“是什麼”(介面)的答案,然後(對感興趣的方)公開對“如何”(實現)的答案。

int main(int argc, char *argv[]) {
  float num;

  printf(Exponent of the number is: %x\n, 
            exponent(num = number(argc, argv)));
  printf(Mantissa of the number is: %x\n, mantissa(num));

  return(0);
} /* end of int main(int, char**) */

位域

[edit | edit source]

問題:使用位域實現前一個問題。

Extract_Fields_BF.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

...

位域的定義類似於普通的記錄域。兩者之間的唯一區別是在位域之後跟著的寬度說明。根據以下定義,分數、指數和符號分別佔二十三位、八位和一位。但是,其餘部分很大程度上取決於編譯器的實現。

首先,正如預處理指令所體現的那樣,struct SINGLE_FP型別變數的記憶體佈局取決於處理器的位元組序。位打包的方式也不保證在不同的硬體之間相同。這兩個因素有效地將位域的使用限制在與機器相關的程式中。

struct SINGLE_FP {
#ifdef BIG_ENDIAN /* e.g. Motorola */
  unsigned int sign : 1;
  unsigned int exponent : 8;
  unsigned int fraction : 23;
#else /* if LITTLE_ENDIAN, e.g. Intel */
  unsigned int fraction : 23;
  unsigned int exponent : 8;
  unsigned int sign : 1;
#endif
};

BIG_INT exponent(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;

C 中有兩個域訪問運算子:.->。前者用於訪問結構的域,而後者用於透過指向結構的指標訪問結構的域。現在lnump 被定義為指向SINGLE_FP 結構的指標,所有宣告為 SINGLE_FP 型別的變數都可以透過使用 -> 訪問位域。

觀察structure->field 等價於 (*structure).field。例如,lnump->exponent 等價於 (*lnump).exponent

  return(lnump->exponent);
} /* end of BIG_INT exponent(float) */

BIG_INT mantissa(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;  

  return(lnump->fraction);
} /* end of BIG_INT mantissa(float) */

float number(int argc, char *argv[]) { ... }

int main(int argc, char *argv[]) { ... }

靜態區域性變數(記憶化)

[edit | edit source]

問題:使用記憶化編寫低成本的遞迴階乘函式。

類似於快取,記憶化可用於透過儲存已完成計算的結果來加快程式速度。兩者之間的區別在於它們的範圍。當我們談論快取時,我們指的是系統級或應用程式級的最佳化技術。另一方面,記憶化是一種函式特定的技術。當接收到請求時,我們首先檢查是否可以避免從頭開始計算結果。否則,從頭開始進行計算,並將結果新增到我們的資料庫中。換句話說,我們更喜歡空間計算而不是時間計算,並節省了一些寶貴的計算機時間。

將此技術應用於手頭的問題,我們將把已經計算的階乘集合儲存在static 區域性陣列中。現在,對這個陣列所做的任何更改都將在不同的呼叫之間保持永續性,遞迴的基準條件將更改為達到已計算的階乘,而不是引數值為 1 或 0。也就是說,我們有

Mathematical definition of the factorial function
階乘函式的數學定義


Memoize.c
#include <stdio.h>

#define MAX_COMPUTABLE_N 20
#define TRUE 1

unsigned long long factorial(unsigned char n) {

一旦程式開始執行,以下初始化將生效。事實上,由於static 區域性變數是在靜態資料區域分配的,因此它們在可執行檔案的磁碟映象中包含初始值。

標記用於儲存已進行計算的陣列的初始化程式。雖然它具有MAX_COMPUTABLE_N 個分量,但在初始化程式中只提供了兩個值。其餘插槽將填充預設的初始值 0。換句話說,它等價於

static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1, 0, 0, , .., 0};

請注意,我們可以提供更多初始值以避免更多初始成本。

static unsigned char largest_computed = 1;
static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1};

如果我們已經計算了等於或大於當前引數值的數字的階乘,我們會檢索此值並將其返回給呼叫者。

請注意,返回值的接收者可以是main 函式或階乘函式的另一次呼叫。在第二種情況下,我們進行部分計算並使用先前計算中的一些部分結果。

  if (n <= largest_computed) return computed_factorials[n];

  printf("N: %d\t", n);

一旦計算出新的值,它就會儲存在我們的陣列中,並且已經計算了階乘的最大引數值將相應地更新以反映這一事實。

  computed_factorials[n] = n * factorial(n - 1);
  if (largest_computed < n) largest_computed = n;

  return computed_factorials[n];
} /* end of unsigned long long factorial(unsigned char) */

int main(void) {
  short n;

  printf("Enter a negative value for termination of the program...\n");
  do {
    printf("Enter an integer in [0..20]: ");

h 在轉換字母 (d) 前表示輸入應為 short int。類似地,可以使用 l 指定 long int

    scanf("%hd", &n);
    if (n < 0) break;
    if (n > 20) {
      printf("Value out of range!!! No computations done.\n");

break 一樣,continue 用於改變迴圈內的控制流;它終止最內層封閉的 whiledo whilefor 語句的執行。在我們的例子中,控制將轉移到 do-while 語句的開頭。

      continue;
    } /* end of if (n > 20) */
    printf("%d! is %Lu\n", n, factorial((unsigned char) n));
  } while (TRUE);

  return(0);
} /* end of int main(void) */
gcc –o MemoizedFactorial.exe Memoize.c↵
MemoizedFactorial↵
輸入負值以終止程式
輸入 [0..20] 之間的整數:1
1! 為 1
輸入 [0..20] 之間的整數:5
N:5 N:4 N:3 N:2 5! 為 120
輸入 [0..20] 之間的整數:5
5! 為 120
輸入 [0..20] 之間的整數:10
N:10 N:9 N:8 N:7 N:6 10! 為 3628800
輸入 [0..20] 之間的整數:-1

檔案操作

[edit | edit source]

問題:編寫一個程式,該程式可以剝離 C 程式的註釋。


Strip_Comments.c
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BOOL char
#define FALSE 0
#define TRUE 1

#define MAX_LINE_LENGTH 500

#define NORMAL_COMPLETION 0
#define CANNOT_OPEN_INPUT_FILE 11
#define CANNOT_OPEN_OUTPUT_FILE 12
#define TOO_FEW_ARGUMENTS 21
#define TOO_MANY_ARGUMENTS 22

當識別符號定義被 const 限定時,它被認為是不可變的。這樣的識別符號不能作為左值出現。這意味著我們不能將識別符號宣告為常量,然後對其賦值;常量必須在其宣告點提供一個值。換句話說,常量必須被初始化。

全域性常量的初始化程式只能包含可以在編譯時計算的子表示式。另一方面,區域性常量可以包含執行時值。例如,

#include <stdio.h> #include <stdlib.h> void f(int ipar) { const int i = ipar * 3; printf(i: %d\n, i); } /* end of void f(int) */ int main(void) { int i = 6; f(5); f(10); f(i); f(i + 7); exit(0); } /* end of int main(void) */

將產生以下輸出

i: 15
i: 30
i: 18
i: 39

這表明對於每次函式呼叫都會建立常量,它們可以具有不同的值。但它們在整個函式呼叫過程中仍然無法被修改。

const char file_ext[] = .noc;
const char temp_file[] = TempFile;
const char usage[] = "USAGE: StripComments filename";

在 C 中,所有識別符號都必須在使用之前宣告!這包括檔名、變數和結構標籤。相應的定義可以在同一檔案中或不同的檔案中在宣告之後提供。雖然可以有多個宣告,但只能有一個定義。

以下宣告是函式的原型,這些函式的定義在程式的後面提供。請注意,引數名稱與定義中提供的引數名稱不一致。事實上,甚至不必提供名稱。但是,您仍然必須列出引數型別以方便型別檢查:如果函式的定義或任何對它的使用與它的原型不一致,則會發生錯誤。

可以透過重新排列定義的順序來避免使用原型。對於這個例子,將 main 函式放在最後可以消除對原型的需要。

char* filename(int argumentCount, char* argumentVector[]);
void trimWS(const char *filename);
void strip(const char *filename);

用逗號分隔的表示式被認為是一個單一的表示式,它的結果是最後一個表示式的返回值。對用逗號分隔的表示式的計算是嚴格從左到右的;編譯器不能改變這個順序。

int main(int argc, char* argv[]) {
  char *fname = filename(argc, argv);
  (strip(fname), trimWS(fname));

  return(NORMAL_COMPLETION);
} /* end of int main(int, char**) */

char* filename(int argc, char* argv[]) {

printf 的一個更通用的版本 fprintf 執行輸出格式化並將輸出傳送到作為第一個引數指定的流。因此我們可以將 printf 視為以下內容的等效形式

fprintf(stdin, "...", ...); /* read it as "file printf..." */

您可能希望在輸出格式化中考慮的另一個 printf 的朋友是 sprintf 函式。這個函式不是將輸出寫入某個媒體,而是將其儲存在一個字元字串中。

typedef double currency; currency expenditure = 234.0; ... char *str = (char*) malloc(); ... sprintf(str, %f.2$, expenditure); printf(Total spending: %s, str); /* will print “Total spending: 234.00$” */ ... free(str); ...

  switch (argc) {
    case 1: 
      fprintf(stderr, "No file name passed!\n %s\n", usage);
      exit(TOO_FEW_ARGUMENTS);
    case 2: return(argv[1]);
    default: 
      fprintf(stderr, "Too many arguments!\n %s\n",usage);
      exit(TOO_MANY_ARGUMENTS);
  } /* end of switch(argc) */
} /* end of char* filename(int, char**) */

void trimRight(char *line) {
  int i = strlen(line) - 1;

  do 
    i--; 
  while (i >= 0 && isspace(line[i])) ;

  line[i + 1] = '\n';
  line[i + 2] = '\0';
} /* end of void trimRight(char*) */

void trimWS(const char *infilename) {
  char next_line[MAX_LINE_LENGTH];
  char outfilename[strlen(infilename) + strlen(file_ext) + 1];
  FILE *infile, *outfile;
  BOOL empty_line = FALSE;
MS Visual C/C++ 編譯器將為突出顯示的行發出編譯錯誤。但是,根據 ISO C,自動(即區域性)陣列可以具有未知大小,此大小由執行時的初始化程式確定。為了讓此程式使用 MS Visual C/C++ 編譯,您需要將此陣列定義更改為指標定義,並使用 malloc/free 函式在堆中分配/釋放陣列空間。


以下語句嘗試以讀取模式開啟檔案。它將儲存在變數 temp_file 中的物理檔案的名稱對映到名為 infile 的邏輯檔案。如果此嘗試成功,您對邏輯檔案執行的每個操作都將對物理檔案執行。可以將 infile 變數視為對真實檔案的控制代碼。控制代碼與物理檔案之間的對映不是一對一的。就像多個控制代碼可以顯示同一個物件一樣,一個檔案可以有多個控制代碼指向同一個物理檔案。只要所有控制代碼都以讀取模式使用,就沒有問題。但是,如果不同的控制代碼同時嘗試修改同一個檔案,情況就會變得糟糕。[關鍵詞是作業系統、併發和同步。]

如果開啟操作失敗,我們就無法獲得物理檔案的控制代碼。這反映在 fopen 函式的返回值中:NULL。一個具有 NULL 值的指標意味著我們不能將其用於進一步操作。我們能做的就是將其與 NULL 進行比較。因此,我們首先檢查此條件。除非它為 NULL,否則我們繼續;否則,我們將有關異常條件性質的內容寫入標準錯誤檔案 stderr 並退出程式。

與標準輸出一樣,標準錯誤檔案預設情況下也對映到螢幕。那麼,為什麼我們要寫入 stderr 而不是 stdio 呢?答案是,我們可以選擇將這些標準檔案重新對映到不同的物理單元。在這種情況下,如果我們繼續將所有內容寫入同一個邏輯檔案,例如 stdio,錯誤將混淆有效的輸出資料;我們將難以分辨哪一個是哪一個。

  infile = fopen(temp_file, "r");
  if (infile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (infile == NULL) */

  strcpy(outfilename, infilename); strcat(outfilename, file_ext);
  outfile = fopen(outfilename, "w");
  if (outfile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", outfilename, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (outfile == NULL) */

  while (fgets(next_line, MAX_LINE_LENGTH + 1, infile)) {
    trimRight(next_line);
    if (strlen(next_line) == 1) {
      if (!empty_line) fputs(next_line, outfile);
      empty_line = TRUE;
      } else {
        fputs(next_line, outfile);
        empty_line = FALSE;
      } /* end of else */
  } /* end of while (fgets(next_line, …) */

  fclose(infile); fclose(outfile); remove(temp_file);

  return;
} /* end of void trimWS(const char*) */

void strip(const char *filename) {
  int next_ch;
  BOOL inside_comment = FALSE;
  FILE *infile, *outfile;

  infile = fopen(filename, "r");
  if (infile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", filename, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (infile == NULL) */

  outfile = fopen(temp_file, "w");
  if (outfile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (outfile == NULL) */

可以使用以下有限自動機對問題的解決方案進行建模。

Finite automaton providing the solution
提供解決方案的有限自動機

請注意,將基於 FA 的解決方案轉換為 C 程式碼的容易程度。這又是另一個例子,說明這種理論模型儘管看起來可能毫無用處和無聊,但它在實踐中卻非常有用。

透過將問題的領域表示轉換為相應的解決方案領域表示來解決問題。一個人對錶示問題的模型瞭解得越多,她就越容易想出問題的解決方案。

  while ((next_ch = fgetc(infile)) != EOF) {
    switch (inside_comment) {
      case FALSE:
        if (next_ch != '/') { fputc(next_ch, outfile); break; }
        if ((next_ch = fgetc(infile)) == '*') inside_comment = TRUE;
          else { fputc('/', outfile); ungetc(next_ch, infile); }
      break;
      case TRUE:
        if (next_ch != '*') break; 
        if ((next_ch = fgetc(infile)) == '/') inside_comment = FALSE;
    } /* end of switch(inside_comment) */
  } /* end of while ((next_ch = fgetc(infile)) != EOF) */

fclose 將邏輯檔案(控制代碼)與物理檔案斷開連線。如果程式設計師沒有這樣做,C 執行時退出序列中的程式碼保證在程式結束時所有開啟的檔案都會關閉。但是,將其留給退出序列有兩個缺點。

  1. 應用程式可以同時開啟的最大檔案數有限。如果我們將關閉檔案推遲到退出序列,我們可能更頻繁地更快地達到此限制。
  2. 除非你顯式地使用 fflush 重新整理,否則你寫入檔案的資料實際上被寫入記憶體快取,而不是寫入磁碟。 每當你向檔案寫入換行符或快取區滿時,它會自動重新整理。 也就是說,如果在你可以關閉檔案的最早時間和程式結束時退出序列關閉檔案之間發生災難性故障(例如中斷),則快取區中剩餘的資料將不會提交到磁碟。 這不是一個完美的成功故事!

所以你應該要麼顯式地重新整理,要麼儘早關閉檔案。

  fclose(infile); 
  fclose(outfile);

  return;
} /* end of void strip(const char *) */

堆記憶體分配

[edit | edit source]

問題:編寫一個加密程式,從檔案讀取資料並將編碼後的字元寫入另一個檔案。 使用以下簡單的加密方案: 字元 c 的加密形式是 c ^ key[i],其中 key 是作為命令列引數傳遞的字串。 該程式以迴圈方式使用 key 中的字元,直到所有輸入都被讀取。 使用相同的 key 對編碼後的文字進行重新加密會生成原始文字。 如果沒有傳遞 key 或傳遞空字串,則不進行加密。

Cyclic_Encryption.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CANNOT_OPEN_INPUT_FILE 11
#define CANNOT_OPEN_OUTPUT_FILE 12
#define TOO_FEW_ARGS 21
#define TOO_MANY_ARGS 22

const char *usage = "USAGE: CyclicEncryption inputfilename [key [outputfilename]]";
const char file_ext[] = ".enc";

struct FILES_AND_KEY {
  char *infname;
  char *outfname;
  char *key;
};

typedef struct FILES_AND_KEY f_and_k;

f_and_k getfilenameandkey(int argc, char **argv) { 
  f_and_k retValue = { "\0", "\0", "\0" };

  switch (argc) {
    case 1:
      fprintf(stderr, "No file name passed!\n %s", usage);
      exit(TOO_FEW_ARGS);

malloc 用於從堆中分配儲存空間,堆是程式設計師自己管理的記憶體區域。 這意味著程式設計師有責任將透過 malloc 分配的每位元組記憶體返回到可用記憶體池。

這種記憶體是透過指標間接操作的。 不同的指標可以指向同一個記憶體區域。 換句話說,它們可以共享同一個物件。

使用指標操作堆物件並不意味著指標只能指向堆物件。 也不意味著指標本身是在堆中分配的。 指標可以指向非堆物件,它們可以駐留在靜態區域或執行時堆疊中。 事實上,在之前的例子中就是這樣。

    case 2:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.outfname = (char *) malloc(
      strlen(argv[1]) + strlen(file_ext) + 1);
      strcpy(retValue.outfname, argv[1]);
      strcat(retValue.outfname, file_ext);
      return(retValue);
    case 3:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.key= (char *) malloc(strlen(argv[2]) + 1);
      strcpy(retValue.key, argv[2]);
      retValue.outfname = (char *) malloc(
      strlen(argv[1]) + strlen(file_ext) + 1);
      strcpy(retValue.outfname, argv[1]);
      strcat(retValue.outfname, file_ext);
      break;
    case 4:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.key = (char *) malloc(strlen(argv[2]) + 1);
      strcpy(retValue.key, argv[2]);
      retValue.outfname = (char *) malloc(strlen(argv[3]) + 1);
      strcpy(retValue.outfname, argv[3]);
      break;
    default:
      fprintf(stderr, "Too many arguments!\n %s", usage);
      exit(TOO_MANY_ARGS);
  } /* end of switch(argc) */

  return retValue;
} /* end of f_and_k getfilenameandkey(int, char**) */

void encrypt(f_and_k fileandkey) {
  int i = 0, keylength = strlen(fileandkey.key);
  int next_ch;
  FILE *infile;
  FILE *outfile;

  if (keylength == 0) return;

以下程式碼行開啟一個可以逐位元組(二進位制模式)讀取的檔案,而不是逐字元(文字模式)讀取。 在像 UNIX 這樣的作業系統中,每個字元都對映到程式碼表中的一個條目,兩者之間沒有區別。 但是,在 MS Windows 中,換行符對映為回車符後跟換行符,兩者之間存在區別,這可能會讓不小心的人員陷入困境。 以下 C 程式演示了這一點。 用多行檔案執行它,你會看到需要的 fgetc 的數量將不同。 你讀取的字元數將少於你讀取的位元組數。 這種差異是換行符處理方式不同的結果,當你將你的工作程式碼從 Linux 移動到 MS Windows 時,它可能會導致很大的麻煩。

Newline.c

#include <errno.h> #include <stdio.h> #include <string.h> #define CANNOT_OPEN_INPUT_FILE 1 int main(void) { int counter; int next_ch; FILE *in1, *in2; in1 = fopen("Text.txt", "r"); if (!in1) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in1) */ counter = 1; while((next_ch = fgetc(in1)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); printf("\n"); fclose(in1); in2 = fopen("Text.txt", "rb"); if (!in2) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in2) */ counter = 1; while((next_ch = fgetc(in2)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); fclose(in2); return(0); } /* end of int main(void) */

Text.txt

First line Second line Third Line Fourth and last line

如果你在基於 Unix 的環境中編譯並執行上一頁列出的程式,它將為兩種模式(文字和二進位制)生成相同的輸出。 對於 MS Windows 和 DOS 環境,它們將生成以下輸出。

gcc –o Newline Newline.c↵
Newline↵
1.ch: 70 ...
...
11.ch: 10 12.ch: 83 ...
...
... 54.ch: 101
1.ch: 70 ...
...
11.ch: 13 12.ch: 10 13.ch: 83 ...
...
...
... 57.ch: 101
  infile = fopen(fileandkey.infname, "rb");
  if (!infile) {
    fprintf(stderr, "Error in opening file %s: %s\n",
    fileandkey.infname, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (!infile) */

  outfile = fopen(fileandkey.outfname, "wb");
  if (!outfile) {
    fprintf(stderr, "Error in opening output file %s: %s\n", 
    fileandkey.outfname, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (!outfile) */

  while ((next_ch = fgetc(infile)) != EOF) {
    fprintf(outfile, "%c",(char) (next_ch ^ fileandkey.key[i++]));
    if (keylength == i) i = 0;
  } /* end of while ((next_ch = fgetc(infile)) != EOF) */

  fclose(outfile); fclose(infile);

  return;
} /* end of void encrypt(f_and_k) */

int main(int argc, char *argv[]) {
  f_and_k fandk = getfilenameandkey(argc, argv);

  encrypt(fandk);
  free(fandk.infname);
  fandk.infname = fandk.outfname; 
  fandk.outfname = (char *) 
  malloc(strlen(fandk.infname) + strlen(file_ext) + 1);
  strcpy(fandk.outfname, fandk.infname);
  strcat(fandk.outfname, file_ext);
  encrypt(fandk);

  return(0);
} /* end of int main(int, char **) */

指向函式的指標(回撥)

[edit | edit source]

問題:編寫一個通用的氣泡排序例程。 測試你的程式碼以對 int 陣列和字元字串進行排序。

General.h
#ifndef GENERAL_H
#define GENERAL_H

...

#define BOOL char
#define FALSE 0
#define TRUE 1
...
typedef void* Object;

以下 typedef 語句將 COMPARISON_FUNC 定義為指向函式的指標,該函式接受兩個 Object 型別的引數(即 void*),並返回一個 int。 在此定義之後,在這個標頭檔案中或包含此標頭檔案的任何檔案中,我們可以像使用任何其他資料型別一樣使用 COMPARISON_FUNC。 實際上,我們在 Bubble_Sort.h 和 Bubble_Sort.c 中就是這麼做的。 bubble_sort 函式的第三個引數被定義為 COMPARISON_FUNC 型別。 也就是說,我們可以傳遞任何符合以下原型函式的地址。

void* 用作通用指標。換句話說,指標指向的資料不假定屬於特定型別。從某種意義上說,它與 Java 類層次結構頂部的 Object 類具有相同的目的。就像任何物件都可以被視為屬於 Object 類一樣,任何值,無論是像 char 這樣簡單的東西還是像資料庫這樣複雜的東西,都可以被這個指標指向。但是,這樣的指標不能使用 * 或下標運算子進行解引用。在使用它之前,必須先將指標強制轉換為適當的型別。

typedef int (*COMPARISON_FUNC)(Object, Object);
...
#endif


Bubble_Sort.h
#ifndef BUBBLE_SORT_H
#define BUBBLE_SORT_H

#include "General.h"

bubble_sort 函式對一個 Object 陣列(第一個引數)進行排序,其大小作為第二個引數提供。由於現在沒有通用的方法來比較兩個元件,而我們希望我們的實現是通用的,所以我們必須能夠動態地確定比較兩個任意型別項的函式。透過使用指向函式的指標,可以實現對函式的動態確定。將不同的值分配給此指標可以啟用使用不同的函式,這意味著對於不同的型別,會有不同的行為。找到對所有可能的​​資料型別通用的引數型別是使指向函式的指標對所有型別都有效的關鍵。為此,comp_func 接受兩個型別為 Object 的引數,即 void*,它可以被解釋為屬於任何型別。

extern void bubble_sort (Object arr[], int sz, COMPARISON_FUNC comp_func);

#endif


Bubble_Sort.c
#include <stdio.h>

#include "General.h"
#include "algorithms\sorting\Bubble_Sort.h"

在以下定義中,static 限定符將 swap 的可見性限制在此檔案中。從某種意義上說,它與 OOPL 中類中的 private 限定符相同。不同之處在於檔案是作業系統概念(即由 OS 管理),而類是程式語言概念(即由編譯器管理)。後者無疑是更高層次的抽象。在沒有更高層次的抽象的情況下,可以使用低層抽象來模擬它(更高層次的抽象)。需要外部代理的干預來提供這種模擬,這會導致更脆弱的解決方案。在這種解決方案中就是這種情況:程式設計師,干預代理,必須透過使用一些程式設計約定來模擬這種更高層次的抽象。

static void swap(Object arr[], int lhs_index, int rhs_index) {
  Object temp = arr[lhs_index];
  arr[lhs_index] = arr[rhs_index];
  arr[rhs_index] = temp;

  return;
} /* end of void swap(Object[], int, int) */

void bubble_sort (Object arr[], int sz, COMPARISON_FUNC cmp) {
  int pass, j;

  for(pass = 1; pass < sz; pass++) {
    BOOL swapped = FALSE;

    for (j = 0; j < sz - pass; j++)

我們可以按如下方式編寫以下行

if (cmp(arr[j], arr[j + 1]) > 0) {

它仍然會做同樣的事情。因此,指向函式的變數可以使用得像函式一樣:只需使用它的名稱並將引數括在括號中即可。這將導致指向變數的函式被呼叫。透過指標呼叫函式的好處是可以簡單地透過更改指標變數的值來呼叫不同的函式。如果將 compare_ints 的地址傳遞給 bubble_sort 函式,它將呼叫 compare_ints;如果將 compare_strings 的地址傳遞給它,它將呼叫 compare_strings;如果 ...

      if ((*cmp)(arr[j], arr[j + 1]) > 0) {
        swap(arr, j, j + 1);
        swapped = TRUE;
      } /* end of if ((*cmp)(arr[j], arr[j + 1]) > 0) */

    if (!swapped) return; 
  } /* end of outer for loop */
} /* void bubble_sort(Object[], int, COMPARISON_FUNC) */


Pointer2Function.c
#include <stdio.h>
#include <string.h>

#include “General.h”

以下行引入了 bubble_sort 函式的原型,而不是它的原始碼或目的碼。

編譯器利用此原型對函式的使用進行型別檢查。這涉及檢查引數的數量、型別、它(函式)是否在正確的上下文中使用。一旦確認了這一點,連結器就會接管並(如果它在單獨的原始檔中)引入 bubble_sort 的目的碼。所以,

  1. 預處理器透過將指令替換為 C 原始碼來預處理原始碼。
  2. 編譯器使用提供的元資訊(例如變數宣告/定義和函式原型)檢查程式的語法和語義。請注意,當編譯器接管時,所有預處理器指令都將被替換為 C 原始碼。也就是說,編譯器對預處理器一無所知。
  3. 連結器將目標檔案組合成單個可執行檔案。此可執行檔案稍後由載入器載入到記憶體中,並在作業系統的監督下執行。
#include "algorithms\sorting\Bubble_Sort.h"

typedef int* Integer;

void print_args(char *argv[], int sz) {
  int i = 0;

  if (sz == 0) { 
    printf("No command line args passed!!! Unable to test strings...\n");
    return;
  }
  for (; i < sz; i++)
    printf("Arg#%d: %s\t", i + 1, argv[i]);
  printf("\n");
} /* end of void print_args(char **, int) */

void print_ints(Integer seq[], int sz) {
  int i = 0;

  for (; i < sz; i++) 
  printf("Item#%d: %d\t", i + 1, *(seq[i]));
  printf("\n");
} /* end of void print_ints(Integer[], int) */

接下來的兩個函式是為排序演算法的實現進行比較所需的。它們比較兩個相同型別的​​值。

bubble_sort 函式的實現者無法預先知道要排序的無數個物件型別,並且沒有適用於所有型別的通用比較方法。因此,排序演算法的使用者必須實現比較函式並讓實現者瞭解此函式。實現者利用此函式成功地提供服務。在這樣做時,它會“回撥”到使用者程式碼。因此,這種型別的呼叫稱為*回撥*。

int compare_strings(Object lhs, Object rhs) {
  return(strcmp((char *) lhs, (char *) rhs));
} /* end of int compare_strings(Object, Object) */

看起來這是一種非常複雜的方式來比較兩個 int?沒錯!但是請記住:我們必須能夠比較任何型別的兩個物件(值),並且我們必須使用一個函式簽名來完成它。比較兩個 int 很簡單:只需比較值即可。但是,字元字串呢?比較指標不會產生準確的結果;我們必須比較指標指向的字元字串。當我們比較包含巢狀結構的兩個結構時,它變得更加複雜。解決此問題的辦法是將球傳給最瞭解它的人(演算法的使用者),同時傳遞指向資料的通用指標 (void *),而不是資料本身。在此過程中,使用者將把它強制轉換為適當的型別,並相應地進行比較。

int compare_ints(Object lhs, Object rhs) {
  Integer ip1 = (Integer) lhs;
  Integer ip2 = (Integer) rhs;

  if (*ip1 > *ip2) return 1;
  else if (*ip1 < *ip2) return -1;
    else return 0;
} /* end of int compare_ints(Object, Object) */

int main(int argc, char *argv[]) {
  int seq[] = {1, 3, 102, 6, 34, 12, 35}, i;
  Integer int_seq[7];

  for(i = 0; i< 7; i++) int_seq[i] = &seq[i];

  printf("\nTESTING FOR INTEGERS\n\nBefore Sorting\n");
  print_ints(int_seq, 7);

傳遞給 bubble_sort 函式的第三個引數是指向函式的指標。此指標包含一個可以解釋為函式入口點的值。從概念上講,這種指標與指向某些資料型別的指標之間沒有太大區別。唯一的區別在於它們指向的記憶體區域。後者指向資料段中的某個地址,而前者指向程式碼段中的某個地址。但是有一點保持不變:地址值始終以[特定方式]解釋。

請注意,當您將函式作為引數傳遞時,您不必應用地址運算子。

  bubble_sort((Object*) int_seq, 7, &compare_ints);
  printf("\nAfter Sorting\n");	print_ints(int_seq, 7);

  printf("\nTESTING FOR STRINGS\n\nBefore Sorting\n");
  print_args(&argv[1], argc - 1);
  bubble_sort((Object*) &argv[1], argc - 1, compare_strings);
  printf("After Sorting\n");	print_args(&argv[1], argc - 1);

  return(0);
} /* end of int main(int, char **) */

連結到目標檔案

[edit | edit source]
cl /c /ID:\include Bubble_Sort.c↵

/ID:\include 新增到要搜尋標頭檔案的目錄列表的開頭,這些目錄最初包括原始檔所在的目錄以及在 %INCLUDE% 中找到的目錄。類似於 Java 的 CLASSPATH,它用於組織標頭檔案。使用此資訊,預處理器將 D:\include\algorithms\sorting\Bubble_Sort.h 引入到當前檔案(Bubble_Sort.c)中。一旦預處理器完成了它的工作,編譯器將嘗試編譯結果檔案並輸出一個名為 Bubble_Sort.obj 的目標檔案。

cl /FeTest_Sort.exe /ID:\include Bubble_Sort.obj Pointer2Function.c↵

上面的命令編譯 Pointer2Function.c,如前一段所述。一旦建立了 Pointer2Function.obj,它將與 Bubble_Sort.obj 連結起來形成名為 Test_Sort.exe 的可執行檔案。此連結是引入 bubble_sort 函式的目的碼所需的。請記住:包含 Bubble_Sort.h 引入了原型,而不是目的碼!

模組化程式設計

[edit | edit source]

問題:用 C 語言編寫一個(偽)隨機數生成器。保證單個應用程式只使用一個生成器,並且不同的應用程式會反覆使用它。

現在我們的生成器將被不同的應用程式使用,我們最好把它放在一個單獨的檔案中,這樣透過連結客戶端程式,我們就可以從不同的來源使用它。這類似於(如果不是完全一樣的話)Modula-2 等語言中支援的模組的概念。區別在於它們的抽象級別:模組是程式語言提供的實體,所有使用者都知道它,而檔案是作業系統提供的實體,所有使用者都知道它。程式語言編譯器(即程式設計語言規範的實現)作為作業系統提供服務的使用者和概念,這意味著模組概念是一個更高的抽象。

現在,由於 C 語言中沒有模組概念(更高的抽象),我們需要使用其他可能更低級別的抽象來模擬它。在這種情況下,我們使用檔案(更低的抽象)。這樣做,我們無法完全恢復模組帶來的所有好處。編譯器不知道模組的概念;模組化程式語言中編譯器強制執行的某些規則必須由程式設計師自己檢查。例如,程式設計師必須同步模組的介面和實現,這是一個容易出錯的過程。

由於預計所有應用程式都不會使用多個生成器,因此我們可以在靜態資料區域建立與生成器相關的欄位;為了識別函式作用於哪個生成器,我們不需要傳遞一個單獨的、唯一的控制代碼。這意味著我們不需要任何建立或銷燬函式。我們只需要一個初始化生成器的函式和另一個返回下一個(偽)隨機數的函式。

RNGMod.h
#ifndef RNGMOD_H
#define RNGMOD_H

extern void RNGMod_InitializeSeed(long int);
extern double RNGMod_Next(void);

#endif


RNGMod.c
#include "math\RNGMod.h"

static long int seed = 314159;
const int a = 16807;
const long int m = 2147483647;

生成一個(偽)隨機數類似於遍歷一個值列表:我們基本上從某個點開始,一個接一個地遍歷這些值。區別在於生成的這些值是透過一個函式計算出來的,而不是從某個記憶體位置檢索出來的。換句話說,生成器使用時間計算遍歷一個列表,而迭代器使用空間計算遍歷一個列表。

從這個角度來看,用種子初始化一個(偽)隨機數生成器類似於在列表上建立一個迭代器。透過使用不同的種子值,我們可以保證生成器返回不同的值。使用相同的種子值將給出相同的(偽)隨機值序列。這種用法可能希望用於針對不同引數重放相同的模擬場景。

void RNGMod_InitializeSeed(long int s) {
  seed = s;

  return;
} /* end of void RNGMod_InitializeSeed(long int) */

以下函式計算序列中的下一個數字。使用一個眾所周知的公式計算的數字意味著該序列實際上不是隨機的。也就是說,它可以提前知道。這就是為什麼這樣的數字通常用“偽”這個詞來限定。

使序列看起來隨機的是它連續生成的不同的值的個數。以下函式中使用的公式將生成 1 到 m - 1 之間的所有值,然後重複序列。超過二十億個值!

實際上,使用這些大數字沒有意義。因此,我們選擇 // 將值歸一化到 [0..1) 中。

double RNGMod_Next(void) {
  long int gamma, q = m / a;
  int r = m % a;

  gamma = a * (seed % q) - r * (seed / q);
  if (gamma > 0) seed = gamma;
    else seed = gamma + m;

  return((double) seed / m);
} /* end of double RNGMod_Next(void) */


RNGMod_Test.c
#include <stdio.h>

#include "math\RNGMod.h"

int main(void) {
printf("TESTING RNGMod…\n");
printf("Before initialization: %g\n", RNGMod_Next());
RNGMod_InitializeSeed(35000);
printf("After initialization: %g\t", RNGMod_Next());
printf("%g\t", RNGMod_Next());
printf("%g\t", RNGMod_Next());
printf("%g\n", RNGMod_Next());

return(0);
} /* end of int main(void) */

使用 make 構建程式

[edit | edit source]

隨著編譯/連結程式所需的檔案數量增加,跟蹤檔案之間的相互依賴關係變得越來越困難。用於解決此類情況的一個工具是在 UNIX 環境中找到的 make 實用程式。該實用程式會自動確定大型程式的哪些部分需要重新編譯,併發出命令重新編譯它們。

make 的輸入是一個檔案,該檔案包含告訴哪些檔案依賴哪些檔案的規則。這些規則通常採用以下形式

target : prerequisites
TAB command
TAB command
TAB ...

上述規則的解釋如下:如果目標已過期,請使用以下命令將其更新。如果目標不存在或比任何先決條件舊(透過比較上次修改時間),則它已過期。目標是指要更新的檔案,而先決條件是指用於生成目標檔案的其他檔案。

Makefile

以下規則告訴 make 實用程式 RNGMod_Test.exe 依賴於 RNGMod.o 和 RNGMod_Test.c。如果 RNGMod_Test.exe 不存在,或者比 RNGMod.o 和 RNGMod_Test.c 舊,則 RNGMod_Test.exe 已過期。如果這兩個檔案中的任何一個被修改,我們必須使用下一行中提供的命令來更新 RNGMod_Test.exe。

請注意,命令之前的製表符不是為了使檔案更易讀;用於更新目標的命令必須以製表符開頭。

$@ 是一個特殊的變數,用於表示目標檔名。

RNGMod_Test.exe : RNGMod.o RNGMod_Test.c
  gcc -o $@ -ID:\include RNGMod.o RNGMod_Test.c

RNGMod.o : RNGMod.c D:\include\math\RNGMod.h
  gcc -c -ID:\include RNGMod.c

.PHONY 是一個預定義的目標,用於定義虛假目標。它確保 make 實用程式該目標實際上不是要更新的檔案,而是一個入口點。在本例中,我們使用 clean 作為入口點,使我們能夠刪除當前目錄中的所有相關目標檔案。

.PHONY : clean
clean:
  del RNGMod.o RNGMod_Test.exe

儲存此檔案後,我們只需要發出 make 命令即可。此命令將在當前目錄中查詢名為 makefile 或 Makefile 的檔案。如果使用 GNU 版本的 make,也會嘗試 GNUmakefile。找到此類檔案後,make 會嘗試從頂部更新第一個規則的目標檔案。

make↵
gcc –c –ID:\include RNGMod.c
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 2.263 seconds
RNGMod_Test↵
Testing RNGMod...
Before initialization: 0.458724
After initialization: 0.273923 0.822585 0.186277 0.754617

請注意,make 實用程式完成其任務所需的時間可能因處理器速度及其負載而異。如果可能只修改了 RNGMod_Test.c,我們將看到以下輸出。

make↵
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 1.673 seconds

我們有時可能希望 make 實用程式從其他目標開始。在這種情況下,我們必須在命令列中提供目標的名稱作為引數。例如,如果我們需要刪除當前目錄中找到的相關目標檔案,則發出以下命令即可完成此操作。

make clean↵
del RNGMod.o RNGMod_Test.exe
Time: 0.171 seconds

以上介紹是對 make 實用程式的有限介紹。有關更多資訊,請參閱 GNU make 實用程式的手冊。

基於物件的程式設計(資料抽象)

[edit | edit source]

問題:用 C 語言編寫一個(偽)隨機數生成器。您的解決方案應該使單個應用程式能夠同時使用多個生成器(對這個數字沒有上限)。我們還應該滿足生成器能夠從不同應用程式中使用的要求。

如 [#Modular Programming模組化|程式設計部分] 中所述,可以透過在單獨的檔案中提供生成器來滿足第二個要求。

為了能夠使用多個生成器,其中確切的數字未知,我們必須設計一種方法來根據需要動態建立生成器(類似於 OOPL 中的建構函式所做的)。這種建立方案還必須給我們一些東西來唯一識別每個生成器(類似於建構函式返回的控制代碼)。我們還應該能夠返回不再需要的生成器(類似於 OOPL 中沒有自動垃圾收集的解構函式所做的)。

RNGDA.h
#ifndef RNGDA_H
#define RNGDA_H

如要求中所述,我們必須提出一個方案,讓使用者在同一個程式中擁有多個共存的生成器。我們應該能夠以某種方式識別這些生成器的每一個,並將它與其他生成器區分開來。這意味著我們不能使用我們在前面的示例中使用的相同方法。我們必須讓相關函式根據特定生成器的狀態表現出不同的行為。這種行為差異可以透過傳遞一個額外的引數來實現,即對生成器當前狀態的控制代碼。此控制代碼應向用戶隱藏生成器的實現細節;它應該具有不可變的特性,我們可以利用這些特性來隱藏生成器的可變屬性。聽起來像是 Java 中控制代碼的概念?沒錯!不幸的是,C 語言不支援控制代碼。我們需要其他可能不太抽象的概念來模擬它。我們將使用的這個不太抽象的概念是指標的概念:無論資料的大小是多少,它所指的物件的大小都不會改變。

在以下幾行中,我們首先對一個名為 struct_RNG 進行前向宣告,然後定義一個新型別,作為指向此未定義 struct 的指標。透過使用前向宣告,我們不會洩露任何實現細節;我們只是讓編譯器知道我們打算使用這種型別。透過定義指向此型別的指標,我們在使用者和實現者之間設定了一道屏障:使用者擁有某種東西(指標,其大小不會改變)來間接訪問生成器(底層物件,其大小可以透過實現決策來改變)。改變的自由意味著可以使用生成器而不必參考底層物件,這就是為什麼我們將這種方法稱為資料抽象,而以這種方式定義的任何型別都是抽象資料型別。請注意傳遞給函式的指標引數。函式的行為會根據底層物件的改變而改變,底層物件是透過該指標間接訪問的。這就是為什麼這種程式設計風格被稱為基於物件的程式設計。

struct _RNG;
typedef struct _RNG* RNG;

extern RNG RNGDA_Create(long int);
extern void RNGDA_Destroy(RNG);
extern double RNGDA_Next(RNG);

#endif


RNGDA.c
#include <stdio.h>
#include <stdlib.h>

#include "math\RNGDA.h"

struct _RNG { long int seed; };

const int a = 16807;
const long int m = 2147483647;

RNG RNGDA_Create(long int s) {
  RNG newRNG = (RNG) malloc(sizeof(struct _RNG));
  if (!newRNG) {
    fprintf(stderr, "Out of memory...\n");
    return(NULL);
  }
  newRNG->seed = s;

  return(newRNG);
} /* end of RNG RNGDA_Create(long int) */

void RNGDA_Destroy(RNG rng) { free(rng); }

double RNGDA_Next(RNG rng) {
  long int gamma;
  long int q = m / a;
  int r = m % a;

  gamma = a * (rng->seed % q) - r * (rng->seed / q);
  if (gamma > 0) rng->seed = gamma;
    else rng->seed = gamma + m;

  return((double) rng->seed / m);
} /* end of double RNGDA_Next(RNG) */


RNGDA_Test.c
#include <stdio.h>
#include "math\RNGDA.h"

int main(void) {
  int i;
  RNG rng[3]; 

  printf("TESTING RNGDA\n");
  rng[0] = RNGDA_Create(1245L);
  rng[1] = RNGDA_Create(1245L);
  rng[2] = RNGDA_Create(2345L);
  for (i = 0; i < 5; i++) {
    printf("1st RNG, %d number: %f\n", i, RNGDA_Next(rng[0]));
    printf("2nd RNG, %d number: %f\n", i, RNGDA_Next(rng[1]));
    printf("3rd RNG, %d number: %f\n", i, RNGDA_Next(rng[2]));
  } /* end of for (i = 0; i < 5; i++)*/
  RNGDA_Destroy(rng[0]);
  RNGDA_Destroy(rng[1]);
  RNGDA_Destroy(rng[2]);

  return(0);
} /* end of int main(void) */

說明

[edit | edit source]
  1. ISO C 允許在同一原始碼行上的 # 字元之前和之後有空格,但舊的編譯器不支援。
  2. 正如我們稍後將看到,void * 是對此的例外。
  3. 在 Java 中,有兩個右移運算子:>>>>>:前者符號擴充套件其運算元,而後者透過零擴充套件來執行其工作。
  4. 這並不意味著這些階段不能同時進行。它指的是階段必須按照一定的順序開始。例如,一旦初步設計完成,實施者就可以開始實施,但不能在此之前。但這兩個團隊必須互相聯絡並提供反饋。當設計師進行更改時,這些更改會傳達給實施團隊;當實施中出現問題時,這些問題會反饋給設計團隊。請注意,軟體生產週期中的其他團隊之間可能也存在類似的關係。
華夏公益教科書