天天看點

校長講堂第九講

語義“陷阱”

一個句子可以是精确拼寫的并且沒有文法錯誤,但仍然沒有意義。在這一節中,我們将會看到一些程式的寫法會使得它們看起來是一個意思,但實際上是另一種完全不同的意思。

我們還要讨論一些表面上看起來合理但實際上會産生未定義結果的環境。我們這裡讨論的東西并不保證能夠在所有的 C 實作中工作。我們暫且忘記這些能夠在一些實作中工作但可能不能在另一些實作中工作的東西,直到以後讨論可以執行問題為止。

3.3 C 并不總是轉換實參

下面的程式段由于兩個原因會失敗:

double s;

s = sqrt(2);

printf("%g\n", s);

第一個原因是 sqrt()需要一個 double 值作為它的參數,但沒有得到。第二個原因是它傳回一個double 值但沒有這樣聲名。改正的方法隻有一個:

double s, sqrt();

s = sqrt(2.0);

printf("%g\n", 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'和'\0'。是以,如果我們現在執行:

q = p;

p 和 q 會指向同一塊記憶體。記憶體中的字元沒有因為指派而被複制。這種情況看起來是這樣的:要記住的是,複制一個指針并不能複制它所指向的東西。是以,如果之後我們執行:

q[1] = 'Y';

q 所指向的記憶體包含字元串 xYz。p 也是,因為 p 和 q 指向相同的記憶體。

3.6 空指針不是空字元串

将一個整數轉換為一個指針的結果是實作相關的(implementation-dependent),除了一個例外。這個例外是常數 0,它可以保證被轉換為一個與其它任何有效指針都不相等的指針。這個值通常類似這樣定義:

#define NULL 0

但其效果是相同的。要記住的一個重要的事情是,當用 0 作為指針時它決不能被解除引用。換句話說,當你将 0 賦給一個指針變量後,你就不能通路它所指向的記憶體。不能這樣寫:

if(p == (char *)0) ...

也不能這樣寫:

if(strcmp(p, (char *)0) == 0) ...

因為 strcmp()總是通過其參數來檢視記憶體位址的。

如果 p 是一個空指針,這樣寫也是無效的:

printf(p);

printf("%s", p);

3.7 整數溢出

C 語言關于整數操作的上溢或下溢定義得非常明确。

隻要有一次操作數是無符号的,結果就是無符号的,并且以 2n為模,其中 n 為字長。如果兩個操作

數都是帶符号的,則結果是未定義的。

例如,假設 a 和 b 是兩個非負整型變量,你希望測試 a + b 是否溢出。一個明顯的辦法是這樣的:

if(a + b < 0)

complain();

通常,這是不會工作的。

一旦 a + b 發生了溢出,對于結果的任何賭注都是沒有意義的。例如,在某些機器上,一個加法運算會将一個内部寄存器設定為四種狀态:正、負、零或溢出。 在這樣的機器上,編譯器有權将上面的例子實作為首先将 a 和 b 加在一起,然後檢查内部寄存器狀态是否為負。如果該運算溢出,内部寄存器将處于溢出狀态,這個測試會失敗。

使這個特殊的測試能夠成功的一個正确的方法是依賴于無符号算術的良好定義,既要在有符号和無符

号之間進行轉換:

if((int)((unsigned)a + (unsigned)b) < 0)

complain();

3.8 移位運算符

兩個原因會令使用移位運算符的人感到煩惱:

1. 在右移運算中,空出的位是用 0 填充還是用符号位填充?

2. 移位的數量允許使用哪些數?

第一個問題的答案很簡單,但有時是實作相關的。如果要進行移位的操作數是無符号的,會移入 0。如果操作數是帶符号的, 則實作有權決定是移入 0 還是移入符号位。如果在一個右移操作中你很關心空位,那麼用 unsigned 來聲明變量。這樣你就有權假設空位被設定為 0。

第二個問題的答案同樣簡單:如果待移位的數長度為 n,則移位的數量必須大于等于 0 并且嚴格地小于 n。是以,在一次單獨的操作中不可能将所有的位從變量中移出。

例如,如果一個 int 是 32 位,且 n 是一個 int,寫 n << 31 和 n << 0 是合法的,但 n << 32

和 n << -1 是不合法的。

注意,即使實作将符号為移入空位,對一個帶符号整數的右移運算和除以 2 的某次幂也不是等價的。

為了證明這一點,考慮(-1) >> 1 的值,這是不可能為 0 的。[譯注:(-1) / 2 的結果是 0。]