C陷阱與缺陷(更新中,總結了全書中核心內容,讀者可以根據核心決定是否需要學習該書)

1、初值與初始化

該部分解釋到,我們在定義變量時最好作初始化操作,比如int a[N];定義數組時,N也被定義了,但是沒有初始化,編譯器不會報錯,後續代碼使用N時會陷入死循環。如果經常寫代碼,從經驗上也不會這樣定義,我一般會直接定義一個確定的值比如a[100]。

2、== 與= 、&& 與&  、-- - 與- --、+=與=+區別

這類問題不需要看C陷阱與缺陷也會知道吧,代碼寫多了,自然就知道了,平時的積累,而且很多奇怪而容易出錯的用法,一般我們避免使用。

3、函數聲明是個大問題

這部分很經典,我們試着理解下這行代碼( *( void(*) () ) 0 ) ();如果你感到害怕,那你很有必要看看這章節,並去學會理解,這塊融入了指針的精髓,後續你可以再去理解containerof宏與offset宏,說明你對指針有了深刻的理解。

接下來解釋這段話,( void(*) () )這個可以理解爲(int)強制類型轉換,即將常量0轉換成( void(*) () )類型的函數指針,比如fp是一個指向返回值爲void類型的函數指針,聲明爲void (*fp)(),然後我們可以使用fp指向其他函數,而其他函數也必須是一個void(*)()類型,使用(void(*)())0就是將0地址處按照(void(*)()類型格式化,這個函數體實體佔用多大內存,從0地址開始往上也就是多大,調用0地址處的代碼時,也會按照(void(*)()類型格式執行完函數體;定義一個該類型的函數指針時可以用fp =(void(*)()0 ,將fp指向0地址,或者直接用( *( void(*) () ) 0 ) ()定義。

4、運算符的優先級問題

這部分常用的我們都知道,不常用的一般也不會用,真用到了在看書。譚浩強版本也講過。

5、語句結束標誌分號

(1)if(),多寫一個分號就等效於if(){}

if(x > big);
    big = x;

(2)return漏掉;編譯器會默認爲if() return a = 1;將a的值返回。如果該函數返回值與a一致或者爲空,編譯器則不會報錯,這是很難發現的bug,如果函數定義的返回值與a不一致報錯後容易發現並解決。

if(n<3)
    return
a = 1;
b = 2;

(3)結構體遺漏;這樣會導致main的返回值是結構體a類型,這樣main函數在實際返回時函數體內可能爲int 或空等。

struct a
{
    int date;
}
main()
{
    ...
}

6、switch、else引發問題

這部分其實對於老手也不是問題,只有代碼規範,主要if else的層次就ok。switch注意break;

7、指針與數組

這一塊其實在很多blog與譚浩強以及一些視頻中都講過,包括數組指針,指針數組,*a 與a[ ]的表達,而導致的一些問題,這部分陷阱就是你很難去理解,對於熟練的程序員不算陷阱

8、非數組的指針(重點)這部分可以說是我寫博客的真正原因

(1)陷阱1:我們這裏只是定義了一個指針r,不確定指向何處,從函數strcpy中我們可以看到,指針r應該是一個輸出型參數,不會加上const(題外話),這裏傳入指針前,該指針需要指向一個實體,函數內部並沒有使用statci分配內存,所以出了函數體後,棧上的內存會被釋放。

char *r;
strcpy(r,s);//將s內容複製到r
strcat(r,t);//連接兩個字符串

修改上面代碼 如下:似乎可以了,只要s和t指向的字符串小於r就ok,但是如果大於呢?而我們有必須給定一個確定大小的數組r,這是C語言的規定。

char r[100];
strcpy(r,s);//將s內容複製到r
strcat(r,t);//連接兩個字符串

修改上面代碼如下:所以你覺得ok了嗎,其一:r申請的堆內存使用完沒有釋放,這個很容易忘記,而導致內存泄漏,其二,strlen函數計算字符串大小沒有加上一個字符串的最後一個‘\0’字符,而實際這個字符是佔用內存的。

char *r,*malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r,s);//將s內容複製到r
strcat(r,t);//連接兩個字符串

修改上面代碼如下:加1並釋放r

char *r,*malloc();
r = malloc(strlen(s) + strlen(t) + 1);
strcpy(r,s);//將s內容複製到r
strcat(r,t);//連接兩個字符串
free(r);

9、指針類型0與數字0

第一行是合法的,如果p之前指向了0地址指針,而0地址存放的是它指向變量的地址值,所以p的值實際爲變量的地址。第二行是錯誤的,因爲0被轉換爲指針使用時,這個指針絕不能被解引用,也就是當我們將0賦值給一個指針變量時,絕不能企圖使用或操作該指針所指向的內存中存儲的內容。而strcmp操作了0地址存儲的內容,將其進行對比等操作。

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

10、邊界計算

這部分講的挺多的,我只說我比較關注的問題,如下代碼:數字下標不可能爲N,所以注意。

if(bufptr == &buffer[N])

if(bufptr > &buffer[N - 1])

11、緩衝輸出與內存分配

C庫函數的緩衝機制實現原理。當一個程序生成輸出時,是否有必要將輸出立即顯示

程序輸出有兩種方式:一種是即時處理方式,另一種是先暫存起來,然後再大塊寫入的方式,前者往往造成較高的系統負擔。因此,c語言實現通常都允許程序員進行實際的寫操作之前控制產生的輸出數據量。

這種控制能力一般是通過庫函數setbuf實現的。如果buf是一個大小適當的字符數組,那麼:setbuf(stdout,buf);

語句將通知輸入/輸出庫,所有寫入到stdout的輸出都應該使用buf作爲輸出緩衝區,直到buf緩衝區被填滿或者程序員直接調用fflush(譯註:對於由寫操作打開的文件,調用fflush將導致輸出緩衝區的內容被實際地寫入該文件),buf緩衝區中的內容才實際寫入到stdout中。這段話很關鍵,如下代碼:puts放入的字符超過了10,被填滿時,系統將這15個字節直接寫入stdout,這纔打印出來(但是也要等主函數結束後纔會打印,不知道爲什麼,按理說超過了應該直接與fflush效果一樣纔對),也可以通過fflush函數直接寫入stdout(這個不用等主函數結束)。要知道使用setbuf後,執行了puts並不會直接寫入stdout,而是放在緩衝區。

#include <stdio.h>
#include <string.h>
#include <unistd.h>

void main()
{
	int c;
	char buf[10];
	setbuf(stdout,buf);
	puts("this is my book");

	sleep(3);	
//	fflush(stdout);
}

所以你以爲這樣就OK了?加上setbuf機制後,必須等主函數結束後,系統纔會去setbuf中讀取並自動打印出來,而buf定義在main函數內部,所以main函數返回時,buf會被釋放,所以這裏要申請靜態內存或堆內存,也可以放在主函數外,代碼如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

char buf[10];
void main()
{
	int c;
	setbuf(stdout,buf);
	puts("this is my book");

	sleep(3);	
//	fflush(stdout);
}

這樣,緩衝區數據一直存在,最後主函數結束後也會打印出this is my book,這樣一來,你寫入的信息,都會被存放在buf,等函數結束後纔會打印,而不是寫入一個打印一個。

12、errno檢測錯誤

errno 是記錄系統的最後一次錯誤代碼。代碼是一個int型的值,在errno.h中定義。查看錯誤代碼errno是調試程序的一個重要方法。當linux C api函數發生異常時,一般會將errno變量(需include errno.h)賦一個整數值,不同的值表示不同的含義,可以通過查看該值推測出錯的原因。在實際編程中用這一招解決了不少原本看來莫名其妙的問題。

注意:只有當一個庫函數失敗時,errno纔會被設置。當函數成功運行時,errno的值不會被修改。這意味着我們不能通過測試errno的值來判斷是否有錯誤存在。反之,只有當被調用的函數提示有錯誤發生時檢查errno的值纔有意義。

如下代碼:這樣的例子並不能得到somecall這個函數的運行所產生的錯誤代碼,因爲很可能是printf這個函數產生的。

if (somecall() == -1) 
{
    printf("somecall() failed\n");
    if (errno == ...) 
    { 
        ...
    }
}

如下改正: 這樣才能真正得到運行somecall函數所帶來的錯誤代碼

if (somecall() == -1)
{
    int errsv = errno;
    printf("somecall() failed\n");
    if (errsv == ...)
     { 
        ... 
     }
}

13、signal陷阱

這部分內容太多,太經典,在另一篇博客中詳細介紹

https://blog.csdn.net/qq_40334837/article/details/96423711

14、內存位置0

C++中定義爲0,C中定義爲空類型指針,空類型指針,可以賦值給任何類型指針。

#undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif

NULL指針不指向任何對象,因此,除了用於賦值(p=NULL)或比較運算(p == NULL),出去其他任何目的使用NULL指針都是非法的。某些平臺C語言實現實現了對內存0的讀或者寫。但是這部分代碼不具有移植性,對於一些無法對NULL進行其他操作的平臺,將會出現致命錯誤。

要檢查是否能對NULL進行操作很容易,定義一個指針p,將NULL賦值給它,然後printf(“%d”,*p),如果出現段錯誤,說明該系統下不能對NULL進行其他操作,如果讀取到值,說明內存地址0可被操作。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章