校長講堂第九講
- 2020 年 4 月 10 日
- 筆記
語義「陷阱」
一個句子可以是精確拼寫的並且沒有語法錯誤,但仍然沒有意義。在這一節中,我們將會看到一些程式的寫法會使得它們看起來是一個意思,但實際上是另一種完全不同的意思。 我們還要討論一些表面上看起來合理但實際上會產生未定義結果的環境。我們這裡討論的東西並不保證能夠在所有的 C 實現中工作。我們暫且忘記這些能夠在一些實現中工作但可能不能在另一些實現中工作的東西,直到以後討論可以執行問題為止。
3.3 C 並不總是轉換實參
下面的程式段由於兩個原因會失敗: double s; s = sqrt(2); printf("%gn", s); 第一個原因是 sqrt()需要一個 double 值作為它的參數,但沒有得到。第二個原因是它返回一個double 值但沒有這樣聲名。改正的方法只有一個: double s, sqrt(); s = sqrt(2.0); printf("%gn", s); C 中有兩個簡單的規則控制著函數參數的轉換:(1)比 int 短的整型被轉換為 int;(2)比 double短的浮點類型被轉換為 double。所有的其它值不被轉換。確保函數參數類型的正確行使程式設計師的責任。 因此,一個程式設計師如果想使用如 sqrt()這樣接受一個 double 類型參數的函數,就必須僅傳遞給它float 或 double 類型的參數。常數 2 是一個 int,因此其類型是錯誤的。 當一個函數的值被用在表達式中時,其值會被自動地轉換為適當的類型。然而,為了完成這個自動轉換,編譯器必須知道該函數實際返回的類型。沒有更進一步聲名的函數被假設返回 int,因此聲名這樣的函數並不是必須的。然而,sqrt()返回 double,因此在成功使用它之前必須要聲名。 實際上,C 實現通常允許一個文件包含 include 語句來包含如 sqrt()這些庫函數的聲名,但是對那 些自己寫函數的程式設計師來說,書寫聲名也是必要的——或者說,對那些書寫非凡的 C 程式的人來說是有必 要的。 這裡有一個更加壯觀的例子: main() { int i; char c; for(i = 0; i < 5; i++) { scanf("%d", &c); printf("%d", i); } printf("n"); } 表面上看,這個程式從標準輸入中讀取五個整數並向標準輸出寫入 0 1 2 3 4。實際上,它並不總是這麼做。譬如在一些編譯器中,它的輸出為 0 0 0 0 0 1 2 3 4。 為什麼?因為 c 的聲名是 char 而不是 int。當你令 scanf()去讀取一個整數時,它需要一個指向一個整數的指針。但這裡它得到的是一個字元的指針。但 scanf()並不知道它沒有得到它所需要的:它將輸入看作是一個指向整數的指針並將一個整數存貯到那裡。由於整數佔用比字元更多的記憶體,這樣做會影響到 c 附近的記憶體。 c 附近確切是什麼是編譯器的事;在這種情況下這有可能是 i 的低位。因此,每當向 c 中讀入一個值,i 就被置零。當程式最後到達文件結尾時,scanf()不再嘗試向 c 中放入新值,i 才可以正常地增長,直到循環結束。
3.4 指針不是數組
C 程式通常將一個字元串轉換為一個以空字元結尾的字元數組。 假設我們有兩個這樣的字元串 s 和 t,並且我們想要將它們連接為一個單獨的字元串 r。我們通常使用庫函數 strcpy()和 strcat()來完成。 下面這種明顯的方法並不會工作: char *r; strcpy(r, s); strcat(r, t); 這是因為 r 沒有被初始化為指向任何地方。儘管 r 可能潛在地表示某一塊記憶體,但這並不存在,直到你分配它。 讓我們再試試,為 r 分配一些記憶體: char r[100]; strcpy(r, s); strcat(r, t); 這隻有在 s 和 t 所指向的字元串不很大的時候才能夠工作。不幸的是,C 要求我們為數組指定的大小是一個常數,因此無法確定 r 是否足夠大。然而,很多 C 實現帶有一個叫做 malloc()的庫函數,它接受一個數字並分配這麼多的記憶體。通常還有一個函數成為 strlen(),可以告訴我們一個字元串中有多少個字元: 因此,我們可以寫: char *r, *malloc(); r = malloc(strlen(s) + strlen(t)); strcpy(r, s); strcat(r, t); 然而這個例子會因為兩個原因而失敗。首先,malloc()可能會耗盡記憶體,而這個事件僅通過靜靜地返回一個空指針來表示。 其次,更重要的是,malloc()並沒有分配足夠的記憶體。一個字元串是以一個空字元結束的。而strlen()函數返回其字元串參數中所包含字元的數量,但不包括結尾的空字元。因此,如果 strlen(s)是 n,則 s 需要 n + 1 個字元來盛放它。因此我們需要為 r 分配額外的一個字元。再加上檢查 malloc()是否成功,我們得到: char *r, *malloc(); r = malloc(strlen(s) + strlen(t) + 1); if(!r) { complain(); exit(1); } strcpy(r, s); strcat(r, t);
3.5 避免提喻法
提喻法(Synecdoche, sin-ECK-duh-key)是一種文學手法,有點類似於明喻或暗喻,在牛津英文詞典中解釋如下:「a more comprehensive term is used for a less comprehensive or vice versa;as whole for part or part for whole, genus for species or species for genus, etc.(將全面的單位用作不全面的單位,或反之;如整體對局部或局部對整體、一般對特殊或特殊對一般,等等。)」 這可以精確地描述 C 中通常將指針誤以為是其指向的數據的錯誤。正將常會在字元串中發生。例如: char *p, *q; p = "xyz"; 儘管認為 p 的值是 xyz 有時是有用的,但這並不是真的,理解這一點非常重要。p 的值是指向一個有四個字元的數組中第 0 個元素的指針,這四個字元是'x'、'y'、'z'和'