碼字不易,對你有幫助 點贊/轉發/關注 支持一下作者
微信搜公衆號:不會編程的程序圓
看更多幹貨,獲取第一時間更新
代碼,練習上傳至:https://github.com/hairrrrr/C-CrashCourse
0. 指針與數組
C 語言中數組與指針這兩個概念之間的聯繫密不可分。
關於數組:
- C 語言中只有一維數組,而且數組大小必須在編譯期就作爲一個常數確定下來。數組元素可以是任何類型的對象,也可以是另外一個數組。(C99 允許變長數組)
- 對於一個數組,我們只能夠做兩件事:確定該數組的大小,以及獲得指向該數組下標爲 0 的元素的指針。
任何一個數組下標運算都等同於一個對應的指針運算。
聲明數組
int a[3];
聲明瞭一個擁有 3 個整型元素的數組。
struct{
int p[4];
double x;
}b[14];
聲明瞭一個擁有 17 個元素的數組,且每個元素都是一個結構。
int calendar[12][31];
聲明瞭擁有 12 個數組類型的元素,其中每個元素都是擁有 31 個整型元素的數組。因此 sizeof(calendar)
的值是 12x31 與 sizeof(int)
的乘積。
關於指針
任何指針都是指向某種類型的變量。
int *ip;
表明 ip 是一個指向整型變量的指針。
我們可以將整型變量 i 的地址賦值給指針 ip :
int i;
ip = &i;
如果我們給 *ip 賦值,就可以改變 i 的取值:
*ip = 17;
數組與指針
如果一個指針指向的是數組中的一個元素,那麼我們只要給這個指針加 1,就能夠得到指向該數組中下一個元素的指針。減法同理。
如果兩個指針指向的是同一個數組中的元素,那麼兩個指針相減是有意義的:
int *q = p + i;
我們可以通過 q - p 得到 i 的值。
int a[3];
int* p = a;
數組名被當作指向數組下標爲 0 的元素的地址。
注意,我們沒有寫成:
p = &a;
這樣的寫法在 ANSI C 中是非法的,因爲 &a
是一個指向數組的指針,而 p 是指向整型變量的指針,它們了類型並不匹配。
繼續我們的討論,現在 p 指向數組 a 中下標爲 0 的元素,p + 1 指向下標爲 1 的元素,以此類推。如果希望 p 指向下標爲 1 的元素,可以這樣寫:
p = p + 1;
當然,也可以這樣寫:
p++;
*a
是數組 a 中下標爲 0 的元素的引用。同理,*(a + 1)
是數組中下標爲 1 的元素的引用,*(a + i)
是數組中下標爲 i 的元素的引用,簡寫爲 a[i]
。
由於 a + i
和 i + a
的含義一致,因此a[i]
和i[a]
也具有相同的含義。但我們絕不推薦這種寫法。
二維數組
int calendar[12][31];
請思考,calendar[4]
含義是什麼?
calender[4]
是 calendar 數組第 5 個元素,是 calendar 數組 12 個擁有着 31 個整型元素的數組之一。sizeof(calendar[4])
大小爲 31 與 sizeof(int)
的乘積。
p = calendar[4];
這個語句使 p 指向了數組 calendar 下標爲 0 的元素。
如果 calendar 是數組,我們可以:
i = calender[4][7];
上式等價於:
i = *(calender[4] + 7);
等價於:
i = *(*(calender + 4) + 7);
下面我們再看:
p = calender;
這個語句是非法的。因爲 calendar 是一個二維數組,即數組的數組,calendar 是一個指向數組的指針,而 p 是指向整型變量的指針。
我們需要聲明一種指向數組的指針,經過上一章的討論,我們不難得出:
int (*ap)[31];
這個語句的效果是:聲明瞭 *ap 是一個擁有 31 個元素的數組,所以,ap 就是指向這樣的數組的指針。因此,我們可以這樣寫:
int calender[12][31];
int (*monthp)[31];
monthp = calendar;
這樣 monthp 指向 calendar 數組的第一個元素,也就是 calendar 的 12 個擁有 31 個整型變量的數組類型的元素之一。
假定在新的一年開始時,我們需要清空 calendar 數組,用下標的形式可以很容易的做到:
int month;
for(month = 0; month < 12; month++){
int day;
for(day = 0; day < 31; day++)
calendar[month][day] = 0;
}
上面的代碼用指針應該如何表示?
int (*month)[31] = calander;
for(;month < calendar + 12; month++){
int *day = *month;
for(; day < *month + 31; day++)
*day = 0;
}
原書中的代碼爲:
int (*monthp)[31];
for(monthp = calendar; monthp < &calendar[12]; monthp++){
int *dayp;
for(dayp = *monthp; dayp < &(*monthp)[31]; dayp++)
*dayp = 0;
}
1. 非數組的指針
假定我們兩個這樣的字符串 s 和 t,我們希望將這兩個字符串連接成單個字符串 r :
char* r;
strcpy(r, s);
strcat(r, t);
我們不確定 r 指向何處,而且 r 所指向的地址處不一定有內存空間可供容納字符串。這一次,我們爲 r 分配空間:
char r[100];
strcpy(r, s);
strcat(r, t);
C 語言強制要求我們必須聲明數組大小爲一個常量,因此我們不能保證 r 足夠大。這時,我們可以利用庫函數 malloc :
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
這個例子還是錯的,原因有 3 :
- malloc 函數可能無法提供請求的內存
- 給 r 分配的內存在使用完後應該及時釋放
- strlen(s) 的值如果是 n ,那麼字符串 s 的實際長度爲 n + 1,因爲,strlen 會忽略作爲結束標誌的空字符。所以,malloc 時,切記給字符串結尾的空字符留有空間。
修改:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r){
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
//一段時間後再使用
free(r);
2. 作爲參數的數組聲明
C 語言中,我們沒有辦法可以將一個數組作爲函數參數直接傳遞。如果我們使用數組名作爲參數,那麼數組名會立刻被轉換爲指向該數組第 1 個元素的指針。例如:
char hello[] = "hello";
printf("%s\n", hello);
printf 函數調用等價於:
printf("%s\n", &hello[0]);
所以,C 語言中會自動的將作爲參數的數組聲明轉換爲相應的指針聲明。也就是像這樣的寫法:
int strlen(char s[]){
}
或:
int strlen(char* s){
}
C 程序員經常錯誤的假設,在其他情況下也會有這種自動的轉換。後面我們會說到:
extern char* hello;
和下面的語句有着天壤之別:
extern char hello[];
另一個常見的例子就是 main 函數的參數:
int main(int argc, char* argv[]){
}
等價於:
int main(int argc, char** argv){
}
需要注意的是,前一種寫法強調 argv 是一個指向某數組元素爲字符指針的起始元素的指針。因爲這兩種寫法是等價的,所以可以任選一種最能清晰反應自己意圖的寫法。
3. 避免“舉隅法”
指針的複製並不同時複製指針所指向的數據。
char *p, *q;
p = "xyz";
p 的值並不是字符串 "xyz"
,而是指向該字符串起始元素的指針。因此,如果我們執行下面的語句:
q = p;
現在 p 和 q 是兩個指向內存中同一地址的指針。如圖:
因此,當我們執行完語句:
q[1] = 'Y';
q 所指向的內存存儲的字符串是"xYz",p 所指向的內存中存儲的當然也是字符串"xYz" 。
注意:ANSI C 中禁止對 string literal (字符串字面量)作出修改。K&R 對這一行爲的說明是:試圖修改字符串常量的行爲是未定義的。
4. 空指針並非空字符串
常數 0 轉換而來的指針不等於任何有效的指針。
#define NULL 0
無論是用 0 還是符號 NULL,效果都是完全相同的。空指針絕不能被解引用。
下面的寫法是合法的:
if(p == (char*)0){...}
但是如果寫成這樣:
if(strcmp(p, (char*)0) == 0){...}
就是非法的了。因爲庫函數 strcmp 的實現中會查看它的指針參數所指向的內存中的內容。
如果 p 是一個空指針,即使
printf(p);
和
printf("%s\n", p);
的行爲也是未定義的。
5.邊界計算與不對稱邊界
如果一個數組有 10 個元素,那麼這個數組下標允許取值範圍是什麼呢?
在 C 語言中,這個數組下標的範圍是 0 ~ 9 。
欄杆錯誤
也稱差一錯誤(off-by-one error)。
解決這種問題的通用原則:
- 首先考慮最簡單情況下的特例,然後將得到的結果外推。
- 仔細計算邊界,絕不掉以輕心。
不對稱邊界
解決差一錯誤的一個方法是使用不對稱邊界的思想。
比如,一個字符串中由下標爲 16 到下標爲 37 的字符元素組成的字串,如何表示這個範圍?
我們採用不對稱邊界:x >= 16 && x <38
而不是採用x >= 16 && x <= 37
。這樣,這個字串的長度明顯就是 38 - 16,也就是 22 。
用 for 循環遍歷一個大小爲 10 的數組:
for(i = 0; i < 10; i++){
}
而非:
for(i = 0; i <= 9; i++){
}
6. 求值順序
C 語言中只有 4 個運算符(&&
,||
,?:
,,
)存在規定的求值順序。
- 運算符 && 和 || 首先對左操作數求值,只有在需要時纔對右操作數求值。
- 運算符 ?: 有 3 個操作數:在
a ? b : c
中,首先對 a 求值,根據 a 的值再對操作數 b 或 操作數 c 求值。 - 逗號運算符從左向右一次求值。(求值然後丟棄再繼續求值。)
運算符 && 和 || 對於保證檢查操作按照正確的順序執行至關重要。例如在語句
if(y != 0 && x / y > tolerance)
complain();
中,就必須保證僅當 y 非 0 時纔對 x / y 求值。
下面這種從數組 x 中複製前 n 個元素到數組 y 中的做法是不正確的:
i = 0;
while(i < n)
y[i] = x[i++];
問題出在哪裏呢?上面的代碼假設 y[i]
的地址在 i 的自增操作指向前被求值,這一點並沒有任何保證。
同樣的道理,下面的代碼也是錯誤的:
i = 0;
while(i < n)
y[i++] = x[i];
應該使用這一種寫法:
i = 0;
while(i < n){
y[i] = x[i];
i++;
}
或:
for(i = 0; i < n; i++){
y[i] = x[i];
}
7. 運算符 && 和 || 與 運算符 & 和 |
按位運算 &,|,^ ,~ 對操作數的處理方式是將其視爲一個二進制的位序列,分別對其每一位進行操作。
邏輯運算 &&,||,! 對操作數的處理方式是將其視爲要麼是“真” 要麼是“假”。通常將 0 視爲 假,非 0 視爲 真。它們的結果只可能是 1 或 0 。
需要注意的是邏輯運算中的 && 和 || 是有求值順序的。
考慮下面的代碼段,其作用是在表中查詢一個特定的元素:
i = 0;
while(i < tabsize && tab[i] != x)
i++;
假定我們無意中用 & 替換了 &&:
i = 0;
while(i < tabsize & tab[i] != x)
i++;
這個循環也可能正常工作,但這僅僅是因爲兩個僥倖的原因:
- while 循環中的表達式 & 兩側都是比較運算,其結果只會是 1 或 0 。因此 x && y 和 x & y 會具有相同的結果。然而,如果兩個比較運算中的任意一個使用除 1 之外的非 0 的數表示“真”,那麼這個循環就不能正常個工作了。
- 對於數組結尾後的下一個元素(實際上是不存在的),只要程序不去修改該元素的值,而僅僅讀取它的值,一般情況下是不會有什麼危害的。運算符 && 和 & 不同,& 要求 兩側的操作數都必須被求值。因此,在後一個代碼中,最後一次循環當 i 等於 tabsize 時,儘管 tab[i] 並不存在,程序依然會查看 tab[i] 的值。
8. 整數溢出
C 語言中存在兩類整數算術運算,有符號運算與無符號運算。在無符號運算中,沒有所謂“溢出”一說:所有無符號運算都是以 2 的 n 次方爲模,這裏 n 是結果中的位數。
如果算數運算符中的一個操作數是無符號整數一個是有符號整數,有符號整數會被轉換爲無符號整數。“溢出”同樣不會發生。
但是當兩個操作數都爲有符號整數時,溢出就可能發生,而且“溢出”的結果是未定義的。
例如,假定 a 和 b 爲連個非負整形變量,我們要檢查 a + b 是否會“溢出”,一種想當然的方式:
if(a + b < 0)
complain();
這並不能正常運行。當 a + b 確實發生“溢出”時,所有關於結果如何的假設都是不可靠的。例如,有的計算機上,加法運算將設置內部寄存器爲四種狀態之一:正,負,零和溢出。在這種機器上,上面 if 語句的檢測就會失效。
一種正確的方式爲將 a 和 b 強轉爲無符號整數:
if((unsigned)a + (unsigned)b > INT_MAX)
complain();
此處的 INT_MAX 是一個已定義常量,代表可能的最大整數值。ANSI C 標準在<limits.h>中定義了 INT_MAX 。
不需要用到無符號整數運算的另一種可行的辦法是:
if(a > INT_MAX - b)
complain();
9. 爲 main 函數提供返回值
已在 【C 必知必會】系列詳細講解過。不再贅述。
參考資料:《C 缺陷與陷阱》
推薦大家一個學習 C 的 Github 項目
倉庫不斷更新,還能免費獲取C語言必讀經典電子書
https://github.com/hairrrrr/C-CrashCourse
↑↑↑ 在 README 末尾有電子書免費下載的方式
以上就是本次的內容,感謝觀看。
如果文章有錯誤歡迎指正和補充,感謝!
最後,如果你還有什麼問題或者想知道到的,可以在評論區告訴我呦,我在後面的文章可以加上。
最後,關注我,看更多幹貨!
我是程序圓,我們下次再見。