C語(yǔ)言深度解剖一 (關(guān)鍵字)

注:這是第三遍讀《C語(yǔ)言深度解剖》,想想好像自從大學(xué)開(kāi)始就沒(méi)讀完過(guò)幾本書,其中譚浩強(qiáng)的那本《C語(yǔ)言程序設(shè)計(jì)(第四版)》倒是讀過(guò)有五遍吧,其次就是這本書了。以前讀書從來(lái)不做筆記,最多的就是用筆在書上隨性標(biāo)記幾下,但是覺(jué)得那樣記憶不深刻,于是這次就做了筆記。本想記下一些要點(diǎn),但是這本書確實(shí)太出色了,記著記著就變成了抄書了,不過(guò)去掉了書里的一些索然無(wú)味的東西并且加了自己親手寫的代碼在里面。整個(gè)筆記共分為7個(gè)部分,這是第一部分也是最多的一部分。目前就記到這里,以后有時(shí)間再慢慢更新。本筆記同步在簡(jiǎn)書上,有興趣的可以去看一下。

C語(yǔ)言里有32個(gè)關(guān)鍵字

  1. auto 生聲明自動(dòng)變量,缺省時(shí)編譯器一般默認(rèn)為auto

  2. int 聲明整型變量

  3. double 聲明雙精度變量

  4. long 聲明長(zhǎng)整型變量

  5. char 聲明字符型變量

  6. float 聲明浮點(diǎn)型變量

  7. short 聲明短整型變量

  8. signed 聲明有符號(hào)類型變量

  9. unsigned 聲明無(wú)符號(hào)類型變量

  10. struct 聲明結(jié)構(gòu)體變量

  11. union 聲明聯(lián)合數(shù)據(jù)類型

  12. enum 聲明枚舉類型

  13. static 聲明靜態(tài)變量

  14. switch 用于開(kāi)關(guān)語(yǔ)句

  15. case 開(kāi)關(guān)語(yǔ)句分支

  16. default 開(kāi)關(guān)語(yǔ)句中的“其他”分支

  17. break 跳出當(dāng)前循環(huán)

  18. register 聲明寄存器變量

  19. const 聲明只讀變量

  20. volatile 說(shuō)明變量在程序執(zhí)行中可被隱含的改變

  21. typedef 用以給數(shù)據(jù)類型取別名

  22. extern 聲明變量是在其他文件中聲明(也可以看做是引用變量)

  23. return 子程序返回語(yǔ)句(可以帶參數(shù),也可以不帶參數(shù))

  24. void 聲明函數(shù)無(wú)返回值或無(wú)參數(shù),聲明空類型指針

  25. continue 結(jié)束當(dāng)前循環(huán),開(kāi)始下一輪循環(huán)

  26. do 循環(huán)語(yǔ)句的循環(huán)體

  27. while 循環(huán)語(yǔ)句的循環(huán)條件

  28. if 條件語(yǔ)句

  29. else 條件語(yǔ)句否定分支(與if連用)

  30. for 一種循環(huán)語(yǔ)句

  31. goto 無(wú)條件跳轉(zhuǎn)語(yǔ)句

  32. sizeof 計(jì)算對(duì)象所占內(nèi)存空間大小

什么是定義,什么是聲明?

什么是定義:所謂的定義就是(編譯器)創(chuàng)建一個(gè)對(duì)象,為這個(gè)對(duì)象分配一塊內(nèi)存并給它取上一個(gè)名字,這個(gè)名字就是我們經(jīng)常所說(shuō)的變量名或?qū)ο竺?。但注意,這個(gè)名字一旦和這塊內(nèi)存匹配起來(lái),它們就同生共死,終生不離不棄。并且這塊內(nèi)存的位置也不能被改變。一個(gè)變量或?qū)ο笤谝欢ǖ膮^(qū)域內(nèi)(比如函數(shù)內(nèi),全局等)只能被定義一次,如果定義多次,編譯器會(huì)提示你重復(fù)定義同一個(gè)變量或?qū)ο蟆?/p>

什么是聲明:第一重含義:告訴編譯器,這個(gè)名字已經(jīng)匹配到一塊內(nèi)存上了(伊人已嫁,吾將何去何從?何以解憂,唯有稀粥),下面的代碼用到變量或?qū)ο笫窃趧e的地方定義的。聲明可以出現(xiàn)多次。第二重含義:告訴編譯器,我這個(gè)名字我先預(yù)定了,別的地方再也不能用它來(lái)作為變量名或?qū)ο竺?/p>

記住:定義創(chuàng)建了對(duì)象并為這個(gè)對(duì)象分配了內(nèi)存,聲明沒(méi)有分配內(nèi)存。

1.1 最寬恒大量的關(guān)鍵字----auto

auto: 它很寬恒大量的,你就當(dāng)他不存在吧。編譯器在默認(rèn)的缺省情況下,所有變量都是auto的。

1.2 最快樂(lè)的關(guān)鍵字----register

register:這個(gè)關(guān)鍵字請(qǐng)求編譯器盡可能的將變量存在 CPU 內(nèi)部寄存器中而不是通過(guò)內(nèi)存尋址訪問(wèn)以提高效率。注意是盡可能,不是絕對(duì)。你想想,一個(gè) CPU 的寄存器也就那么幾個(gè)或幾十個(gè),你要是定義了很多很多 register 變量,它累死也可能不能全部把這些變量放入寄存器吧。

1.2.1 皇帝身邊的小太監(jiān)----寄存器

CPU就是我們的皇帝同志.大臣就相當(dāng)于我們的內(nèi)存,數(shù)據(jù)從他這拿出來(lái)。那小太監(jiān)就是我們的寄存器了(這里先不考慮 CPU 的高速緩存區(qū))。數(shù)據(jù)從內(nèi)存里拿出來(lái)先放到寄存器,然后 CPU 再?gòu)募拇嫫骼镒x取數(shù)據(jù)來(lái)處理,處理完后同樣把數(shù)據(jù)通過(guò)寄存器存放到內(nèi)存里, CPU 不直接和內(nèi)存打交道。注意:小太監(jiān)是主動(dòng)的從大臣手里接過(guò)奏章,然后主動(dòng)的交給皇帝同志,但寄存器沒(méi)這么自覺(jué),它從不主動(dòng)干什么事。一個(gè)皇帝可能有好些小太監(jiān),那么一個(gè) CPU 也可以有很多寄存器,不同型號(hào)的 CPU 擁有寄存器的數(shù)量不一樣。

寄存器其實(shí)就是一塊一塊小的存儲(chǔ)空間,只不過(guò)其存取速度要比內(nèi)存快得多。

1.2.2 使用 register 修飾符的注意點(diǎn)

雖然register寄存器速度非??欤鞘褂胷egister修飾符是有限制的:register變量必須是能被CPU寄存器所接受的類型,意味著register變量必須是一個(gè)單個(gè)的值,并且長(zhǎng)度應(yīng)該小于或等于整形的長(zhǎng)度。由于register可能不放在內(nèi)存中,所以不能用取地址符"&"來(lái)獲取register變量的地址。

1.3 最名不符實(shí)的關(guān)鍵字----static

不要誤以為關(guān)鍵字 static 很安靜,其實(shí)它一點(diǎn)也不安靜。這個(gè)關(guān)鍵字在 C 語(yǔ)言里主要有兩個(gè)作用, C++對(duì)它進(jìn)行了擴(kuò)展。

1.3.1 作用一:修飾變量

變量又分為局部變量和全局變量,被修飾后他們都存在內(nèi)存的靜態(tài)區(qū)域。

靜態(tài)全局變量:

作用域僅限于變量被定義的文件中,其他文件即使使用extern聲明也沒(méi)有辦法使用它。準(zhǔn)確的說(shuō)作用域從定義之處開(kāi)始,到文件結(jié)尾處結(jié)束,在定義之處前面的代碼行也不能使用它。想要使用就得在前面加extern *** 。要想不這樣,直接把它放在文件開(kāi)頭出就好了。

靜態(tài)局部變量:

在函數(shù)體內(nèi)定義的,就只能在這個(gè)函數(shù)里用了,同一個(gè)文檔里的其他函數(shù)也用不了(等會(huì)給你打臉)。由于被static修飾的變量總是存在內(nèi)存的靜態(tài)區(qū),所以即使這個(gè)函數(shù)運(yùn)行結(jié)束,這個(gè)靜態(tài)變量的值還是不會(huì)被銷毀,函數(shù)下次使用時(shí)仍然能用到這個(gè)值。注意:局部靜態(tài)變量只會(huì)被初始化一次。

看完了上面,我們來(lái)看下面這段代碼:

#include<stdio.h>

static int i = 0;

void fun1(int times) {
    i = 0;
    i ++;
    printf("k = %d, i = %d\r\n", times, i);
}

void fun2(int times) {
    static int j = 0;
    j ++;
    printf("k = %d, j = %d\r\n", times, j);
}

int main(int argc, char* argv[]) {
    int k = 0;
    for(k = 0; k < 10; k++) {
        fun1(k);
        fun2(k);
    }
    return 0;
}

這段代碼會(huì)輸出什么?好了,我們來(lái)分析:
i是全局靜態(tài)變量,j是局部靜態(tài)變量。每次循環(huán)調(diào)用fun1會(huì)把i置為0。然后對(duì)i++,這樣每次i輸出的都是1。當(dāng)?shù)谝淮窝h(huán)調(diào)用fun2的時(shí)候會(huì)創(chuàng)建一個(gè)局部靜態(tài)變量j,并存儲(chǔ)在內(nèi)存靜態(tài)區(qū)函數(shù)執(zhí)行完不會(huì)被銷毀。這樣之后再次循環(huán)的時(shí)候就不會(huì)再static int j了,因?yàn)閮?nèi)存已經(jīng)有了。所以每次就執(zhí)行了j++的操作。輸出1、2、3...

那么我在函數(shù)內(nèi)部再定義一個(gè)和全局靜態(tài)變量名稱相同的局部靜態(tài)變量會(huì)怎樣?會(huì)不會(huì)覆蓋掉全局的或者報(bào)錯(cuò)?答案是否定的,大家都相自安好。

開(kāi)始打臉:如果你認(rèn)真讀的話,會(huì)看到我上面留了一個(gè)打臉的標(biāo)記。開(kāi)始打臉。

上面我們說(shuō)了全局靜態(tài)變量的作用域只是在定義它的那個(gè)文件中,別的文件就無(wú)法訪問(wèn)。也說(shuō)了局部靜態(tài)變量的作用域是在函數(shù)的內(nèi)部,外部無(wú)法訪問(wèn)。但是我們就想用外部文件的全局靜態(tài)變量,我們就想獲取到局部靜態(tài)變量的值怎么辦?很簡(jiǎn)單,帶你干。。。

//外部文件test.h
static int j = 0;
int getValue(void) {
    return j;
}
void setValue(int value) {
    j = value;
}

//主文件main.c
#include <stdio.h>
#include "test.h"
int configNum(int value) {
    static int i = 0;
    i = value;
    return i;
}
int main(int argc, char* argv[]) {
    int res1 = 0, res2 = 0;
    res1 = configNum(9);
    printf("%d\r\n", res1);
    
    res2 = getValue();
    printf("%d\r\n", res2);
    
    setValue(521);
    res2 = getValue();
    printf("%d\r\n", res2);
    
    printf("Hello,World!");
    return 0;
}

我們?cè)谕獠课募est.h中定義了這個(gè)文件的全局變量j。按照道理j只能在該文件中被訪問(wèn)得到。但是我們通過(guò)兩個(gè)方法getValue和setValue就可以在外部main.c中使用這個(gè)全局靜態(tài)變量j了。我們?cè)趍ain.c文件中有一個(gè)configNum函數(shù),這里定義了一個(gè)局部靜態(tài)變量,我們可以通過(guò)return將這個(gè)局部靜態(tài)變量返回出去,外部函數(shù)就可以拿到他了。

1.3.2 作用二:修飾函數(shù)

函數(shù)前加static使得函數(shù)成為靜態(tài)函數(shù)。但此處“static”不是指存儲(chǔ)方式,而是指對(duì)函數(shù)的作用域僅局限于本文件(所以又稱內(nèi)部函數(shù))。使用內(nèi)部函數(shù)的好處是:不同的人編寫不同的函數(shù)時(shí),不用擔(dān)心自己定義的函數(shù)是否會(huì)與其他文件中的函數(shù)同名。

起初,在C中引入關(guān)鍵字static是為了表示退出一個(gè)塊后仍然存在局部變量。隨后,static在C中有了第二種含義:用來(lái)表示不能被其他文件訪問(wèn)的全局變量和函數(shù)。

1.4 基本數(shù)據(jù)類型----short、int、long、char、float、double

C語(yǔ)言包含的數(shù)據(jù)類型如下:

數(shù)據(jù)類型 ————
基本類型 數(shù)值類型、字符類型char
構(gòu)造類型 數(shù)組、結(jié)構(gòu)體struct、 共用體union、 枚舉類型enum
指針類型
空類型void

其中數(shù)值類型又分為整型和浮點(diǎn)型

數(shù)值類型 ————
整型 短整型short、整型int、長(zhǎng)整型long、
浮點(diǎn)型 單精度f(wàn)loat、雙精度double

1.4.1 數(shù)據(jù)類型與模子

short、int、char、float、double這六個(gè)關(guān)鍵字代表C語(yǔ)言里的6種基本的數(shù)據(jù)類型。 這6種基本數(shù)據(jù)類型就是6個(gè)模子,你想要什么樣的數(shù)據(jù)就找那個(gè)樣的模子去刻出來(lái),比如在32位機(jī)上,short就是2個(gè)字節(jié),int就是4個(gè)字節(jié),char就是一個(gè)字節(jié),float就是4個(gè)字節(jié),doublue就是8個(gè)字節(jié)。但也不是絕對(duì)的,不同的系統(tǒng)或編譯環(huán)境可能是不一樣的,最好用sizeof測(cè)量一下。

你想要一個(gè)4字節(jié)的整型數(shù)據(jù)那你就用int去刻一個(gè),但是你刻出來(lái)這么多怎么分得清?我們這時(shí)可以給他起個(gè)名字,叫大毛、二毛都行。

1.4.2 變量的命名規(guī)則

叫大毛、二毛也太俗了,人家都叫andy、coco多洋氣。具體起什么名看下面:

  • 命名應(yīng)當(dāng)直觀且可以拼讀,可望文知意,便于記憶和閱讀。

    標(biāo)識(shí)符最好采用英文單詞或其組合,不允許使用拼音。程序中的英文單詞一般不要太復(fù)雜,用詞應(yīng)當(dāng)準(zhǔn)確。

  • 命名的長(zhǎng)度應(yīng)當(dāng)符合“min-length && max-information”原則。

    C 是一種簡(jiǎn)潔的語(yǔ)言, 命名也應(yīng)該是簡(jiǎn)潔的。例如變量名 MaxVal 就比MaxValueUntilOverflow 好用。標(biāo)識(shí)符的長(zhǎng)度一般不要過(guò)長(zhǎng),較長(zhǎng)的單詞可通過(guò)去掉“元音”形成縮寫。

  • 當(dāng)標(biāo)識(shí)符由多個(gè)詞組成時(shí),每個(gè)詞的第一個(gè)字母大寫,其余全部小寫。

  • 盡量避免名字中出現(xiàn)數(shù)字編號(hào),如 Value1,Value2 等,除非邏輯上的確需要編號(hào)。比如驅(qū)動(dòng)開(kāi)發(fā)時(shí)為管腳命名,非編號(hào)名字反而不好。

  • 對(duì)在多個(gè)文件之間共同使用的全局變量或函數(shù)要加范圍限定符(建議使用模塊名(縮寫)作為范圍限定符)。

  • 標(biāo)識(shí)符的命名規(guī)則:太多了不想寫

  • 程序中不得出現(xiàn)僅靠大小寫區(qū)分的相似的標(biāo)識(shí)符。

  • 一個(gè)函數(shù)名禁止被用于其它之處。

  • 所有宏定義、枚舉常數(shù)、只讀變量全用大寫字母命名,用下劃線分割單詞。

  • 考慮到習(xí)慣性問(wèn)題,局部變量中可采用通用的命名方式,僅限于 n、 i、 j 等作為循環(huán)變量使用。

    一般來(lái)說(shuō)習(xí)慣上用 n,m,i,j,k 等表示 int 類型的變量; c, ch 等表示字符類型變量; a 等表示數(shù)組; p 等表示指針。當(dāng)然這僅僅是一般習(xí)慣,除了 i,j,k 等可以用來(lái)表示循環(huán)變量外,別的字符變量名盡量不要使用。

  • 定義變量的同時(shí)千萬(wàn)千萬(wàn)別忘了初始化。定義變量時(shí)編譯器并不一定清空了這塊內(nèi)存,它的值可能是無(wú)效的數(shù)據(jù)。

  • 不同類型數(shù)據(jù)之間的運(yùn)算要注意精度擴(kuò)展問(wèn)題,一般低精度數(shù)據(jù)將向高精度數(shù)據(jù)擴(kuò)展。

1.5 最冤枉的關(guān)鍵字----sizeof

1.5.1 常年被人誤認(rèn)為函數(shù)

sizeof 是關(guān)鍵字不是函數(shù),看下面的例子:

int i=0;
A),sizeof(int); B),sizeof(i); C),sizeof int; D),sizeof i;

毫無(wú)疑問(wèn), 32 位系統(tǒng)下 A), B)的值為 4。那 C)的呢? D)的呢?
在 32 位系統(tǒng)下,通過(guò) Visual C++6.0 或任意一編譯器調(diào)試,我們發(fā)現(xiàn) D)的結(jié)果也為 4。沒(méi)有括號(hào)居然也行,那想想,函數(shù)名后面沒(méi)有括號(hào)行嗎?由此輕易的出 sizeof 絕非函數(shù)。但是 C)出錯(cuò)了,因?yàn)閟izeof是關(guān)鍵字,int也是關(guān)鍵字,兩個(gè)關(guān)鍵字在一起是啥?編譯器嘗試去結(jié)合關(guān)鍵字(像const int這樣)但是失敗。

總結(jié):sizeof 在計(jì)算變量所占空間大小時(shí),括號(hào)可以省略,而計(jì)算類型(模子)大小時(shí)不能省略。

1.5.2 sizeof(int)*p 表示什么意思?

1.6 if、else組合

1.6.1 bool 變量與“零值”進(jìn)行比較

bool變量與“零值”進(jìn)行比較的if語(yǔ)句怎么寫?

bool bTestFlag = FALSE;//想想為什么一般初始化為 FALSE 比較好?
A), if(bTestFlag == 0); if(bTestFlag == 1);
B), if(bTestFlag == TRUE); if(bTestFlag == FLASE);
C), if(bTestFlag); if(!bTestFlag);

哪種方法比較好?我們來(lái)分析:

A)寫法: bTestFlag 是什么?整型變量?如果要不是這個(gè)名字遵照了前面的命名規(guī)范,肯怕很容易讓人誤會(huì)成整型變量。所以這種寫法不好。

B)寫法: FLASE 的值大家都知道,在編譯器里被定義為 0;但 TRUE 的值呢?都是 1嗎?很不幸,不都是 1。 Visual C++定義為 1,而它的同胞兄弟Visual Basic 就把 TRUE 定義為-1.那很顯然,這種寫法也不好。

大家都知道 if 語(yǔ)句是靠其后面的括號(hào)里的表達(dá)式的值來(lái)進(jìn)行分支跳轉(zhuǎn)的。表達(dá)式如果為真,則執(zhí)行 if 語(yǔ)句后面緊跟的代碼;否則不執(zhí)行。那顯然,本組的寫法很好,既不會(huì)引起誤會(huì),也不會(huì)由于 TRUE 或 FLASE 的不同定義值而出錯(cuò)。記住:以后寫代碼就得這樣寫。

1.6.2 float 變量與“零值”進(jìn)行比較

float變量與“零值”進(jìn)行比較的if語(yǔ)句怎么寫?

float fTestVal = 0.0;
A), if(fTestVal == 0.0); if(fTestVal != 0.0);
B), if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON));//EPSINON 為定義好的精度。

哪一組或是那些組正確呢?我們來(lái)分析分析:

float 和 double 類型的數(shù)據(jù)都是有精度限制的,這樣直接拿來(lái)與 0.0 比,能正確嗎?明顯不能,看例子: 的值四舍五入精確到小數(shù)點(diǎn)后 ? 10 位為: 3.1415926536,你拿它減去0.00000000001 然后再四舍五入得到的結(jié)果是多少?你能說(shuō)前后兩個(gè)值一樣嗎?EPSINON 為定義好的精度,如果一個(gè)數(shù)落在[0.0-EPSINON,0.0+EPSINON] 這個(gè)閉區(qū)間內(nèi),我們認(rèn)為在某個(gè)精度內(nèi)它的值與零值相等;否則不相等。擴(kuò)展一下,把 0.0 替換為你想比較的任何一個(gè)浮點(diǎn)數(shù),那我們就可以比較任意兩個(gè)浮點(diǎn)數(shù)的大小了,當(dāng)然是在某個(gè)精度內(nèi).同樣的也不要在很大的浮點(diǎn)數(shù)和很小的浮點(diǎn)數(shù)之間進(jìn)行運(yùn)算,如下代碼:

#include <stdio.h>
int main(int argc, char *argv[]) {
    float a = 10000000000.00;
    float b = 0.000001;
    float c = 0.0000001;
    printf("a + b = %f\r\n", a + b);
    printf("a + c = %f\r\n", a + c);
    return 0;
}
//結(jié)果:
//a + b = 10000000000.000002
//a + c = 10000000000.000000

1.6.3 指針變量與“零值”進(jìn)行比較

指針變量與“零值”進(jìn)行比較的if語(yǔ)句怎么寫?

int *p = NULL;//定義指針一定要同時(shí)初始化
A), if(p == 0); if(p != 0);
B), if(p); if(!p);
C) , if(NULL == p); if(NULL != p);

哪一組或是那些組正確呢?我們來(lái)分析分析:

A)寫法: p 是整型變量?容易引起誤會(huì),不好。盡管 NULL 的值和 0 一樣,但意義不同。

B)寫法: p 是 bool 型變量?容易引起誤會(huì),不好。

C)寫法:這個(gè)寫法才是正確的,但樣子比較古怪。為什么要這么寫呢?是怕漏寫一個(gè)“=”號(hào):if(p = NULL),這個(gè)表達(dá)式編譯器當(dāng)然會(huì)認(rèn)為是正確的,但卻不是你要表達(dá)的意思。所以,非常推薦這種寫法。

1.6.4 else到底與哪個(gè)if配對(duì)呢?

else常常與if語(yǔ)句配對(duì),但要注意書寫規(guī)范??创a:

if(0 == x) 
if(0 == y) error();
else {
    //program code
}

C 語(yǔ)言有這樣的規(guī)定: else始終與同一括號(hào)內(nèi)最近的未匹配的 if 語(yǔ)句結(jié)合。

1.6.5 if 語(yǔ)句后面的分號(hào)

關(guān)于 if-else 語(yǔ)句還有一個(gè)容易出錯(cuò)的地方就是與空語(yǔ)句的連用。

if(NULL != p);
fun();

這里的 fun()函數(shù)并不是在 NULL != p 的時(shí)候被調(diào)用,而是任何時(shí)候都會(huì)被調(diào)用。問(wèn)題就出在 if 語(yǔ)句后面的分號(hào)上。在 C 語(yǔ)言中,分號(hào)預(yù)示著一條語(yǔ)句的結(jié)尾,但是并不是每條 C 語(yǔ)言語(yǔ)句都需要分號(hào)作為結(jié)束標(biāo)志。if 語(yǔ)句的后面并不需要分號(hào), 但如果你不小心寫了個(gè)分號(hào),編譯器并不會(huì)提示出錯(cuò)。因?yàn)榫幾g器會(huì)把這個(gè)分號(hào)解析成一條空語(yǔ)句。上面的代碼實(shí)際等效于:

if(NULL != p) {
    ;
}
fun();

建議在真正需要用空語(yǔ)句時(shí)寫成這樣:NULL;而不是單用一個(gè)分號(hào)。這就好比匯編語(yǔ)言里面的空指令,比如 ARM 指令中的 NOP

1.6.6 使用if語(yǔ)句的其他注意事項(xiàng)

  • 先處理正常情況,再處理異常情況。

    在編寫代碼是,要使得正常情況的執(zhí)行代碼清晰,確認(rèn)那些不常發(fā)生的異常情況處理代碼不會(huì)遮掩正常的執(zhí)行路徑。這樣對(duì)于代碼的可讀性和性能都很重要。因?yàn)椋?if 語(yǔ)句總是需要做判斷,而正常情況一般比異常情況發(fā)生的概率更大(否則就應(yīng)該把異常正常調(diào)過(guò)來(lái)了),如果把執(zhí)行概率更大的代碼放到后面,也就意味著 if 語(yǔ)句將進(jìn)行多次無(wú)謂的比較。另外,非常重要的一點(diǎn)是,把正常情況的處理放在 if 后面,而不要放在 else 后面。當(dāng)然這也符合把正常情況的處理放在前面的要求。

1.7 switch、case組合

既然有了 if、 else 組合為什么還需要 switch、 case 組合呢?

1.7.1 不要拿青龍偃月刀去削蘋果

if、 else 一般表示兩個(gè)分支或是嵌套表示少量的分支,但如果分支很多的話……還是用switch、 case 組合吧。格式吧,你們都會(huì)。

  • 每個(gè) case 語(yǔ)句的結(jié)尾絕對(duì)不要忘了加 break,否則將導(dǎo)致多個(gè)分支重疊(除非有意使多個(gè)分支重疊)。

  • 最后必須使用 default 分支。即使程序真的不需要 default 處理,也應(yīng)該保留語(yǔ)句:這樣做并非畫蛇添足,可以避免讓人誤以為你忘了 default 處理。(有的公司代碼review的時(shí)候會(huì)有人挑你的這個(gè)刺)

default:
    break;

1.7.2 case 關(guān)鍵字后面的值有什么要求嗎?

#include<stdio.h>
int main(int argc, char *argv[]) {
    int a = 4, b = 0;
    switch(a) {
    case 'a':
        break;
    case "abc":
        break;
    case 1 + 1:
        break;  
    case 3/2:
        break;
    case -1:
        break;  
    case 1.0 + 2:
        break;  
    case b:
        break;
    case NULL:
        break;
    case 4:
        break;          
    default:
        break;  
    }
    return 0;
}

上面的代碼哪幾個(gè)case是對(duì)的哪幾個(gè)是錯(cuò)的。

記?。?case 后面只能是整型或字符型的常量或常量表達(dá)式(想想字符型數(shù)據(jù)在內(nèi)存里是怎么存的)。(js里case后面想放啥放啥)

上面的case中,case "abc":是錯(cuò)的,不能是字符串,可以是字符;case 1.0 + 2:是錯(cuò)的,不能是浮點(diǎn)數(shù),可以是整數(shù);case: b:是錯(cuò)的,不能是變量;case NULL:是錯(cuò)的,不能是空; 有的人會(huì)問(wèn)case 3/2:不也是浮點(diǎn)數(shù)嗎?不是啦,3/2得出來(lái)的是1不是1.5。

1.7.3 case語(yǔ)句的排列順序

  • 按字母或數(shù)字順序排列各條 case 語(yǔ)句。

    如果所有的 case 語(yǔ)句沒(méi)有明顯的重要性差別,那就按 A-B-C 或 1-2-3 等順序排列 case語(yǔ)句。這樣做的話,你可以很容易的找到某條 case 語(yǔ)句。

  • 把正常情況放在前面,而把異常情況放在后面。

    如果有多個(gè)正常情況和異常情況,把正常情況放在前面,并做好注釋;把異常情況放在后面,同樣要做注釋。

  • 按執(zhí)行頻率排列 case 語(yǔ)句

    把最常執(zhí)行的情況放在前面,而把最不常執(zhí)行的情況放在后面。最常執(zhí)行的代碼可能也是調(diào)試的時(shí)候要單步執(zhí)行的最多的代碼。如果放在后面的話,找起來(lái)可能會(huì)比較困難,而放在前面的話,可以很快的找到

1.7.4 使用case語(yǔ)句的其他注意事項(xiàng)

  • 簡(jiǎn)化每種情況對(duì)應(yīng)的操作。

    使得與每種情況相關(guān)的代碼盡可能的精煉。case 語(yǔ)句后面的代碼越精煉, case 語(yǔ)句的結(jié)果就會(huì)越清晰。如果某個(gè) case 語(yǔ)句確實(shí)需要這么多的代碼來(lái)執(zhí)行某個(gè)操作,那可以把這些操作寫成一個(gè)或幾個(gè)子程序,然后在 case 語(yǔ)句后面調(diào)用這些子程序就 ok 了。一般來(lái)說(shuō) case語(yǔ)句后面的代碼盡量不要超過(guò) 20 行。

  • 不要為了使用 case 語(yǔ)句而刻意制造一個(gè)變量。

    case 語(yǔ)句應(yīng)該用于處理簡(jiǎn)單的,容易分類的數(shù)據(jù)。如果你的數(shù)據(jù)并不簡(jiǎn)單,那可能使用if else if 的組合更好一些。為了使用 case 而刻意構(gòu)造出來(lái)的變量很容易把人搞糊涂,應(yīng)該避免這種變量。

  • 把 default 子句只用于檢查真正的默認(rèn)情況。

    有時(shí)候,你只剩下了最后一種情況需要處理,于是就決定把這種情況用 default 子句來(lái)處理。這樣也許會(huì)讓你偷懶少敲幾個(gè)字符,但是這卻很不明智。這樣將失去 case 語(yǔ)句的標(biāo)號(hào)所提供的自說(shuō)明功能,而且也喪失了使用 default 子句處理錯(cuò)誤情況的能力。所以,奉勸你不要偷懶,老老實(shí)實(shí)的把每一種情況都用 case 語(yǔ)句來(lái)完成,而把真正的默認(rèn)情況的處理交給 default 子句。

1.8 do、while、for關(guān)鍵字

C 語(yǔ)言中循環(huán)語(yǔ)句有三種: while 循環(huán)、 do-while 循環(huán)、 for 循環(huán)。

while 循環(huán):先判斷 while 后面括號(hào)里的值,如果為真則執(zhí)行其后面的代碼;否則不執(zhí)行。 while( 1)表示死循環(huán)。

#include<stdio.h>
int main(int argc, char *argv[]) {
    while(1) {
        if('#' == getchar()) {
            break;
        }
    }
    printf("Hello,World!");
}

執(zhí)行一個(gè)死循環(huán),等待用戶輸入'#'后退出死循環(huán)執(zhí)行輸出"Hello,World!"

1.8.1 break 與 continue 的區(qū)別

  • break關(guān)鍵字很重要,表示終止本層循環(huán)。上面這個(gè)例子只有一個(gè)循環(huán),當(dāng)代碼執(zhí)行到break時(shí)候就會(huì)停止。

  • continue表示終止本次循環(huán),當(dāng)代碼執(zhí)行到continue時(shí),本次循環(huán)終止,進(jìn)入下一次循環(huán)。

  • while(1)也可以寫成while(true)或者while(1 == 1)或者while((bool) 1)等形式的,效果一樣的。

  • do-while循環(huán):先執(zhí)行do后面的代碼,然后再判斷while后面括號(hào)里的值,如果為真,循環(huán)開(kāi)始;否則,循環(huán)不開(kāi)始。也就是說(shuō)無(wú)論如何都會(huì)執(zhí)行一次,用法與while差不多,但是相對(duì)少用。

  • for循環(huán): for的好處是很容易控制循環(huán)的次數(shù),多用于事先知道循環(huán)次數(shù)的情況下。

  • 在switch case語(yǔ)句中能否能使用continue關(guān)鍵字?為什么?
    不說(shuō)廢話,寫份代碼不就知道了嗎?

    #include<stdio.h>
    int main(int argc, char *argv[]) {
        int a = 3;
        switch(a) {
            case 1:
            case 2:
            case 3:
                printf("%d\r\n", a);
                continue;
            default: 
                break;
        }
        printf("結(jié)束了\r\n");
        return 0;
    }
    

    上面的代碼編譯之后出現(xiàn)了錯(cuò)誤,提示是:error: continue statement not within a loop也就是說(shuō)continue需要在一個(gè)循環(huán)里面,不然就會(huì)報(bào)錯(cuò)。

1.8.2循環(huán)語(yǔ)句注意點(diǎn)

  • 在多重循環(huán)中,盡量將最長(zhǎng)的循環(huán)放在最內(nèi)層,最短的循環(huán)放在最外層,以減少CPU跨切循環(huán)層的次數(shù)。

  • 建議 for 語(yǔ)句的循環(huán)控制變量的取值采用“半開(kāi)半閉區(qū)間”寫法。半開(kāi)半閉區(qū)間寫法和閉區(qū)間寫法雖然功能是相同,但相比之下,半開(kāi)半閉區(qū)間寫法寫法更加直觀。如:半開(kāi)半閉區(qū)間寫法:for (n = 0; n < 10; n++) 閉區(qū)間寫法:for (n = 0; n <= 9; n++)

  • 不要在for循環(huán)體內(nèi)修改循環(huán)變量,防止循環(huán)失控。

    for(n = 0; n < 10; n++) {
        ...
        n = 8;//不建議,
        ...
    }
    
  • 循環(huán)要盡可能的短,使代碼清晰,一目了然。不要超過(guò)一屏。

    • 從新設(shè)計(jì)循環(huán)
    • 將循環(huán)封裝到函數(shù)內(nèi)
    • 一般來(lái)說(shuō)循環(huán)內(nèi)代碼不要超過(guò)20行
  • 循環(huán)控制在3層內(nèi)

1.9 goto關(guān)鍵字

建議少用或禁用關(guān)鍵字,不過(guò)看過(guò)一些系統(tǒng)級(jí)的代碼里面就有好多goto,但是他們使用goto并不會(huì)使代碼混亂,反而使得更容易理解閱讀。作為小白,還是不要用了。

自從提倡結(jié)構(gòu)化設(shè)計(jì)以來(lái), goto 就成了有爭(zhēng)議的語(yǔ)句。首先,由于 goto 語(yǔ)句可以靈活跳轉(zhuǎn),如果不加限制,它的確會(huì)破壞結(jié)構(gòu)化設(shè)計(jì)風(fēng)格;其次, goto 語(yǔ)句經(jīng)常帶來(lái)錯(cuò)誤或隱患。它可能跳過(guò)了變量的初始化、重要的計(jì)算等語(yǔ)句。

1.10 void關(guān)鍵字

1.10.1 void a

void的字面意思是“空類型”,void * 則為“空類型指針”,void * 可以指向任何類型的數(shù)據(jù)。

void a;會(huì)是什么? 這樣語(yǔ)句在編譯時(shí)候會(huì)出錯(cuò),即使不出錯(cuò)也沒(méi)有意義。

void真正發(fā)揮的作用在于:

  1. 對(duì)函數(shù)返回的限定;
  2. 對(duì)函數(shù)參數(shù)的限定;

眾所周知, 如果指針 p1 和 p2 的類型相同, 那么我們可以直接在 p1 和 p2 間互相賦值;如果 p1 和 p2 指向不同的數(shù)據(jù)類型,則必須使用強(qiáng)制類型轉(zhuǎn)換運(yùn)算符把賦值運(yùn)算符右邊的指針類型轉(zhuǎn)換為左邊指針的類型。
例如:

float *p1;
int *p2;
p1 = p2;

其中p1 = p2;語(yǔ)句會(huì)編譯出錯(cuò),提示“'=' : cannot convertfrom 'int *' to 'float *'”,必須改為:p1 = (float *)p2;

而void *則不同,任何類型的指針都可以直接給他賦值,無(wú)需進(jìn)行強(qiáng)制類型轉(zhuǎn)換:

void *p1;
int *p2;
p1 = p2;

但這并不意味著, void *也可以無(wú)需強(qiáng)制類型轉(zhuǎn)換地賦給其它類型的指針。因?yàn)椤翱疹愋汀笨梢园荨坝蓄愋汀?,而“有類型”則不能包容“空類型”。下面代碼就會(huì)出錯(cuò)。

void *p1;
int *p2;
p2 = p1;

提示“'=' : cannot convert from 'void *' to 'int *'”。

1.10.2 void修飾函數(shù)返回值和參數(shù)

  • 如果函數(shù)沒(méi)有返回值,那么應(yīng)聲明為 void 類型。在C語(yǔ)言中,凡不加返回值類型限定的函數(shù),就會(huì)被編譯器作為返回整型值處理。但是許多程序員卻誤以為其為void類型。如下:
add(int a, int b) {
    return a + b;
}

int mian(int argc, char *argv[]) {//甚至很多人認(rèn)為main函數(shù)無(wú)返回值或認(rèn)為返回值為void型,其實(shí)人家是int型的
    printf("2 + 3 = %d", add(2, 3));
}

程序運(yùn)行的輸出結(jié)果為輸出: 2 + 3 = 5
這說(shuō)明不加返回值聲明的函數(shù)的確為int函數(shù)。

因此,為了避免混亂,我們?cè)诰帉?C 程序時(shí),對(duì)于任何函數(shù)都必須一個(gè)不漏地指定其類型。如果函數(shù)沒(méi)有返回值,一定要聲明為 void 類型。這既是程序良好可讀性的需要,也是編程規(guī)范性的要求。另外,加上 void 類型聲明后,也可以發(fā)揮代碼的“自注釋”作用。所謂的代碼的“自注釋”即代碼能自己注釋自己。

如果函數(shù)無(wú)參數(shù),那么應(yīng)聲明其參數(shù)為void

在C++語(yǔ)言中聲明一個(gè)這樣的函數(shù):

int function(void) {
    return 1;
}

則進(jìn)行下面的調(diào)用時(shí)不合法的: function(2);

因?yàn)樵贑++中,函數(shù)參數(shù)是void的意思表示這個(gè)函數(shù)不接受任何參數(shù)。但是在Turbo C2.0中編譯:

#include<stdio.h>
fun() {
    return 1;
}
main() {
    printf("%d", fun(2));
    getchar();
}

編譯正確輸出1,這說(shuō)明,在C語(yǔ)言中,可以給無(wú)參數(shù)的函數(shù)傳送任意類型的參數(shù),但是在C++編譯器中編譯同樣的代碼則會(huì)出錯(cuò)。在C++中,不能向無(wú)參數(shù)的函數(shù)傳送任何線參數(shù),出錯(cuò)提示“'fun' : function does not take 1 parameters”。所以,無(wú)論在 C 還是 C++中,若函數(shù)不接受任何參數(shù),一定要指明參數(shù)為 void

1.10.3 void指針

  • 千萬(wàn)小心使用void指針

按照 ANSI(American National Standards Institute)標(biāo)準(zhǔn),不能對(duì) void 指針進(jìn)行算法操作,即下列操作都是不合法的:

void *pvoid;
pvoid++;//ANSI:錯(cuò)誤
pvoid+=1;//ANSI:錯(cuò)誤

ANSI標(biāo)準(zhǔn)之所以這樣認(rèn)定,是因?yàn)樗鼒?jiān)持:進(jìn)行算法操作的指針必須是知道指向數(shù)據(jù)類型大小的。也就是說(shuō)必須知道內(nèi)存目的地址的確切值。
例如:

int *pint;
pint++;//ANSI:正確

但是大名鼎鼎的 GNU(GNU's Not Unix 的縮寫)則不這么認(rèn)定,它指定 void *的算法操作與 char *一致。因此下列語(yǔ)句在 GNU 編譯器中皆正確:

pvoid++;//GNU:正確
pvoid+=1;//GNU:正確

在實(shí)際的程序設(shè)計(jì)中,為符合 ANSI 標(biāo)準(zhǔn),并提高程序的可移植性,我們可以這樣編寫實(shí)現(xiàn)同樣功能的代碼:

void *pvoid;
(char *)pvoid ++;//ANSI:正確;GNU:正確
(char *)pvoid +=1;//ANSI:錯(cuò)誤;GNU:正確

GNU 和 ANSI 還有一些區(qū)別,總體而言, GNU 較 ANSI 更“開(kāi)放”,提供了對(duì)更多語(yǔ)法的支持。但是我們?cè)谡鎸?shí)設(shè)計(jì)時(shí),還是應(yīng)該盡可能地符合 ANSI 標(biāo)準(zhǔn)。

  • 如果函數(shù)的參數(shù)可以是任意類型指針,那么應(yīng)聲明其參數(shù)為void *

典型的如內(nèi)存操作函數(shù)memcpy和memset的函數(shù)原型分別為:

void *memcpy(void *dest, const void *src, size_t len);
void *memset(void *buffer, int c, size_t num);

這樣,任何類型的指針都可以傳入 memcpy 和 memset 中,這也真實(shí)地體現(xiàn)了內(nèi)存操作函數(shù)的意義, 因?yàn)樗僮鞯膶?duì)象僅僅是一片內(nèi)存, 而不論這片內(nèi)存是什么類型。

1.10.4 void不能代表一個(gè)真實(shí)的變量

因?yàn)槎x變量時(shí)必須分配內(nèi)存空間,定義void類型變量,編譯器到底分配多大內(nèi)存呢?下面試圖讓void代表一個(gè)真實(shí)的變量,因此都是錯(cuò)誤代碼:

void a;//錯(cuò)誤
function(void a);//錯(cuò)誤

void 體現(xiàn)了一種抽象,這個(gè)世界上的變量都是“有類型”的,譬如一個(gè)人不是男人就是女人(人妖不算)。void 的出現(xiàn)只是為了一種抽象的需要,如果你正確地理解了面向?qū)ο笾小俺橄蠡悺钡母拍?,也很容易理?void 數(shù)據(jù)類型。正如不能給抽象基類定義一個(gè)實(shí)例,我們也不能定義一個(gè) void(讓我們類比的稱 void 為“抽象數(shù)據(jù)類型”)變量。

1.11 return關(guān)鍵字

return 用來(lái)終止一個(gè)函數(shù)并返回其后面跟著的值。

  • return (val);//此括號(hào)可以省略。但一般不省略,尤其在返回一個(gè)表達(dá)式的值的時(shí)候。

    char *Func(void) {
        char str[30];
        ...
        return str;
    }
    
  • str屬于局部變量,位于棧內(nèi)存中,在Func結(jié)束的時(shí)候被釋放,所以返回str將導(dǎo)致錯(cuò)誤。

  • return語(yǔ)句不可返回指向“棧內(nèi)存”的“指針”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。

1.12 const關(guān)鍵字也許該被替換為readonly

很多人認(rèn)為const修飾的值為常量,這是不準(zhǔn)確的,準(zhǔn)確的說(shuō)應(yīng)該是只讀的變量,其值在編譯時(shí)不能被使用,因?yàn)榫幾g器在編譯的時(shí)候不知道其存儲(chǔ)的內(nèi)容?;蛟S當(dāng)初這個(gè)關(guān)鍵字應(yīng)該被定義為readonly。

const推出的初始目的,正是為了取代預(yù)編譯指令,消除他的缺點(diǎn),同時(shí)繼承他的優(yōu)點(diǎn)。我們看看他與define宏的區(qū)別。(很多人誤認(rèn)為define是關(guān)鍵字,在這里我提醒你再回到本章前面看看32個(gè)關(guān)鍵字里是否有define)

1.12.1 const修飾的只讀變量

定義const只讀變量,具有不可變性。例如:

const int Max = 100;
int Array[Max];

這里請(qǐng)?jiān)赩C++6.0里分別創(chuàng)建.c和.cpp文件測(cè)試一下。你會(huì)發(fā)現(xiàn)在.c文件中,編譯器會(huì)提示出錯(cuò),而在.cpp文件中則順利運(yùn)行。為什么呢?我們知道定義一個(gè)數(shù)組必須制定其元素個(gè)數(shù)。這也側(cè)面證明在C語(yǔ)言中,const修飾的Max仍然是變量,之不過(guò)是只讀罷了;而在C++里,擴(kuò)展了const的含義。

注意:const修飾的只讀變量必須在定義的同時(shí)初始化,因?yàn)樵谶\(yùn)行的過(guò)程中還能被賦值的話那說(shuō)明就不是只讀的了。

想一想case后面是否可以是const修飾的只讀變量呢?

分析:我們知道case后面需要的是整型、字符型的常量或常量表達(dá)式,上面我們又知道了case在C語(yǔ)言里其實(shí)是只讀的變量,所以它不能放在case后面。

1.12.2 節(jié)省空間,避免不必要的內(nèi)存分配,同時(shí)提高效率

編譯器通常不為普通的const只讀變量分配存儲(chǔ)空間,而是將他們保存在符號(hào)表中,這使得它成為一個(gè)編譯期間的值,沒(méi)有了存儲(chǔ)與讀內(nèi)存的操作,使得他的效率也很高。看下面代碼:

#define M 3//宏常量
const int N = 5; // 此時(shí)并未將N放入內(nèi)存中
...
int i = N; //此時(shí)為N分配內(nèi)存,以后不再分配!
int I = M; //預(yù)編譯期間進(jìn)行宏替換,分配內(nèi)存
int j = N; //沒(méi)有分配內(nèi)存
int J = M; //再進(jìn)行宏替換,又分配一次內(nèi)存!

const和#define的對(duì)比:

const定義的只讀變量從匯編的角度來(lái)看,只是給出了內(nèi)存的地址,而不是像#define一樣給出的是立即數(shù),所以const定義的只讀變量在程序運(yùn)行過(guò)程中只有一份拷貝(因?yàn)樗侨值闹蛔x變量,放在靜態(tài)區(qū)域),而#define定義的宏常量在內(nèi)存中有若干個(gè)拷貝。#define宏是在預(yù)編譯階段進(jìn)行宏替換,而const修飾的只讀變量是在編譯的時(shí)候確定其值。#define宏沒(méi)有類型,而const修飾的只讀變量有特定的類型。

1.12.3 修飾一般變量

一般的常量是指簡(jiǎn)單類型的只讀變量。這種只讀變量在定義時(shí),修飾符const可以放在類型說(shuō)明符之前,也可用在類型說(shuō)明符之后。例如:

int const i = 2; 或
const int i = 2;

i.12.4 修飾數(shù)組

定義說(shuō)說(shuō)明一個(gè)只讀數(shù)組可采用如下格式:

int const a[5] = {1, 2, 3, 4, 5};或
const int a[5] = {1, 2, 3, 4, 5};

1.12.5 修飾指針

  • const int *p; //p可變,p指向的對(duì)象不可變

  • int const *p; //p可變,p指向的對(duì)象不可變

  • int *const p; //p不可變,p指向的對(duì)象可變

  • const int *const p; //指針p和p指向的值都不可變

記憶:“左值右指” const在*的左邊表示值不變,const在*的右邊表示指針不變,const即在*左邊又在*的右邊表示值和指針都不可變。

1.12.6修飾函數(shù)的參數(shù)

const修飾符也可以修飾函數(shù)的參數(shù),當(dāng)不希望這個(gè)參數(shù)值被函數(shù)體內(nèi)意外改變時(shí)使用。例如:

void fun(const int i);

告訴編譯器i在函數(shù)體中不可改變,從而防止了使用者的一些無(wú)意的或錯(cuò)誤的修改。

1.12.7 修飾函數(shù)的返回值

const修飾符也可以修飾函數(shù)的返回值,返回值不可被改變。例如:

const int fun(void);

在另一個(gè)鏈接文件中引用const只讀變量:

extern const int i;//正確的聲明
extern const int j = 10; //錯(cuò)誤,只讀變量的值不能改變

在C++里還有很多其他的特性。。。

1.13 最易變得關(guān)鍵字 ---- volatile

volatile是易變的、不穩(wěn)定的意思。很多人根本就沒(méi)見(jiàn)過(guò)這個(gè)關(guān)鍵字,不知道他的存在。也有很多程序員知道它的存在,但從來(lái)沒(méi)用過(guò)它。

volatile關(guān)鍵字和const一樣是一種類型修飾符,簡(jiǎn)單的說(shuō)就是告訴編譯器volatile 關(guān)鍵字修飾的變量是隨時(shí)可能發(fā)生變化的,每次使用它的時(shí)候必須從內(nèi)存中取出變量的值。從而避免一些奇怪的問(wèn)題出現(xiàn),比如在操作系統(tǒng)中有兩個(gè)線程都來(lái)讀取這個(gè)變量,一個(gè)已經(jīng)改變了它的值,另一個(gè)又來(lái)讀,但編譯器卻把沒(méi)修改之前的值給了第二個(gè)線程??创a:

int i = 10;
int j = i; //語(yǔ)句一
int k = i; //語(yǔ)句二

這時(shí)候編譯器對(duì)代碼進(jìn)行優(yōu)化,因?yàn)樵冢?)、(2)兩條語(yǔ)句中,i沒(méi)有被作用左值。這時(shí)候編譯器認(rèn)為i的值沒(méi)有發(fā)生改變,所以在(1)語(yǔ)句時(shí)從內(nèi)存中去取i的值賦給j之后,這個(gè)值并沒(méi)有被丟掉,而是在(2)語(yǔ)句時(shí)繼續(xù)用這個(gè)值給k賦值。編譯器不會(huì)生成出匯編代碼重新從內(nèi)存里取i的值,這樣提高了效率。但要注意:(1)、(2)語(yǔ)句之間i沒(méi)有被作用左值才行。

再看另外一個(gè)例子:

volatile int i = 10;
int j = i; //(3)語(yǔ)句
int k = i; //(4)語(yǔ)句

volatile 關(guān)鍵字告訴編譯器 i 是隨時(shí)可能發(fā)生變化的,每次使用它的時(shí)候必須從內(nèi)存中取出 i的值,因而編譯器生成的匯編代碼會(huì)重新從 i 的地址處讀取數(shù)據(jù)放在 k 中。

1.14 最會(huì)帶帽子的關(guān)鍵字----extern

extern,外面的、外來(lái)的意思。

extern 可以置于變量或者函數(shù)前,以標(biāo)示變量或者函數(shù)的定義在別的文件中,下面的代碼用到的這些變量或函數(shù)是外來(lái)的,不是本文件定義的,提示編譯器遇到此變量和函數(shù)時(shí)在其他模塊中尋找其定義。就好比在本文件中給這些外來(lái)的變量或函數(shù)帶了頂帽子,告訴本文件中所有代碼,這些家伙不是土著。

看代碼:

//A.c
int i = 0;
void fun(void) {
    //code
}

//B.c文件中用external修飾:
extern int i; //寫成i = 10;行嗎?
extern void fun(void);//兩個(gè)void是否可以省略

//C.h文件中定義:
int j = 1;
int k = 2;

//D.c文件中用external修飾:
extern double j; // 這樣行嗎?為什么
j = 3.0;//這樣行嗎?為什么

我們來(lái)分析:

A.c文件中定義了變量i和函數(shù)fun,在B.c中如果想使用就需要先聲明再使用。聲明不僅要聲明變量或函數(shù)本身還要聲明他們的類型。因此extern int i;是正確的。能不能寫成extern int i = 10;?我們暫不看這里的extern,后面的int i = 10;這是表示定義一個(gè)變量啊,這個(gè)變量我們?cè)贏.c中已經(jīng)定義了,這時(shí)候不報(bào)錯(cuò)才怪。所以說(shuō)聲明的時(shí)候是不能有賦值操作的。D.c文件中的很簡(jiǎn)答都錯(cuò)了。

1.15 struct關(guān)鍵字

struct是個(gè)神奇的關(guān)鍵字,它將一些相關(guān)聯(lián)的數(shù)據(jù)打包成一個(gè)整體,方便使用。

在網(wǎng)絡(luò)協(xié)議、通信控制、嵌入式系統(tǒng)、驅(qū)動(dòng)開(kāi)發(fā)等地方,我們經(jīng)常要傳送的不是簡(jiǎn)單的字節(jié)流( char 型數(shù)組),而是多種數(shù)據(jù)組合起來(lái)的一個(gè)整體,其表現(xiàn)形式是一個(gè)結(jié)構(gòu)體。經(jīng)驗(yàn)不足的開(kāi)發(fā)人員往往將所有需要傳送的內(nèi)容依順序保存在 char 型數(shù)組中,通過(guò)指針偏移的方法傳送網(wǎng)絡(luò)報(bào)文等信息。這樣做編程復(fù)雜,易出錯(cuò),而且一旦控制方式及通信協(xié)議有所變化,程序就要進(jìn)行非常細(xì)致的修改,非常容易出錯(cuò)。這個(gè)時(shí)候只需要一個(gè)結(jié)構(gòu)體就能搞定。平時(shí)我們要求函數(shù)的參數(shù)盡量不多于 4 個(gè),如果函數(shù)的參數(shù)多于 4 個(gè)使用起來(lái)非常容易出錯(cuò) (包括每個(gè)參數(shù)的意義和順序都容易弄錯(cuò)), 效率也會(huì)降低(與具體 CPU 有關(guān),ARM芯片對(duì)于超過(guò) 4 個(gè)參數(shù)的處理就有講究,具體請(qǐng)參考相關(guān)資料)。這個(gè)時(shí)候,可以用結(jié)構(gòu)體壓縮參數(shù)個(gè)數(shù)。

1.15.1 空結(jié)構(gòu)體多大?

不考慮內(nèi)存對(duì)其的情況下,結(jié)構(gòu)體所占的內(nèi)存大小是其成員所占內(nèi)存之和(關(guān)于結(jié)構(gòu)體的內(nèi)存對(duì)齊,請(qǐng)參考預(yù)處理那章)。

struct student {
}stu;

sizeof(stu)的值是多少呢?是1而不是0。如果我們把 struct student 看成一個(gè)模子的話,你能造出一個(gè)沒(méi)有任何容積的模子嗎?顯然不行。編譯器也是如此認(rèn)為。編譯器認(rèn)為任何一種數(shù)據(jù)類型都有其大小,用它來(lái)定義一個(gè)變量能夠分配確定大小的空間。既然如此,編譯器就理所當(dāng)然的認(rèn)為任何一個(gè)結(jié)構(gòu)體都是有大小的,哪怕這個(gè)結(jié)構(gòu)體為空。那萬(wàn)一結(jié)構(gòu)體真的為空,它的大小為什么值比較合適呢?假設(shè)結(jié)構(gòu)體內(nèi)只有一個(gè) char 型的數(shù)據(jù)成員,那其大小為 1byte(這里先不考慮內(nèi)存對(duì)齊的情況) .也就是說(shuō)非空結(jié)構(gòu)體類型數(shù)據(jù)最少需要占一個(gè)字節(jié)的空間,而空結(jié)構(gòu)體類型數(shù)據(jù)總不能比最小的非空結(jié)構(gòu)體類型數(shù)據(jù)所占的空間大吧。這就麻煩了,空結(jié)構(gòu)體的大小既不能為 0,也不能大于 1,怎么辦?定義為 0.5個(gè) byte?但是內(nèi)存地址的最小單位是 1 個(gè) byte, 0.5 個(gè) byte 怎么處理?解決這個(gè)問(wèn)題的最好辦法就是折中,編譯器理所當(dāng)然的認(rèn)為你構(gòu)造一個(gè)結(jié)構(gòu)體數(shù)據(jù)類型是用來(lái)打包一些數(shù)據(jù)成員的,而最小的數(shù)據(jù)成員需要 1 個(gè) byte,編譯器為每個(gè)結(jié)構(gòu)體類型數(shù)據(jù)至少預(yù)留 1 個(gè) byte的空間。所以,空結(jié)構(gòu)體的大小就定為 1 個(gè) byte。還有就是你sizeof一個(gè)函數(shù)的大小永遠(yuǎn)是1.

1.15.2 柔性數(shù)組

C99中,結(jié)構(gòu)中的最后一個(gè)元素允許是未知大小的數(shù)組,這就叫柔性數(shù)組成員,但結(jié)構(gòu)中的柔性數(shù)組成員前面必須至少一個(gè)其他成員。柔性數(shù)組成員允許結(jié)構(gòu)中包含一個(gè)大小可變的數(shù)組。sizeof返回的這種結(jié)構(gòu)大小不包括柔性數(shù)組的內(nèi)存。包含柔性數(shù)組成員的結(jié)構(gòu)用malloc()函數(shù)進(jìn)行內(nèi)存在動(dòng)態(tài)分配,并且分配的內(nèi)存應(yīng)該大于結(jié)構(gòu)的大小,以適用柔性數(shù)組的預(yù)期大小。
柔性數(shù)組到底如何適用?看下面代碼:

typedef  struct st_type {
    int i;
    int a[0];
}type_a;

有些編譯器無(wú)法編譯會(huì)報(bào)錯(cuò),可以改成下面:

typeof struct st_type {
    int i;
    int a[];
}type_a;

這樣我們就可以定義一個(gè)可變長(zhǎng)的結(jié)構(gòu)體,用sizeof(type_a)得到的只有4,就是sizeof(i) = sizeof(int)。那個(gè)0個(gè)元素的數(shù)組沒(méi)有占用空間,而后我們可以進(jìn)行變長(zhǎng)的操作了。通過(guò)如下操作給結(jié)構(gòu)體分配內(nèi)存:

type_a *p = (type_a *)malloc(sizeof(type_a) +  100 * sizeof(int) );

這樣我們?yōu)榻Y(jié)構(gòu)體指針p分配了一塊內(nèi)存。用p->item[n]就能簡(jiǎn)單的訪問(wèn)可變長(zhǎng)元素。但是這時(shí)候我們?cè)谟胹izeof(*p)測(cè)試結(jié)構(gòu)體的大小,發(fā)現(xiàn)仍然是4。在定義這個(gè)結(jié)構(gòu)體的時(shí)候,模子的大小就已經(jīng)確定不包含柔性數(shù)組的內(nèi)存大小。柔性數(shù)組只是編外人員,不占結(jié)構(gòu)體的編制。只是說(shuō)在使用柔性數(shù)組時(shí)需要把它當(dāng)作結(jié)構(gòu)體的一個(gè)成員,僅此而已。再說(shuō)白點(diǎn),柔性數(shù)組其實(shí)與結(jié)構(gòu)體沒(méi)什么關(guān)系,只是“掛羊頭賣狗肉”而已,算不得結(jié)構(gòu)體的正式成員。

1.16 union關(guān)鍵字

union關(guān)鍵字的用法與struct的用法非常相似。

union 維護(hù)足夠的空間來(lái)置放多個(gè)數(shù)據(jù)成員中的“一種”,而不是為每一個(gè)數(shù)據(jù)成員配置空間,在 union 中所有的數(shù)據(jù)成員共用一個(gè)空間,同一時(shí)間只能儲(chǔ)存其中一個(gè)數(shù)據(jù)成員,所有的數(shù)據(jù)成員具有相同的起始地址。例子如下:

union StateMachine {
    char character;
    int number;
    char *str;
    double exp;
};

一個(gè)union值配置一個(gè)足夠大的空間用來(lái)容納最大長(zhǎng)度的數(shù)據(jù)成員,上例中,最大的長(zhǎng)度是double型,所以StateMachine的空間大小就是double數(shù)據(jù)類型的大小。如果一些數(shù)據(jù)不可能在同一時(shí)間同時(shí)被用到,則可以使用 union。

1.16.1 大小端模式對(duì)union類型數(shù)據(jù)的影響

union {
    int m;
    char a[2];
}*p, u;
p = &u;
p->a[0] = 1;
p->a[1] = 2;
printf("%d\r\n", (p->a[0]));//1
printf("%d\r\n", (p->a[1]));//2
printf("%d\r\n", (p->m));//513

那么p.i的值應(yīng)該是多少?這里需要考慮是大端存儲(chǔ)模式還是小端存儲(chǔ)模式。

  • 大端模式:數(shù)據(jù)的高字節(jié)在低地址處,數(shù)據(jù)的低字節(jié)在高地址處。

  • 小端模式:數(shù)據(jù)的高字節(jié)在高地址處,數(shù)據(jù)ed低字節(jié)在低地址處。

為什么上面的值是513呢?因?yàn)閜->a[0]的值是1(00000001),p->a[1]的值是2(00000010)。而513是(00000010 00000001)。這樣就看出了a[1]在高位,a[0]在低位??梢钥闯銎脚_(tái)是小端模式。

1.16.2 如何用程序來(lái)確認(rèn)當(dāng)前系統(tǒng)的存儲(chǔ)默認(rèn)?

大端模式返回1 小端模式返回0

int check(void) {
    union {
        int i;
        char ch;
    }u;
    u.i = 1;
    return (u.ch != 1);
}
printf("您當(dāng)前存儲(chǔ)模式為:%d\r\n", check());

1.17 enum關(guān)鍵字

1.17.1 枚舉類型的使用方法

一般定義如下:

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

注意:enum_type_name是自定義的一種數(shù)據(jù)類型名,而enum_variable_name為enum_type_name類型的一個(gè)變量,也就是我們平時(shí)常說(shuō)的枚舉變量。實(shí)際上enum_type_name類型是對(duì)一個(gè)變量取值范圍的限定,而花括號(hào)是對(duì)它取值范圍,即enum_type_name類型的變量enum_variable_name只能取值為花括號(hào)內(nèi)的任何一值,如果賦給該類型變量的值不在列表中,則會(huì)報(bào)錯(cuò)或警告。ENUM_CONST_1、ENUM_CONST_2、……、ENUM_CONST_n,這些成員都是整型常量,也就是我們平時(shí)所說(shuō)的枚舉常量(常量一般用大寫)。enum變量類型還可以給其中的常量符號(hào)賦值,如果不賦值則會(huì)從被賦值的那個(gè)常量開(kāi)始一次加1,如果都沒(méi)有賦值,它們的值從0開(kāi)始一次遞增1.如分別用一個(gè)常數(shù)表示不同的顏色:

enum Color {
    GREEN = 1,
    RED,
    BLUE,
    GREEN_RED = 10,
    GREEN_BLUE
}ColorVal;
其中各常量名代表的數(shù)值分別為:
GREEN = 1
RED = 2
BLUE = 3
GREEN_RED = 10
GREEN_BLUE = 11

1.17.2 枚舉與#define宏的區(qū)別

  • #define宏常量是在預(yù)編譯階段進(jìn)行簡(jiǎn)單替換。枚舉常量則是在編譯的時(shí)候確定其值。

  • 一般在編譯器里,可以調(diào)試枚舉常量,但是不能調(diào)試宏常量。

  • 枚舉可以一次定義大量相關(guān)的常量,而#define宏一次只能定義一個(gè)。

問(wèn)題:sizeof(ColorVal) 的大小是多少?為什么?因?yàn)槊杜e變量只能取其中一個(gè)元素的值,而這個(gè)元素是整型常量,整型在x86下是4個(gè)字節(jié)。

1.18 偉大的縫紉機(jī)----typeof關(guān)鍵字

1.18.1 歷史的誤會(huì)----也許應(yīng)該是typerename

很多人認(rèn)為 typedef 是定義新的數(shù)據(jù)類型,這可能與這個(gè)關(guān)鍵字有關(guān)。是因?yàn)椋?type 是數(shù)據(jù)類型的意思; def(ine)是定義的意思,合起來(lái)就是定義數(shù)據(jù)類型啦。typedef 的真正意思是給一個(gè)已經(jīng)存在的數(shù)據(jù)類型(注意:是類型不是變量)取一個(gè)別名,而非定義一個(gè)新的數(shù)據(jù)類型。

在實(shí)際項(xiàng)目中,為了方便,可能很多數(shù)據(jù)類型(尤其是結(jié)構(gòu)體之類的自定義數(shù)據(jù)類型)需要我們重新取一個(gè)適用實(shí)際情況的別名。這時(shí)候 typedef 就可以幫助我們。例如:

typedef struct student {
    //code
}Stu_st, *Stu_pst;//
  • struct student stu1; 和 Stu_st stu1;沒(méi)有區(qū)別

  • struct student *stu2; 和Stu_st *stu2; 和Stu_pst stu2;沒(méi)有區(qū)別。

    其實(shí)很好理解。我們把“ struct student { /*code*/}”看成一個(gè)整體, typedef 就是給“ struct student {/*code*/}”取了個(gè)別名叫“ Stu_st”;同時(shí)給“ struct student { /*code*/} *”取了個(gè)別名叫“ Stu_pst”。

下面把typeof與const放在一起看看:

  • (1) const Stu_pst stu3; (2) Stu_pst const stu4;

大多數(shù)人認(rèn)為(1)里const修飾的是stu3指向的對(duì)象,(2)里const修飾的值stu4這個(gè)指針。很遺憾,(1)里const修飾的并不是stu3指向的對(duì)象。那const到底修飾的是什么呢?我們?cè)谥v解 const int i 的時(shí)候說(shuō)過(guò) const 放在類型名 “ int”前后都行; 而 const int *p 與 int * const p則完全不一樣。也就是說(shuō),我們看 const 修飾誰(shuí)都時(shí)候完全可以將數(shù)據(jù)類
型名視而不見(jiàn),當(dāng)它不存在。反過(guò)來(lái)再看“ const Stu_pst stu3”, Stu_pst 是“ struct student{ /*code*/} *”的別名, “ struct student{/*code*/} *”是一個(gè)整體。對(duì)于編譯器來(lái)說(shuō),只認(rèn)為Stu_pst 是一個(gè)類型名,所以在解析的時(shí)候很自然的把“ Stu_pst”這個(gè)數(shù)據(jù)類型名忽略掉。就變成了const stu3/4;而stu3/4是個(gè)指針,所以修飾的就是stu3/4這個(gè)指針,而不是stu3/4指向的對(duì)象。

1.18.2 typeof 與 #define的區(qū)別

//(1)
#define INT32 int
unsigned INT32 i = 10;
//(2)
typedef int int32;
unsigned int32 i = 10;

其中(2)編譯出錯(cuò),(1)不會(huì)出錯(cuò),這很好理解。因?yàn)樵陬A(yù)編譯的時(shí)候 INT32
被替換為 int,而 unsigned int i = 10;語(yǔ)句是正確的。但是,很可惜,用 typedef 取的別名不支持這種類型擴(kuò)展。

另外,想想 typedef static int int32 行不行?為什么?

因?yàn)閠ypedef是定義一個(gè)數(shù)據(jù)類型的別名,或者說(shuō)是為一個(gè)數(shù)據(jù)類型取別名。而static不是數(shù)據(jù)類型,他是一個(gè)修飾符,用來(lái)修飾變量存儲(chǔ)的位置。所以會(huì)報(bào)錯(cuò)。

再看下一個(gè)例子:

//(3)
#define PCHAR char*
PCHAR p3, p4;
//(4)
typeof char* pchar;
pchar p1, p2;

兩組代碼編譯都沒(méi)有問(wèn)題,但是,這里的 p4 卻不是指針,僅僅是一個(gè) char 類型的字符。這種錯(cuò)誤很容易被忽略,所以用#define 的時(shí)候要慎之又慎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,691評(píng)論 1 51
  • C 語(yǔ)言標(biāo)準(zhǔn)定義的 32 個(gè)關(guān)鍵字有什么,分別怎么用,用的時(shí)候需要注意到什么,便是這篇文章所要告訴的一切。內(nèi)容較長(zhǎng)...
    hylerrix閱讀 1,605評(píng)論 2 16
  • 深陷了泥潭 無(wú)法自拔 環(huán)望 只獨(dú)有空寂凄涼的風(fēng) 掙脫!掙脫!掙脫! 力不從心 唯有下陷 窒息! 誰(shuí) 能將我拉離……...
    中二的豆芽閱讀 366評(píng)論 0 0
  • 總有一段時(shí)光讓你在無(wú)數(shù)次想起,那段有你的光陰! …… 就如現(xiàn)在我用筆寫下一様。 安,還早的夜!
    見(jiàn)或不見(jiàn)閱讀 87評(píng)論 0 0
  • 1,自動(dòng)解壓文件如果下載文件到downloads文件夾的是zip,自動(dòng)解壓。 2,自動(dòng)用指定軟件打開(kāi)pdf 3,下...
    鴨梨山大哎閱讀 1,022評(píng)論 0 2

友情鏈接更多精彩內(nèi)容