1. 程式人生 > >C語言深度剖析-讀書簡記

C語言深度剖析-讀書簡記

陳正衝的《C語言深度剖析. 第2版》這本書很不錯,對C語言中的一些易錯和重要的知識點進行了深度剖析,碰巧在網上看到這篇部落格,對這本書中的關於C語言的一些易錯的和重要的知識點做了一些整理,故轉載過來,以便後續查閱。

寫在前面

最近再次溫習C語言深度剖析,對C語言的一些易錯的和重要的知識點做了以下整理。

第一章 關鍵字

一 register

1 關鍵字請求編譯器儘可能將變數存在CPU內部的暫存器內中,避免存入聶功通過定址訪問來提高效率。
2 變數型別必須是CPU暫存器可以接受的型別,即必須是一個單個的值,其長度小於或者等於整型的長度。
3 不能用&來獲取register變數的地址(因為是存在了暫存器而不是記憶體中)。

二 static

1 修飾變數,變數存在記憶體的靜態區。當修飾全域性變數時,表示變數只能在被定義的檔案中使用,其他檔案即時使用extern也不能使用。修飾區域性變數時,表示只能在函式裡使用。跳出函式後值並不銷燬。

2 修飾函式,不表示儲存方式是靜態的,而是針對函式作用域,表示作用域僅侷限於本檔案。在C++中,static有第3個作用:類中的靜態資料成員,可以實現多個物件間的資料共享。靜態資料成員需要相應的靜態成員函式訪問。

三 sizeof

1 sizeof是關鍵字而不是函式。在計算變數所佔空間大小時括號可以省略,而計算型別大小時不可以省略。

2 sizeof是在編譯時求值(在C99中,計算柔性陣列所佔空間大小是其是執行時求值)。sizeof操作符裡面不要有其他運算,否則不會達到預期的目的。

四 signed和unsigned關鍵字

先看一段程式,考慮其輸出結果。

int main()
{
    signed char a[1000];
    int i;
    for(i=0;i<1000;i++)
    {
        a[i]=-1-i;
    }
    printf("%d",strlen(a));
    return 0;
}

輸出為255。答對了嗎?答對了可以跳過下面的分析。

首先介紹一下計算機資訊表示的幾個易混淆的概念:原碼、反碼、補碼。原碼是符號位加上真值的絕對值,即第一位表示符號,其餘表示值;反碼規定,正數的反碼是本身,負數的反碼是在其原碼的基礎上,符號位不變,其餘位各個取反。補碼是計算機系統中數值的表示方法,正數的補碼與原碼一致,即也與反碼一致,負數的補碼是其反碼加1.用補碼可以將符號位和其他位統一處理,減法按加法處理。兩個用補碼錶示的數相加時,如果最高位(符號位)有進位,則進位被捨棄。

下面來介紹有符號數和無符號數之間的轉換。在C語言中,對有符號數和無符號數之間的強制轉換,保持不變的是位模式。由此可以把有符號數和無符號數之間的變換,看成有符號數的補碼錶示和無符號數之間的轉換。

下圖是補碼到無符號數之間的轉換示意圖:

用公式表示即為:

下圖是無符號數到補碼之間的轉換示意圖:

用公式表示即為:

迴歸到題目,strlen函式是計算字串長度的,遇到’\0’即認為字串的結束(長度值不包括最後的’\0’)。則題目可轉換為計算從a[0]到a[n]之間的長度,其中n為第一個a[n]位模式為0的位置。-1,-2..到多少的位模式會為0?根據補碼到無符號數之間的轉換公式,可以得到-256的位模式為0。則長度為255。

另外,無符號數的表示範圍為02w,有符號數為2w12w11w為型別的bit數。可以看到,有符號數的正數表示範圍比負數少1,是因為-0和+0,人為規定+0視為0,-0視為2w1

五 if和else的組合

1 bool變數與“零值”比較,寫成if(flag)if(!flag)最好。
2 float與“零值”進行比較,使用if(val>=-EPSINON&&val<=EPSINON)EPSIONO為定義好的精度。

3 說到浮點數,還要注意不要在很大的浮點數和很小的浮點數之間進行運算,如:

#include <stdio.h>
int main()
{
    double i = 1000000000.00;
    double j = 0.00000000001;
    printf("%.15f",i+j);
    return 0;
}

輸出:1000000000.000000000000000,會有截斷。為什麼?涉及到浮點數在計算機裡的表示。

float:1bit(符號位) 8bits(指數位) 23bits(尾數位)
double:1bit(符號位) 11bits(指數位) 52bits(尾數位)

精度由尾數的位數決定。比如,對於float,log10223=6.923689900271567,則其精度最多有7位有效數字,但絕對能保證的為6位,也即float的精度為6~7位有效數字;對於double類似,精度為15~16位。題目中,i+j的有效位數為21位,超過了double的最高有效位數,則會產生截斷。

4 指標變數與“零值”比較,使用if(NULL==p)和if(NULL!=p)。

六 switch和case

1 分支比較多的話使用switch和case會提高效率。
2 每個case後不要忘了break。
3 case後面只能是整型或字元型的常量或常量表達式(不可以是字串)。

七 const

1 在ANSI C標準中(不適用於C++),const精確來說應該是隻讀變數而不是常量,其值在編譯時不能被使用,編譯器在編譯時不知道其儲存的內容。所以case後也不可以跟const修飾的只讀變數。

2 修飾指標

const int *p;      //常量指標,p可變,p指向的物件不可變
int const *p;      //常量指標,p可變,p指向的物件不可變
int *const p;      //常指標,p不可變,指向的物件可以變
const int* const p;//指標p和其指向的內容都不可以變

記憶方法:忽略型別名,看const離誰近就修飾誰。

八 volatile

1 修飾的變量表示可以被某些編譯器未知的因素更改,不然作業系統、硬體或者其他執行緒。
2 volatile int i=10volatile告訴編譯器,i是隨時可能發生更改的,每次使用時必須從記憶體中取出i的值。保證對特殊地址的穩定訪問。

九 struct關鍵字

1 空結構體大小為1位元組。

2 柔性陣列。C99中,結構中的最後一個元素允許是未知大小的陣列,即柔性陣列成員,但其前必須有至少一個其他成員。sizeof返回的結構大小不包括柔性陣列的記憶體。

十 union關鍵字

1 union中的所有資料成員共用一個空間,同一時間只能儲存一個數據成員,所有的成員有相同的起始地址。
2 union預設屬性為public,大小為最大程度的資料成員的空間。
3 利用union來確定當前系統的儲存模式(大小端)。

首先看什麼是大小端模式,大端模式表示字資料的高位元組儲存在低地址,而字資料的低位元組儲存在高地址;小短模式表示字資料的高位元組儲存在高地址,而字資料的低位元組儲存在低地址。 比如,一個存在地址0x100處的int型的變數x的16進製表示為0x01234567,則其大小端的表示為:

則利用union可以很快的寫出檢測大小端模式的程式:

int check_system()
{
    union check
    {
        int i;
        char ch;
    }c;

    c.i=1;
    return (c.ch==1);    //true為小端模式,false為大端模式。
}

留一個問題,在x86系統下,以下程式的輸出值為多少

#include <stdio.h>
int main(void)
{
    int a[5] = {1,2,3,4,5};
    int *ptr1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf("%x,%x\n",ptr1[-1],*ptr2);
    return 0;
}

正確答案是5,2000000。為什麼?詳細分析可轉到後文中的第四章。

十一 enum關鍵字

1 一般的定義方法如下:

enum enum_type_name
{
    ENUM_CONST_1,
    ENUM_CONST_2,
    ...
    ENUM_CONST_n
} enum_variable_name;

enum_type_name為資料型別名,enum_variable_nameenum_type_name型別的一個變數。ENUM_CONST_*如果不賦值,會從被賦值的那個常量開始加1,預設的第一個常量值為0.
2 列舉在編譯時確定值,define在預編譯確定。

十二 typedef

1 用於給一個已經存在的資料型別取一個別名。看例子:

typedef struct student
{
    //Code
} Stu_st,*Stu_pst;

以上程式碼表示給struct student{}取了個別名叫Stu_st,給struct student{}*取了個別名叫Stu_pst

第二章 符號

一 註釋符號

1 編譯器處理註釋時不是簡單的剔除,而是用空格取代。
2 "/""*"之間如果沒有空格,都會被當做註釋的開始。可以y=x/ *p或者y=x/(*p),但不可以y=x/*p
3 C語言裡反斜槓”\”表示斷行,編譯器會將反斜槓剔除掉,跟在反斜槓後面的字元自動接續到前一行。但要注意,反斜槓之後不能有空格,反斜槓的下一行也不能有空格。

二 位運算子

1 按位異或可以實現不用第三個臨時變數來交換兩個變數的值: a^=b;b^=a;a^=b;但不推薦這樣做,不易讀。
2 如果位操作符”~”和”<<”應用於基本型別無符號字元型或無符號短整型的運算元,結果會立即轉換成運算元的基本型別。

uint8_t port = 0x5aU;
unit8_t result_8;
unit16_t result_16;

result_8 = (~port) >> 4;                    //不能得到期待的0xa5
result_8 = ((uint8_t)(~port))>>4;           //正確的寫法
result_16=((uint16_t)(~(uint16_t)port))>>4; //正確的寫法

3 位運算子不能用於基本型別是有符號數的運算元上。
4 一元減運算子不能用在基本型別為無符號的表示式上。將一元減運算子用在unsigned intunsigned long的表示式上會分別產生型別為unsigned intunsigned long的結果,是無意義的操作。

unsigned int a = 12;
unsigned int b = -a;
int c=-a;           // U2T

cout<<a<<endl;      //12
cout<<b<<endl;      //4294967284,及-12+2^32
cout<<c<<endl;      //-12
cout<<(b==c)<<endl; //1

5 左移,右移運算子。左移時,高位丟棄,低位補0;右移時,低位丟棄,符號位隨同移動(一般正數補0,負數補什麼取決於編譯系統的規定)。左移和右移的位數不能大於和等於資料長度,不能小於0(取決於編譯器支援與否);

三 ++和–操作符

1 字尾的++,–是在本計算單位計算結束後再自加或自減。

int i=1;
int k=(i++)+(i++)+(i++); //3 

2 貪心法則。C語言有這樣一個規則:每一個符號應該包含儘可能多的字元。

int a = 3, b = 1;
int c=a+++b;
cout<<a<<" "<<b<<" "<<c<<endl; // 4 1 4

四 除法運算子

1 假定q=a/b,r=a%b,先假定b>0,則由整數除法和餘數操作應具備的相知有:

  • q*b+r==a。
  • 如果改變a的正負號,希望q的正負號隨著改變,但q的絕對值不變。
  • 當b>0,希望保證r>=0且r

五 運算子的優先順序

1 運算子的優先順序順序如下。

記憶技巧:
① 偽運算子的優先順序最高,單目運算子優先順序總是高於雙目;
② 對於雙目運算子而言,算術運算>位運算>邏輯運算;
③ 自右向左結合的運算子只有單目運算子和賦值運算子。

2 一些容易出錯的優先順序問題,見下表。

第三章 預處理

一 巨集定義

1 註釋先與預處理指令被處理,所以不可以用define巨集定義註釋符號。
2 C的巨集只能擴充套件為用大括號括起來的初始化、常量、小括號括起來的表示式、型別限定符、儲存類識別符號或do-while-zero。
3 在定義函式巨集時,每個引數例項都應該以小括號括起來,除非他們做為#(字串化操作符)或##(粘合劑)的運算元。

4 注意巨集定義中的空格。

#define SUM(x) (x)+(x) //錯誤
#define SUM(x) (x)+(x) //正確寫法

二 一些其他預處理

1 遇到·#error·後會生成一個編譯錯誤提示訊息並停止編譯,#error error-message
2 #pragma comment,將一個諸世紀路放入一個物件檔案或可執行檔案中。

#pragma comment(lib,"user32.lib") //將user32.lib庫放入到本工程

3 #pragma pack,用於記憶體對齊。
首先介紹下什麼是記憶體對齊,看一個例子:

struct TestStruct
{
    char c1;
    short s;
    char c2;
    int i;
};

求上述結構體所佔位元組數。由於記憶體對齊的存在,使得答案不是8(1 11 1 1111)而是12(1x 11 1xxx 1111),1代表使用記憶體,x代表空記憶體。

什麼時候會產生記憶體對齊?一個字(2個位元組)或者雙字(4個位元組)跨越了4位元組邊界,或者1個四字(8個位元組)跨越了8位元組邊界,被認為是未對齊的,從而需要兩次匯流排來訪問記憶體。一個字起始地址是奇數但是沒有跨越字邊界被認為是對齊的。預設條件下編譯器會將結構、棧中的成員函式進行記憶體對齊。

接下來來看下#pragma pack的使用方法。使用它可以改變編譯器預設的記憶體對齊方式。

#pragma pack(n)      //告訴編譯器按n位元組對齊
#pragma pack()       //取消自定義位元組對齊方式
#pragma pack(push)   //儲存當前對齊方式到packing stack
#pragma pack(push,n) //等效於#pragma pack(push); #pragma pack(n);
#pragma pack(pop)    //packing stack出棧,並將對齊方式設定為出棧的對齊方式

看一個例子

#pragma pack(8)
struct TestStruct1
{
    char a;
    long b;       //假定所用編譯器long為4位元組
};

struct TestStruct2
{
    char c;
    TestStruct1 d;
    long long e; //假定所用編譯器long為8位元組
};
#pragma pack()

sizeof(TestStruct1)sizeof(TestStruct2)。直接放上答案:8, 24。具體記憶體佈局為:TestStruct1(1xxx(a) 1111(b)),TestStruct2(1xxx(c) 1xxx(d.a) 1111(d.b) xxxx 11111111(e))。為什麼?

① 每個成員按照其型別的對齊引數(通常是其大小)和指定對齊引數(上例中n=8)中較小的一個對齊,並且結構的長度必須為所用過的所有對齊引數的整數倍,不夠就補空;
② 複雜型別(如結構體)的預設對齊方式是它最長的成員的對齊方式;
③ 對齊後的長度必須是成員中最大的對齊引數的整數倍。

第四章 指標和陣列

指標

1int *p=NULLint *p; *p=NULL的區別。前者是表示把p的值設定為0x00000000,後者是把*p的值設定為0x00000000(即p指向的地址設為0x00000000)。
2 將數值儲存到指定的記憶體地址。比如往記憶體地址0x12ff7c上儲存一個整型數0x100。

int *p = (int *)0x12ff7c;
*p = 0x100;

//或者這樣寫
*(int *)0x12ff7c = 0x100;

陣列和指標

1 &aa的區別。看一個例子:

int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a +1);
printf("%d, %d",*(a+1),*(ptr-1))

分析:&a+1表示區取陣列a的首地址,該地址的值加上sizeof(a)的值,即:&a+5*sizeof(int); *(a+1),雖然a&a的值是一樣的,但a表示陣列首元素的首地址,(a+1)的地址為&a[0]+1*sizeof(int),即a[1]的地址。所以上例中輸出為2,5。用類似的分析,第一章第九小結中留的問題應該可以解決了吧?
2 不可以定義為陣列,宣告為指標。例如,檔案1中定義char a[100]; 檔案2中宣告extern char *a;這樣的話,編譯檔案2是編譯器會認為a是一個指標變數,佔四位元組。會取a[0]~a[4]四個位元組去定址。
3 不可以定義為指標宣告為陣列。例如檔案1中定義char *p="abcdefg"; 檔案2中宣告extern char p[]; 這樣編譯檔案2是編譯器會認為p是一個數組,會把直接讀p的地址,並不會通過p間接尋到"abcdefg"的實際地址。

指標陣列和陣列指標

看下例,指出哪個是陣列指標,哪個是指標陣列:

int *p1[10];
int (*p2)[10];

分析:[]的優先順序高於*,p1表示指標陣列(存放指標的陣列);p2表示陣列指標(指向一個包含10個int型別資料的陣列)。

多維陣列和多級指標

1 看例題,計算列印的結果:

int a[3][2]={(0,1),(2,3),(4,5)};
int *p;
p=a[0];
printf("%d",p[0]);

答案是1,答對了嗎?分析:注意初始化的時候錯把大括號用成了小括號,賦值相當於了a[3][2]={1,3,5}。初始化注意寫法。

2 看一個面試經常遇到的例子:

int a[5][5];
int *(p)[4];
p=a;

問:&p[4][2]-&a[4][2]的值。問題不難,答案是-4,答錯的可以結合下面記憶體佈局圖仔細理解下。

陣列引數和指標引數

1 一級指標引數無法把指標變數本身傳遞給一個函式,可以用return或者二級指標引數來實現。看例子:

void GetMemory(char* p, int num)
{
    p=(char *)malloc(num*sizeof(char));
}

char *str=NULL
GetMomory(str,10); //str本身並沒有改變,還是指向NULL

通過return實現:

char* GetMemory(char* p, int num)
{
    p=(char *)malloc(num*sizeof(char));
    return p;
}

通過二級指標:

void GetMemory(char** p, int num)
{
    *p=(char *)malloc(num*sizeof(char));
}

char *str=NULL;
GetMemory(&str,10); //OK

函式指標

1 一個函式指標長的樣子: char* (*fun)(char* p1, char* p2)

2 *(int *) &p是個什麼鬼。看例子:

void Function()
{
    printf("Call Function! \n");
}

int main()
{
    void (*p)();
    *(int*)&p=(int)Function;
    *(p)();
    return 0;
}

分析:p是一個函式指標變數,*(int*)&p=(int)Function表示將函式Function的入口地址賦值給指標變數p。

3 函式指標陣列,長這個樣子:char* (*pf[3])(char *p)。使用時可以直接pf[0]=fun;或者pf[0]=&fun;

4 函式指標陣列指標。讀完後是不是想抓狂。其實沒那麼複雜,就是一個指標,指標指向一個數組,數組裡存放的是指向函式的指標而已。 大概面貌:char* (*(*pf)[3])(char* p)。結合下面的例子會更好的理解。

char* fun1(char* p)
{
    printf("%s\n",p);
    return p;
}

char* fun2(char* p)
{
    printf("%s\n",p);
    return p;
}

char* fun3(char* p)
{
    printf("%s\n",p);
    return p;
}

int main()
{
    char* (*a[3])(char* p);     //函式指標陣列
    char* (*(*pf)[3])(char* p); //函式指標陣列指標
    pf=&a;
    a[0]=fun1;
    a[1]=fun2;
    a[2]=fun3;

    pf[0][0]("fun1"); //也可以用(*pf)[0]("fun1");
    pf[0][1]("fun2"); //也可以用(*pf)[1]("fun2");
    pf[0][2]("fun3"); //也可以用(*pf)[2]("fun3");

    return 0;
}

第五章 記憶體洩露

沒涉及到多少易錯點和重點。記得定義指標變數的同時最好初始化為NULL,用完後也置為NULL;訪問提防越界;記得給結構體裡的指標分配記憶體;分配的記憶體記得釋放。

第六章 函式

沒有什麼需要特別注意的。

第七章 檔案結構

1 需要對外公開的常量放在標頭檔案中,不需要的放在原始檔。
2 不要在標頭檔案中定義物件或函式體。

第八章 關於面試的祕密

態度是一種習慣,習慣決定一切。

小結

C語言最難的部分是設計到指標的部分,還有很多易錯的細節問題。本文中的整理的內容都弄明白後,你可以在簡歷上自信的寫上能熟練掌握和運用C語言了。結合原書會更好的理解,文章意在作知識點回顧和速查之用。

參考文獻

[1] C語言深度剖析. 第2版. 陳正衝
[2] 深入理解計算機系統.