我很懶,自己不練筆就根本記不下來東西。學習《C陷阱與缺陷》的筆記如下,自己備忘。
1.1 = 賦值運算符
== 比較運算符
1.2 && 邏輯或 ||邏輯與
& 按位或 | 按位與
1.3如果(編譯器)輸入流截止至某個字符之前都已經被分解爲一個個符號,那麼下一個符號將包括從該字符之後可能組成一個符號的最長字符串。(編譯器喜歡這麼做。)
1.4 進制問題
010 = 8
0215 = 141 0195!=141
1.5 字符與字符串
a[0] = 'aaa';
printf(a[0]);
a[0] = 97 * 256 * 256 + 97 * 256 + 97;
可以用此字符串表示一個大數來節約空間,但是不推薦這麼做,這樣會報錯(gcc 4.1.1):
warning:multi-character character constant
2 語法“陷阱”
2.1 理解函數聲明
float *g(), (*h)();
*g() = *(g())
g是一個函數,該函數的返回值類型爲指向浮點數的指針;h是一個函數指針,h是一個所指函數返回值爲浮點類型的函數指針。
float (*h)();
h是一個指向返回值爲浮點類型的函數的指針。
(float (*)());
一個“指向返回值爲浮點類型函數的指針”的類型轉換符。
2.2 運算符的優先級問題
(個人喜歡多加括號,但是看了tiro的一個帖子覺得加括號也不是完全正確的方法。)
2.3 分號
for( ; ; )與for
{};
2.4 switch語句
case與break的對應關係
2.5 函數調用
f(); 運行該語句。
f; 計算函數f的地址
2.6 if與else匹配的問題
(不要被眼睛所迷惑)
3 語義“陷阱”
3.1指針與數組
int calender[12][31];
int (*monthp)[31];
monthp = calender;
3.2非數組的指針
C語言強制要求必須聲明數組大小爲一個常量
char *r,*malloc();
r=malloc(strlen(s)+strlen(t)+1);
if(!r)
{
complain();
exit(1);
}
strcpy(r,s);
strcpy(r,t);
free(r);
3.3作爲參數的數組聲明
char *Hello; char 指針;
char Hello[]; char數組;
3.4避免“舉耦法”
ANSI C標準中禁止對string literal修改,試圖修改字符串常量的行爲是未定義的。
#include <stdio.h>
main()
{
char *p;
char *q;
p="xyz";
q = p;
q[1] = 'Y';
printf("%c/n",p[1]);
}
3.5空指針並非空字符串
在C語言中將一個整數的轉換爲一個指針,最後得到的結果取決於具體的編譯器,只有一個例外:
常數0;
由0轉換而來的指針不等於任何有效的指針。
將0 賦值給一個指針變量,絕對不能企圖使用該指針所指向的內存存儲的內容。
if(p==(char*)0) 合法;
if(strcmp(p,(char*)0)==0) 非法;
strcmp實現中會帶有指針參數所指向內存的內容的操作。
如p爲一個空指針
printf(p);
與printf("%s",p);
也是未定義的。
3.6邊界計算與不對稱邊界
int i,a[10];
for(i = 1; i <= 10; i++)
{
a[i] = 0;
}
有的編譯器認爲a[10]這個數所在的內存實際上已分配給了i;所以死循環!
避免“欄杆錯誤”的兩個通用原則:
1)考慮最簡單情況下的特例,然後將得到的結果外推;
2)仔細計算邊界,絕不掉以輕心。
3.7求值順序
(&&,||,?:和,)存在規定的求值順序。
&&和||首先對左側操作數求值,只有在需要時纔對右側操作數求值。
在a?b:c中,操作數a首先被求值,再根據a的值再求操作數b或c的值。
在,中,首先對左側操作數求值,然後該值被“丟棄”,再對右側操作數求值。
分隔函數參數的逗號並非逗號運算符。
例外:當x和y在函數f(x,y)中的求值順序是未定義的,而在函數g((x,y))中卻是確定先x後y的順序。在後一個例子中,函數g只有一個參數。這個參數的值是這樣求得的,先對X求值,然後x的值被丟棄,接着求y的值。
錯誤實例:i = 0;
while(i < 0)
{
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];
}
對於數組結尾之後的下一個元素,取它的地址是合法的,而且絕少有C編譯器能檢查出這個錯誤。
3.8運算符&&,||和!
按位運算中:10||12的結果爲1,因爲10不是0,而且12根本不會被求值;在表達式10||f()中,f()也不會被求值。
3.9整數溢出
判斷a與b是否溢出的關鍵在於:最高的符號位!
這時,很有可能要判斷C編譯器中的內部寄存器的狀態!(可能爲正,負,0和溢出的與或運算)
正確判斷溢出的方法:
if((unsigned)a + (unsigned)b > INT_MAX)
complain();
此處INT_MAX是一個已定義常量,代表可能的最大整數值。ANSI C標準在<limits.h>中定義了INT_MAX;其他C語言實現上,讀者要自己重新定義。
另一種方法:
if(a > INT_MAX-b)
complain();
3.10 爲函數main提供返回值
main()
{
complain();
return 0;
}
第4章 連接
4.1什麼是連接器
連接器的輸入是一組目標模塊和庫文件。連接器的輸出是一個輸入模塊。連接器輸入目標模塊和庫文件,同時生成載入模塊,看是否已有同步的外部對象。如果沒有,連接器就將該外部對象增加到載入模塊中;如果有,連接器就要開始處理命名衝突。
如果C語言實現中提供lint程序,切記要使用!
4.2聲明與定義
int a;
如果其位置出現在所有函數體外,外部對象a的定義!說明了a是一個外部整型變量,同時爲a分配了存儲空間。
extern int a;
並不是對a的定義。只說明了a是一個外部整型變量,包括了extern關鍵字,顯式的說明了a的存儲空間是在程序的其他地方分配的。從連接器的角度看:這是一個對外部變量a的引用,而不是對a的定義。
4.3命名衝突與static修辭符
static int a;的含義與int a;相同。只不過,a的作用域限制在了一個源文件,對於其他的源文件,a是不可見的。
static也適用於函數
static int g(int x)
{
//g函數體
}
void f()
{
//其他內容
b = g(a);
}
4.4形參,實參與返回值
1.形參變量值在被調用時才分配內存單元,在調用結束時,即可釋放所分配的內存單元。
因此,形參只有在函數內部有效,函數調用結束返回主調函數後則不能使用該形參變量。
2.實參可以是常量,變量,表達式,函數等,無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值,以便將值傳送給形參。
因此應預先用賦值,輸入等方法使實參獲得確定值。
3.實參和形參在數量上,類型上,順序上應該嚴格保持一致。否則會發生“類型不匹配”錯誤。
4.函數調用中發生的數據傳遞是單向的,即只能把實參的值傳遞給形參,而不能把形參的值反向傳遞給實參。
在函數調用的過程中,形參的值發生變化,而實參中的值不會變化。
4.5檢查外部類型
extren int n; 出現在一個源文件中。
long n; 出現在另一個源文件中。
這樣會產生問題。
保證一個特定名稱的所有外部定義在每個目標模塊中都有相同的類型。
char filename[] ="/etc/passwd";
與extern char* filename;不同。
4.6頭文件
將產生的重複命名的東西放到頭文件中去。
庫函數
5.1返回整數的getchar函數
getchar()的返回值爲int型。
5.2更新順序文件
爲了保持與過去不能同時進行讀寫操作的程序的向下兼容性,一個輸入操作不能直接隨後緊跟一個輸出操作。
while( fread(char *)&rec , sizeof(rec) , 1 , fp) == 1)
{
//對rec執行某些操作
if(//rec必須重新寫入)
{
fseek(fp , -(long)sizeof(rec), 1 );
fwrite((char *) &rec, sizeof(rec), 1, fp);
fseek(fp ,0L ,1);
}
}
5.3緩衝輸出與內存分配
sendbuf控制程序輸出的兩種方式:即時處理。
暫存。
setbuf(stdout, buf);
1.static char buf[BUFSIZ];
可以將buf聲明完全移到main函數以外。
2.動態分配緩衝區
char *malloc();
setbuf(stdout ,malloc(BUFSIZ));
5.4使用errno檢測錯誤
在調用庫函數時,應該首先檢測作爲錯誤指示的返回值,確定程序執行已經失敗,然後再檢查errno。搞清楚出錯原因。
//調用庫函數
if(返回的錯誤值)
檢查errno
5.5庫函數signal
#include <signal.h>
signal(signal type,handler function)
type標識signal函數要捕獲的信號類型
handler function指定事件發生時,加以調用的事件處理函數
對於算術錯誤,signal處理函數唯一安全,可移植的操作就是打印一條出錯信息,然後用longjmp或者exit退出程序。
signal函數儘可能簡單,並將其組織在一起。
6預處理
6.1不能忽視宏定義中的空格
#define f (x) ((x) - 1)
等於
#define f(x) (x)((x) - 1) 編譯報錯
6.2宏不是函數
宏中的每一個參數最好加上括號,以免影響優先級的問題。
但有的宏會對變量運行兩次,這樣的宏一定要注意,能少用就少用。
biggest = x[0];
i = 1;
while(i < n)
biggest = max(biggest, x[i++]);
x[0] = 2;
x[1] = 3; biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
x[2] = 1;
比較4個數的大小:
biggest = a;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
#define max(x,y) ( {/
typeof (x) _x = (x);/
typeof (y) _y = (y);/
(void) (&_x == &_y);/
_x > _y ? _x : _y;} )
第四行是比較x與y的類型是否匹配。
6.3宏不是語句
宏如果定義了if語句而未加else,很可能與程序中的if - else語句產生關聯,這樣的錯誤極難發現。
#define assert(e)
( (void)( (e) || _assert_error (_FILE_ , _LINE)) )
6.4宏不是類型定義
#define TI struct foo*
typedef struct foo *T2;
T1 a, b;
T2 a,b;
第一個struct foo *a, b;
第7章 可移植缺陷
7.1應對C語言標準變更
double square(double);
main()
{
printf("%g/n",squre(3));
}
7.2標識符的限制
ANSI標準只能保證的是:C實現必須能夠區別前6個字符不同的外部名稱。
7.3整數的大小
整數的大小可改,可以定義
typedef long tenmil;再使用,可移植性會好很多。
7.4字符是有符號整數還是無符號整數
如果編程者關注一個最高位是1 的字符其數值究竟是正還是負,可以將這個字符聲明爲無符號字符,(unsigned char)。這樣,無論是什麼編譯器,在將該字符轉換成整數時都只需將多餘的位填充爲0即可。而如果聲明爲一般的字符常量,那麼在某些編譯器上可能作爲有符號數來處理,在另一些編譯器上會作爲無符號數來處理。
7.5移位運算符
在向右移位時,如果爲無符號數,空出的位被0填充,
如果爲有符號數,空出的位被0,也可用其副本填充。
移位運算允許的範圍爲: 移位對象長爲n,
移位計數大於或等於0,嚴格小於n。
low + high爲非負。
mid = (low + high) >> 1;
mid = (low + high) / 2;
前者執行的速度快很多。
7.6NULL指針/內存位置0
#include <stdio.h>
int main(void)
{
char *p;
p = NULL;
printf("Location 0 contains %d/n", *p );
}
揭示了某個機器是如何處理內存地址0的
7.7除法運算時發生的截斷
???
7.8隨機數的大小
ANSI C定義了一個常數RAND_MAX;
7.9大小寫的轉換
tolower
toupper