大家好,我是唐唐!
歡迎大家來到c語言的世界,c語言是一種強大的專業(yè)化的編程語言。
貝爾實驗室的Dennis Ritchie在1972年開發(fā)了C,當時他正與ken Thompson一起設計UNIX操作系統(tǒng),然而,C并不是完全由Ritchie構想出來的。它來自Thompson的B語言。
在過去的幾十年中,c語言已成為最流行和最重要的編程語言之一。它之所以得到發(fā)展,是因為人們嘗試使用它后都喜歡它。過去很多年中,許多人從c語言轉而使用更強大的c++語言,但c有其自身的優(yōu)勢,仍然是一種重要的語言,而且它還是學習c++的必經之路。
1.3.1 K&R C
起初,C語言沒有官方標準。1978年由美國電話電報公司(AT&T)貝爾實驗室正式發(fā)表了C語言。布萊恩·柯林漢(Brian Kernighan) 和 丹尼斯·里奇(Dennis Ritchie) 出版了一本書,名叫《The C Programming Language》。這本書被 C語言開發(fā)者們稱為K&R,很多年來被當作 C語言的非正式的標準說明。人們稱這個版本的 C語言為K&R C。
K&R C主要介紹了以下特色:結構體(struct)類型;長整數(shù)(long int)類型;無符號整數(shù)(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年,美國國家標準協(xié)會(ANSI)成立了一個委員會X3J11,來制定 C語言標準。
1989年,美國國家標準協(xié)會(ANSI)通過了C語言標準,被稱為ANSI X3.159-1989 "Programming Language C"。因為這個標準是1989年通過的,所以一般簡稱C89標準。有些人也簡稱ANSI C,因為這個標準是美國國家標準協(xié)會(ANSI)發(fā)布的。
1990年,國際標準化組織(ISO)和國際電工委員會(IEC)把C89標準定為C語言的國際標準,命名為ISO/IEC 9899:1990 - Programming languages -- C[5] 。因為此標準是在1990年發(fā)布的,所以有些人把簡稱作C90標準。不過大多數(shù)人依然稱之為C89標準,因為此標準與ANSI C89標準完全等同。
1994年,國際標準化組織(ISO)和國際電工委員會(IEC)發(fā)布了C89標準修訂版,名叫ISO/IEC 9899:1990/Cor 1:1994[6] ,有些人簡稱為C94標準。
1995年,國際標準化組織(ISO)和國際電工委員會(IEC)再次發(fā)布了C89標準修訂版,名叫ISO/IEC 9899:1990/Amd 1:1995 - C Integrity[7] ,有些人簡稱為C95標準。
1.3.3 C99標準
1999年1月,國際標準化組織(ISO)和國際電工委員會(IEC)發(fā)布了C語言的新標準,名叫ISO/IEC 9899:1999 - Programming languages -- C ,簡稱C99標準。這是C語言的第二個官方標準。
例如:
2.1.1 數(shù)據類型概念
什么是數(shù)據類型?為什么需要數(shù)據類型? 數(shù)據類型是為了更好進行內存的管理,讓編譯器能確定分配多少內存。
我們現(xiàn)實生活中,狗是狗,鳥是鳥等等,每一種事物都有自己的類型,那么程序中使用數(shù)據類型也是來源于生活。
當我們給狗分配內存的時候,也就相當于給狗建造狗窩,給鳥分配內存的時候,也就是給鳥建造一個鳥窩,我們可以給他們各自建造一個別墅,但是會造成內存的浪費,不能很好的利用內存空間。
我們在想,如果給鳥分配內存,只需要鳥窩大小的空間就夠了,如果給狗分配內存,那么也只需要狗窩大小的內存,而不是給鳥和狗都分配一座別墅,造成內存的浪費。
當我們定義一個變量,a = 10,編譯器如何分配內存?計算機只是一個機器,它怎么知道用多少內存可以放得下10?
所以說,數(shù)據類型非常重要,它可以告訴編譯器分配多少內存可以放得下我們的數(shù)據。
狗窩里面是狗,鳥窩里面是鳥,如果沒有數(shù)據類型,你怎么知道冰箱里放得是一頭大象!
數(shù)據類型基本概念:
2.1.2 數(shù)據類型別名
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數(shù)據類型
void字面意思是”無類型”,void* 無類型指針,無類型指針可以指向任何類型的數(shù)據。
void定義變量是沒有任何意義的,當你定義void a,編譯器會報錯。
void真正用在以下兩個方面:
//1. void修飾函數(shù)參數(shù)和函數(shù)返回
void test01(void){
printf("hello world");
}
//2. 不能定義void類型變量
void test02(){
void val; //報錯
}
//3. void* 可以指向任何類型的數(shù)據,被稱為萬能指針
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* 常用于數(shù)據類型的封裝
void test04(){
//void * memcpy(void * _Dst, const void * _Src, size_t _Size);
}
2.1.4 sizeof 操作符
sizeof 是 c語言中的一個操作符,類似于++、--等等。sizeof 能夠告訴我們編譯器為某一特定數(shù)據或者某一個類型的數(shù)據在內存中分配空間時分配的大小,大小以字節(jié)為單位。
基本語法:
sizeof(變量);
sizeof 變量;
sizeof(類型);
sizeof 注意點:
//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 碰到數(shù)組
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));
//數(shù)組名在某些情況下等價于指針
int* pArr = arr;
printf("arr[2]:%d\n",pArr[2]);
printf("array size: %d\n", sizeof(pArr));
//數(shù)組做函數(shù)函數(shù)參數(shù),將退化為指針,在函數(shù)內部不再返回數(shù)組大小
TestArray(arr);
}
2.1.5 數(shù)據類型總結
2.1.1 變量的概念
既能讀又能寫的內存對象,稱為變量;
若一旦初始化后不能修改的對象則稱為常量。
變量定義形式: 類型 標識符, 標識符, … , 標識符
2.1.2 變量名的本質
修改變量的兩種方式:
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.1 內存分區(qū)
2.3.1.1 運行之前
我們要想執(zhí)行我們編寫的c程序,那么第一步需要對這個程序進行編譯。1)預處理:宏定義展開、頭文件展開、條件編譯,這里并不會檢查語法
2)編譯:檢查語法,將預處理后文件編譯生成匯編文件
3)匯編:將匯編文件生成目標文件(二進制文件)
4)鏈接:將目標文件鏈接為可執(zhí)行程序
?代碼區(qū)
存放 CPU 執(zhí)行的機器指令。通常代碼區(qū)是可共享的(即另外的執(zhí)行程序可以調用它),使其可共享的目的是對于頻繁被執(zhí)行的程序,只需要在內存中有一份代碼即可。代碼區(qū)通常是只讀的,使其只讀的原因是防止程序意外地修改了它的指t令。另外,代碼區(qū)還規(guī)劃了局部變量的相關信息。
?全局初始化數(shù)據區(qū)/靜態(tài)數(shù)據區(qū)(data段)
該區(qū)包含了在程序中明確被初始化的全局變量、已經初始化的靜態(tài)變量(包括全局靜態(tài)變量和t)和常量數(shù)據(如字符串常量)。
?未初始化數(shù)據區(qū)(又叫 bss 區(qū))
存入的是全局未初始化變量和未初始化靜態(tài)變量。未初始化數(shù)據區(qū)的數(shù)據在程序開始執(zhí)行之前被內核初始化為 0 或者空(NULL)。
總體來講說,程序源代碼被編譯之后主要分成兩種段:程序指令(代碼區(qū))和程序數(shù)據(數(shù)據區(qū))。代碼段屬于程序指令,而數(shù)據域段和.bss段屬于程序數(shù)據。
那為什么把程序的指令和程序數(shù)據分開呢?
程序被load到內存中之后,可以將數(shù)據和代碼分別映射到兩個內存區(qū)域。由于數(shù)據區(qū)域對進程來說是可讀可寫的,而指令區(qū)域對程序來講說是只讀的,所以分區(qū)之后呢,可以將程序指令區(qū)域和數(shù)據區(qū)域分別設置成可讀可寫或只讀。這樣可以防止程序的指令有意或者無意被修改;
當系統(tǒng)中運行著多個同樣的程序的時候,這些程序執(zhí)行的指令都是一樣的,所以只需要內存中保存一份程序的指令就可以了,只是每一個程序運行中數(shù)據不一樣而已,這樣可以節(jié)省大量的內存。比如說之前的Windows Internet Explorer 7.0運行起來之后, 它需要占用112 844KB的內存,它的私有部分數(shù)據有大概15 944KB,也就是說有96 900KB空間是共享的,如果程序中運行了幾百個這樣的進程,可以想象共享的方法可以節(jié)省大量的內存。
2.3.1.1 運行之后
程序在加載到內存前,代碼區(qū)和全局區(qū)(data和bss)的大小就是固定的,程序運行期間不能改變。然后,運行可執(zhí)行程序,操作系統(tǒng)把物理硬盤程序load(加載)到內存,除了根據可執(zhí)行程序的信息分出代碼區(qū)(text)、數(shù)據區(qū)(data)和未初始化數(shù)據區(qū)(bss)之外,還額外增加了棧區(qū)、堆區(qū)。
?代碼區(qū)(text segment)
加載的是可執(zhí)行文件代碼段,所有的可執(zhí)行代碼都加載到代碼區(qū),這塊內存是不可以在運行期間修改的。
?未初始化數(shù)據區(qū)(BSS)
加載的是可執(zhí)行文件BSS段,位置可以分開亦可以緊靠數(shù)據段,存儲于數(shù)據段的數(shù)據(全局未初始化,靜態(tài)未初始化數(shù)據)的生存周期為整個程序運行過程。
?全局初始化數(shù)據區(qū)/靜態(tài)數(shù)據區(qū)(data segment)
加載的是可執(zhí)行文件數(shù)據段,存儲于數(shù)據段(全局初始化,靜態(tài)初始化數(shù)據,文字常量(只讀))的數(shù)據的生存周期為整個程序運行過程。
?棧區(qū)(stack)
棧是一種先進后出的內存結構,由編譯器自動分配釋放,存放函數(shù)的參數(shù)值、返回值、局部變量等。在程序運行過程中實時加載和釋放,因此,局部變量的生存周期為申請到釋放該段??臻g。
?堆區(qū)(heap)
堆是一個大容器,它的容量要遠遠大于棧,但沒有棧那樣先進后出的順序。用于動態(tài)內存分配。堆在內存中位于BSS區(qū)和棧區(qū)之間。一般由程序員分配和釋放,若程序員不釋放,程序結束時由操作系統(tǒng)回收。
2.3.2 分區(qū)模型
2.3.2.1 棧區(qū)
由系統(tǒng)進行內存的管理。主要存放函數(shù)的參數(shù)以及局部變量。在函數(shù)完成執(zhí)行,系統(tǒng)自行釋放棧區(qū)內存,不需要用戶管理。
#char* func(){
char p[] = "hello world!"; //在棧區(qū)存儲 亂碼
printf("%s\n", p);
return p;
}
void test(){
char* p = NULL;
p = func();
printf("%s\n",p);
}
2.3.2.2 堆區(qū)
由編程人員手動申請,手動釋放,若不手動釋放,程序結束后由系統(tǒng)回收,生命周期是整個程序運行期間。使用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);
功能:
在內存動態(tài)存儲區(qū)中分配nmemb塊長度為size字節(jié)的連續(xù)區(qū)域。calloc自動將分配的內存置0。
參數(shù):
nmemb:所需內存單元數(shù)量 size:每個內存單元的大小(單位:字節(jié))
返回值:
成功:分配空間的起始地址
失?。篘ULL
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
功能:
重新分配用malloc或者calloc函數(shù)在堆中分配內存空間的大小。realloc不會自動清理增加的內存,需要手動清理,如果指定的地址后面有連續(xù)的空間,那么就會在已有地址基礎上增加內存,如果指定的地址后面沒有空間,那么realloc會重新分配新的連續(xù)內存,把舊內存的值拷貝到新內存,同時釋放舊內存。
參數(shù):
ptr:為之前用malloc或者calloc分配的內存地址,如果此參數(shù)等于NULL,那么和realloc與malloc功能一致
size:為重新分配內存的大小, 單位:字節(jié)
返回值:
成功:新分配的堆內存地址
失?。篘ULL
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 全局/靜態(tài)區(qū)
全局靜態(tài)區(qū)內的變量在編譯階段已經分配好內存空間并初始化。這塊內存在程序運行期間一直存在,它主要存儲全局變量、靜態(tài)變量和常量。
注意:
(1)這里不區(qū)分初始化和未初始化的數(shù)據區(qū),是因為靜態(tài)存儲區(qū)內的變量若不顯示初始化,則編譯器會自動以默認的方式進行初始化,即靜態(tài)存儲區(qū)內不存在未初始化的變量。
(2)全局靜態(tài)存儲區(qū)內的常量分為常變量和字符串常量,一經初始化,不可修改。靜態(tài)存儲內的常變量是全局變量,與局部常變量不同,區(qū)別在于局部常變量存放于棧,實際可間接通過指針或者引用進行修改,而全局常變量存放于靜態(tài)常量區(qū)則不可以間接修改。
(3)字符串常量存儲在全局/靜態(tài)存儲區(qū)的常量區(qū)。
int v1 = 10;//全局/靜態(tài)區(qū)
const int v2 = 20; //常量,一旦初始化,不可修改
static int v3 = 20; //全局/靜態(tài)區(qū)
char *p1; //全局/靜態(tài)區(qū),編譯器默認初始化為NULL
//那么全局static int 和 全局int變量有什么區(qū)別?
void test(){
static int v4 = 20; //全局/靜態(tài)區(qū)
}
char* func(){
static char arr[] = "hello world!"; //在靜態(tài)區(qū)存儲 可讀可寫
arr[2] = 'c';
char* p = "hello world!"; //全局/靜態(tài)區(qū)-字符串常量區(qū)
//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++內存分區(qū)時,常會碰到如下術語:數(shù)據區(qū),堆,棧,靜態(tài)區(qū),常量區(qū),全局區(qū),字符串常量區(qū),文字常量區(qū),代碼區(qū)等等,初學者被搞得云里霧里。在這里,嘗試捋清楚以上分區(qū)的關系。
數(shù)據區(qū)包括:堆,棧,全局/靜態(tài)存儲區(qū)。
可以說,C/C++內存分區(qū)其實只有兩個,即代碼區(qū)和數(shù)據區(qū)。
2.3.3 函數(shù)調用模型
2.3.3.1 函數(shù)調用流程
棧(stack)是現(xiàn)代計算機程序里最為重要的概念之一,幾乎每一個程序都使用了棧,沒有棧就沒有函數(shù),沒有局部變量,也就沒有我們如今能見到的所有計算機的語言。在解釋為什么棧如此重要之前,我們先了解一下傳統(tǒng)的棧的定義:
在經典的計算機科學中,棧被定義為一個特殊的容器,用戶可以將數(shù)據壓入棧中(入棧,push),也可以將壓入棧中的數(shù)據彈出(出棧,pop),但是棧容器必須遵循一條規(guī)則:先入棧的數(shù)據最后出棧(First In Last Out,FILO).
在經典的操作系統(tǒng)中,??偸窍蛳略鲩L的。壓棧的操作使得棧頂?shù)牡刂窚p小,彈出操作使得棧頂?shù)刂吩龃蟆?/p>
棧在程序運行中具有極其重要的地位。最重要的,棧保存一個函數(shù)調用所需要維護的信息,這通常被稱為堆棧幀(Stack Frame)或者活動記錄(Activate Record).一個函數(shù)調用過程所需要的信息一般包括以下幾個方面:
我們從下面的代碼,分析以下函數(shù)的調用過程:
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 調用慣例
現(xiàn)在,我們大致了解了函數(shù)調用的過程,這期間有一個現(xiàn)象,那就是函數(shù)的調用者和被調用者對函數(shù)調用有著一致的理解,例如,它們雙方都一致的認為函數(shù)的參數(shù)是按照某個固定的方式壓入棧中。如果不這樣的話,函數(shù)將無法正確運行。
如果函數(shù)調用方在傳遞參數(shù)的時候先壓入a參數(shù),再壓入b參數(shù),而被調用函數(shù)則認為先壓入的是b,后壓入的是a,那么被調用函數(shù)在使用a,b值時候,就會顛倒。
因此,函數(shù)的調用方和被調用方對于函數(shù)是如何調用的必須有一個明確的約定,只有雙方都遵循同樣的約定,函數(shù)才能夠被正確的調用,這樣的約定被稱為”調用慣例(Calling Convention)”.一個調用慣例一般包含以下幾個方面:
函數(shù)參數(shù)的傳遞順序和方式
函數(shù)的傳遞有很多種方式,最常見的是通過棧傳遞。函數(shù)的調用方將參數(shù)壓入棧中,函數(shù)自己再從棧中將參數(shù)取出。對于有多個參數(shù)的函數(shù),調用慣例要規(guī)定函數(shù)調用方將參數(shù)壓棧的順序:從左向右,還是從右向左。有些調用慣例還允許使用寄存器傳遞參數(shù),以提高性能。
棧的維護方式
在函數(shù)將參數(shù)壓入棧中之后,函數(shù)體會被調用,此后需要將被壓入棧中的參數(shù)全部彈出,以使得棧在函數(shù)調用前后保持一致。這個彈出的工作可以由函數(shù)的調用方來完成,也可以由函數(shù)本身來完成。
為了在鏈接的時候對調用慣例進行區(qū)分,調用慣例要對函數(shù)本身的名字進行修飾。不同的調用慣例有不同的名字修飾策略。
事實上,在c語言里,存在著多個調用慣例,而默認的是cdecl.任何一個沒有顯示指定調用慣例的函數(shù)都是默認是cdecl慣例。比如我們上面對于func函數(shù)的聲明,它的完整寫法應該是:
int _cdecl func(int a,int b);
注意: cdecl不是標準的關鍵字,在不同的編譯器里可能有不同的寫法,例如gcc里就不存在_cdecl這樣的關鍵字,而是使用__attribute_((cdecl)).
2.3.3.2 函數(shù)變量傳遞分析
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(){
//高位字節(jié) -> 地位字節(jié)
int num = 0xaabbccdd;
unsigned char* p = #
//從首地址開始的第一個字節(jié)
printf("%x\n",*p);
printf("%x\n", *(p + 1));
printf("%x\n", *(p + 2));
printf("%x\n", *(p + 3));
}
聯(lián)系客服