http://www.cnblogs.com/hbiner/p/3591335.html?utm_source=tuicool&utm_medium=referralhtml
這段時間把《C陷阱和缺陷》看了,沒時間本身寫總結。就轉一下別人的學習筆記吧http://bbs.chinaunix.net/thread-749888-1-1.html數組
Chapter 1 詞法陷阱
程序中的單個字符孤立起來看並無什麼意義,只有結合上下文才有意義,如p->s = "->";兩處的-意義
是不一樣的。
程序的基本單元是token ,至關於天然語言中的單詞。 一個token的意義是不會變的。 而組成token 的字
符序列則隨上下文的不一樣而改變。
token之間的空格將被忽略。
1.1 = 不一樣於 ==
1.2 &和|不一樣於&&和||
1.3 詞法分析中的貪心法
token分爲單字符token和多字符token,如/ 和 == ,當有岐義時,c語言的規則是:每個token應包括
儘量多的字符。
另外token的中間不能有空白(空格,製表符, 換行符)
y = x /*p 應寫爲y = x / *p 或者y = x / (*p);
老編譯器容許用=+來表明如今+=的含義。因此它們會將a=-1理解爲a=- 1 即a = (a-1);
它們還會將複合賦值語句當作兩個token,因而能夠處理 a>> =1, 而現代的編譯器會報錯。
1.4 整型常量
常量前加0表明是8進制。
1.5 字符與字符串
用雙引號引發的字符串, 表明的是一個指向無名數組起始字符的指針
a+++++b的含義是什麼?
C不容許嵌套註釋。
Chapter 2 語法陷阱
2.1 構造函數聲明
構造函數聲明的規則:按照使用的方式來聲明。
任何C聲明都由兩部分組成:類型及相似表達式的聲明符(declarator)。
float *g(), (*h)();
g是一個函數,該函數的返回值類型爲指向浮點數的指針。 h是一個函數指針, h所指向函數的返回值爲
浮點類型。()的優先級高於*。
由於float (*g)();表示g是一個指向返回值爲浮點類型的函數的指針。因此(float (*)())表示一個「指向
返回值爲浮點類型的函數的指針」的類型轉換符。
一旦咱們知道如何聲明一個給定類型的變量, 那麼該類型的類型轉換符就很容易獲得了:只須要把聲明
中的參量名和聲明末尾的分號去掉,再將剩餘的部分用一個括號整個「封裝」起來便可。
(*(void(*)())0)()表示什麼意思呢?
若是fp是一個函數指針, 那麼(*fp)()就表示對其所指的函數的調用。簡寫爲fp()。但這只是簡寫而已。
而*((*fp)())能夠簡寫爲*fp()
根據上文(void(*)()) 表示一個「指向返回值爲void的函數的指針」的類型。這裏不過是對0做強制轉換而
已。其實用typedef更好:
typedef void (*funcptr)();
(*(funcptr)0)();
signal的聲明以下:
void (*signal(int, void(*)(int)))(int);
或者用typedef:
typedef void (*HANDLER)(int);
HANDLER signal(int, HANDLER);
2.2 運算符的優先級問題
注意條件運算符優先級比賦值運算符高,書上第22頁是錯的。
& > ^ > |
2.3 分號
2.4 switch 語句
2.5 函數調用
f();
是個函數調用。而f;則計算函數f的地址。
2.6 else
C語言容許初始化列表中出現多餘的逗號。
Chapter 3 語義陷阱
3.1 指針與數組
C語言中只有一維數組, 並且數組的大小必須在編譯期間就做爲一個常數肯定下來。多維數組是經過一維
數組仿真的,由於數組的元素能夠是任何對象,固然也能夠是數組。
對數組,咱們只能作兩件事,肯定其大小,以及得到指向該數組下標爲0的元素的指針。其它的有關數組
的操做,其實是經過指針進行的。
若是兩個指針指向的是同一個數組中的元素,咱們能夠把這兩個指針相減。若是它們指向的不是同一個數
組中的元素,即便它們指向的地址在內存中的位置正好間隔一個數組元素的整數倍,所得的結果仍然是無
法保證其正確性的。
若是在應該出現指針的地方出現了數組名,則數組名就被看成指向該數組下標爲0的元素的指針。
int a;
p = a;
int *p;
是對的。但p = &a在ansi C中則是非法的。由於&a 是一個指向數組的指針,而p是一個指向整型變量的指針,
它們的類型不匹配。
因爲a[i] 即*(a+i);而a+i即i+a;因此a[i]即i[a];但不推薦後者的寫法
int cal[12][31];
int *p;
int i;
i = cal[4][7]等於i = *(cal[4] + 7);也等於i = *(*(cal + 4) +7);
p = cal; 是錯誤的,類型不匹配,後者是指向數組的指針。
咱們來聲明指向數組的指針:
int (*ap)[31];
因而咱們能夠這樣寫:
int cal[12][31];
int (*monthp)[31];
monthp = cal;
兩 個指針不能相加。負數的移位運算不等於相應的乘或除運算。
3.2 非數組的指針
咱們要將s和t鏈接成r.
s = "abc";
t = "efg";
char *r;
strcpy(r,s);
strcat(r,t);
這並不能達到目的。
由於一是不能肯定r指向何處, 二是不能保證r所指向的地址處還應該有內存空間可供容納字符串。
較好的是把第一行改成char r[100];只是這樣的話,大小固定了。
正確的應該是:
#include <stdio.h>
#include <ctype.h>
int main (void)
{
char s[10];
char t[10];
char *r;
char *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r)
{
complain();
exit(1);
}
scanf("%s",s);
/*getchar();*/
scanf("%s",t);
strcpy(r,s);
strcat(r,t);
printf("%s\n",r);
free(r);
}
3.3 做爲參數的數組聲明
咱們沒有辦法將一個數組做爲函數參數直接傳遞。數組名會被轉爲指向該數組第一個元素的指針。
int strlen(char s[]){}
與下面的寫法徹底相同:
int strlen(char* s){}
但其它地方就未必相同了。
下面兩 個語句是徹底不一樣的。
extern char *hello;
extern char hello[];
下面則是同樣的
main(int argc, char* argv[]){}
main(int argc, char** argv){}
3.4 避免「舉隅法」
複製指針並不一樣時複製指針所指向的數據。
3.5 空指針並不是空字符串
把常數0轉爲指針,則指針不等於任何有效的指針,即 void 指針。其它將整數轉爲指針獲得的結果未定
義。當常數0被轉爲指針時,這個指針絕對不能被解除引用(dereferenc)。換句話說,當咱們將0賦給一個指
針變量時,絕對不能企圖使用該指針所指向的內存中存儲的內容。
下面的是合法的:
if (p == (char *) 0)
但下面是非法的
if (strcmp(p, (char *) 0) == 0)
若是p是一個空指針,即便printf(p);和printf("%s",p);的行爲也是未定義的。
3.6 邊界計算與不對稱邊界
數組的下標若是用入界口加出界口來表達(即10個元素,其下標爲0 <= n < 10 ),則元素個數即爲上界與下界
之差,即下界。若爲空,則上界等於下界。任何狀況下上界也永遠不可能小於下界。
儘可能採用非對稱邊界法。
一個有N個元素的數組 ,咱們可使用a[N]進行比較和賦值,但不能引用其內容。
3.7 求值順序
C語言只有四個運算符(&&, ||, ?: , 和 ,)存在規定的求值順序。另外,分隔函數參數的逗號並不是逗號
運算符。例如,在x和y在函數f(x,y)中的求值順序是未定義的,而在函數g((x,y))是先算x,再算y,y
的值爲參數。特別是賦值運算符沒有規定求值順序。
3.9 整數溢出
無符號算術運算中,沒有所謂的「溢出」一說。有符號運算中發生溢出,則結果未定義。
下面檢測溢出的方法不可靠:
if(a + b <0)
complain();
應該這樣:
if((unsigned) a + (unsigned) b >INT_MAX)
complain();
或者這樣
if(a > INT_MAX - b)
complain();
3.10 爲函數main提供返回值
若是沒 有爲函數聲明返回類型,則默認爲int.
free以後最好立刻就p = NULL;
Chapter 4 鏈接
4.1 什麼是鏈接器
鏈接器一般把目標模塊當作是由一組外部對象組成的。 第個外部對象都表明着機器內存中的某個部分,並
通達一個外部名稱來識別。所以, 程序中的每一個函數和每一個外部變量,若是沒有被聲明爲static,就都是一個
外部對象。 某些編譯器會對靜態函數和靜態變量的名稱作必定改變,將它們也做爲外部對象。
除了外部對象,目標模塊還可能包括了對其它模塊中的外部對象的引用。
4.2 聲明與定義
每一個外部變量只能定義一次。
4.3 命名衝突與static修飾符
4.4 形參、實參與返回值
每一個函數都要在調用以前進行聲明定義,否則返回類型爲int
若是一個函數沒有float,short或者char類型的參數,在函數聲明中徹底能夠省略類型聲明(定義不能省
略)
4.5 檢查外部類型
同一個外部變量在不一樣的地方被聲明爲不一樣的類型,這種錯誤大部分編譯器是檢不出來的。
char file[]= "/etc/password";
與
extern char* file;
是不同的。
4.6 頭文件
Chapter 5 庫函數
C標準沒有定義執行底層I/O操做的read和write函數。
5.1 返回整數的getchar函數
5.2 更新順序文件
爲了與之前的程序保持兼容,一個輸入操做不能隨後緊跟一個輸出操做,反之亦然。若是要同時進行輸入
和輸出操做,必須在其中插入fseek函數的調用。
FILE *fp;
struct record rec;
while (fread((char *)&rec, sizeof(rec),1,fp) = 1)
{
/* */
if(/* */)
{
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char *)&rec, sizeof(rec), 1,fp);
fseek(fp, 0l,1);
}
}
5.3 緩衝輸出與內存分配
#include <stdio.h>
void main(void)
{
int c;
char buf[BUFSIZ];
setbuf(stdout,buf);
while((c = getchar()) != EOF)
putchar(c);
}
這個是不對的。buf最後一次被清空是在何時?答案是在main函數結束以後,做爲程序交回控制給操做系
統以前C運行時庫所必須進行的清理工做的一部分。可是在此以前buf已經被釋放。
解決方法一是加上static 聲明。也能夠把buf聲明徹底移到main函數以外。第二種辦法是動態分配緩衝區,
在程序中並不主動釋放分配的緩衝區
5.4 使用erron檢測錯誤
不少的庫函數,特別是那些與操做系統有關的,當執行失敗時會經過一個名稱爲errno的外部變量,通知
程序該函數調用失敗。
下面的是錯誤的:
/*調用庫函數*/
if(errno)
/*處理錯誤*/
由於,在庫函數調用沒有失敗的狀況下,並無強制要求庫函數必定要設置errno爲0,這樣errno的值可能
就是前一個執行失敗的庫函數設置的值。
下面更正了,可仍是錯誤的:
errno = 0;
/*調用庫函數*/
if(errno)
/*處理錯誤*/
庫函數在調用成功時,既沒有強制要求對errno清零,但同時也沒有禁止設置errno。
下面纔是對的:
/* 調用庫函數 */
if(返回的錯誤值)
檢查errno
5.5 庫函數signal
從理論上說,一個信號可能在C程序執行期間的任什麼時候刻上發生,甚至可能出如今某些複雜的庫函數(如
malloc)的執行過程當中。所以從安全的角度講,信號的處理函數不該該調用上述類型的庫函數。基於一樣的原
因,從signal處理函數中使用longjump退出,一般狀況下也是不安全的:由於信號可能發生在malloc 或者其它
庫函數開始更新某個數據結構,卻又沒有最後完成的過程當中。所以signal處理函數可以作的安全的事情,彷佛
就只有設置一個標誌而後返回,期待之後主程序可以檢查到這個標誌,發現一個信號已經發生。
然而,就算這樣作也並不老是安全的。當一個算術運算錯誤引起一個信號時,某些機器在signal處理函
數返回後還將從新執行失敗的操做。所以對於算術運算錯誤,signal處理函數的唯一安全、可移植的操做就是
打印一條出錯消息,而後使用longjump或exit當即退出程序。
當一個程序異常終止時,程序輸出的最後幾行經常會丟失,緣由是緩衝。
Chapter 6 預處理器
6.1 不能忽視空格
6.2 宏並非函數
6.3 宏並非語句
#define assert(e) ((void)((e)||_assert_error(_FILE_,_LINE_)))
6.4 宏並非類型定義
咱們沒有辦法在一個C表達式的內部聲明一個臨時變量。
避免反作用的一個辦法就是再引入一個變量。
在某個上下文中本應須要函數而實際上卻用了函數指針,那麼該指針所指向的函數將會自動地被取得並替換這
個函數指針。
Chapter 7 可移植性缺陷
7.1 應對C語言標準變動
7.2 標識符名稱的限制
c標準所能保證的只是,c實現必須可以區別出前6個字符不一樣的外部名稱,且並無要求區分大小寫。
7.3 整數的大小
一個普通(int)整數足夠大以容納任何數組下標。
字符長度由硬件決定
7.4 字符是有符號整數仍是無符號整數
若爲有符號,則將其轉爲int時,應該同時複製符號位,而無符號,則填 0便可。
一個常見的錯誤是:若是c是一個字符變量,使用(unsigned)c就可獲得與c等價的無符號整數。這是錯誤
的,由於在將字符c轉換爲無符號整數以前,c將先被轉爲int型,而此時可能獲得非預期的結果。
正確的是使用語句(unsigned char)c,這樣就直接轉換。
7.5 移位運算符
若是被移位的對象長度是n位,那麼移位計數必須大於或等於0,而嚴格小於n。
即便某些c實現將符號位複製 到空出的位中,有符號整數的向右移位運算也並不等於除以2的某次冪。
(-1)>>1這通常不可能爲0,但(-1)/2通常爲0.
7.5 內存位置0
NULL指針並不指向任何對象,只能用於賦值或比較運算。
7.7 除法運算的截斷
q = a / b;
r = a % b;
C 語言的定義只保證q*b+r==a,以及當a>=0且b>0時,保證|r|<|b|以及r>=0.最好避免a爲負值。
7.8 隨機數的大小
RAND_MAX
7.9 大小寫轉換
7.10 首先釋放,而後從新分配
注意早期的C實現能夠realloc一個已經free了的指示針。
7.11 一個例子
由於字符串常量能夠用來表示一個字符數組,因此在數組名出現的地方均可以用字符串常量末端替換。
如:
"0123456789"[n%10]
-n可能溢出,由於最小負數的絕對值大於最大正數的絕對值。因此改亦正數的符號不會有問題,而改變
負數的符號則可能有問題。
void printnum(long n, void (*p)())
{
if(n<0)
{
(*) ('-');
n=-n;
}
if(n>=10)
printnum(n/10,p);
(*p)((int)(n%10) + '0');
}
上面的是有問題的。下面的纔是對的:
void printneg(long n, void (*p)())
{
long q;
int r;
q = n / 10;
r = n % 10;
if(r>0)
{r -= 10;
q++;
}
if (n <= -10)
printneg(q,p);
(*p)("0123456789"[-r]);
}
void printnum (long n, void (*p)())
{
if(n < 0)
{
(*p)('-');
printneg(n,p);
}
else
printneg(-n,p);
}安全