1.指針沒有指向一塊合法的內存
定義了指針變量,但是沒有為指針分配內存,即指針沒有指向一塊合法的內淺顯的例子就不舉了,這里舉幾個比較隱蔽的例子。
1.1結構體成員指針未初始化
struct student { char *name; int score; }stu,*pstu;
int main() { strcpy(stu.name,"Jimy"); stu.score = 99; return 0; }
很多初學者犯了這個錯誤還不知道是怎么回事。這里定義了結構體變量stu,但是他沒想到這個結構體內部char *name這成員在定義結構體變量stu時,只是給name這個指針變量本身分配了4個字節。name指針並沒有指向一個合法的地址,這時候其內部存的只是一些亂碼。所以在調用strcpy函數時,會將字符串"Jimy"往亂碼所指的內存上拷貝,而這塊內存name指針根本就無權訪問,導致出錯。解決的辦法是為name指針malloc一塊空間。
同樣,也有人犯如下錯誤:
int main() { pstu = (struct student*)malloc(sizeof(struct student)); strcpy(pstu->name,"Jimy"); pstu->score = 99; free(pstu); return 0; }
為指針變量pstu分配了內存,但是同樣沒有給name指針分配內存。錯誤與上面第一種情況一樣,解決的辦法也一樣。這里用了一個malloc給人一種錯覺,以為也給name指針分配了內存。
1.2 沒有為結構體指針分配足夠的內存
int main() { pstu = (struct student*)malloc(sizeof(struct student*)); strcpy(pstu->name,"Jimy"); pstu->score = 99; free(pstu); return 0; }
為pstu分配內存的時候,分配的內存大小不合適。這里把sizeof(struct student)誤寫為sizeof(struct student*)。當然name指針同樣沒有被分配內存。解決辦法同上。
1.3 函數的入口處做指針校驗
一般在函數入口處使用
assert(NULL != p)
對參數進行校驗。在非參數的地方使用
if(NULL != p)
來校驗。但這都有一個要求,即p在定義的同時被初始化為NULL了。比如上面的例子,即使用if(NULL != p)校驗也起不了作用,因為name指針並沒有被初始化為NULL,其內部是一個非NULL的亂碼。
assert是一個宏,而不是函數,包含在assert.h頭文件中。如果其后面括號里的值為假,則程序終止運行,並提示出錯;如果后面括號里的值為真,則繼續運行后面的代碼。這個宏只在Debug版本上起作用,而在Release版本被編譯器完全優化掉,這樣就不會影響代碼的性能。
有人也許會問,既然在Release版本被編譯器完全優化掉,那Release版本是不是就完全沒有這個參數入口校驗了呢?這樣的話那不就跟不使用它效果一樣嗎?是的,使用assert宏的地方在Release版本里面確實沒有了這些校驗。
但是我們要知道,assert宏只是幫助我們調試代碼用的,它的一切作用就是讓我們盡可能的在調試函數的時候把錯誤排除掉,而不是等到Release之后。它本身並沒有除錯功能。再有一點就是,參數出現錯誤並非本函數有問題,而是調用者傳過來的實參有問題。assert宏可以幫助我們定位錯誤,而不是排除錯誤。
這里其實涉及到了c語言的編程風格問題。
2. 為指針分配的內存太小
為指針分配了內存,但是內存大小不夠,導致出現越界錯誤。
char *p1 = “abcdefg”; char *p2 = (char *)malloc(sizeof(char)*strlen(p1)); strcpy(p2,p1);
p1是字符串常量,其長度為7個字符,但其所占內存大小為8個byte。初學者往往忘了字符串常量的結束標志“\0”。這樣的話將導致p1字符串中最后一個空字符“\0”沒有被拷貝到p2中。解決的辦法是加上這個字符串結束標志符:
char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
這里需要注意的是,只有字符串常量才有結束標志符。比如下面這種寫法就沒有結束標志符了:
char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
另外,不要因為char類型大小為1個byte就省略sizof(char)這種寫法。這樣只會使你的代碼可移植性下降。
3.內存分配成功,但並未初始化
犯這個錯誤往往是由於沒有初始化的概念或者是以為內存分配好之后其值自然為0。未初始化指針變量也許看起來不那么嚴重,但是它確確實實是個非常嚴重的問題,而且往往出現這種錯誤很難找到原因。
曾經有一個學生在寫一個windows程序時,想調用字庫的某個字體。而調用這個字庫需要填充一個結構體。他很自然的定義了一個結構體變量,然后把他想要的字庫代碼賦值給了相關的變量。但是,問題就來了,不管怎么調試,他所需要的這種字體效果總是不出來。我(陳正沖)在檢查了他的代碼之后,沒有發現什么問題,於是單步調試。在觀察這個結構體變量的內存時,發現有幾個成員的值為亂碼。就是其中某一個亂碼惹得禍!因為系統會按照這個結構體中的某些特定成員的值去字庫中尋找匹配的字體,當這些值與字庫中某種字體的某些項匹配時,就調用這種字體。但是很不幸,正是因為這幾個亂碼,導致沒有找到相匹配的字體!因為系統並無法區分什么數據是亂碼,什么數據是有效的數據。只要有數據,系統就理所當然的認為它是有效的。
也許這種嚴重的問題並不多見,但是也絕不能掉以輕心。所以在定義一個變量時,第一件事就是初始化。你可以把它初始化為一個有效的值,比如:
int i = 10; char *p = (char *)malloc(sizeof(char));
但是往往這個時候我們還不確定這個變量的初值,這樣的話可以初始化為0或NULL。
int i = 0; char *p = NULL;
如果定義的是數組的話,可以這樣初始化:
int a[10] = {0};
或者用memset函數來初始化為0:
memset(a,0,sizeof(a));
memset函數有三個參數,第一個是要被設置的內存起始地址;第二個參數是要被設置的值;第三個參數是要被設置的內存大小,單位為byte。這里並不想過多的討論memset函數的用法,如果想了解更多,請參考相關資料。
至於指針變量如果未被初始化,會導致if語句或assert宏校驗失敗。這一點,上面已有分析。
4.內存越界
內存分配成功,且已經初始化,但是操作越過了內存的邊界。這種錯誤經常是由於操作數組或指針時出現“多1”或“少1”。比如:
int a[10] = {0}; for (i=0; i<=10; i++) { a[i] = i; }
所以,for循環的循環變量一定要使用半開半閉的區間,而且如果不是特殊情況,循環變量盡量從0開始。
參考:陳正沖老師的《c語言深度剖析》。