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。
另外,無符號數的表示範圍為
五 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,
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=10
,volatile
告訴編譯器,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_name
為enum_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 int
或unsigned long
的表示式上會分別產生型別為unsigned int
或unsigned 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=NULL
和int *p; *p=NULL
的區別。前者是表示把p的值設定為0x00000000,後者是把*p的值設定為0x00000000(即p指向的地址設為0x00000000)。
2 將數值儲存到指定的記憶體地址。比如往記憶體地址0x12ff7c上儲存一個整型數0x100。
int *p = (int *)0x12ff7c;
*p = 0x100;
//或者這樣寫
*(int *)0x12ff7c = 0x100;
陣列和指標
1 &a
和a
的區別。看一個例子:
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] 深入理解計算機系統.