C語言中有不少有趣的特性,這些東西不知道也無妨,若知道了則能夠錦上添花,提升生產力或減小bug。數組
因爲這些東西比較雜,內容也很少,不值得專門去介紹他們,也沒有合適的地方在其餘文章內安插這些,因此就以番外篇的形式作個大雜燴。安全
在C89(C90)以及以前,塊內的申明(好比函數內)必須放在塊開頭(即全部申明在語句以前)。而從C99開始,申明能夠放在任何位置。好比如下代碼,在C99中能夠經過編譯,而在C89(C90)中不行。app
int main(void) {
int var1 = 0;
var1 = 5;
int var2 = 3; // 不在塊開頭
}
複製代碼
雖然標準這麼規定,但在實現中仍然採用C89(C90)的方法,「能夠放在任何位置」只是編譯器對代碼進行重排的結果。若是用調試器跟蹤變量,會發如今進入函數時兩個變量就已經申明,只是在各自申明的位置進行初始化。編輯器
受早期沒有規範以及譚浩強的影響,不少人喜歡寫void main();
。其實這是一種徹底錯誤的寫法。函數
從C89(C90)開始,C標準就規定main
函數必須返回int
類型,正常狀況下應返回0
。優化
C語言代碼從main
函數開始執行,爲何返回值如此重要?ui
代碼執行從main
開始,但編譯後的二進制程序的執行卻並非從main
開始。在進入main
以前,操做系統還有不少工做要作,如初始化環境和傳遞參數等,main
的返回值用於操做系統評估程序是否正常退出。(Windows常常在某些不規範程序退出後彈出「**程序是否正常退出」的詢問)spa
若是main
函數忘記寫return 0;
,那麼編譯時會自動補上,這也是C標準規定的。(可是其餘本該有返回值的函數若是沒有return
,其行爲是未定義的)操作系統
說到傳遞參數,C標準規定了兩種main
函數參數形式:指針
int main(void);
int main(int argc, char **argv); // int main(int argc, char *argv[]);
複製代碼
參數能夠爲空或者兩個參數,參數名能夠任意,但類型必須爲第一個int
,第二個char **
。
同時,C標準規定能夠針對不一樣平臺擴展參數。(如下內容摘抄自維基百科:Entry_point)
好比在Unix和Windows中能夠經過第三個參數指定運行環境:
int main(int argc, char **argv, char **envp);
複製代碼
基於Darwin的操做系統(例如macOS)具備第四個參數,該參數包含操做系統提供的任意信息:
int main(int argc, char **argv, char **envp, char **apple);
複製代碼
此外,int main();
也是能夠經過編譯的。在C++中int main();
與int main(void);
相同,但在C中是不一樣的。C語言中若是函數的參數列表爲()
,表示不肯定參數的數量,能夠向函數傳遞任意數量的參數。若是要表示沒有參數,須要添加關鍵詞void
。
通常而言,第一個參數argc
表示操做系統調用此程序的參數計數,包括文件名。第二個參數argv
表示參數向量,指向一個指針數組,數組中的每一個指針指向一個字符串(此處的數組和字符串都不是嚴謹的意義)。第一個字符指針所指的字符串是文件名,具體內容與調用程序時終端的寫法一致;最後一個字符指針是個空指針,也就是說argv[argc] == (char *)0
。
好比,調用一個名爲a.out
的程序:
$ ./a.out I love C
複製代碼
argc == 4;
argv[0] == "./a.out";
argv[1] == "I";
argv[2] == "love";
argv[3] == "C";
argv[4] == (char *)0;
複製代碼
以0做爲參數向量結尾與字符串末尾的0有相同的妙用。
有趣的是,即便main
寫爲int main(void);
,代表不接受參數,但在終端調用程序時仍然能夠輸入參數,而且這些參數也會傳遞給程序,只是咱們在程序內沒法使用這些參數。
switch語句大概是C語言中最使人討厭的語句了,不只幾乎用不到,語法還特別麻煩。(因此某些語言刪除了switch語句)
swith語句的原理其實就是匹配標籤與跳轉,跟goto語句相似,能夠當作是goto語句的加強魔改版。
首先,switch語句只支持整數匹配,好比int
、char
,不支持float
、double
等浮點數。由於浮點數在計算機中使用二進制表示,並不能徹底準確地表示十進制小數,尤爲在進行計算後尾數很不肯定,幾乎不可能按照預期匹配。
switch語句首創了case
標籤。在「語言結構」那篇我講過,case
標籤只能用在switch語句內,標籤做用域只是當前switch語句而不是當前函數,而且case
標籤容許且只容許標籤名使用整數類型常量表達式。整數能夠理解,上一段已經說了switch只支持整數匹配;由於要進行匹配,要求一個返回值,因此得是表達式;至於常量,case
標籤畢竟是個標籤,在編譯時就必須獲得肯定的值,而變量的值在運行時才能肯定,因此只能是常量表達式。
case
標籤中的表達式在編譯時進行計算,因此下面兩個標籤相同,若是在同一個swith語句中編譯會報錯。
case 0 + 2:
case 1 + 1:
複製代碼
因爲switch語句只是如goto般的匹配標籤與跳轉,跳轉後就按照順序結構繼續執行了,再也不理會其餘標籤,因此才一般在switch語句內使用break跳出,只執行須要的部分。不過,也能夠利用這個特性讓多個匹配結果執行相同的操做。
switch (var) {
case 0:
case 1:
case 2:
printf("var < 3");
break;
default:
printf("var >= 3");
}
複製代碼
此外,switch語句內的申明必須寫在塊內,以免跳過初始化。以下:
switch (var) {
case 0:
{
int a = 5; // 編譯經過
printf("%d\n", a);
}
case 1:
int a = 5; // 編譯報錯
printf("%d\n", a);
}
複製代碼
for語句的存在是爲了簡化循環,跟三元運算符 ? :
相似。
for語句緊跟的括號內是由分號;
分隔的三個表達式(是的,此處的;
是分隔符而不是語句結尾,三個份量都是表達式而不是語句),三個表達式均可以省略(直接空着就行,不能寫void
),若是中間的表達式爲空,默認爲1。
PS:if
、while
等語句括號內的判斷條件不能省略。
雖然申明函數時在函數名前加void
表示沒有返回值,但函數調用表達式仍然會返回一個void
類型的值,但這個值不能被使用,只能丟棄。
for語句括號內的第二個表達式是循環的判斷條件,若是此處調用了一個void
函數,那麼void
值就被使用了,編譯就會出錯,除此以外,這個位置能夠是任意表達式。至於另外兩個表達式則沒有任何限制。
從C99開始,for語句括號內的第一個表達式能夠換成一個不徹底的申明(徹底的申明以分號結尾,而此處的分號是分隔符,不是申明的一部分)。
for語句括號內變量的做用域小於for所在的塊,大於for語句內部的塊。舉例以下:
int iter = 0;
for (int iter = 0; iter < 5; iter++) {
int iter = 0;
printf("%d\n", iter++);
}
printf("%d\n", iter);
複製代碼
在這段代碼中申明瞭三個iter
變量,分別在for語句外,for語句的括號內,for語句的塊內。
首先申明瞭最外層的iter
,接着進入for語句,又申明瞭一個iter
。
括號內的空間能夠看做是外層的一個子域,因此能夠申明和外層同名的變量,而且屏蔽掉了外層的iter
。
括號中另外兩個iter
都是在括號內申明的那個iter
。
而後進入for語句內的塊,又申明瞭一個iter
。
這個塊內部能夠看做是括號內空間的子域,因此此處申明的iter
又屏蔽掉了外層的兩個iter
。
每次循環都會從新申明一個iter
並初始化爲0,因此每次循環輸出一個0
,自增操做沒有實際做用。
因爲塊內的iter
屏蔽掉了括號內的iter
,因此控制條件不受塊內代碼的影響,循環執行5次。
跳出循環後,for語句的兩個iter
都被銷燬,打印的是最外層的的iter
。
因此程序的執行結果是輸出6個0
。
若是for語句的塊內有continue;
語句,那麼在執行此語句後會跳過塊內的剩餘代碼,並執行for語句括號內的第三個表達式,以後纔是判斷條件。
若是for語句的塊內有break;
語句,那麼在執行此語句後會直接跳出循環,不執行for語句括號內的第三個表達式。
因此在for語句內使用continue;
和break;
語句都是安全的。
所謂字符串常量,就是指"fake_string"
這種類型的東西。
衆所周知,C語言沒有真正意義上的字符串類型,只是以0
結尾的字符數組。
"fake_string"
並無它看上去那麼簡單,實際上它作了不少事。首先,申請一段連續的內存空間,依次放入char
字符f
、a
、k
、e
、_
、s
、t
、r
、i
、n
、g
、\0
(注意最後補了個0),而後返回char
數組。與數組同樣,字符串常量也能夠當指針來使用,類型爲char *
,且這個指針同時有底層和頂層const
屬性。
既然能夠當指針來用,那麼指針運算在字符串常量上也適用。
"string"[0] == 's';
"string"[6] == 0;
char *ch = "string";
複製代碼
字符串常量也能夠像其餘指針同樣做爲if
、while
等的判斷條件。
不過,和數組同樣,對字符串常量使用sizeof
運算符獲得的是所佔內存空間的大小。
對字符串常量取地址獲得的是一個行指針。
(兩者都包括末尾的0)
sizeof("1234") == 5;
char (*pt)[5] = &"1234";
複製代碼
首先,誰都知道但常常忽略的一點,定義結構體末尾必須加分號。
結構體定義由4部分組成,struct
關鍵字、結構體名、成員列表、結構體變量。
一個完整的結構體定義能夠寫爲:
struct StructName {
int member1;
char member2;
} var, *pt;
複製代碼
若是要使用此結構體來申明變量,能夠寫爲:
struct StructName var2;
複製代碼
其中struct StructName
是一個完整的類型名。
在結構體定義中,結構體名和結構體變量能夠省略。若是省略結構體名,就沒有辦法在其餘地方使用此結構體,因此不該該同時省略結構體名和結構體變量。
結構體定義僅僅是類型定義,不能對結構體成員初始化。(C++能夠指定成員默認值)
除了定義,也能夠申明一個結構體。僅需struct
關鍵字和結構體名。
struct StructName;
複製代碼
申明的結構體甚至能夠沒有定義(只要不去使用它)。
結構體也能夠和typedef
結合使用,該類型定義能夠在結構體定義以前。一樣,若是沒有使用此類型,結構體也能夠沒有定義。
typedef struct StructName TypeName;
複製代碼
結構體名,類型名分別屬於不一樣的名字空間,也就是說,這兩個名稱能夠相同。以下:
typedef struct TreeNode TreeNode;
struct TreeNode {
int data;
TreeNode *left;
TreeNode *right;
};
複製代碼
不過仍是建議使用不一樣的名字,避免混淆。
結構體的成員能夠是結構體變量。如:
struct S1 {
int member1;
int member2;
};
struct S2 {
struct S1 member1;
int member2;
};
複製代碼
或者
struct S1 {
struct S2 {
int member1;
int member2;
} member1;
int member2;
};
複製代碼
儘管在第二例中,S2
在S1
內部定義,但其做用域與S1
相同。(這點與C++不一樣)
和數組同樣,結構體也可使用列表初始化。列表按照成員的申明順序依次初始化,沒有指定的成員初始化爲0。
對於包含結構體成員的結構體,可使用相似多維數組那樣的初始化方法。
struct S1 {
struct S2 {
int member1;
int member2;
} member1;
int member2;
};
struct S2 var1 = { 1, 2 };
struct S1 var2 = { { 1, 2 }, 3 };
複製代碼
結構體也支持指定初始化,以下:
struct S1 var = { .member1.member2 = 1, .member2 = 2 };
複製代碼
PS:C++不支持指定初始化。
從C99開始,可使用類型加列表創造一個臨時結構體。如:
struct S {
int member1;
int member2;
};
(struct S){ 1, 2 };
複製代碼
臨時結構體能夠像通常結構體那樣使用。
(struct S){ 1, 2 }.member1 == 1;
複製代碼
臨時結構體也可使用多維列表以及指定初始化。
PS:C++不支持臨時結構體。
若是對一個結構體使用sizeof
運算符,會發現它的大小並不必定等於成員大小之和。
C爲了提升效率,採用了內存對齊,是一種犧牲空間換時間的方案。
內存對齊能夠簡述爲:
關於結構體的內存對齊,網上有不少很詳細的說明,我就再也不贅述。
其實,在C中到處都有內存對齊,好比定義變量,只是咱們通常無需關心。
爲了提升運算效率,C在運算時會將比int
小的整數類型提高爲int
類型。
這是由於int
的大小通常就是處理器的字長。雖然內存按字節編址,但處理器是以字爲單位處理數據的,這種提高是硬件層面的語言優化。
char a = 0, b = 0;
printf("%d\n", sizeof(a + b));
複製代碼
上面的代碼將輸出4
。
事實上,除了在字符串裏爲了節省空間而使用char
類型之外,C幾乎不會使用char
類型。
好比字符常量'a'
,實際上是int
類型。(C++中爲char
)
sizeof('a') == 4;
複製代碼
此外,咱們熟知的getchar
、fgetc
等函數也是返回int
類型。
C中的字符常量還有另一個特性,好比有這些寫法:'ab'
、'abc'
、'abcd'
。
這些可都不是字符串,而是int
類型整數。由於int
類型的大小通常是char
類型的4倍,因此能夠容納最多4個ASCII
字符。
暫且無論大小端存儲,只從邏輯上分析。以abcd
爲例,將這4個字符的二進制碼依次排列,再組合成一個數,就是abcd
的值。爲了簡便,我用十六進制表示二進制。
'a' == 0x61;
'b' == 0x62;
'c' == 0x63;
'd' == 0x64;
'abcd' == 0x61626364;
'abcd' == 1633837924;
複製代碼
關於浮點數,C也幾乎不使用float
,由於其精度過低。
咱們經常使用的浮點數常量如0.5
實際上是double
類型,math,h中接受或返回浮點數的函數也幾乎都是double
類型。
C並不原生支持布爾類型,直到c99才引入了關鍵字及類型_Bool
。
而咱們經常使用的stdbool.h頭文件的主要內容只有:
#define bool _Bool
#define true 1
#define false 0
複製代碼
雖然理論上布爾類型只有1bit,但實際上它佔用1個字節。
_Bool
類型只有0
和1
兩個值,任何非0數值賦值給_Bool
變量都轉換爲1
。
由於並無原生支持布爾類型,因此在C中條件表達式如1 < 2
、邏輯表達式如1 && 0
的返回值都是int
類型。事實上,通常狀況下,在C中使用布爾類型沒有實際意義。
PS:C++有對布爾類型的完整支持。
注意:C中的auto
與C++中的auto
徹底不一樣。
C中的auto
表示動態生存期。
auto
大概是C中最沒有存在感的東西了,事實上,它確實沒什麼用。
局部變量默認爲動態生存期,無需指定auto
;而全局變量默認靜態生存期,不能指定auto
。
正因如此,C++纔將auto
的做用改成了自動判斷類型。
C中有兩種申明函數的方式。傳統的「函數申明」以及「函數原型」。
函數申明僅需指定返回值類型和函數名,參數列表爲空。
函數原型還需指定參數類型(參數名可選)。
int fn1(); // 傳統申明
int fn2(int, double) // 函數原型 int fn3(void) // 函數原型 複製代碼
因爲在C中函數參數列表爲()
表示不肯定參數,因此編譯器不會在函數調用時檢查參數。
建議在申明函數時使用函數原型,最好連參數名都加上。這樣可讓編譯器來檢查函數調用的正確性,若是使用比較智能的編輯器,還能夠在編寫代碼時得到正確的參數提示。