注:這是第三遍讀《C語言深度解剖》,想想好像自從大學開始就沒讀完過幾本書,其中譚浩強的那本《C語言程序設計(第四版)》倒是讀過有五遍吧,其次就是這本書了。以前讀書從來不做筆記,最多的就是用筆在書上隨性標記幾下,但是覺得那樣記憶不深刻,于是這次就做了筆記。本想記下一些要點,但是這本書確實太出色了,記著記著就變成了抄書了,不過去掉了書里的一些索然無味的東西并且加了自己親手寫的代碼在里面。整個筆記共分為7個部分,這是第一部分也是最多的一部分。目前就記到這里,以后有時間再慢慢更新。本筆記同步在簡書上,有興趣的可以去看一下。
C語言里有32個關鍵字
auto 生聲明自動變量,缺省時編譯器一般默認為auto
int 聲明整型變量
double 聲明雙精度變量
long 聲明長整型變量
char 聲明字符型變量
float 聲明浮點型變量
short 聲明短整型變量
signed 聲明有符號類型變量
unsigned 聲明無符號類型變量
struct 聲明結構體變量
union 聲明聯合數據類型
enum 聲明枚舉類型
static 聲明靜態變量
switch 用于開關語句
case 開關語句分支
default 開關語句中的“其他”分支
break 跳出當前循環
register 聲明寄存器變量
const 聲明只讀變量
volatile 說明變量在程序執行中可被隱含的改變
typedef 用以給數據類型取別名
extern 聲明變量是在其他文件中聲明(也可以看做是引用變量)
return 子程序返回語句(可以帶參數,也可以不帶參數)
void 聲明函數無返回值或無參數,聲明空類型指針
continue 結束當前循環,開始下一輪循環
do 循環語句的循環體
while 循環語句的循環條件
if 條件語句
else 條件語句否定分支(與if連用)
for 一種循環語句
goto 無條件跳轉語句
sizeof 計算對象所占內存空間大小
什么是定義,什么是聲明?
什么是定義:所謂的定義就是(編譯器)創建一個對象,為這個對象分配一塊內存并給它取上一個名字,這個名字就是我們經常所說的變量名或對象名。但注意,這個名字一旦和這塊內存匹配起來,它們就同生共死,終生不離不棄。并且這塊內存的位置也不能被改變。一個變量或對象在一定的區域內(比如函數內,全局等)只能被定義一次,如果定義多次,編譯器會提示你重復定義同一個變量或對象。
什么是聲明:第一重含義:告訴編譯器,這個名字已經匹配到一塊內存上了(伊人已嫁,吾將何去何從?何以解憂,唯有稀粥),下面的代碼用到變量或對象是在別的地方定義的。聲明可以出現多次。第二重含義:告訴編譯器,我這個名字我先預定了,別的地方再也不能用它來作為變量名或對象名。
記住:定義創建了對象并為這個對象分配了內存,聲明沒有分配內存。
1.1 最寬恒大量的關鍵字----auto
auto: 它很寬恒大量的,你就當他不存在吧。編譯器在默認的缺省情況下,所有變量都是auto的。
1.2 最快樂的關鍵字----register
register:這個關鍵字請求編譯器盡可能的將變量存在 CPU 內部寄存器中而不是通過內存尋址訪問以提高效率。注意是盡可能,不是絕對。你想想,一個 CPU 的寄存器也就那么幾個或幾十個,你要是定義了很多很多 register 變量,它累死也可能不能全部把這些變量放入寄存器吧。
1.2.1 皇帝身邊的小太監----寄存器
CPU就是我們的皇帝同志.大臣就相當于我們的內存,數據從他這拿出來。那小太監就是我們的寄存器了(這里先不考慮 CPU 的高速緩存區)。數據從內存里拿出來先放到寄存器,然后 CPU 再從寄存器里讀取數據來處理,處理完后同樣把數據通過寄存器存放到內存里, CPU 不直接和內存打交道。注意:小太監是主動的從大臣手里接過奏章,然后主動的交給皇帝同志,但寄存器沒這么自覺,它從不主動干什么事。一個皇帝可能有好些小太監,那么一個 CPU 也可以有很多寄存器,不同型號的 CPU 擁有寄存器的數量不一樣。
寄存器其實就是一塊一塊小的存儲空間,只不過其存取速度要比內存快得多。
1.2.2 使用 register 修飾符的注意點
雖然register寄存器速度非常快,但是使用register修飾符是有限制的:register變量必須是能被CPU寄存器所接受的類型,意味著register變量必須是一個單個的值,并且長度應該小于或等于整形的長度。由于register可能不放在內存中,所以不能用取地址符"&"來獲取register變量的地址。
1.3 最名不符實的關鍵字----static
不要誤以為關鍵字 static 很安靜,其實它一點也不安靜。這個關鍵字在 C 語言里主要有兩個作用, C++對它進行了擴展。
1.3.1 作用一:修飾變量
變量又分為局部變量和全局變量,被修飾后他們都存在內存的靜態區域。
靜態全局變量:
作用域僅限于變量被定義的文件中,其他文件即使使用extern聲明也沒有辦法使用它。準確的說作用域從定義之處開始,到文件結尾處結束,在定義之處前面的代碼行也不能使用它。想要使用就得在前面加extern *** 。要想不這樣,直接把它放在文件開頭出就好了。
靜態局部變量:
在函數體內定義的,就只能在這個函數里用了,同一個文檔里的其他函數也用不了(等會給你打臉)。由于被static修飾的變量總是存在內存的靜態區,所以即使這個函數運行結束,這個靜態變量的值還是不會被銷毀,函數下次使用時仍然能用到這個值。注意:局部靜態變量只會被初始化一次。
看完了上面,我們來看下面這段代碼:
#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;
}
這段代碼會輸出什么?好了,我們來分析:
i是全局靜態變量,j是局部靜態變量。每次循環調用fun1會把i置為0。然后對i++,這樣每次i輸出的都是1。當第一次循環調用fun2的時候會創建一個局部靜態變量j,并存儲在內存靜態區函數執行完不會被銷毀。這樣之后再次循環的時候就不會再static int j了,因為內存已經有了。所以每次就執行了j++的操作。輸出1、2、3...
那么我在函數內部再定義一個和全局靜態變量名稱相同的局部靜態變量會怎樣?會不會覆蓋掉全局的或者報錯?答案是否定的,大家都相自安好。
開始打臉:如果你認真讀的話,會看到我上面留了一個打臉的標記。開始打臉。
上面我們說了全局靜態變量的作用域只是在定義它的那個文件中,別的文件就無法訪問。也說了局部靜態變量的作用域是在函數的內部,外部無法訪問。但是我們就想用外部文件的全局靜態變量,我們就想獲取到局部靜態變量的值怎么辦?很簡單,帶你干。。。
//外部文件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;
}
我們在外部文件test.h中定義了這個文件的全局變量j。按照道理j只能在該文件中被訪問得到。但是我們通過兩個方法getValue和setValue就可以在外部main.c中使用這個全局靜態變量j了。我們在main.c文件中有一個configNum函數,這里定義了一個局部靜態變量,我們可以通過return將這個局部靜態變量返回出去,外部函數就可以拿到他了。
1.3.2 作用二:修飾函數
函數前加static使得函數成為靜態函數。但此處“static”不是指存儲方式,而是指對函數的作用域僅局限于本文件(所以又稱內部函數)。使用內部函數的好處是:不同的人編寫不同的函數時,不用擔心自己定義的函數是否會與其他文件中的函數同名。
起初,在C中引入關鍵字static是為了表示退出一個塊后仍然存在局部變量。隨后,static在C中有了第二種含義:用來表示不能被其他文件訪問的全局變量和函數。
1.4 基本數據類型----short、int、long、char、float、double
C語言包含的數據類型如下:
數據類型 | ———— |
---|---|
基本類型 | 數值類型、字符類型char |
構造類型 | 數組、結構體struct、 共用體union、 枚舉類型enum |
指針類型 | |
空類型void |
其中數值類型又分為整型和浮點型
數值類型 | ———— |
---|---|
整型 | 短整型short、整型int、長整型long、 |
浮點型 | 單精度float、雙精度double |
1.4.1 數據類型與模子
short、int、char、float、double這六個關鍵字代表C語言里的6種基本的數據類型。 這6種基本數據類型就是6個模子,你想要什么樣的數據就找那個樣的模子去刻出來,比如在32位機上,short就是2個字節,int就是4個字節,char就是一個字節,float就是4個字節,doublue就是8個字節。但也不是絕對的,不同的系統或編譯環境可能是不一樣的,最好用sizeof測量一下。
你想要一個4字節的整型數據那你就用int去刻一個,但是你刻出來這么多怎么分得清?我們這時可以給他起個名字,叫大毛、二毛都行。
1.4.2 變量的命名規則
叫大毛、二毛也太俗了,人家都叫andy、coco多洋氣。具體起什么名看下面:
-
命名應當直觀且可以拼讀,可望文知意,便于記憶和閱讀。
標識符最好采用英文單詞或其組合,不允許使用拼音。程序中的英文單詞一般不要太復雜,用詞應當準確。
-
命名的長度應當符合“min-length && max-information”原則。
C 是一種簡潔的語言, 命名也應該是簡潔的。例如變量名 MaxVal 就比MaxValueUntilOverflow 好用。標識符的長度一般不要過長,較長的單詞可通過去掉“元音”形成縮寫。
當標識符由多個詞組成時,每個詞的第一個字母大寫,其余全部小寫。
盡量避免名字中出現數字編號,如 Value1,Value2 等,除非邏輯上的確需要編號。比如驅動開發時為管腳命名,非編號名字反而不好。
對在多個文件之間共同使用的全局變量或函數要加范圍限定符(建議使用模塊名(縮寫)作為范圍限定符)。
標識符的命名規則:太多了不想寫
程序中不得出現僅靠大小寫區分的相似的標識符。
一個函數名禁止被用于其它之處。
所有宏定義、枚舉常數、只讀變量全用大寫字母命名,用下劃線分割單詞。
-
考慮到習慣性問題,局部變量中可采用通用的命名方式,僅限于 n、 i、 j 等作為循環變量使用。
一般來說習慣上用 n,m,i,j,k 等表示 int 類型的變量; c, ch 等表示字符類型變量; a 等表示數組; p 等表示指針。當然這僅僅是一般習慣,除了 i,j,k 等可以用來表示循環變量外,別的字符變量名盡量不要使用。
定義變量的同時千萬千萬別忘了初始化。定義變量時編譯器并不一定清空了這塊內存,它的值可能是無效的數據。
不同類型數據之間的運算要注意精度擴展問題,一般低精度數據將向高精度數據擴展。
1.5 最冤枉的關鍵字----sizeof
1.5.1 常年被人誤認為函數
sizeof 是關鍵字不是函數,看下面的例子:
int i=0;
A),sizeof(int); B),sizeof(i); C),sizeof int; D),sizeof i;
毫無疑問, 32 位系統下 A), B)的值為 4。那 C)的呢? D)的呢?
在 32 位系統下,通過 Visual C++6.0 或任意一編譯器調試,我們發現 D)的結果也為 4。沒有括號居然也行,那想想,函數名后面沒有括號行嗎?由此輕易的出 sizeof 絕非函數。但是 C)出錯了,因為sizeof是關鍵字,int也是關鍵字,兩個關鍵字在一起是啥?編譯器嘗試去結合關鍵字(像const int這樣)但是失敗。
總結:sizeof 在計算變量所占空間大小時,括號可以省略,而計算類型(模子)大小時不能省略。
1.5.2 sizeof(int)*p 表示什么意思?
1.6 if、else組合
1.6.1 bool 變量與“零值”進行比較
bool變量與“零值”進行比較的if語句怎么寫?
bool bTestFlag = FALSE;//想想為什么一般初始化為 FALSE 比較好?
A), if(bTestFlag == 0); if(bTestFlag == 1);
B), if(bTestFlag == TRUE); if(bTestFlag == FLASE);
C), if(bTestFlag); if(!bTestFlag);
哪種方法比較好?我們來分析:
A)寫法: bTestFlag 是什么?整型變量?如果要不是這個名字遵照了前面的命名規范,肯怕很容易讓人誤會成整型變量。所以這種寫法不好。
B)寫法: FLASE 的值大家都知道,在編譯器里被定義為 0;但 TRUE 的值呢?都是 1嗎?很不幸,不都是 1。 Visual C++定義為 1,而它的同胞兄弟Visual Basic 就把 TRUE 定義為-1.那很顯然,這種寫法也不好。
大家都知道 if 語句是靠其后面的括號里的表達式的值來進行分支跳轉的。表達式如果為真,則執行 if 語句后面緊跟的代碼;否則不執行。那顯然,本組的寫法很好,既不會引起誤會,也不會由于 TRUE 或 FLASE 的不同定義值而出錯。記住:以后寫代碼就得這樣寫。
1.6.2 float 變量與“零值”進行比較
float變量與“零值”進行比較的if語句怎么寫?
float fTestVal = 0.0;
A), if(fTestVal == 0.0); if(fTestVal != 0.0);
B), if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON));//EPSINON 為定義好的精度。
哪一組或是那些組正確呢?我們來分析分析:
float 和 double 類型的數據都是有精度限制的,這樣直接拿來與 0.0 比,能正確嗎?明顯不能,看例子: 的值四舍五入精確到小數點后 ? 10 位為: 3.1415926536,你拿它減去0.00000000001 然后再四舍五入得到的結果是多少?你能說前后兩個值一樣嗎?EPSINON 為定義好的精度,如果一個數落在[0.0-EPSINON,0.0+EPSINON] 這個閉區間內,我們認為在某個精度內它的值與零值相等;否則不相等。擴展一下,把 0.0 替換為你想比較的任何一個浮點數,那我們就可以比較任意兩個浮點數的大小了,當然是在某個精度內.同樣的也不要在很大的浮點數和很小的浮點數之間進行運算,如下代碼:
#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;
}
//結果:
//a + b = 10000000000.000002
//a + c = 10000000000.000000
1.6.3 指針變量與“零值”進行比較
指針變量與“零值”進行比較的if語句怎么寫?
int *p = NULL;//定義指針一定要同時初始化
A), if(p == 0); if(p != 0);
B), if(p); if(!p);
C) , if(NULL == p); if(NULL != p);
哪一組或是那些組正確呢?我們來分析分析:
A)寫法: p 是整型變量?容易引起誤會,不好。盡管 NULL 的值和 0 一樣,但意義不同。
B)寫法: p 是 bool 型變量?容易引起誤會,不好。
C)寫法:這個寫法才是正確的,但樣子比較古怪。為什么要這么寫呢?是怕漏寫一個“=”號:if(p = NULL),這個表達式編譯器當然會認為是正確的,但卻不是你要表達的意思。所以,非常推薦這種寫法。
1.6.4 else到底與哪個if配對呢?
else常常與if語句配對,但要注意書寫規范。看代碼:
if(0 == x)
if(0 == y) error();
else {
//program code
}
C 語言有這樣的規定: else始終與同一括號內最近的未匹配的 if 語句結合。
1.6.5 if 語句后面的分號
關于 if-else 語句還有一個容易出錯的地方就是與空語句的連用。
if(NULL != p);
fun();
這里的 fun()函數并不是在 NULL != p 的時候被調用,而是任何時候都會被調用。問題就出在 if 語句后面的分號上。在 C 語言中,分號預示著一條語句的結尾,但是并不是每條 C 語言語句都需要分號作為結束標志。if 語句的后面并不需要分號, 但如果你不小心寫了個分號,編譯器并不會提示出錯。因為編譯器會把這個分號解析成一條空語句。上面的代碼實際等效于:
if(NULL != p) {
;
}
fun();
建議在真正需要用空語句時寫成這樣:NULL;
而不是單用一個分號。這就好比匯編語言里面的空指令,比如 ARM 指令中的 NOP
1.6.6 使用if語句的其他注意事項
-
先處理正常情況,再處理異常情況。
在編寫代碼是,要使得正常情況的執行代碼清晰,確認那些不常發生的異常情況處理代碼不會遮掩正常的執行路徑。這樣對于代碼的可讀性和性能都很重要。因為, if 語句總是需要做判斷,而正常情況一般比異常情況發生的概率更大(否則就應該把異常正常調過來了),如果把執行概率更大的代碼放到后面,也就意味著 if 語句將進行多次無謂的比較。另外,非常重要的一點是,把正常情況的處理放在 if 后面,而不要放在 else 后面。當然這也符合把正常情況的處理放在前面的要求。
1.7 switch、case組合
既然有了 if、 else 組合為什么還需要 switch、 case 組合呢?
1.7.1 不要拿青龍偃月刀去削蘋果
if、 else 一般表示兩個分支或是嵌套表示少量的分支,但如果分支很多的話……還是用switch、 case 組合吧。格式吧,你們都會。
每個 case 語句的結尾絕對不要忘了加 break,否則將導致多個分支重疊(除非有意使多個分支重疊)。
最后必須使用 default 分支。即使程序真的不需要 default 處理,也應該保留語句:這樣做并非畫蛇添足,可以避免讓人誤以為你忘了 default 處理。(有的公司代碼review的時候會有人挑你的這個刺)
default:
break;
1.7.2 case 關鍵字后面的值有什么要求嗎?
#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;
}
上面的代碼哪幾個case是對的哪幾個是錯的。
記住: case 后面只能是整型或字符型的常量或常量表達式(想想字符型數據在內存里是怎么存的)。(js里case后面想放啥放啥)
上面的case中,case "abc":
是錯的,不能是字符串,可以是字符;case 1.0 + 2:
是錯的,不能是浮點數,可以是整數;case: b:
是錯的,不能是變量;case NULL:
是錯的,不能是空; 有的人會問case 3/2:
不也是浮點數嗎?不是啦,3/2得出來的是1不是1.5。
1.7.3 case語句的排列順序
-
按字母或數字順序排列各條 case 語句。
如果所有的 case 語句沒有明顯的重要性差別,那就按 A-B-C 或 1-2-3 等順序排列 case語句。這樣做的話,你可以很容易的找到某條 case 語句。
-
把正常情況放在前面,而把異常情況放在后面。
如果有多個正常情況和異常情況,把正常情況放在前面,并做好注釋;把異常情況放在后面,同樣要做注釋。
-
按執行頻率排列 case 語句
把最常執行的情況放在前面,而把最不常執行的情況放在后面。最常執行的代碼可能也是調試的時候要單步執行的最多的代碼。如果放在后面的話,找起來可能會比較困難,而放在前面的話,可以很快的找到
1.7.4 使用case語句的其他注意事項
-
簡化每種情況對應的操作。
使得與每種情況相關的代碼盡可能的精煉。case 語句后面的代碼越精煉, case 語句的結果就會越清晰。如果某個 case 語句確實需要這么多的代碼來執行某個操作,那可以把這些操作寫成一個或幾個子程序,然后在 case 語句后面調用這些子程序就 ok 了。一般來說 case語句后面的代碼盡量不要超過 20 行。
-
不要為了使用 case 語句而刻意制造一個變量。
case 語句應該用于處理簡單的,容易分類的數據。如果你的數據并不簡單,那可能使用if else if 的組合更好一些。為了使用 case 而刻意構造出來的變量很容易把人搞糊涂,應該避免這種變量。
-
把 default 子句只用于檢查真正的默認情況。
有時候,你只剩下了最后一種情況需要處理,于是就決定把這種情況用 default 子句來處理。這樣也許會讓你偷懶少敲幾個字符,但是這卻很不明智。這樣將失去 case 語句的標號所提供的自說明功能,而且也喪失了使用 default 子句處理錯誤情況的能力。所以,奉勸你不要偷懶,老老實實的把每一種情況都用 case 語句來完成,而把真正的默認情況的處理交給 default 子句。
1.8 do、while、for關鍵字
C 語言中循環語句有三種: while 循環、 do-while 循環、 for 循環。
while 循環:先判斷 while 后面括號里的值,如果為真則執行其后面的代碼;否則不執行。 while( 1)表示死循環。
#include<stdio.h>
int main(int argc, char *argv[]) {
while(1) {
if('#' == getchar()) {
break;
}
}
printf("Hello,World!");
}
執行一個死循環,等待用戶輸入'#'后退出死循環執行輸出"Hello,World!"
1.8.1 break 與 continue 的區別
break關鍵字很重要,表示終止本層循環。上面這個例子只有一個循環,當代碼執行到break時候就會停止。
continue表示終止本次循環,當代碼執行到continue時,本次循環終止,進入下一次循環。
while(1)也可以寫成while(true)或者while(1 == 1)或者while((bool) 1)等形式的,效果一樣的。
do-while循環:先執行do后面的代碼,然后再判斷while后面括號里的值,如果為真,循環開始;否則,循環不開始。也就是說無論如何都會執行一次,用法與while差不多,但是相對少用。
for循環: for的好處是很容易控制循環的次數,多用于事先知道循環次數的情況下。
-
在switch case語句中能否能使用continue關鍵字?為什么?
不說廢話,寫份代碼不就知道了嗎?#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("結束了\r\n"); return 0; }
上面的代碼編譯之后出現了錯誤,提示是:error: continue statement not within a loop也就是說continue需要在一個循環里面,不然就會報錯。
1.8.2循環語句注意點
在多重循環中,盡量將最長的循環放在最內層,最短的循環放在最外層,以減少CPU跨切循環層的次數。
建議 for 語句的循環控制變量的取值采用“半開半閉區間”寫法。半開半閉區間寫法和閉區間寫法雖然功能是相同,但相比之下,半開半閉區間寫法寫法更加直觀。如:半開半閉區間寫法:
for (n = 0; n < 10; n++)
閉區間寫法:for (n = 0; n <= 9; n++)
-
不要在for循環體內修改循環變量,防止循環失控。
for(n = 0; n < 10; n++) { ... n = 8;//不建議, ... }
-
循環要盡可能的短,使代碼清晰,一目了然。不要超過一屏。
- 從新設計循環
- 將循環封裝到函數內
- 一般來說循環內代碼不要超過20行
循環控制在3層內
1.9 goto關鍵字
建議少用或禁用關鍵字,不過看過一些系統級的代碼里面就有好多goto,但是他們使用goto并不會使代碼混亂,反而使得更容易理解閱讀。作為小白,還是不要用了。
自從提倡結構化設計以來, goto 就成了有爭議的語句。首先,由于 goto 語句可以靈活跳轉,如果不加限制,它的確會破壞結構化設計風格;其次, goto 語句經常帶來錯誤或隱患。它可能跳過了變量的初始化、重要的計算等語句。
1.10 void關鍵字
1.10.1 void a
void的字面意思是“空類型”,void *
則為“空類型指針”,void *
可以指向任何類型的數據。
void a;
會是什么? 這樣語句在編譯時候會出錯,即使不出錯也沒有意義。
void真正發揮的作用在于:
- 對函數返回的限定;
- 對函數參數的限定;
眾所周知, 如果指針 p1 和 p2 的類型相同, 那么我們可以直接在 p1 和 p2 間互相賦值;如果 p1 和 p2 指向不同的數據類型,則必須使用強制類型轉換運算符把賦值運算符右邊的指針類型轉換為左邊指針的類型。
例如:
float *p1;
int *p2;
p1 = p2;
其中p1 = p2;語句會編譯出錯,提示“'=' : cannot convertfrom 'int *' to 'float *'”,必須改為:p1 = (float *)p2;
而void *則不同,任何類型的指針都可以直接給他賦值,無需進行強制類型轉換:
void *p1;
int *p2;
p1 = p2;
但這并不意味著, void *也可以無需強制類型轉換地賦給其它類型的指針。因為“空類型”可以包容“有類型”,而“有類型”則不能包容“空類型”。下面代碼就會出錯。
void *p1;
int *p2;
p2 = p1;
提示“'=' : cannot convert from 'void *' to 'int *'”。
1.10.2 void修飾函數返回值和參數
- 如果函數沒有返回值,那么應聲明為 void 類型。在C語言中,凡不加返回值類型限定的函數,就會被編譯器作為返回整型值處理。但是許多程序員卻誤以為其為void類型。如下:
add(int a, int b) {
return a + b;
}
int mian(int argc, char *argv[]) {//甚至很多人認為main函數無返回值或認為返回值為void型,其實人家是int型的
printf("2 + 3 = %d", add(2, 3));
}
程序運行的輸出結果為輸出: 2 + 3 = 5
這說明不加返回值聲明的函數的確為int函數。
因此,為了避免混亂,我們在編寫 C 程序時,對于任何函數都必須一個不漏地指定其類型。如果函數沒有返回值,一定要聲明為 void 類型。這既是程序良好可讀性的需要,也是編程規范性的要求。另外,加上 void 類型聲明后,也可以發揮代碼的“自注釋”作用。所謂的代碼的“自注釋”即代碼能自己注釋自己。
如果函數無參數,那么應聲明其參數為void
在C++語言中聲明一個這樣的函數:
int function(void) {
return 1;
}
則進行下面的調用時不合法的: function(2);
因為在C++中,函數參數是void的意思表示這個函數不接受任何參數。但是在Turbo C2.0中編譯:
#include<stdio.h>
fun() {
return 1;
}
main() {
printf("%d", fun(2));
getchar();
}
編譯正確輸出1,這說明,在C語言中,可以給無參數的函數傳送任意類型的參數,但是在C++編譯器中編譯同樣的代碼則會出錯。在C++中,不能向無參數的函數傳送任何線參數,出錯提示“'fun' : function does not take 1 parameters”。所以,無論在 C 還是 C++中,若函數不接受任何參數,一定要指明參數為 void
1.10.3 void指針
- 千萬小心使用void指針
按照 ANSI(American National Standards Institute)標準,不能對 void 指針進行算法操作,即下列操作都是不合法的:
void *pvoid;
pvoid++;//ANSI:錯誤
pvoid+=1;//ANSI:錯誤
ANSI標準之所以這樣認定,是因為它堅持:進行算法操作的指針必須是知道指向數據類型大小的。也就是說必須知道內存目的地址的確切值。
例如:
int *pint;
pint++;//ANSI:正確
但是大名鼎鼎的 GNU(GNU's Not Unix 的縮寫)則不這么認定,它指定 void *的算法操作與 char *一致。因此下列語句在 GNU 編譯器中皆正確:
pvoid++;//GNU:正確
pvoid+=1;//GNU:正確
在實際的程序設計中,為符合 ANSI 標準,并提高程序的可移植性,我們可以這樣編寫實現同樣功能的代碼:
void *pvoid;
(char *)pvoid ++;//ANSI:正確;GNU:正確
(char *)pvoid +=1;//ANSI:錯誤;GNU:正確
GNU 和 ANSI 還有一些區別,總體而言, GNU 較 ANSI 更“開放”,提供了對更多語法的支持。但是我們在真實設計時,還是應該盡可能地符合 ANSI 標準。
- 如果函數的參數可以是任意類型指針,那么應聲明其參數為void *
典型的如內存操作函數memcpy和memset的函數原型分別為:
void *memcpy(void *dest, const void *src, size_t len);
void *memset(void *buffer, int c, size_t num);
這樣,任何類型的指針都可以傳入 memcpy 和 memset 中,這也真實地體現了內存操作函數的意義, 因為它操作的對象僅僅是一片內存, 而不論這片內存是什么類型。
1.10.4 void不能代表一個真實的變量
因為定義變量時必須分配內存空間,定義void類型變量,編譯器到底分配多大內存呢?下面試圖讓void代表一個真實的變量,因此都是錯誤代碼:
void a;//錯誤
function(void a);//錯誤
void 體現了一種抽象,這個世界上的變量都是“有類型”的,譬如一個人不是男人就是女人(人妖不算)。void 的出現只是為了一種抽象的需要,如果你正確地理解了面向對象中“抽象基類”的概念,也很容易理解 void 數據類型。正如不能給抽象基類定義一個實例,我們也不能定義一個 void(讓我們類比的稱 void 為“抽象數據類型”)變量。
1.11 return關鍵字
return 用來終止一個函數并返回其后面跟著的值。
-
return (val);//此括號可以省略。但一般不省略,尤其在返回一個表達式的值的時候。
char *Func(void) { char str[30]; ... return str; }
str屬于局部變量,位于棧內存中,在Func結束的時候被釋放,所以返回str將導致錯誤。
return語句不可返回指向“棧內存”的“指針”,因為該內存在函數體結束時被自動銷毀。
1.12 const關鍵字也許該被替換為readonly
很多人認為const修飾的值為常量,這是不準確的,準確的說應該是只讀的變量,其值在編譯時不能被使用,因為編譯器在編譯的時候不知道其存儲的內容。或許當初這個關鍵字應該被定義為readonly。
const推出的初始目的,正是為了取代預編譯指令,消除他的缺點,同時繼承他的優點。我們看看他與define宏的區別。(很多人誤認為define是關鍵字,在這里我提醒你再回到本章前面看看32個關鍵字里是否有define)
1.12.1 const修飾的只讀變量
定義const只讀變量,具有不可變性。例如:
const int Max = 100;
int Array[Max];
這里請在VC++6.0里分別創建.c和.cpp文件測試一下。你會發現在.c文件中,編譯器會提示出錯,而在.cpp文件中則順利運行。為什么呢?我們知道定義一個數組必須制定其元素個數。這也側面證明在C語言中,const修飾的Max仍然是變量,之不過是只讀罷了;而在C++里,擴展了const的含義。
注意:const修飾的只讀變量必須在定義的同時初始化,因為在運行的過程中還能被賦值的話那說明就不是只讀的了。
想一想case后面是否可以是const修飾的只讀變量呢?
分析:我們知道case后面需要的是整型、字符型的常量或常量表達式,上面我們又知道了case在C語言里其實是只讀的變量,所以它不能放在case后面。
1.12.2 節省空間,避免不必要的內存分配,同時提高效率
編譯器通常不為普通的const只讀變量分配存儲空間,而是將他們保存在符號表中,這使得它成為一個編譯期間的值,沒有了存儲與讀內存的操作,使得他的效率也很高。看下面代碼:
#define M 3//宏常量
const int N = 5; // 此時并未將N放入內存中
...
int i = N; //此時為N分配內存,以后不再分配!
int I = M; //預編譯期間進行宏替換,分配內存
int j = N; //沒有分配內存
int J = M; //再進行宏替換,又分配一次內存!
const和#define的對比:
const定義的只讀變量從匯編的角度來看,只是給出了內存的地址,而不是像#define一樣給出的是立即數,所以const定義的只讀變量在程序運行過程中只有一份拷貝(因為他是全局的只讀變量,放在靜態區域),而#define定義的宏常量在內存中有若干個拷貝。#define宏是在預編譯階段進行宏替換,而const修飾的只讀變量是在編譯的時候確定其值。#define宏沒有類型,而const修飾的只讀變量有特定的類型。
1.12.3 修飾一般變量
一般的常量是指簡單類型的只讀變量。這種只讀變量在定義時,修飾符const可以放在類型說明符之前,也可用在類型說明符之后。例如:
int const i = 2; 或
const int i = 2;
i.12.4 修飾數組
定義說說明一個只讀數組可采用如下格式:
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指向的對象不可變
int const *p; //p可變,p指向的對象不可變
int *const p; //p不可變,p指向的對象可變
const int *const p; //指針p和p指向的值都不可變
記憶:
“左值右指” const在*的左邊表示值不變,const在*的右邊表示指針不變,const即在*左邊又在*的右邊表示值和指針都不可變。
1.12.6修飾函數的參數
const修飾符也可以修飾函數的參數,當不希望這個參數值被函數體內意外改變時使用。例如:
void fun(const int i);
告訴編譯器i在函數體中不可改變,從而防止了使用者的一些無意的或錯誤的修改。
1.12.7 修飾函數的返回值
const修飾符也可以修飾函數的返回值,返回值不可被改變。例如:
const int fun(void);
在另一個鏈接文件中引用const只讀變量:
extern const int i;//正確的聲明
extern const int j = 10; //錯誤,只讀變量的值不能改變
在C++里還有很多其他的特性。。。
1.13 最易變得關鍵字 ---- volatile
volatile是易變的、不穩定的意思。很多人根本就沒見過這個關鍵字,不知道他的存在。也有很多程序員知道它的存在,但從來沒用過它。
volatile關鍵字和const一樣是一種類型修飾符,簡單的說就是告訴編譯器volatile 關鍵字修飾的變量是隨時可能發生變化的,每次使用它的時候必須從內存中取出變量的值。從而避免一些奇怪的問題出現,比如在操作系統中有兩個線程都來讀取這個變量,一個已經改變了它的值,另一個又來讀,但編譯器卻把沒修改之前的值給了第二個線程。看代碼:
int i = 10;
int j = i; //語句一
int k = i; //語句二
這時候編譯器對代碼進行優化,因為在(1)、(2)兩條語句中,i沒有被作用左值。這時候編譯器認為i的值沒有發生改變,所以在(1)語句時從內存中去取i的值賦給j之后,這個值并沒有被丟掉,而是在(2)語句時繼續用這個值給k賦值。編譯器不會生成出匯編代碼重新從內存里取i的值,這樣提高了效率。但要注意:(1)、(2)語句之間i沒有被作用左值才行。
再看另外一個例子:
volatile int i = 10;
int j = i; //(3)語句
int k = i; //(4)語句
volatile 關鍵字告訴編譯器 i 是隨時可能發生變化的,每次使用它的時候必須從內存中取出 i的值,因而編譯器生成的匯編代碼會重新從 i 的地址處讀取數據放在 k 中。
1.14 最會帶帽子的關鍵字----extern
extern,外面的、外來的意思。
extern 可以置于變量或者函數前,以標示變量或者函數的定義在別的文件中,下面的代碼用到的這些變量或函數是外來的,不是本文件定義的,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。就好比在本文件中給這些外來的變量或函數帶了頂帽子,告訴本文件中所有代碼,這些家伙不是土著。
看代碼:
//A.c
int i = 0;
void fun(void) {
//code
}
//B.c文件中用external修飾:
extern int i; //寫成i = 10;行嗎?
extern void fun(void);//兩個void是否可以省略
//C.h文件中定義:
int j = 1;
int k = 2;
//D.c文件中用external修飾:
extern double j; // 這樣行嗎?為什么
j = 3.0;//這樣行嗎?為什么
我們來分析:
A.c文件中定義了變量i和函數fun,在B.c中如果想使用就需要先聲明再使用。聲明不僅要聲明變量或函數本身還要聲明他們的類型。因此extern int i;
是正確的。能不能寫成extern int i = 10;
?我們暫不看這里的extern,后面的int i = 10;
這是表示定義一個變量啊,這個變量我們在A.c中已經定義了,這時候不報錯才怪。所以說聲明的時候是不能有賦值操作的。D.c文件中的很簡答都錯了。
1.15 struct關鍵字
struct是個神奇的關鍵字,它將一些相關聯的數據打包成一個整體,方便使用。
在網絡協議、通信控制、嵌入式系統、驅動開發等地方,我們經常要傳送的不是簡單的字節流( char 型數組),而是多種數據組合起來的一個整體,其表現形式是一個結構體。經驗不足的開發人員往往將所有需要傳送的內容依順序保存在 char 型數組中,通過指針偏移的方法傳送網絡報文等信息。這樣做編程復雜,易出錯,而且一旦控制方式及通信協議有所變化,程序就要進行非常細致的修改,非常容易出錯。這個時候只需要一個結構體就能搞定。平時我們要求函數的參數盡量不多于 4 個,如果函數的參數多于 4 個使用起來非常容易出錯 (包括每個參數的意義和順序都容易弄錯), 效率也會降低(與具體 CPU 有關,ARM芯片對于超過 4 個參數的處理就有講究,具體請參考相關資料)。這個時候,可以用結構體壓縮參數個數。
1.15.1 空結構體多大?
不考慮內存對其的情況下,結構體所占的內存大小是其成員所占內存之和(關于結構體的內存對齊,請參考預處理那章)。
struct student {
}stu;
sizeof(stu)的值是多少呢?是1而不是0。如果我們把 struct student 看成一個模子的話,你能造出一個沒有任何容積的模子嗎?顯然不行。編譯器也是如此認為。編譯器認為任何一種數據類型都有其大小,用它來定義一個變量能夠分配確定大小的空間。既然如此,編譯器就理所當然的認為任何一個結構體都是有大小的,哪怕這個結構體為空。那萬一結構體真的為空,它的大小為什么值比較合適呢?假設結構體內只有一個 char 型的數據成員,那其大小為 1byte(這里先不考慮內存對齊的情況) .也就是說非空結構體類型數據最少需要占一個字節的空間,而空結構體類型數據總不能比最小的非空結構體類型數據所占的空間大吧。這就麻煩了,空結構體的大小既不能為 0,也不能大于 1,怎么辦?定義為 0.5個 byte?但是內存地址的最小單位是 1 個 byte, 0.5 個 byte 怎么處理?解決這個問題的最好辦法就是折中,編譯器理所當然的認為你構造一個結構體數據類型是用來打包一些數據成員的,而最小的數據成員需要 1 個 byte,編譯器為每個結構體類型數據至少預留 1 個 byte的空間。所以,空結構體的大小就定為 1 個 byte。還有就是你sizeof一個函數的大小永遠是1.
1.15.2 柔性數組
C99中,結構中的最后一個元素允許是未知大小的數組,這就叫柔性數組成員,但結構中的柔性數組成員前面必須至少一個其他成員。柔性數組成員允許結構中包含一個大小可變的數組。sizeof返回的這種結構大小不包括柔性數組的內存。包含柔性數組成員的結構用malloc()函數進行內存在動態分配,并且分配的內存應該大于結構的大小,以適用柔性數組的預期大小。
柔性數組到底如何適用?看下面代碼:
typedef struct st_type {
int i;
int a[0];
}type_a;
有些編譯器無法編譯會報錯,可以改成下面:
typeof struct st_type {
int i;
int a[];
}type_a;
這樣我們就可以定義一個可變長的結構體,用sizeof(type_a)得到的只有4,就是sizeof(i) = sizeof(int)。那個0個元素的數組沒有占用空間,而后我們可以進行變長的操作了。通過如下操作給結構體分配內存:
type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int) );
這樣我們為結構體指針p分配了一塊內存。用p->item[n]就能簡單的訪問可變長元素。但是這時候我們在用sizeof(*p)測試結構體的大小,發現仍然是4。在定義這個結構體的時候,模子的大小就已經確定不包含柔性數組的內存大小。柔性數組只是編外人員,不占結構體的編制。只是說在使用柔性數組時需要把它當作結構體的一個成員,僅此而已。再說白點,柔性數組其實與結構體沒什么關系,只是“掛羊頭賣狗肉”而已,算不得結構體的正式成員。
1.16 union關鍵字
union關鍵字的用法與struct的用法非常相似。
union 維護足夠的空間來置放多個數據成員中的“一種”,而不是為每一個數據成員配置空間,在 union 中所有的數據成員共用一個空間,同一時間只能儲存其中一個數據成員,所有的數據成員具有相同的起始地址。例子如下:
union StateMachine {
char character;
int number;
char *str;
double exp;
};
一個union值配置一個足夠大的空間用來容納最大長度的數據成員,上例中,最大的長度是double型,所以StateMachine的空間大小就是double數據類型的大小。如果一些數據不可能在同一時間同時被用到,則可以使用 union。
1.16.1 大小端模式對union類型數據的影響
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的值應該是多少?這里需要考慮是大端存儲模式還是小端存儲模式。
大端模式:數據的高字節在低地址處,數據的低字節在高地址處。
小端模式:數據的高字節在高地址處,數據ed低字節在低地址處。
為什么上面的值是513呢?因為p->a[0]的值是1(00000001),p->a[1]的值是2(00000010)。而513是(00000010 00000001)。這樣就看出了a[1]在高位,a[0]在低位。可以看出平臺是小端模式。
1.16.2 如何用程序來確認當前系統的存儲默認?
大端模式返回1 小端模式返回0
int check(void) {
union {
int i;
char ch;
}u;
u.i = 1;
return (u.ch != 1);
}
printf("您當前存儲模式為:%d\r\n", check());
1.17 enum關鍵字
1.17.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_type_name類型是對一個變量取值范圍的限定,而花括號是對它取值范圍,即enum_type_name類型的變量enum_variable_name只能取值為花括號內的任何一值,如果賦給該類型變量的值不在列表中,則會報錯或警告。ENUM_CONST_1、ENUM_CONST_2、……、ENUM_CONST_n,這些成員都是整型常量,也就是我們平時所說的枚舉常量(常量一般用大寫)。enum變量類型還可以給其中的常量符號賦值,如果不賦值則會從被賦值的那個常量開始一次加1,如果都沒有賦值,它們的值從0開始一次遞增1.如分別用一個常數表示不同的顏色:
enum Color {
GREEN = 1,
RED,
BLUE,
GREEN_RED = 10,
GREEN_BLUE
}ColorVal;
其中各常量名代表的數值分別為:
GREEN = 1
RED = 2
BLUE = 3
GREEN_RED = 10
GREEN_BLUE = 11
1.17.2 枚舉與#define宏的區別
#
define宏常量是在預編譯階段進行簡單替換。枚舉常量則是在編譯的時候確定其值。一般在編譯器里,可以調試枚舉常量,但是不能調試宏常量。
枚舉可以一次定義大量相關的常量,而#define宏一次只能定義一個。
問題:sizeof(ColorVal) 的大小是多少?為什么?因為枚舉變量只能取其中一個元素的值,而這個元素是整型常量,整型在x86下是4個字節。
1.18 偉大的縫紉機----typeof關鍵字
1.18.1 歷史的誤會----也許應該是typerename
很多人認為 typedef 是定義新的數據類型,這可能與這個關鍵字有關。是因為, type 是數據類型的意思; def(ine)是定義的意思,合起來就是定義數據類型啦。typedef 的真正意思是給一個已經存在的數據類型(注意:是類型不是變量)取一個別名,而非定義一個新的數據類型。
在實際項目中,為了方便,可能很多數據類型(尤其是結構體之類的自定義數據類型)需要我們重新取一個適用實際情況的別名。這時候 typedef 就可以幫助我們。例如:
typedef struct student {
//code
}Stu_st, *Stu_pst;//
struct student stu1; 和 Stu_st stu1;沒有區別
-
struct student *stu2; 和Stu_st *stu2; 和Stu_pst stu2;沒有區別。
其實很好理解。我們把“ struct student { /*code*/}”看成一個整體, typedef 就是給“ struct student {/*code*/}”取了個別名叫“ Stu_st”;同時給“ struct student { /*code*/} *”取了個別名叫“ Stu_pst”。
下面把typeof與const放在一起看看:
- (1) const Stu_pst stu3; (2) Stu_pst const stu4;
大多數人認為(1)里const修飾的是stu3指向的對象,(2)里const修飾的值stu4這個指針。很遺憾,(1)里const修飾的并不是stu3指向的對象。那const到底修飾的是什么呢?我們在講解 const int i 的時候說過 const 放在類型名 “ int”前后都行; 而 const int *p 與 int * const p
則完全不一樣。也就是說,我們看 const 修飾誰都時候完全可以將數據類
型名視而不見,當它不存在。反過來再看“ const Stu_pst stu3”, Stu_pst 是“ struct student{ /*code*/} *
”的別名, “ struct student{/*code*/} *
”是一個整體。對于編譯器來說,只認為Stu_pst 是一個類型名,所以在解析的時候很自然的把“ Stu_pst”這個數據類型名忽略掉。就變成了const stu3/4;而stu3/4是個指針,所以修飾的就是stu3/4這個指針,而不是stu3/4指向的對象。
1.18.2 typeof 與 #define的區別
//(1)
#define INT32 int
unsigned INT32 i = 10;
//(2)
typedef int int32;
unsigned int32 i = 10;
其中(2)編譯出錯,(1)不會出錯,這很好理解。因為在預編譯的時候 INT32
被替換為 int,而 unsigned int i = 10;語句是正確的。但是,很可惜,用 typedef 取的別名不支持這種類型擴展。
另外,想想 typedef static int int32 行不行?為什么?
因為typedef是定義一個數據類型的別名,或者說是為一個數據類型取別名。而static不是數據類型,他是一個修飾符,用來修飾變量存儲的位置。所以會報錯。
再看下一個例子:
//(3)
#define PCHAR char*
PCHAR p3, p4;
//(4)
typeof char* pchar;
pchar p1, p2;
兩組代碼編譯都沒有問題,但是,這里的 p4 卻不是指針,僅僅是一個 char 類型的字符。這種錯誤很容易被忽略,所以用#define 的時候要慎之又慎。