一. C語言概述
歡迎你們來到c語言的世界,c語言是一種強大的專業化的編程語言。c++
1.1 C語言的起源
貝爾實驗室的Dennis Ritchie在1972年開發了C,當時他正與ken Thompson一塊兒設計UNIX操做系統,然而,C並非徹底由Ritchie構想出來的。它來自Thompson的B語言。數組
1.2 使用C語言的理由
在過去的幾十年中,c語言已成爲最流行和最重要的編程語言之一。它之因此獲得發展,是由於人們嘗試使用它後都喜歡它。過去不少年中,許多人從c語言轉而使用更強大的c++語言,但c有其自身的優點,仍然是一種重要的語言,並且它仍是學習c++的必經之路。編程語言
-
高效性。c語言是一種高效的語言。c表現出一般只有彙編語言才具備的精細的控制能力(彙編語言是特定cpu設計所採用的一組內部制定的助記符。不一樣的cpu類型使用不一樣的彙編語言)。若是願意,您能夠細調程序以得到最大的速度或最大的內存使用率。函數
-
可移植性。c語言是一種可移植的語言。意味着,在一個系統上編寫的c程序通過不多改動或不通過修改就能夠在其餘的系統上運行。性能
-
強大的功能和靈活性。c強大而又靈活。好比強大靈活的UNIX操做系統即是用c編寫的。其餘的語言(Perl、Python、BASIC、Pascal)的許多編譯器和解釋器也都是用c編寫的。結果是當你在一臺Unix機器上使用Python時,最終由一個c程序負責生成最後的可執行程序。學習
1.3 C語言標準
1.3.1 K&R Curl
起初,C語言沒有官方標準。1978年由美國電話電報公司(AT&T)貝爾實驗室正式發表了C語言。布萊恩•柯林漢(Brian Kernighan) 和 丹尼斯•裏奇(Dennis Ritchie) 出版了一本書,名叫《The C Programming Language》。這本書被 C語言開發者們稱爲K&R,不少年來被看成 C語言的非正式的標準說明。人們稱這個版本的 C語言爲K&R C。spa
K&R C主要介紹瞭如下特點:結構體(struct)類型;長整數(long int)類型;無符號整數(unsigned int)類型;把運算符=+和=-改成+=和-=。由於=+和=-會使得編譯器不知道使用者要處理i = -10仍是i =- 10,使得處理上產生混淆。
即便在後來ANSI C標準被提出的許多年後,K&R C仍然是許多編譯器的最準要求,許多老舊的編譯器仍然運行K&R C的標準。
1.3.2 ANSI C/C89標準
1970到80年代,C語言被普遍應用,從大型主機到小型微機,也衍生了C語言的不少不一樣版本。1983年,美國國家標準協會(ANSI)成立了一個委員會X3J11,來制定 C語言標準。
1989年,美國國家標準協會(ANSI)經過了C語言標準,被稱爲ANSI X3.159-1989 "Programming Language C"。由於這個標準是1989年經過的,因此通常簡稱C89標準。有些人也簡稱ANSI C,由於這個標準是美國國家標準協會(ANSI)發佈的。
1990年,國際標準化組織(ISO)和國際電工委員會(IEC)把C89標準定爲C語言的國際標準,命名爲ISO/IEC 9899:1990 - Programming languages -- C[5] 。由於此標準是在1990年發佈的,因此有些人把簡稱做C90標準。不過大多數人依然稱之爲C89標準,由於此標準與ANSI C89標準徹底等同。
1994年,國際標準化組織(ISO)和國際電工委員會(IEC)發佈了C89標準修訂版,名叫ISO/IEC 9899:1990/Cor 1:1994[6] ,有些人簡稱爲C94標準。
1995年,國際標準化組織(ISO)和國際電工委員會(IEC)再次發佈了C89標準修訂版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7] ,有些人簡稱爲C95標準。
1.3.3 C99標準
1999年1月,國際標準化組織(ISO)和國際電工委員會(IEC)發佈了C語言的新標準,名叫ISO/IEC 9899:1999 - Programming languages -- C ,簡稱C99標準。這是C語言的第二個官方標準。
例如:
-
增長了新關鍵字 restrict,inline,_Complex,_Imaginary,_Bool
-
支持 long long,long double _Complex,float _Complex 這樣的類型
-
支持了不定長的數組。數組的長度就能夠用變量了。聲明類型的時候呢,就用 int a[*] 這樣的寫法。不過考慮到效率和實現,這玩意並非一個新類型。
2、內存分區
2.1 數據類型
2.1.1 數據類型概念
什麼是數據類型?爲何須要數據類型? 數據類型是爲了更好進行內存的管理,讓編譯器能肯定分配多少內存。
咱們現實生活中,狗是狗,鳥是鳥等等,每一種事物都有本身的類型,那麼程序中使用數據類型也是來源於生活。
當咱們給狗分配內存的時候,也就至關於給狗建造狗窩,給鳥分配內存的時候,也就是給鳥建造一個鳥窩,咱們能夠給他們各自建造一個別墅,可是會形成內存的浪費,不能很好的利用內存空間。
咱們在想,若是給鳥分配內存,只須要鳥窩大小的空間就夠了,若是給狗分配內存,那麼也只須要狗窩大小的內存,而不是給鳥和狗都分配一座別墅,形成內存的浪費。
當咱們定義一個變量,a = 10,編譯器如何分配內存?計算機只是一個機器,它怎麼知道用多少內存能夠放得下10?
因此說,數據類型很是重要,它能夠告訴編譯器分配多少內存能夠放得下咱們的數據。
狗窩裏面是狗,鳥窩裏面是鳥,若是沒有數據類型,你怎麼知道冰箱裏放得是一頭大象!
數據類型基本概念:
-
類型是對數據的抽象;
-
類型相同的數據具備相同的表示形式、存儲格式以及相關操做;
-
程序中全部的數據都一定屬於某種數據類型;
-
數據類型能夠理解爲建立變量的模具: 固定大小內存的別名;
2.1.2 數據類型別名
typedef unsigned int u32;
typedef struct _PERSON{
char name[64];
int age;
}Person;
void test(){
u32 val; //至關於 unsigned int val;
Person person; //至關於 struct PERSON person;
}
2.1.3 void數據類型
void字面意思是」無類型」,void* 無類型指針,無類型指針能夠指向任何類型的數據。
void定義變量是沒有任何意義的,當你定義void a,編譯器會報錯。
void真正用在如下兩個方面:
-
對函數返回的限定;
-
對函數參數的限定;
//1. void修飾函數參數和函數返回 void test01(void){ printf("hello world"); } //2. 不能定義void類型變量 void test02(){ void val; //報錯 } //3. void* 能夠指向任何類型的數據,被稱爲萬能指針 void test03(){ int a = 10; void* p = NULL; p = &a; printf("a:%d\n",*(int*)p); char c = 'a'; p = &c; printf("c:%c\n",*(char*)p); } //4. void* 經常使用於數據類型的封裝 void test04(){ //void * memcpy(void * _Dst, const void * _Src, size_t _Size); }
2.1.4 sizeof 操做符
sizeof 是 c語言中的一個操做符,相似於++、--等等。sizeof 可以告訴咱們編譯器爲某一特定數據或者某一個類型的數據在內存中分配空間時分配的大小,大小以字節爲單位。
基本語法:
sizeof(變量); sizeof 變量; sizeof(類型);
sizeof 注意點:
-
sizeof返回的佔用空間大小是爲這個變量開闢的大小,而不僅是它用到的空間。和現今住房的建築面積和實用面積的概念差很少。因此對結構體用的時候,大多狀況下就得考慮字節對齊的問題了;
-
sizeof返回的數據結果類型是unsigned int;
-
要注意數組名和指針變量的區別。一般狀況下,咱們總以爲數組名和指針變量差很少,可是在用sizeof的時候差異很大,對數組名用sizeof返回的是整個數組的大小,而對指針變量進行操做的時候返回的則是指針變量自己所佔得空間,在32位機的條件下通常都是4。並且當數組名做爲函數參數時,在函數內部,形參也就是個指針,因此再也不返回數組的大小;
//1. sizeof基本用法 void test01(){ int a = 10; printf("len:%d\n", sizeof(a)); printf("len:%d\n", sizeof(int)); printf("len:%d\n", sizeof a); } //2. sizeof 結果類型 void test02(){ unsigned int a = 10; if (a - 11 < 0){ printf("結果小於0\n"); } else{ printf("結果大於0\n"); } int b = 5; if (sizeof(b) - 10 < 0){ printf("結果小於0\n"); } else{ printf("結果大於0\n"); } } //3. sizeof 碰到數組 void TestArray(int arr[]){ printf("TestArray arr size:%d\n",sizeof(arr)); } void test03(){ int arr[] = { 10, 20, 30, 40, 50 }; printf("array size: %d\n",sizeof(arr)); //數組名在某些狀況下等價於指針 int* pArr = arr; printf("arr[2]:%d\n",pArr[2]); printf("array size: %d\n", sizeof(pArr)); //數組作函數函數參數,將退化爲指針,在函數內部再也不返回數組大小 TestArray(arr); }
2.1.5 數據類型總結
-
數據類型本質是固定內存大小的別名,是個模具,C語言規定:經過數據類型定義變量;
-
數據類型大小計算(sizeof);
-
能夠給已存在的數據類型起別名typedef;
-
數據類型的封裝(void 萬能類型);
2.2 變量
2.1.1 變量的概念
既能讀又能寫的內存對象,稱爲變量;
若一旦初始化後不能修改的對象則稱爲常量。
變量定義形式: 類型 標識符, 標識符, … , 標識符
2.1.2 變量名的本質
-
變量名的本質:一段連續內存空間的別名;
-
程序經過變量來申請和命名內存空間 int a = 0;
-
經過變量名訪問內存空間;
-
不是向變量名讀寫數據,而是向變量所表明的內存空間中讀寫數據;
修改變量的兩種方式:
void test(){ int a = 10; //1. 直接修改 a = 20; printf("直接修改,a:%d\n",a); //2. 間接修改 int* p = &a; *p = 30; printf("間接修改,a:%d\n", a); }
2.3 程序的內存分區模型
2.3.1 內存分區
2.3.1.1 運行以前
咱們要想執行咱們編寫的c程序,那麼第一步須要對這個程序進行編譯。 1)預處理:宏定義展開、頭文件展開、條件編譯,這裏並不會檢查語法
2)編譯:檢查語法,將預處理後文件編譯生成彙編文件
3)彙編:將彙編文件生成目標文件(二進制文件)
4)連接:將目標文件連接爲可執行程序
代碼區
存放 CPU 執行的機器指令。一般代碼區是可共享的(即另外的執行程序能夠調用它),使其可共享的目的是對於頻繁被執行的程序,只須要在內存中有一份代碼便可。代碼區一般是隻讀的,使其只讀的緣由是防止程序意外地修改了它的指t令。另外,代碼區還規劃了局部變量的相關信息。
全局初始化數據區/靜態數據區(data段)
該區包含了在程序中明確被初始化的全局變量、已經初始化的靜態變量(包括全局靜態變量和t)和常量數據(如字符串常量)。
未初始化數據區(又叫 bss 區)
存入的是全局未初始化變量和未初始化靜態變量。未初始化數據區的數據在程序開始執行以前被內核初始化爲 0 或者空(NULL)。
整體來說說,程序源代碼被編譯以後主要分紅兩種段:程序指令(代碼區)和程序數據(數據區)。代碼段屬於程序指令,而數據域段和.bss段屬於程序數據。
那爲何把程序的指令和程序數據分開呢?
-
程序被load到內存中以後,能夠將數據和代碼分別映射到兩個內存區域。因爲數據區域對進程來講是可讀可寫的,而指令區域對程序來說說是隻讀的,因此分區以後呢,能夠將程序指令區域和數據區域分別設置成可讀可寫或只讀。這樣能夠防止程序的指令有意或者無心被修改;
-
當系統中運行着多個一樣的程序的時候,這些程序執行的指令都是同樣的,因此只須要內存中保存一份程序的指令就能夠了,只是每個程序運行中數據不同而已,這樣能夠節省大量的內存。好比說以前的Windows Internet Explorer 7.0運行起來以後, 它須要佔用112 844KB的內存,它的私有部分數據有大概15 944KB,也就是說有96 900KB空間是共享的,若是程序中運行了幾百個這樣的進程,能夠想象共享的方法能夠節省大量的內存。
2.3.1.1 運行以後
程序在加載到內存前,代碼區和全局區(data和bss)的大小就是固定的,程序運行期間不能改變。而後,運行可執行程序,操做系統把物理硬盤程序load(加載)到內存,除了根據可執行程序的信息分出代碼區(text)、數據區(data)和未初始化數據區(bss)以外,還額外增長了棧區、堆區。
代碼區(text segment)
加載的是可執行文件代碼段,全部的可執行代碼都加載到代碼區,這塊內存是不能夠在運行期間修改的。
未初始化數據區(BSS)
加載的是可執行文件BSS段,位置能夠分開亦能夠緊靠數據段,存儲於數據段的數據(全局未初始化,靜態未初始化數據)的生存週期爲整個程序運行過程。
全局初始化數據區/靜態數據區(data segment)
加載的是可執行文件數據段,存儲於數據段(全局初始化,靜態初始化數據,文字常量(只讀))的數據的生存週期爲整個程序運行過程。
棧區(stack)
棧是一種先進後出的內存結構,由編譯器自動分配釋放,存放函數的參數值、返回值、局部變量等。在程序運行過程當中實時加載和釋放,所以,局部變量的生存週期爲申請到釋放該段棧空間。
堆區(heap)
堆是一個大容器,它的容量要遠遠大於棧,但沒有棧那樣先進後出的順序。用於動態內存分配。堆在內存中位於BSS區和棧區之間。通常由程序員分配和釋放,若程序員不釋放,程序結束時由操做系統回收。
2.3.2 分區模型
2.3.2.1 棧區
由系統進行內存的管理。主要存放函數的參數以及局部變量。在函數完成執行,系統自行釋放棧區內存,不須要用戶管理。
#char* func(){ char p[] = "hello world!"; //在棧區存儲 亂碼 printf("%s\n", p); return p; } void test(){ char* p = NULL; p = func(); printf("%s\n",p); }
2.3.2.2 堆區
由編程人員手動申請,手動釋放,若不手動釋放,程序結束後由系統回收,生命週期是整個程序運行期間。使用malloc或者new進行堆的申請。
char* func(){ char* str = malloc(100); strcpy(str, "hello world!"); printf("%s\n",str); return str; } void test01(){ char* p = NULL; p = func(); printf("%s\n",p); } void allocateSpace(char* p){ p = malloc(100); strcpy(p, "hello world!"); printf("%s\n", p); } void test02(){ char* p = NULL; allocateSpace(p); printf("%s\n", p); }
堆分配內存API:
#include <stdlib.h> void *calloc(size_t nmemb, size_t size);
功能:
在內存動態存儲區中分配nmemb塊長度爲size字節的連續區域。calloc自動將分配的內存 置0。
參數:
nmemb:所需內存單元數量 size:每一個內存單元的大小(單位:字節)
返回值:
成功:分配空間的起始地址
失敗:NULL
#include <stdlib.h> void *realloc(void *ptr, size_t size);
功能:
從新分配用malloc或者calloc函數在堆中分配內存空間的大小。 realloc不會自動清理增長的內存,須要手動清理,若是指定的地址後面有連續的空間,那麼就會在已有地址基礎上增長內存,若是指定的地址後面沒有空間,那麼realloc會從新分配新的連續內存,把舊內存的值拷貝到新內存,同時釋放舊內存。
參數:
ptr:爲以前用malloc或者calloc分配的內存地址,若是此參數等於NULL,那麼和realloc與malloc功能一致
size:爲從新分配內存的大小, 單位:字節
返回值:
成功:新分配的堆內存地址
失敗:NULL
void test01(){ int* p1 = calloc(10,sizeof(int)); if (p1 == NULL){ return; } for (int i = 0; i < 10; i ++){ p1[i] = i + 1; } for (int i = 0; i < 10; i++){ printf("%d ",p1[i]); } printf("\n"); free(p1); } void test02(){ int* p1 = calloc(10, sizeof(int)); if (p1 == NULL){ return; } for (int i = 0; i < 10; i++){ p1[i] = i + 1; } int* p2 = realloc(p1, 15 * sizeof(int)); if (p2 == NULL){ return; } printf("%d\n", p1); printf("%d\n", p2); //打印 for (int i = 0; i < 15; i++){ printf("%d ", p2[i]); } printf("\n"); //從新賦值 for (int i = 0; i < 15; i++){ p2[i] = i + 1; } //再次打印 for (int i = 0; i < 15; i++){ printf("%d ", p2[i]); } printf("\n"); free(p2); }
2.3.2.3 全局/靜態區
全局靜態區內的變量在編譯階段已經分配好內存空間並初始化。這塊內存在程序運行期間一直存在,它主要存儲全局變量、靜態變量和常量。
注意:
(1)這裏不區分初始化和未初始化的數據區,是由於靜態存儲區內的變量若不顯示初始化,則編譯器會自動以默認的方式進行初始化,即靜態存儲區內不存在未初始化的變量。
(2)全局靜態存儲區內的常量分爲常變量和字符串常量,一經初始化,不可修改。靜態存儲內的常變量是全局變量,與局部常變量不一樣,區別在於局部常變量存放於棧,實際可間接經過指針或者引用進行修改,而全局常變量存放於靜態常量區則不能夠間接修改。
(3)字符串常量存儲在全局/靜態存儲區的常量區。
int v1 = 10;//全局/靜態區
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/靜態區
char *p1; //全局/靜態區,編譯器默認初始化爲NULL
//那麼全局static int 和 全局int變量有什麼區別?
void test(){
static int v4 = 20; //全局/靜態區
}
char* func(){ static char arr[] = "hello world!"; //在靜態區存儲 可讀可寫 arr[2] = 'c'; char* p = "hello world!"; //全局/靜態區-字符串常量區 //p[2] = 'c'; //只讀,不可修改 printf("%d\n",arr); printf("%d\n",p); printf("%s\n", arr); return arr; } void test(){ char* p = func(); printf("%s\n",p); }
2.3.2.4 總結
在理解C/C++內存分區時,常會碰到以下術語:數據區,堆,棧,靜態區,常量區,全局區,字符串常量區,文字常量區,代碼區等等,初學者被搞得雲裏霧裏。在這裏,嘗試捋清楚以上分區的關係。
數據區包括:堆,棧,全局/靜態存儲區。
-
全局/靜態存儲區包括:常量區,全局區、靜態區。
-
常量區包括:字符串常量區、常變量區。
-
代碼區:存放程序編譯後的二進制代碼,不可尋址區。
能夠說,C/C++內存分區其實只有兩個,即代碼區和數據區。
2.3.3 函數調用模型
2.3.3.1 函數調用流程
棧(stack)是現代計算機程序裏最爲重要的概念之一,幾乎每個程序都使用了棧,沒有棧就沒有函數,沒有局部變量,也就沒有咱們現在能見到的全部計算機的語言。在解釋爲何棧如此重要以前,咱們先了解一下傳統的棧的定義:
在經典的計算機科學中,棧被定義爲一個特殊的容器,用戶能夠將數據壓入棧中(入棧,push),也能夠將壓入棧中的數據彈出(出棧,pop),可是棧容器必須遵循一條規則:先入棧的數據最後出棧(First In Last Out,FILO).
在經典的操做系統中,棧老是向下增加的。壓棧的操做使得棧頂的地址減少,彈出操做使得棧頂地址增大。
棧在程序運行中具備極其重要的地位。最重要的,棧保存一個函數調用所須要維護的信息,這一般被稱爲堆棧幀(Stack Frame)或者活動記錄(Activate Record).一個函數調用過程所須要的信息通常包括如下幾個方面:
-
函數的返回地址;
-
函數的參數;
-
臨時變量;
-
保存的上下文:包括在函數調用先後須要保持不變的寄存器。
咱們從下面的代碼,分析如下函數的調用過程:
int func(int a,int b){ int t_a = a; int t_b = b; return t_a + t_b; } int main(){ int ret = 0; ret = func(10, 20); return EXIT_SUCCESS; }
2.3.3.2 調用慣例
如今,咱們大體瞭解了函數調用的過程,這期間有一個現象,那就是函數的調用者和被調用者對函數調用有着一致的理解,例如,它們雙方都一致的認爲函數的參數是按照某個固定的方式壓入棧中。若是不這樣的話,函數將沒法正確運行。
若是函數調用方在傳遞參數的時候先壓入a參數,再壓入b參數,而被調用函數則認爲先壓入的是b,後壓入的是a,那麼被調用函數在使用a,b值時候,就會顛倒。
所以,函數的調用方和被調用方對於函數是如何調用的必須有一個明確的約定,只有雙方都遵循一樣的約定,函數纔可以被正確的調用,這樣的約定被稱爲」調用慣例(Calling Convention)」.一個調用慣例通常包含如下幾個方面:
函數參數的傳遞順序和方式
函數的傳遞有不少種方式,最多見的是經過棧傳遞。函數的調用方將參數壓入棧中,函數本身再從棧中將參數取出。對於有多個參數的函數,調用慣例要規定函數調用方將參數壓棧的順序:從左向右,仍是從右向左。有些調用慣例還容許使用寄存器傳遞參數,以提升性能。
棧的維護方式
在函數將參數壓入棧中以後,函數體會被調用,此後須要將被壓入棧中的參數所有彈出,以使得棧在函數調用先後保持一致。這個彈出的工做能夠由函數的調用方來完成,也能夠由函數自己來完成。
爲了在連接的時候對調用慣例進行區分,調用慣例要對函數自己的名字進行修飾。不一樣的調用慣例有不一樣的名字修飾策略。
事實上,在c語言裏,存在着多個調用慣例,而默認的是cdecl.任何一個沒有顯示指定調用慣例的函數都是默認是cdecl慣例。好比咱們上面對於func函數的聲明,它的完整寫法應該是:
int _cdecl func(int a,int b);
注意: cdecl不是標準的關鍵字,在不一樣的編譯器裏可能有不一樣的寫法,例如gcc裏就不存在_cdecl這樣的關鍵字,而是使用__attribute_((cdecl)).
2.3.3.2 函數變量傳遞分析
2.3.4 棧的生長方向和內存存放方向
//1. 棧的生長方向 void test01(){ int a = 10; int b = 20; int c = 30; int d = 40; printf("a = %d\n", &a); printf("b = %d\n", &b); printf("c = %d\n", &c); printf("d = %d\n", &d); //a的地址大於b的地址,故而生長方向向下 } //2. 內存生長方向(小端模式) void test02(){ //高位字節 -> 地位字節 int num = 0xaabbccdd; unsigned char* p = # //從首地址開始的第一個字節 printf("%x\n",*p); printf("%x\n", *(p + 1)); printf("%x\n", *(p + 2)); printf("%x\n", *(p + 3)); }