基本類型:bool,char,short,int,long,float,doublelinux
對於char,short,int,long:算法
多字節類型賦值給少字節類型,對低字節的細節感興趣,位模式拷貝。編程
少字節類型賦值給多字節類型,進行位模式擴展,注意符號,符號擴展。數組
float表示法:表示爲1.x的(-127~128)次方,能夠對數字進行乘2或除2獲得1.x的模式再進行表示。緩存
在整型與浮點型互相賦值的過程當中,會從新計算值,獲得新的位模式,而不是單純的位模式拷貝。安全
1 int i = 7; 2 float f = i;
直接對原先的位模式進行解析(空間大小相同):服務器
1 int i = 37; 2 float f = *(float*)&i;//對i取地址,並把地址轉換成float的指針,再解引用
直接對原先的位模式進行解析(空間大小不一樣):網絡
1 float f = 7.0; 2 short s = *(short*)&f;
直接對原先的位模式進行解析(空間大小不一樣):數據結構
1 short s = 45; 2 double d = *(double*)&s;
關於大端小端:取地址運算符老是獲得最低字節的地址,大端:低字節存儲對於值貢獻最大的位。linux是小端多線程
結構
1 struct fraction 2 { 3 int num; 4 int denom; 5 }; 6 7 fraction pi; 8 pi.num = 22; 9 pi.denom = 7; 10 ((fraction*)&(pi.denom))->num = 12; 11 ((fraction*)&(pi.denom))->denom = 33;
整個結構的地址老是和第一個域的地址一致
數組
1 int array[10]; 2 3 // array 是等價於 &array[0],即數組地址等價於其第0個元素的地址 4 // array + k 是等價於 &array[k],上邊爲特例,array 等價於 array + 0 5 // *array 是等價於 array[0] 6 // *(array + k) 是等價於 array[k] 7 // 進行指針算數運算,數字k能夠基於類型系統知道實例的空間狀況,根據指針的類型進行成倍數的加減,只考慮劃分數據。 8 // 數組下標運算均可以轉換爲與數組類型相關的隱式指針算數運算。 9 // 不能對void*使用數組操做的緣由與它不能解引用的緣由是同樣的,在它之中沒有元素大小的信息,也就失去了比較的能力。由於不知道是啥就不用知道怎麼比較
傳遞數組到函數,並非將整個數組傳入,而是將數組的第0個元素地址傳入。若是知道數組長度則能夠合理的訪問數組。C和C++中,原始數組不提供邊界檢查,可接受大於數組邊界的下標,和負數下標。
結構數組
C中字符串不像C++和Java中的字符串,就是暴露細節的字符數組,由字符指針表示。
這個只是說明一下內部構造,並無賦予內存。
strcpy函數,會把傳入地址看做任意長度字符序列空間基地址用來寫入字符,直到寫完‘\0’爲止。
C的模板化,通用編程,通用指針,通用算法
實現下面這個交換程序的通用版本
一個錯誤的例子:其中參數中的void並不意味着它什麼都沒有指向,這只是說它指向了沒有任何類型信息的某些東西。void能夠用於函數的返回類型,表示沒什麼能夠返回的。也能夠把void做爲惟一參數傳入一個函數,說明不須要任何參數。或者使用void表示通用指針void*。不容許對void*解引用,由於機器並不知道要提取出多少字節做爲操做的一部分。因此須要指定第三個參數,說明內存佈局和被交換的字節數量。
正確的例子:須要交換位模式,課程中的編譯器容許聲明一個大小依賴於參數的數組。(可是真正的編譯器不必定容許將一個很是數值放入數組聲明中,實際實現方式多是動態申請一個size字節大小的塊。是向堆中而不是棧中拷貝,最後釋放掉。)數組並不是C語言中的字符串緩衝區,而是留出足夠的空間存放指定大小的字節,做爲一個小的存儲單元去拷貝一些東西。memcpy函數不是用於字符串拷貝,不關心'\0',必須顯示指定要從內存位置上拷貝多少個字節。
與C++模板相比,若是這段代碼被編譯,那麼兩種調用產生的彙編代碼是同樣的。不過對於模板來講,它對每一個編譯或者調用,根據模板設定進行擴展成兩種代碼的獨立版本,而後根據特定域爲int或double進行編譯。若是調用次數少的話還好,若是有n多種數據類型調用這個函數,在可執行文件中,就會有n種不一樣的代碼拷貝。
調用方法以下:
1 int x = 17, y = 37; 2 swap(&x, &y, sizeof(int)); 3 double d = 20.0, e = 12.1; 4 swap(&d, &e, sizeof(double));
5 short s = 44; 6 float f = 11.1; 7 swap(&s, &f, sizeof(short));//錯誤的調用方式
再來兩種調用方式:
1 char *husband = strdup("Fred"); 2 char *wife = ("Wilma"); 3 swap(&husband, &wife, sizeof(char*));//正常交換兩個變量的內容 4 swap(husband, wife, sizeof(char*));//不改變變量的值,而是改變指向的字符串的值的一部分
實現下面線性搜索程序的通用版本:
1 int lsearch(int key, int array[], int size) 2 { 3 for(int i = 0; i < size; i++) 4 { 5 if(array[i] == key) 6 { 7 return i; 8 } 9 } 10 return -1; 11 }
視做一小塊的通用內存,爲了能從i=0到i=1前進,須要知道第一個元素從哪裏開始,也要傳入元素大小信息,才能人工計算地址。經過地址指定key,也經過地址指定數組,指定有多少個小盒子,和每個盒子的寬度。就知道了for循環的次數,和每一次迭代中在內存中要前進多少。也須要傳入一個比較函數,指定key和數組中的第i個元素的相等條件(不能只是使用兩個等號進行判斷),比較指針指向的內容。
技巧是把基指針轉換爲char*類型,對char*作指針算數運算和日常算數運算效果同樣。
其中memcmp函數對基本類型進行比較,返回正負零三種,可是對含有指針的類型(結構中含有指針的類型,字符指針,C字符串等不能很比如較)
mencpy版本:
1 void lsearch(void* key, void* base, int n, int elemSize) 2 { 3 for(int i = 0; i < n; i++) 4 { 5 void* elemAddr = (char*)base + i * elemSize; 6 if(memcmp(key, elemAddr, elemSize) == 0) 7 return ememAddr; 8 } 9 return NULL; 10 }
函數指針版本:(傳入一個專門用來比較的函數指針,函數參數類型跟memcmp差很少,用函數替換memcmp)
1 void* lsearch(void* key, void* base, int n, int elemSize, int (*cmpfn)(void*, void*))//括號內的星號表示是函數指針,可寫可不寫 2 { 3 for(int i = 0; i < n; i++) 4 { 5 void* elemAddr = (char*)base + i * elemSize; 6 if(cmpfn(key, elemAddr) == 0) 7 return elemAddr; 8 } 9 return NULL; 10 } 11 12 13 int IntCmp(void* elem1, void* elem2) 14 { 15 int *ip1 = elem1; 16 int *ip2 = elem2; 17 return *ip1 - *ip2; 18 } 19 20 21 int array[] = {4,2,3,7,11,8}; 22 int size = 6; 23 int number = 7; 24 int found1 = lserach(&number, array, 6, sizeof(int), IntCmp); 25 if(found1 == NULL)//未找到 26 else//找到 27 28 29 int StrCmp(void* vp1, void* vp2) 30 { 31 char* s1 = *(char**)vp1; 32 char* s2 = *(char**)vp2; 33 return strcmp(s1, s2);//庫函數 34 } 35 36 37 char* notes[] = {"Ab", "F#", "B", "Gb", "D"}; 38 char* favouriteNote = "Eb"; 39 char** found2 = lsearch(&favouriteNote, notes, 5, sizeof(char*), StrCmp);//傳入函數名子做爲參數 40 if(found2 == NULL)//未找到 41 else//找到
函數(function)與方法(method)的區別在於,方法將與其相關的對象的地址做爲一個隱含的參數(這個隱含的參數叫作this指針)。上文中傳入做爲參數的函數類型要麼就是與類不相關的全局函數,要麼就是類中的一個靜態方法(靜態方法意味着調用時在內部並無this指針做爲參數傳入)。
對於C++的方法來講,定義在類中的各個成員函數一般會使用this這個隱含參數傳入接收對象的地址。能夠在任何方法的實現中使用this關鍵字,它會求值爲被操做對象的地址。普通函數並無調用對象,所以沒有隱含的this指針被傳入做爲參數。
C語言(沒有引用沒有模板沒有class,可是有struct,沒const沒public沒private)實現通用數據結構,相似C++範型模板,vector,queue,map,stack,
C++中把行爲描述和具體實現分開,經過.h和.cc文件方式。
當調用一個類的構造函數時,改函數要訪問到this指針,由於函數會把this做爲第-1個參數傳入,或者做爲一個隱含參數在其餘參數傳入以前傳入。下例中顯式實現爲第0個參數。
malloc只考慮到分配的空間大小(有一個參數指定須要的原始字節的數目),獲取到參數後,到堆中查找合適大小的內存塊,在其上作一個標記,表示這塊內存已經被佔用了,而後哦返回這塊內存的基地址。new操做符會隱式的考慮到數據類型。
實現通用的stack數據結構
將一個元素壓棧的時候,將它的全部權從本身手裏交給了棧。
int版本
1 //stack.h 2 typedef struct 3 { 4 int* elems; 5 int logicallen; 6 int alloclength; 7 } stack; 8 9 10 void stackNew(stack* s);//構造函數 11 12 void stackDispose(stack* s);//析構函數 13 14 void stackPush(stack* s, int value);//入棧 15 16 int stackPop(stack* s);//出棧
1 //stack.c 2 void stackNew(stack* s)//構造函數 3 { 4 s->index = 0; 5 s->sum = 4; 6 s->elems = malloc(4 * sizeof(int)); 7 assert(s->elems != NULL); 8 } 9 10 void stackDispose(stack* s)//析構函數 11 { 12 free(s->elems);//若是存放了char*類型的元素,那麼就要寫個for挨個遍歷釋放了。free實際知道某個塊有多大,由於內部有某個結構登記這些信息, 因此每次都知道要還給堆多少內存 13 } 14 15 void stackPush(stack* s, int value)//入棧 16 { 17 if(s->index == s->sum) 18 { 19 sum *= 2; 20 s->elems = realloc(s->elems, s->sum * sizeof(int));//從新調整堆空間大小,在後邊接上一段新內存或者新分配一塊大內存並複製原內 容,實際上的複雜度爲m(堆的大小,在堆中搜索合適的內存),而不僅是複製時間。 21 assert(s->elems != NULL);//若是不知足從新分配條件的話,函數會保留原來的內存而且返回NULL 22 } 23 s->elems[s->index] = value;//注意順序 24 s->index++; 25 } 26 27 int stackPop(stack* s)//出棧,可是爲了速度考慮,通常並不會縮小所分配的內存塊 28 { 29 assert(s->index > 0); 30 s->index--; 31 return(s->elems[s->s->index]);//出棧減1操做和數組訪問操做和入棧順序相反 32 }
實現上面數據結構的通用版本1(int,bool,double,char)
通用版本:首先,由於void*不能作指針運算了,因此要把指針轉化成char*類型。其次,還不知道對象的大小,因此要傳遞一個表示對象大小的參數。
1 //stack.h 2 typedef struct 3 { 4 void* elems; 5 int elemSize; 6 int index; 7 int sum; 8 }stack; 9 10 void stackNew(stack* s, int elemSize);//構造函數 11 12 void stackDispose(stack* s);//析構函數 13 14 void stackPush(stack* s, void* elemAddr);//入棧 15 16 void stackPop(stack* s, void* elemAddr);//出棧
static關鍵字修飾函數原型時(不是類中的方法,而是一個普通的函數),意味着改函數是私有函數,不該該在函數所在文件以外對其引用。(相似於C++中的private)。全局函數其符號或函數名能夠被導出並能夠在其餘的.o文件中訪問,或者被其餘的.o文件使用。static修飾函數則被稱爲是局部的或者內部的,不能在其餘文件中被調用,只能在本身的.o文件中使用。爲了防止函數版本太多,編譯器不知道調用哪個的狀況的衝突。
1 //stack.c 2 void stackNew(stack* s, int elemSize)//構造函數 3 { 4 assert(elemSize > 0); 5 s->elemSize = elemSize; 6 s->index = 0; 7 s->sum = 4; 8 s->elems = malloc(4 * elemSize); 9 assert(s->elems != NULL); 10 } 11 12 void stackDispose(stack* s)//析構函數 13 { 14 free(s->elems); 15 } 16 17 static void stackGrow(stack* s)//棧擴容 18 { 19 s->sum *= 2; 20 s->elems = realloc(s->elems, s->sum * s->elemSize); 21 } 22 23 void stackPush(stack* s, void* elemAddr)//入棧 24 { 25 if(s->index == s->sum) 26 { 27 stackGrow(s); 28 } 29 void* target = (char*)s->elems + s->index * s->elemSize; 30 memcpy(target, elemAddr, s->elemSize); 31 s->index++; 32 } 33 34 void stackPop(stack* s, void* elemAddr)//出棧,最好由調用它的函數來管理內存(負責分配和回收內存),責任對等(對於任何申請了空間的函數,它 也應該負責這些空間的釋放工做) 35 { 36 void source = (char*)s->elems + (index-1) * s->elemSize; 37 memcpy(elemAddr, source, s->elemSize); 38 s->index--; 39 }
1 int main(_, _) 2 { 3 const char* friends[] = {"Al", "Bob", "Carl"}; 4 stack stringStack;//此時裏邊只是無心義的16字節數據 5 stackNew(&stringStack, sizeof(char*));//做相應的初始化工做,可是新申請的空間並未初始化 6 for(int i = 0; i < 3; i++) 7 { 8 char* copy = startup(friends[i]); 9 stackPush(&stringStack, ©); 10 } 11 char* name; 12 for(int i = 0; i < 3; i++) 13 { 14 stackPop(&stringStack, &name); 15 printf("%s\n",name); 16 free(name); 17 } 18 stackDispose(&stringStack);//可是用戶不須要彈出全部棧元素才釋放棧空間。爲了方便,應該先釋放掉這些元素並將它們歸還給堆空間,再釋放掉棧 自己空間。(核心是注意歸還各種動態申請的資源或者是打開的文件資源等等) 19 }
通用版本2(非基本類型,涉及動態內存申請。如字符串指針,結構裏帶字符串,指向結構的指針之類的)
1 typedef struct 2 { 3 void* elems; 4 int elemSize; 5 int index; 6 int sum; 7 void (*freefn)(void*);//基本類型傳入NULL。char*,指向結構的指針,一些成員是指向動態申請內存的指針的結構,就能夠指定一個釋放函數 8 }stack; 9 10 void stackNew(stack* s, int elemSize, void (*freefn)(void*));//構造函數,就是爲了初始化基本信息並初始化對應的釋放函數的指針做用 11 12 void stringFree(void* elem)//針對字符串版本的釋放函數 13 { 14 free(*(char** )(elem)); 15 } 16 17 void stackDispose(stack* s)//析構函數,保證在析構結構體自己以前,先釋放申請的資源 18 { 19 if(s->freefn != NULL) 20 { 21 for(int i = 0; i < s->index; i++) 22 { 23 s->freefn((char*)s->elems + i * s->elemSize); 24 } 25 } 26 free(s->elems); 27 } 28 29 void stackPush(stack* s, void* elemAddr);//入棧 30 31 void stackPop(stack* s, void* elemAddr);//出棧。不須要把彈出棧的元素釋放掉,由於元素全部權要要移交給變量,變量不能得到一塊無用的內存
指針相減和memcpy效率
1 void rotate(void* front, void* middle, void* end) 2 { 3 int frontSize = (char*)middle - (char*)front;//對兩個int*類型相減獲得兩個地址之間的int類型元素的個數,而不是總字節數。指針相減與指 針相加相符合。 4 int backSize = (char*end) - (char*)middle; 5 char buffer[frontSize]; 6 memcpy(buffer, front, frontSize);//儘量使用memcpy函數,效率高 7 memmove(front, middle, backSize);//在複製時由於有可能會重合,而後使用memcpy的低效版本memmove,會作判斷,從低到高或從高到低複製 8 memcpy((char*)end - frontSize, buffer, frontSize); 9 }
堆內存管理(堆在低地址,向高地址生長)
1 int *arr = malloc(40 * sizeof(int)); 2 free(arr);
堆中,分配內存時,每塊被分配的內存塊都比實際的要大一些,前幾字節的頭部會存放整塊內存(包括頭部)的空間大小和其餘額外的信息。釋放內存時,會對傳入的指針向前找幾個字節,解釋成內存塊的大小,並以某種方式釋放,加入到空閒列表數據結構中。
系統維護一個空閒列表數據結構。每一個空閒塊頭部會存儲指向下一個空白塊的地址。
內存壓縮(內存空間不足):malloc,free,realloc返回的是句柄,不是直接指向數據的指針,而是距離實際數據兩跳的指針 。系統會維護一個一級指針的列表,並把列表的首地址返回給用戶,所以用戶與實際數據的距離是兩跳。
棧內存管理(棧在高地址,向地址值生長)
棧中被使用的部分大致上與活動函數的數量成比例(即棧中正在執行的全部函數的深度)。每次調用新函數時,棧指針會減少其棧幀的大小。棧指針實際上是爲了訪問本函數訪問其各個局部變量提供入口地址的基指針,到最後一個參數的偏移距離是0。棧指針實際使用硬件存儲(硬件會確保當前相關的活動記錄的基地址存儲在某個寄存器中),老是跟蹤並指向最近被調用函數的活動記錄。一個棧幀不能訪問以前另外一個棧幀的內容,除非做爲參數傳入。函數調用結束時,棧指針會返回函數調用以前的位置。
代碼段(在更低地址的內存中)
代碼段內存中存儲着C或C++代碼對應的編譯事後的彙編代碼。
通用內存鏈接到寄存器集,寄存器集鏈接到ALU(算數邏輯單元),ALU與內存並不直接相連。經過save & load(store:將內存中某塊區域的值進行了更新 & load:取數到寄存器中(內存,當即數等))方法完成寄存器和內存之間的數據傳輸。運算算流程以下(讀取-執行-寫回,即load-ALU-store):寄存器須要從內存取操做數,執行運算,把運算結果存放到寄存器中,並刷回到內存中。把C和C++代碼翻譯成彙編代碼指令,彙編代碼訪問內存,執行而且模擬C和C++代碼中的功能。
棧段中的局部變量的地址表示爲,以棧指針(SP,stack pointer)爲基址,並加上一個偏移量。內存對4字節整數倍的存儲狀況做了優化。彙編不容許一條指令中同時出現save&load(內存到內存)的狀況。若是一個語言擁有循環結構,那麼彙編語言中會毫無例外的執行跳轉任意長度的某些指令,這些指令用來在一塊兒執行循環體。if和switch語句會根據測試結果決定是否執行,也會有跳轉指令。PC指向當前指令,跳轉就是再加一個偏移。分爲無條件跳轉和有條件跳轉。彙編代碼最好能寫成上下文無關的形式。彙編指令都是4字節的,一共有59種操做模式,4字節前6位是操做碼,後邊的位如何解釋依賴於操做碼的模式。
棧中聲明幾個變量,從高地址向低地址依次賦值。棧中聲明一個數組,數組的0號元素在低字節,最後一個元素在高字節。 棧中聲明一個結構體,結構體的第一個元素也是在低地址,最後一個元素在高地址。(整體上來看是從高地址向低地址生長的,可是有結構的(數組,結構體等)都是在內部從低地址向高地址增加)在彙編中,能夠忘掉是數組仍是結構,只是直接操縱結構中的各個成員域。強制類型轉換容許編譯器繞過類型檢查機制進而經過編譯而產生彙編代碼。
函數的參數也應該在棧幀中存儲,參數從右至左,從高地址到低地址存放,第0個參數總在其它參數下方。全部局部變量都在比參數更低的地址。棧幀中,在參數和局部變量之間有一小塊內存,存放着一些信息,告訴咱們究竟是那段代碼調用了本函數(saved PC,或 return link 表示即將返回繼續執行的地址)。棧中要生成局部變量,先要生成爲局部變量申請內存空間的代碼。SP寄存器(stack pointer)其老是指向執行中的棧的最低地址。局部變量的申請和建立,會使得SP減少一個值,SP指向棧段中使用的和還未使用的內存的分界線。
C語言程序例子1:
1 void foo(int bar, int* baz)//一個C語言函數執行的第一步就是爲局部變量申請空間,壓參數入棧由調用語句實現。同一個函數幀由調用語句和被調用函數共同造成(調用語句壓參數和返回地址入棧,被調用函數壓局部變量入棧)。當進入到被調用函數中時,棧指針(SP)最初指向的是返回地址。 2 { 3 char snink[4]; 4 short *why; 5 //<foo>:SP = SP - 8;將棧指針的值減少來爲兩個變量留出空間,從而完成整個活動記錄。由於編譯器可以在編譯時肯定到底有多少個字節(能夠在產生彙編代碼的時候,能夠看到在全部函數代碼最頭部聲明的局部變量,計算出他們所佔大小的總和,而後在一條彙編代碼中統一申請空間),將活動記錄擴展成完整的版本,並將棧指針下降並保持新增長的棧空間爲未初始化的狀態(C代碼中未初始化一個局部變量,那麼彙編代碼也不會初始化它)。 6 why = (short*)(snink + 2); 7 //R1 = SP + 6;經過SP的相對偏移訪問局部變量 8 //M[SP] = R1; 9 *why = 50; 10 //R1 = M[SP]; 11 //M[R1] = .2 50;表示只存儲2個字節 12 13 //SP = Sp + 8;在函數結束時隱式調用。函數結束時應將SP提高,讓SP回到調用函數以前的狀態,回收局部變量的棧空間。此時SP指向的內存塊存儲着返回地址 14 //RET;返回指令:會把SP指向的內容取出來,放到PC寄存器中,而且讓SP加4,而後繼續執行以後的代碼,好像程序看起來一直在執行PC+4這個位置的指令,好像沒有調用過foo函數同樣。使程序跳轉到第29行。 15 } 16 17 int main(int args, char** argv) 18 { 19 int i = 4;//局部變量的申請或建立,會被編譯成將SP減去一個值,由於在棧中新申請了一個內存塊,而後將變量初始化 20 //sp = sp - 4; 先減SP 21 //M[sp] = 4; 再進行賦值操做 22 foo(i, &i);//調用語句須要爲調用函數留出空間,須要爲foo建立一個部分的活動記錄,調用語句可以指出須要留出多少空間(看函數原型)。並把i的值和i的地址存入對應的內存空間中,並將控制權轉給模擬foo執行的那部分彙編代碼。 23 //SP = SP - 8; 24 //R1 = M[SP + 8]; 25 //R2 = SP + 8; 26 //M[SP] = R1; 27 //M[sp + 4] = R2; 28 //CALL<foo>;爲參數構建完活動記錄後,經過彙編代碼(一條簡單的跳轉指令)將控制權移交給foo函數,會跳轉到foo函數中的第一條彙編代碼指令並執行它,CALL指令經過某種方式保證當執行完成以後跳回這裏。 29 //SP = SP + 8;若是沒有CALL指令,接下來就執行該彙編指令。(本條彙編指令地址被存儲到saved pc那塊內存中,存地址動做是在執行CALL指令時自動完成的。當執行CALL指令時,程序知道當時PC值,也就知道PC+4的值(假設每條彙編指令4字節)。執行CALL指令會將SP減去四個字節,而後將本條彙編指令地址(即CALL指令的下一條彙編指令地址)放進saved pc去,以便foo函數執行完畢以後,根據saved pc的信息跳回調用語句的下一條指令的位置(即有一個晶體管記憶着跳轉過來以前的地方)。)是執行完14行後要接着執行的指令。回收了爲了調用foo參數的棧空間。 30 return 0; 31 //RV = 0;特殊寄存器,用來放置返回信息,專門在調用者和被調用函數之間傳遞返回值。把0返回給main函數的調用者。一旦返回到調用main函數的函數中時,這個函數會當即查看RV,將裏面的值做爲返回值取出。 32 }
(其中 saved pc 指向sp = sp + 8)
函數經過減少SP的值爲局部變量留出空間,並完成活動記錄的建立。CALL指令實際上是一個跳轉指令,跳到被調用代碼的第一條指令。RET指令會跳轉到saved pc中存儲着的地址。調用者負責把返回地址壓入棧中,被調用者負責按照返回地址往回跳。
棧幀中:參數部分和saved pc部分是調用函數申請並留存的,局部變量部分是被調用函數申請並初始化的。整個棧幀構成了一個活動記錄,爲了讓函數可以執行,並讓函數能夠訪問全部的參數和局部變量而構建。棧幀分兩部分構造的緣由是:只有調用者才知道怎樣將有意義的參數值寫入內存,而且局部變量這部份內存空間則不能被調用者設置,由於調用者並不知道被調函數如何實現,其實現中有多少局部變量。調用者可以根據函數原型精確知道調用該函數須要多少參數,以及怎樣初始化這些參數。調用者並不知道函數中有多少局部變量,就更不要說對其操做了。
C語言程序例子2:
1 //遞歸函數 2 int fact(int n) 3 { 4 if(n == 0) 5 //<feat>:R1 = M[SP + 4];標號和第一條語句 6 //BNE R1, 0, PC + 12;測試失敗應該跳過隨後緊挨着的兩個彙編語句(C中的返回語句),假設PC是指向當前指令的,真實編譯器中是指向下一條指令的(PC+12指向的是11行彙編代碼) 7 return 1; 8 //RV = 1; 9 //RET; 10 return n * fact(n - 1); 11 //R1 = M[SP + 4];先計算n-1 12 //R1 = R1 - 1; 13 //SP = SP - 4;建立下一個活動記錄,壓參數(n-1)入棧 14 //M[SP] = R1; 15 //CALL<fact>;SP降低,CALL指令的下一條地址被存入到saved pc中。一個真正的彙編器或鏈接器會遍歷全部的.o文件,而後這些標號轉換成PC相關的地址,這種轉換會推遲到鏈接時刻進行,由於編譯器但願能讓不一樣.o文件中的函數之間的跳轉所有翻譯成這種PC相關的地址的形式 16 //SP = SP + 4;回收局部變量空間內存(本條指令地址被存入saved pc中),return語句使程序執行流跳到這裏 17 //R1 = M[SP + 4];從內存中從新讀取n,(從一條C語句過渡到下一條語句時,應當從新讀取變量,由於可能某個寄存器的值被其餘的調用覆蓋了,上下文無關) 18 //RV = RV * R1; 19 //RET;沒有什麼要清理的局部變量,直接返回 20 }
C++語言可能會使用面向對象範式,所以相比於關注函數以及動做,會更加關注對象和數據。C語言中,調用時老是考慮的是函數名,數據則處於附屬地位,老是將數據做爲參數傳入函數,而不是讓數據接收方法調用的消息進行編程。不過C++的方法的調用和C的函數調用都會一樣的被翻譯成彙編代碼,都經過函數的調用和返回在彙編代碼層次模擬。不管是面向對象仍是面向命令式的過程,只是將這些代碼轉換爲彙編代碼(模擬C或C++代碼)並執行。
程序例子:
1 void swap(int* ap, int* bp)//C版本,指針實現 2 { 3 int temp = *ap; 4 //<swap>:SP = SP - 4;申請局部變量內存 5 //R1 = M[SP + 8]; 6 //R2 = M[R1];解引用(概念上等同於M[M[SP + 8]],可是不支持這種彙編語法) 7 //M[SP] = R2; 8 *ap = *bp; 9 //R1 = M[SP + 12]; 10 //R2 = M[R1];解引用 11 //R3 = M[SP + 8]; 12 //M[R3] = R2;賦值給指針所指變量 13 *bp = temp; 14 //R1 = M[SP]; 15 //R2 = M[SP + 12]; 16 //M[R2] = R1;賦值給指針所指變量 17 //SP = SP + 4;退回局部變量內存,此時SP指向的是saved pc,與剛進入函數時SP指向的位置一致 18 //RET;將SP指向的那個值取出來,將其寫到PC寄存器中,同時將SP從新設置,提高SP,回收掉saved pc,指向參數位置 19 } 20 21 void foo()//一個函數,若是它本身有局部變量或調用其餘函數,則這個函數本身負責分配和回收其局部變量內存和其調用的函數的參數內存。雖然被調用的函數屬於另一個函數幀,可是參數部份內存仍是由調用者管理。 22 { 23 int x; 24 int y; 25 //SP = SP - 8;與第18行對應,分配局部變量內存 26 x = 11; 27 //M[SP + 4] = 11; 28 y = 17; 29 //M[sp] = 17; 30 swap(&x, &y);//指針版本swap和引用版本swap彙編代碼是同樣的 31 //R1 = SP;&y(調用函數首先要求解參數) 32 //R2 = SP + 4;&x 33 //SP = SP - 8;(而後再下降SP)與第17行對應,分配參數內存 34 //M[SP] = R2;(以後再逆序壓入參數) 35 //M[SP + 4] = R1; 36 //CALL <swap>;(最後CALL指令) 37 //SP = SP + 8;與第13行對應,回收參數內存 38 //SP = SP + 8;與第5行對應,回收局部變量內存 39 //RET; 40 } 41 42 //CALL和RET指令都隱藏了對SP的操做和對saved pc的操做,並執行跳轉。CALL下降SP,保存返回地址到saved pc中,並跳轉到被調用函數中。RET從saved pc中獲得返回地址放入PC,提高SP,並跳轉回調用函數中。
1 void swap(int& a, int& b)//C++版本,引用實現(引用的工做方式實際上就是對於指針自動解引用),彙編代碼和指針版本徹底同樣 2 { 3 int temp = a; 4 a = b; 5 b = temp; 6 } 7 8 swap(x, y);//(x,y是左值,實際上必須是某個內存空間對應的名字。只有左值才能夠經過引用的方式更新和交換數據) 9 //C++編譯器在編譯的時候這樣決定:雖然是x和y,沒有加&符號,不過調用中不會對x和y求值。由於根據swap的函數原型,編譯器理解爲應該傳入它們的引用類型。引用只是使用地址傳遞參數的另外一種方式罷了,使用引用不用再接觸*和&,編譯器會根據函數原型,在每次引用變量a和b時,找到a或b中實際存儲的地址中對應的數據。實際上a和b都存儲着的是指針。傳遞引用實際上傳遞的是指針,會在實現中對於全部的引用的指針進行解引用操做。 10 11 int x = 17; 12 int y = x; 13 int &z = y;//編譯器最終會爲z留出空間,與y的地址聯繫起來 14 int *z = &y;
指針實際上只是要傳送數據或者被操縱的內存塊的原始地址。引用給人一種假象,將這個名字做爲其餘某個地方聲明變量的別名。引用與指針的區別:引用一旦賦值就不能將引用從新綁定給新的左值,可是對於指針能夠任意改變。若是隻使用引用是無法構造鏈表的,因此在C++中指針依然存在。函數返回引用意味着在內部返回一個指針,可是不要以對待指針的方式處理返回值,最好假設返回的是一個真實的字符串而不是字符串指針,或者假設返回的是一個整型而不是整型指針。像變量同樣對它們操做,可是在內部看,變量其實是位於內存的其餘位置的。指針和引用從語義來說不太相同,可是實現的彙編代碼相似。
底層實現,結構體和類是以相同的方式存儲在內存中的。C++中結構體中能夠定義方法(構造函數,析構函數,等方法),類也能夠不定義任何方法。C++中類和結構體惟一區別在於默認訪問修飾符分別是private和public。在最開始有一個switch語句,判斷變量是一個結構體聲明仍是一個類聲明,根據結果對沒有指定訪問控制的變量默認爲是private或者是public。
class binky { public: int dunky(int x, int y); char* minky(int* z)//該函數能放問類成員,由於函數知道要操做的對象的地址。this指針老是做爲第-1或第0個參數,其餘全部參數由於都要讓這個指針做爲第一個參數而向後移動一位。從用戶角度參數是隱式傳入的, { int w = *z; return slinky + dunky(winky, winky); //this->dunky(winky, winky);上面調用等價於這個語句,因爲沒有將tihs指針傳入,編譯器會將this指針傳入,而後複製到棧中 } //C++是專一於數據的,C是可能使用動詞類型的函數或是專一於過程。對於編譯器來講只是不一樣語法形式的同一事物,這些不一樣形式的代碼最終都變成了彙編代碼指令流,指令會被順序執行偶爾執行跳轉指令以及返回指令。編譯器確保能模擬面向過程的C和麪向對象的C++。 private: int winky;//數據域 char *blinky; char slinky[8]; }; int n = 17; binky b;//其中變量依次從低地址到高地址順序存放(以下圖) b.minky(&n);//用這種方式調用minky函數,編譯器知道你編寫的是面向對象代碼(參數再也不是一個,而是兩個),編譯器將某個binky類對象地址傳入,或者將某個binky結構體地址傳入做爲第0個參數。 //binky::minky(&b, &n);//與上面調用語句等價,經過名字空間來確認minky是定義在binky中的。實際上上面的調用仍是會在調用時壓入兩個參數。k個參數的成員方法實際上是k+1個參數的函數,第0個參數老是相關對象的地址。
static修飾類中的方法:
1 class fraction 2 { 3 public: 4 fraction(int n , int d = 0); 5 void reduce();//約分函數 6 private: 7 static int gcd(int x. int y);//reduce的輔助函數,此函數並不須要this指針(只須要比較兩個整型數)。成員函數有static關鍵字,意味着調用它的時候,並不須要類的實例做爲參數,能夠將它看成一個單獨的函數來使用,只不過它的做用域是在類的定義中的。靜態方法因爲不須要傳遞隱式的this指針,所以從內部實現被看做普通函數,可被成員函數調用。 8 //static會影響繼承使用特性,不能繼承它,當繼承類調用靜態方法的時候,並不能獲得正確的結果。因此類內通常都寫非static成員函數,非static函數會根據類對象的不一樣有不一樣的反應,而static函數根據類不一樣有不一樣反應。 9 };
makefile命令或gcc命令流程
首先會調用預處理器(預處理器通常處理#define和#include命令),而後調用編譯器(編譯器負責將.c文件.cc文件生成對應的.o文件,這些文件在輸入make指令後就會在目錄下生成。整個編譯以後生成的是可執行文件,其中一個.o函數包含着main函數,在生成可執行文件以前還要連接),而後調用連接器(連接器將一系列.o文件按順序排列,並確保在執行過程當中任何調用的函數都能從該函數所在的.o文件中找到),而後生成a.out文件(其實就是全部a.out文件的打包整合)。
彙編指令中的CALL<標號>指令,其實當連接結束後就會變成CALL<PC + 1234>這種形式,這樣就能夠根據PC的相對地址跳轉到對應標號函數的開始地址了。由於全部的符號在可執行文件建立後都已經有了明確的地址,若是鏈接器知道每個.o文件中的全部符號,以及這些.o文件是怎樣順序排列在一塊兒的話,鏈接器就能夠移除這些符號而用一個相對於PC的地址代替它。
預處理
預處理器依次讀入傳入的.c文件的內容,只關心#符號開頭的行。讀到有#開頭的行時候,預處理內部會有一個hashset,左邊爲key右邊爲value,存儲着被定義過的名字。隨着預處理器一行行讀入代碼,會把遇到的key(字符串常量中的除外)文本替換成value。預處理器就是將一個.c文件內容讀入,並將結果輸出到同一個.c文件,可是去掉了#define和其餘的預處理指令,因此通過預處理後就沒有#define指令了(#define只是大規模的查找和替換,用來爲常數或常量字符串賦予一個有意義的名字)。在預處理階段進行的都只是文本替換,結果做爲數據傳到下一階段,預處理階段並不進行類型檢查,文本替換產生的問題會在以後的編譯階段進行識別。
#define,1定義常量,2做爲宏:
1 #define KWidth 480 2 #define KHeight 720 3 #define KPerimeter 2 * (KWidth + KHeight)//其中KWidth和KHeight也會被替換
1 #define MAX(a, b) (((a) > (b)) ? (a) : (b)) //並非一個定義常量的define,而是做爲一個宏,是參數化的#define表達式,參數就是上下文中出現的a,b。括號用來澄清二意性。#define能夠實現簡單功能的內連函數,無需調用和返回函數的消耗。 2 3 MAX(10, 40) //在預處理階段,將宏擴展爲對應的表達式,轉化成(((10) > (40)) : (10) ? (40)) 4 int max = MAX(fib(100, fract(4000))); 5 //int max = (fib(100) > fract(4000)) ? fib(100) : fract(4000);展開替換後的結果,編譯器不會保留中間結果,函數被調用了兩次,尤爲是大規模的函數會致使性能降低 6 int larger = MAX(m++, n++);//最終會對大的兩次自增,小的一次自增 7 //int larger = ((m++) > (n++)) ? (m++) : (n++);//一共自增了3次
1 #define NthElemAddr(base, elemSize, index) ((char*)base + index * elemSize)//最好將重複代碼寫成函數或者是小段的宏,便於替換。 2 3 void* VectorNth(vector *v, int position) 4 { 5 assert(position > 0) 6 assert(position < v->logLength) 7 return NthElemAddr(Vector->Elem, Vector->Size, positon); 8 //void*能夠接受任何類型指針,就是所謂的上轉型(upcasting),將一個更具體的指針轉換成一個類型更泛化的指針。編譯器知道這種類型轉換並不會帶來風險。若是進行下轉型(downcasting),就告訴編譯器,我如今有一個類型更加泛化的指針,我知道此指針具體類型是什麼,可是若是涉及引用就想要進行強制轉換。 9 }
爲了不define在預處理時不進行類型檢查的缺點,應用static const定義全局變量。
#assert宏:
1 #ifdef NDEBUG //是關於某個define是否存在的判斷,若是定義了NDEDUG,那麼程序中全部的assert都會替換成空操做語句 2 #define assert(cond) (void)0 //將數字0強制轉換爲void類型,不要把這個0用在任何地方,也不準被賦值,做爲一個空操做(nop),在?和:中間佔位。雖然看上去是一條語句,可是它不會被編譯成任何一條彙編指令 3 #else 4 #define assert(cond) \ 5 (cond) ? ((void)0) : fprintf(stderr,"..文件名..行號等.."),exit(0); 6 #endif
#include:
#inlude 使用尖括號包含.h文件時,認爲是系統文件,應該是編譯器提供的,預處理器能夠經過默認路徑找到這些文件。/usr/bin/include和/usr/include中查找。使用雙引號時,編譯器會假設這是用戶編寫的.h文件,會默認從當前工做目錄查找該頭文件。經過makefile能夠設定一些選項告訴編譯器從哪裏尋找這些包含的頭文件。
#include指令也是查找並替換。用文件的內容替換掉該行#include指令。對於#include的處理是遞歸的,若是#include的指定文件自己還包含#include行,那麼預處理器會一直深刻下去直到最底層,層層替換直到生成不包含#include和#define的文本流。因此預處理後的文本流中,全部的#include和#define都被移除了。
gcc -E filename.c 只進行預處理而後將結果輸出,但不進行後續操做。在生成的文件中前半部分都是導入的其餘代碼,在快結尾部分是本身的代碼。
gcc -c filename.c 編譯源文件,可是不生成可執行文件。編譯階段以後就中止,只生成.o文件,不進行連接。
-o選項,指定可執行文件的名稱
避免循環包含頭文件,預處理器也不會讓某個頭文件被包含兩次:
1 #ifndef _vector_h_ 2 #define _vector_h_ 3 //列出全部vector.h中的原型 4 #endif
全部的.h文件都是定義某些原型的,不產生代碼。比如定義某個結構類型,可是不會產生該結構體相應的代碼。並且在.h文件中也不能申請任何存儲空間,除非定義共享全局變量(不多使用)。可是.c和.cpp文件不一樣,它們定義了全局變量,全局函數和類方法等,都要被翻譯成機器碼(一系列01串),機器碼可看做是彙編指令。包含.c和.cpp可認爲是重複定義函數。聲明一個函數和定義一個函數是不一樣的,對於函數實現而言,編譯階段會生成相應的代碼,對於函數聲明卻不會產生任何代碼。
vector.c文件包含了a.h,b.h,c.h文件。經預處理後去掉了#define和#include頭。再通過編譯獲得.o文件(內容是彙編代碼)。而後再通過連接獲得可執行文件。(連接階段將全部相關的.o文件組織到一塊兒,鏈接器嘗試使用這些文件建立可執行文件。這階段須要有一個main函數,鏈接器才知道從哪裏開始執行程序,對於每個要被調用的函數都應該有定義,要求全部定義了的函數只被定義一次)
若是在makefile或者gcc命令沒有加額外選項的話,編譯器會繼續下面各個階段而且生成可執行文件,默認狀況下文件名爲a.out。
連接會將.o文件和其餘.o文件的各個部分進行混合重組(除了本身編寫的模塊,其餘部分都是來自於編譯器標準庫或者是標準的.o代碼)。
1 #include <stdlib.h>//負責malloc,free,realloc,將其註釋掉,就沒有malloc,free,realloc的函數原型了。函數在執行到第7行時,也會將malloc推測成一個函數,而且推測函數有一個int參數,且返回一個int值,編譯器並不會查看賦值函數來推測返回值是什麼類型。所以編譯器會對這行給出兩條警告:第7行根據推測的函數類型,會認爲是對一個指針賦值,而賦值的類型倒是一個普通的整型。第10行編譯器一樣不知道free是什麼,並推測它的原型,free的參數是void*,而且返回值是int,產生的內容和沒有註釋該行的.o文件徹底同樣。只是會報出三個錯誤,其中兩條是說明缺失原型的,而一條是左值和右值類型不兼容,可是仍是生成.o文件。當連接的時候連接器會忘掉這些警告,它不會記錄有沒有包含某個頭文件,也不會記錄編譯時存在的警告。可是生成的.o文件和代碼的語義是徹底一致的,因此當連接並運行程序是沒問題的。 2 #inlcude <stdio.h>//負責printf,將這一行註釋掉以後,預處理器生成的翻譯單元中將不會有printf函數的聲明。有的編譯器會在編譯階段報錯(函數未聲明),gcc則不會報錯。gcc會在編譯時刻分析源程序,看看哪部分像是函數調用,會根據函數調用推測函數原型。編譯器看到了調用printf,printf只有一個字符串做爲參數,發出未找到printf函數原型的警告,可是不會停下來,仍是繼續生成.o文件。gcc推測一個函數原型時,將返回類型推測爲int。若是還有其餘的printf函數,那麼只能一樣是隻有一個字符串參數(推測出的原型,函數參數個數不可變)。推測出的函數原型會與實際的函數原型稍有區別,可是生成的.o文件實際上徹底沒變(由於.h頭文件只是包含結構的定義以及一些原型,對頭文件來講不會產生任何彙編代碼,頭文件的用處只是告訴編譯器一些規則,讓編譯器斷定程序的語法正確與錯誤)。ld命令用來連接,連接命令會根據編譯過程當中出現的警告查找標準庫,printf對應的代碼就在標準庫中,所以在連接階段會被加進來,雖然在連接階段以前並無見過printf的原型。所以include並不能保證相應的函數實如今鏈接時可用,若是某個函數定義在了標準庫中,那麼在連接時就能夠被加進來,而不管咱們是否聲明瞭函數原型。 3 //若是將前兩條include都註釋掉,那麼會產生4條警告,可是依然會生成.o文件,而且會連接生成a.out文件並執行它。其實頭文件作的所有事情就是告訴編譯器有哪些函數原型。可是在.h文件中並無說明這些函數的代碼在哪裏,連接階段則負責去標準庫中尋找這些代碼,而malloc,free,printf正是在標準庫中。只要被調用的函數在標準庫中存在,那麼不管編譯時有沒有警告,生成的.o文件都會沒有區別(包含原先代碼的語義),由於在連接的時候能夠用到標準庫的代碼,並將調用到的函數的代碼加到.o文件集合中,所以會在.o文件中出現相應標號的函數,生成可執行文件。 4 #include <assert.h>//註釋掉以後編譯器遇到第8行,看到的只是assert這個符號,而不是宏替換後的代碼,一次編譯器猜想它是一個函數調用,會在.o文件中出現CALL<assert>,形成編譯成功,可是連接失敗了,緣由是標準庫中根本沒有assert函數。 5 int main() 6 { 7 void *mem = malloc(400); 8 assert(mem != NULL); 9 printf("Yay!"); 10 free(mem); 11 return 0; 12 }
函數原型的存在是爲了讓調用者和被調用者關於saved PC上面的活動記錄的佈局達成一致(就是讓函數的調用參數複合調用參數類型規定)。原型其實質涉及參數,參數在活動記錄中位於saved PC之上,在saved PC之下的那部份內容是被調用者負責的。
1 //沒有任何頭文件(在C語言環境下) 2 //int strlen(char* s, int len); 若是加這個聲明,就能夠消除對推測函數原型的警告, 3 int main() 4 { 5 int num = 65; 6 int length = strlen((char*)&num, num);//解釋的結果應該是空字符串或者是A'。 7 //在.o文件中並無記錄下某個調用有多少個參數,在棧中會給兩個參數留出空間(SP = SP - 8),而函數庫中strlen只是帶有一個參數的形式,可是在連接階段卻不會發生錯誤(參數個數不一樣)。由於在連接階段gcc只會考察符號的名稱而並不檢查形參類型,可是這樣函數調用和簽名就對不上了,由於在連接階段並不會管它,連接階段要作的就只是查找strlen的定義。所以在連接時侯不出錯,而也不會有運行時錯誤(SP = SP + 8)。 8 //strlen的調用語句負責把兩個參數壓入棧中(SP = SP + 8),實際的strlen對應的函數原型倒是隻有一個參數的版本,因此實際的strlen函數只能訪問棧中參數兩個參數中的一個(位於saved PC上邊的四個字節),在strlen返回的時候,這塊空間仍是會被回收(SP = SP - 8)。 9 printf("length = %d\n", length);//分爲小端返回1,大端返回0 10 return 0; 11 }
1 //一個徹底相反的例子,memcmp函數原本須要三個參數,只傳一個參數。 2 //真正memcmp的函數原型爲/*int menmcmp(void * v1, void * v2, int size)*/ 3 int menmcmp(void *v1); 4 5 int n = 17; 6 int m = memcmp(&n);//程序會編譯經過,也能夠運行,可是運行時可能會崩潰(由於可能訪問了隨機的4字節地址,多是非法的棧指針,堆指針,代碼段指針)。C語言程序通常很容易經過編譯,C++是徹底的強類型系統,並不知道裸指針void*還有類型轉換之間的關係,因此在C++中不用void*泛型而用模板,C++編譯很差經過,可是編譯好的程序不容易崩潰,由於模板模板和容器屏蔽了指針的實現細節。以前的例子在C++就會失敗。在C++中對函數進行重載,能夠經過同一個函數名字帶有不一樣的參數個數和類型來解決,可是在C語言中,定義了一個函數名字以後就不能再定義這個函數名字了。C++語言在編譯時候並不會使用函數名做爲該函數第一條指令的標號,它會用函數名和參數列表中的參數的數據類型按照參數順序來構造一個更加複雜的函數符號。 7 //C: CALL<memcmp> 8 //C++: CALL<memcmp_void_p> 9 //C++: CALL<memcmp_void_p_void_p_int> //C++中生成的彙編語言的函數標號不一樣 10 //因此本例在C++環境中,函數聲明和函數庫中的實現不符會致使連接錯誤,看來C++貌似會更安全一些。
總線錯誤和段錯誤
1 *(NULL);
seg fault,段錯誤:常出現於對錯誤的指針解引用,是由於引用了一個並無映射到段中的地址才發生,大多數是對NULL或者很小的地址解引用。若是對NULL指針進行解引用 ,在0地址,操做系統就會識別出該地址不在任何一個段中,操做系統並未將0地址映射到棧,堆,代碼段中,所以對於0地址引用是不對的,由於訪問的是不應訪問的地址(地址並非某個局部變量的地址,也不是malloc調用返回的地址)。對這種狀況就只能發出一個段錯誤。
1 void* vp = value;//首先把一個地址賦值給vp,地址是不肯定的 2 *(char*)vp = 7;//有50%機會獲得一個總線錯誤(奇數偶數) 3 *(int*)vp = 55;//有75%機會獲得一個總線錯誤(4的倍數)
bus error,總線錯誤:在對4個段中地址解引用時發生(棧段,堆段,代碼段,數據段),通常出如今手工打包數據狀況下。總線錯誤能夠代表,你所引用的地址實際上並非你想要的地址。若是vp的值是4個段中某個值的話,程序不會出現段錯誤,由於地址就在段內。可是操做系統,硬件,編譯器共同規定全部的整型數據的地址都應該是4的倍數,而且short數據對應的地址都應該是偶數地址,對於字符類型並無什麼限制,爲了簡便,規定除了字符型和short型,其他類型都是4字節對齊的。
緩衝區溢出
1 int main()//緩衝區溢出,不斷重複賦值的永真循環,死循環 2 { 3 int i; 4 int array[4]; 5 for(i = 0; i <=4; i++) 6 array[i] = 0; 7 return 0; 8 }
1 int main()//緩衝區溢出,有的系統能夠正常工做(不會出現死循環),區別在於大端(多賦值一次),小端(死循環)。圖示爲小端,內存地址佈局左下最小,右上最大。 2 { 3 int i; 4 short array[4]; 5 for(i = 0; i <=4; i++) 6 array[i] = 0; 7 return 0; 8 }
1 void foo()//saved PC本應指向CALL<foo>指令的下一條指令,可是因爲saved PC被減4,因此又從新指向了CALL<foo>指令,這樣當函數要返回時,實際上是再次調用foo函數,就不停的執行foo函數,是一個更高級的死循環,在foo函數的末尾再一次調用foo函數自己。 2 { 3 int array[4]; 4 int i; 5 for(i = 0; i <= 4; i++) 6 array[i] -= 4; 7 }
1 int main()//本例並無全局變量存在 2 //第二個調用的函數與以前調用的函數的活動記錄徹底同樣,它會打出以前函數操做以後留在內存的那些數據的值,由於上一個函數返回後不會清空佔用過的位模式。能夠引出一種編程方式,提早將參數寫入內存爲之後的調用做準備。要善用內存佈局。 3 { 4 DeclareAndInitArray(); 5 PrintArray();//依然能打印出0到99 6 } 7 8 void DeclareAndInitArray() 9 { 10 int array[100]; 11 int i; 12 for(i = 0; i < 100; i++) 13 array[i] = i; 14 } 15 16 void PrintArray() 17 { 18 int array[100]; 19 int i; 20 for(i = 0; i < 100; i++) 21 printf("%d\n", array[i]); 22 }
printf既能夠有一個參數也能夠有任意多個參數,其第一個參數是控制字符串(是傳遞給控制檯輸出的模板),後邊有一個參數是...原型以下:
1 int printf(const char* control, ...);//可添加0到任意個數,任意類型參數。返回值是成功解析的佔位符的個數,若是調用出錯會返回-1,若是遇到文件尾返回-1。 2 printf("hello\n");//返回0 3 printf("%d+%d=%d", 4, 4, 8);//返回3
反向的壓參數入棧緣由:編譯時,根據函數原型編譯器認爲兩個函數調用都是合法的,但當對第二個printf編譯時,編譯器計算參數個數,並計算須要讓棧指針減去多少個字節爲參數提供空間。當函數跳轉到printf函數時,函數並不知道char*參數上面還有多少個參數。因爲printf函數實現使用了特殊符號,所以棧中的參數能夠動態變化。printf函數會經過這個字符的分析控制字符串來指出這些數據都是什麼類型,若是沒有控制字符串,那麼棧中內容就會是未知的,沒法讀取。若是控制字符串位於其餘參數的上方,那麼就沒有一個一致可靠的方式來解釋活動記錄中的其它參數。
函數的第一個參數,結構體變量,類變量的第一個成員域必須是最低地址(第一個域的偏移爲0),有利於設計和實現。通常相關的結構體前半部分保持相同,後半部分進行功能擴展。
1 struct base//將一個數據類型使用結構體進行包裝 2 { 3 int code; 4 }; 5 6 struct type_one 7 { 8 int code;//值爲1 9 //其餘的成員 10 }; 11 12 struct type_two 13 { 14 int code;//值爲2 15 };
多線程
make,是應用程序,讀會取makefile數據文件,指出怎樣調用GCC和G++以及連接器和優化程序,使用這些工具來生成可執行文件。
幾個虛擬地址空間(虛擬暗指地址無限長)。不一樣進程不會共享棧段,堆段,代碼段。make程序覺得本身佔有全部的內存執行操做,操做系統會將虛擬地址映射成實際地址,操做系統經過地址映射隔離各個程序(形成各個程序獨佔硬件的假象)。程序都假定系統有足夠的大的空間創建本身的棧段,堆段等知足程序的要求。可是隻有一個真實地址空間,是物理內存,須要管理各類虛擬地址空間。那些根據代碼運行的程序都存儲在可執行文件中。虛擬地址相同的程序不佔用相同的真實地值。操做系統有一個內存管理單元,會創建一個表格,把xx線程的xxx虛擬地址映射到內存中的真實地址。對虛擬地址的任何操做都被轉化爲對真實物理地址的操做。全部對進程虛擬地址空間的操做都被後臺的守護進程所管理,進行着虛擬地址到物理地址間的映射。虛擬地址中的內存會被整塊的映射到物理內存中。單處理器狀況下,在同一時刻,只有一個處理器和寄存器集合吸取指令。表面上每一個線程都有本身的堆段,棧段。(不一樣的是同一線程中有兩個函數的概念,在一個單線程中同時運行兩個函數。)
在單處理器,單內存的狀況下,多線程網絡程序的優勢是能節約網絡鏈接的時間,而不是節能約處理數據的時間。
150張票,10個代理賣,原始版:
1 int main()//150張票,10個代理賣,硬編碼 2 { 3 int numAgents = 10; 4 for(int i = 0; i < 10; i++) 5 sellTickets(i, 15); //可讓這個函數運行10個不一樣的線程,即產生10個不一樣的線程,使用相同的方法。 6 7 }
改進版:
1 int main()//該例子是順序執行的,輸出160行 2 { 3 int numAgents = 10; 4 int numTickets = 150; 5 for(int agent = 1; agent <= numAgents; agent++) 6 sellTickets(agent, mumAgents/agent); 7 return 0; 8 } 9 10 void SellTickets(int agentID, int numTicketsToSell) 11 { 12 while(numTicketsToSell > 0) 13 { 14 printf("Agent %d sells a ticket \n", agentID); 15 numTicketsToSell--; 16 } 17 printf("Agents %d : all done! \n", agentID); 18 }
使用線程包/庫(package/library)版。多線程無共享資源:
1 int main()//在運行線程以前,必須先執行InitThreadPackage函數一次並設置好全部的線程。 2 { 3 int numAgents = 10; 4 int numTickets = 150; 5 InitThreadPackage(false);//傳入false表示請不要打印線程調試信息,調試的時候能夠傳入true 6 for(int agent; agent <= numAgents; agent++) 7 { 8 char name[32]; 9 sprintf(name, "agent %d thread", agent);//sprintf輸出到緩存字符串中,不輸出到屏幕 10 ThreadNew(name, SellTickets, 2, agent, numTickets/numAgents);//傳入線程名字和線程要執行的函數的地址(函數指針),和該函數須要的參數個數和所需參數。 11 } 12 runAllThreads();//起到引導線程執行的做用,全部線程開始執行 13 return 0; 14 } 15 16 void SellTickets(int agentID, int numTicketsToSell)//十個線程等待執行這一段代碼,他們有各自的指針,指向這段由編譯器生成的代碼。假若有一個線程恰好進入到這個函數中,那麼這個線程陷入到這段代碼中,而後可能被處理器暫停運行,甚至被從就緒隊列裏刪除,放到阻塞隊列中,一直到規定的時間過去。 17 { 18 while(numTicketsToSell > 0) 19 { 20 printf("Agent %d sells a ticket \n", agentID); 21 numTicketsToSell--;//並不是原子操做(三條彙編指令,取值,減一,放回),當它失去對處理器的控制權時,它可能正好處於編譯器生成的三條指令以前,中,後,不過再繼續執行時會接着原來的指令繼續執行。 22 if(RandomChance(0.1))讓線程有10%的概率被強制暫停運行 23 ThreadSleep(1000);//暫停使用處理器至少1秒鐘,傳入數字是時間參數,單位是毫秒 24 } 25 printf("Agents %d : all done! \n", agentID); 26 }
不讓每一個售票點都賣預先設定好的數量的票,而是讓這些售票點訪問同一個共享整數(即主變量)。多線程共享資源(無鎖有bug版):
1 int main() 2 { 3 int numAgents = 10; 4 int numTickets = 150; 5 InitThreadPackage(false); 6 for(int agent; agent <= numAgents; agent++) 7 { 8 char name[32]; 9 sprintf(name, "agent %d thread", agent); 10 ThreadNew(name, SellTickets, 3, agent, &numTickets);//傳入共享資源的地址 11 } 12 runAllThreads(); 13 return 0; 14 } 15 16 void SellTickets(int agentID, int *numTicketsp) 17 { 18 while(*numTickets > 0)//若是有最後一張票幾個線程都想要,因爲某些緣由,未執行減減操做就暫停使用處理器了,當它們從新佔用處理器時,不會從新檢查以前的執行過程,都會試圖減小共享全局變量,則可能變成-9,由於線程都依賴於共享的內存數據。若是不注意操做數據的方式,執行操做的中途退出,並依據很快就要過期的數據作判斷,這樣當它失去處理器控制權的時候,全局數據的完整性就被破壞了。 19 { 20 printf("Agent %d sells a ticket \n", agentID); 21 (*numTickets)--; 22 if(RandomChance(0.1)) 23 ThreadSleep(1000); 24 } 25 printf("Agents %d : all done! \n", agentID); 26 }//因此整個while區域爲臨界區域,即我進入這個區域並對全局變量執行某種操做時,沒人能再次進入這塊區域。應該放一些指令阻塞其餘線程。鎖(二進制鎖) 27 }
幾個線程的棧均可以訪問main中的變量。
增長信號量版本,多線程共享資源(有鎖無bug版):
1 int main() 2 { 3 int numAgents = 10; 4 int numTickets = 150; 5 Semaphore lock = SemaphoreNew(-, 1);//信號量初始化函數,初始化爲1,第一個參數不怎麼用,第二個是個整數。編程中,信號量是一個同步計數變量,老是大於等於0,它支持加一和減一原子操做的變量類型。lock的加一減一的原子操做由兩個不一樣函數實現,這兩函數利用特定硬件或者特定彙編指令改變放在它們之間整數的值。信號量類型是一個指向不徹底類型的指針(該類型內部含有共享的整型變量?)。若是信號量初始化爲0,那麼每一個線程都會認爲是其餘的線程佔用着信號量,因此全部的線程都暫停使用處理器,每一個線程都處於等待狀態,造成死鎖。若是信號量初始化爲2或更多,會讓多個線程進入臨界區(25,26,27行,臨界區裏任什麼時候間只能有一個線程,或沒線程。應該保持儘可能小的臨界區域),則會在同一時間搞亂全局數據,也有可能讓兩線程以一種相互之間不信任的方式處理同一個共享的全局變量。在本例中,用信號量來限制對臨界區的訪問。 6 //SemaphoreWait(lock);減一操做。把信號量想象是在跟蹤一項資源,一個線程要麼容許進入臨界區,要麼在臨界區等侯(即要麼得到資源,要麼等待資源可用,直到不須要該資源爲止)。 7 //SemaphoreSignal(lock);加一操做。當不須要該資源時,要麼signal這個變量,要麼釋放鎖。 8 //信號量從不容許從非負變爲負數,若是SemaphoreWait了一個值爲0信號量,可是並不會把該信號量變成-1(這是不容許的),若是檢測到爲0,就會作一個阻塞(block)的動做,把信號量阻塞起來,這時候它就會暫停佔用處理器資源。 9 InitThreadPackage(false); 10 for(int agent; agent <= numAgents; agent++) 11 { 12 char name[32]; 13 sprintf(name, "agent %d thread", agent); 14 ThreadNew(name, SellTickets, 3, agent, &numTickets, lock);//傳入共享資源的地址,和鎖 15 } 16 runAllThreads(); 17 return 0; 18 } 19 20 void SellTickets(int agentID, int *numTicketsp, Semaphore lock) 21 { 22 while(ture) 23 { 24 SemaphoreWait(lock);//若是檢測到信號量從0變爲1,那麼就能夠接管信號量,並把信號量從1變成0(在浴室外邊等着並不斷的敲門,若是碰巧第一個檢測到門開了,那麼就進去)。 25 if(*numTicketsp == 0)//得到鎖以後,發現無票可賣,當退出的時候,應該釋放鎖(從浴室走的時候不要鎖門,要把門開着) 26 break; 27 (*numTicketsp)--; 28 printf("Agent %d sells a ticket \n", agentID);//這句也能夠移動到鎖外 29 SemaphoreSignal(lock); 30 //sleep... 31 } 32 SemaphoreSignal(lock);//得到鎖以後,發現無票可賣,當退出的時候,應該釋放鎖(從浴室走的時候不要鎖門,要把門開着) 33 } 34 //這個sell函數就是模擬一個線程不斷的得到鎖並釋放鎖的過程:鎖,解鎖,鎖,解鎖這樣的循環。可是有可能在得到了鎖以後卻失去了處理器資源,這時候就很危險,除非這時信號量處於0的狀態,這時其餘線程過來,它們會等待值爲0的信號量而後阻塞,直到得到鎖。鎖住了以後其餘線程不能進入,等原先失去資源的線程再次得到處理器並交出處理器以後,其餘線程才能繼續執行。
讀者寫者問題。模擬計算機服務端,客戶端在同一時間中運行。
1 char buffer[8];//全局變量 2 3 int main()//1,可能出現覆蓋了還沒有被讀取的數。2,可能Reader先獲取處理器,讀入還沒有被寫入的信息。 4 { 5 ITP(false); 6 ThreadNew("Writer", Writer, 0); 7 ThreadNew("Reader", Reader, 0); 8 RunAllThread(); 9 } 10 11 void Writer()//但願在Reader讀取以前寫入數據,可是不要過分的寫,覆蓋了還沒有被讀取的數。 12 { 13 for(int i = 0; i < 40; i++) 14 { 15 char c = PrepareRandomChar(); 16 buffer[i % 8] = c; 17 } 18 } 19 20 void Reader() 21 { 22 for(int i = 0; i < 40; i++) 23 { 24 char c = buffer[i % 8]; 25 ProcessChar(c); 26 } 27 }
增長兩個信號量,線程通訊是1:1
1 char buffer[8]; 2 Semaphore emptyBuffers(8);//只容許在1-8之間 3 Semaphore fullBuffers(0); 4 //能夠改爲(4,0),可是(8,0)自由度更高,吞吐量更大,若是改爲(1,0),那麼就是寫一個讀一個,若是改爲(0,0),那就死鎖。 5 //若是信號量對改爲(8,1),則容許Reader線程領先Writer線程一跳。 6 //若是信號量對改爲(16,0),Writer容許使用兩次循環,兩次遍歷Reader線程,可是Writer最多提早Reader8個插槽,而不是16個。 7 8 int main()//1,可能出現覆蓋了還沒有被讀取的數。2,可能Reader先獲取處理器,讀入還沒有被寫入的信息。 9 { 10 ITP(false); 11 ThreadNew("Writer", Writer, 0); 12 ThreadNew("Reader", Reader, 0); 13 RunAllThread(); 14 } 15 16 void Writer()//但願在Reader讀取以前寫入數據,可是不要過分的寫,覆蓋了還沒有被讀取的數。 17 { 18 for(int i = 0; i < 40; i++) 19 { 20 char c = PrepareRandomChar(); 21 SemaphoreWait(emptyBuffers); 22 buffer[i % 8] = c; 23 SemaphoreSignal(fullBuffers); 24 } 25 } 26 27 void Reader() 28 { 29 for(int i = 0; i < 40; i++) 30 { 31 SemaphoreWait(fullBuffers); 32 char c = buffer[i % 8]; 33 SemaphoreSignal(emptyBuffers); 34 ProcessChar(c); 35 } 36 }
多個Writer狀況,Writer之間互相競爭給單個Reader發信息。(略)
就餐問題,每人拿一個叉子,可能會死鎖
1 Semaphore forks[] = {1, 1, 1, 1, 1};//表示5個全局的信號量,Semaphore數組形式,要調用5次semaphoreNew 2 3 void Philosopher(int id)//知道本身的位置,要獲得fork[i],fork[i+1],當i很大,i+1可能爲0。 4 { 5 for(int i = 0; i < 3; i++) 6 { 7 Think(); 8 SemaphoreWait(forks[id]);//五個線程若是都在兩個SemaphoreWait中間暫停,就會造成死鎖 9 SemaphoreWait(forks[(id+1) % 5)]; 10 Eat(); 11 SemaphoreSignal(forks[id]); //兩個叉子釋放順序隨便 12 SemaphoreSignal(forks[(id+1) % 5)]; 13 } 14 Think(); 15 }
就餐問題,啓發式策略去掉死鎖1
1 Semaphore forks[] = {1, 1, 1, 1, 1}; 2 Semaphore numAllowedToEat(2);//最多容許兩人同時就餐 3 4 void Philosopher(int id) 5 { 6 for(int i = 0; i < 3; i++) 7 { 8 Think(); 9 SemaphoreWait(numAlloedToEat);//若是填2,就有3個線程會被阻塞在這條語句,過早的鈍化掉某些線程。 10 SemaphoreWait(forks[id]); 11 SemaphoreWait(forks[(id+1) % 5)]; 12 Eat(); 13 SemaphoreSignal(forks[id]); 14 SemaphoreSignal(forks[(id+1) % 5)]; 15 SemaphoreSignal(numAlloedToEat); 16 } 17 Think(); 18 }
就餐問題,啓發式策略去掉死鎖2
1 Semaphore forks[] = {1, 1, 1, 1, 1}; 2 Semaphore numAllowedToEat(4);//容許4個之中的1個首先得到叉子,而後SemaphoreWait。限制同時要求吃飯的科學家的數量,某人不被容許拿叉子就餐,那麼至少有一哲學家線程能拿叉子吃飯。 3 4 void Philosopher(int id)//知道本身的位置,要獲得fork[i],fork[i+1],當i很大,i+1可能爲0。 5 { 6 Think(); 7 SemaphoreWait(numAlloedToEat);//若是填4,就只有1個線程會被阻塞在這條語句,大多數線程都在儘可能的前進。 8 SemaphoreWait(forks[id]); 9 SemaphoreWait(forks[(id+1) % 5)]; 10 Eat(); 11 SemaphoreSignal(forks[id]); 12 SemaphoreSignal(forks[(id+1) % 5)]; 13 SemaphoreSignal(numAlloedToEat); 14 } 15 Think(); 16 }
一個信號量本質上表明瞭一個資源的可用性
使用帶二進制鎖的全局變量,叫作忙等待。會浪費處理器時間,而這段時間能夠用於完成其餘線程的工做。能夠用於多處理器的狀況,可是單處理器,沒有必要搞一個自旋鎖線程並不斷檢查一個全局變量的值。
而信號量則會告訴線程管理器把我(該線程)推送到阻塞隊列中去,只有當別的線程釋放了我所被阻塞的信號量,處理器纔會考慮該線程。
FTP下載,每一個文件有一個對應的線程下載。主線程可能會先返回。
1 int DownloadSingleFile(const char* server, const char* path)//主機位置,目錄結構和文件名,返回下載的整個文件的字節數 2 3 int DownloadAllFiles(const char* server,const char* files[], int n)//假設全部文件位於同一臺服務器。這是主線程的一個子線程,子線程又產生大量的子子線程。 4 { 5 int totalBytes = 0; 6 Semaphore lock = 1; 7 for(int i = 0; i < n; i++) 8 { 9 ThreadNew(__, DownloadHelper, 4, server, files[i], &totalBytes, lock);//DownloadHelper是一個代理函數,位於ThreadNew和DownloadAllFiles之間,由於直接調用DownloadAllFiles第二個參數返回值類型應該爲void,無法獲取返回值,代理函數能接收返回值並對總值作加法。這條語句產生一個子子線程,並把該線程加入就緒隊列中去。 10 } 11 //可是,會產生一個問題,沒有等到全部的子子線程都運行完畢(子子線程可能比較耗時),沒有等到任何子子線程取得進展以前,子線程就本身返回了,也許返回結果是0個字節。 12 return totalBytes; 13 } 14 15 void DownloadHelper(const char* server, const char* path, int* numBytesp, Semaphore lock)//Semaphore自己就是指針,不必加&號 16 { 17 //若是把SemaphoreWait(lock)語句提早到這裏,那麼就會順序下載文件,很慢,像沒用線程同樣。 18 int bytesDownloaded = DownloadSingleFile(server, path); 19 SemaphoreWait(lock); 20 (*numBytesp) += bytesDownloaded; 21 SemaphoreSignal(lock); 22 }
FTP下載,線程通訊是1:n,相似於讀者寫者
1 int DownloadSingleFile(const char* server, const char* path) 2 3 int DownloadAllFiles(const char* server,const char* files[], int n) 4 { 5 Semaphore childrenDone = 0; 6 int totalBytes = 0; 7 Semaphore lock = 1; 8 for(int i = 0; i < n; i++) 9 { 10 ThreadNew(__, DownloadHelper, 5, server, files[i], &totalBytes, lock, childrenDone);//把childrenDone額外傳過去 11 } 12 for(int i = 0; i < n; i++)//子線程阻塞在這裏,等到子子線程所有結束,再繼續運行。可是這裏有點忙等待。 13 { 14 SemaphoreWait(childrenDone);//更高級的信號量版本能夠wait一個具體值更多的參數,只要一個語句,而不調用一個外置的for循環,貌似信號量有本身的內置for循環。 15 } 16 return totalBytes; 17 } 18 19 void DownloadHelper(const char* server, const char* path, int* numBytesp, Semaphore lock, Semaphore ParentToSignal) 20 { 21 int bytesDownloaded = DownloadSingleFile(server, path); 22 SemaphoreWait(lock); 23 (*numBytesp) += bytesDownloaded; 24 SemaphoreSignal(lock); 25 SemaphoreSignal(ParentToSignal);//每一個線程釋放信號量一次 26 }
冰淇淋商店模擬(可能有最多52個線程)
經理---------批准是否售賣冰淇淋
//店員和經理交互,得到時間鎖,同一時間只能有一個店員和冰淇淋出如今經理的辦公室
10-40個店員-------------每一個顧客產生一個店員線程,每一個店員負責1個冰淇淋
//顧客見不到經理,只能和店員交互
10個顧客-----------------訂購1-4個冰淇淋
//顧客之間經過收銀員交流
收銀員
1 int main(__,__)//main函數負責產生所須要的全部線程 2 { 3 int totalCones = 0;//甜筒總量 4 InitThreadPackage(); 5 SetupSemaphores(); 6 for(int i = 0; i < 10; i++) 7 { 8 int numCones = RandomInteger(1,4); 9 ThreadNew(__, Customer, 1, numCones);//顧客線程,負責產生店員線程 10 totalCones += numCones; 11 } 12 ThreadNew(__, Cashier, 0);//收銀員線程 13 ThreadNew(__, Manger, 1, totalCones);//經理線程 14 RunAllThreads(); 15 FreeSemaphores();//由於信號量是在堆中建立的,因此要記得釋放。 16 return 0; 17 } 18 19 20 struct inspection//經理和店員之間 21 { 22 bool passed;//檢測甜筒是否經過(false) 23 Semaphore requsted;//店員向經理髮送請求檢查(0) 24 Semaphore finished;//(0) 25 Semaphore lock;//經理辦公室(1) 26 }; 27 28 29 void Manager(int totalConesNeeded)//經理 30 { 31 int numApproved = 0; 32 int numInspected = 0; 33 while(numApproved < totalNumNeed) 34 { 35 SenmaphoreWait(inspection.requsted); 36 numInspected++; 37 inspection.passed = RandomChance(0, 1); 38 if(inspection.passed) 39 numApproved++; 40 SemaphoreSignal(inspection.finished); 41 } 42 } 43 44 45 void Clerk(Semaphore semaToSignal)//店員 46 { 47 bool passed = false; 48 while(!passed) 49 { 50 MakeCone(); 51 SemaphoreWait(inspection.lock);//加鎖 52 SemaphoreSignal(inspection.request); 53 SwmaphoreWait(inspection.finished); 54 passed = inspection.pased;//若是不鎖的話,這個共享值就可能會讀取出錯 55 SemaphoreSignal(inspection.lock);//解鎖 56 } 57 SemaphoreSignal(semaToSignal); 58 } 59 60 61 void Customer(int numCones)//顧客 62 { 63 BrowX(); 64 Semaphore ClerksDone;(0) 65 for(int i = 0; i < numCones; i++)//兩個for不能和並,若是和並那麼就變成一步一步的了 66 { 67 ThreadNew(__, Clerk, 1, ClerksDone); 68 } 69 for(int i = 0; i < numCones; i++) 70 { 71 SemaphoreWait(ClerksDone); 72 } 73 SemaphoreFree(ClerksDone); 74 WalkToCashier(); 75 SemaphoreWait(line.lock); 76 int place = line.number++; 77 SemaphoreSignal(line.lock); 78 SemaphoreSignal(line.required); 79 SemaphoreWait(line.customers[place]); 80 } 81 82 83 struct line//顧客和收銀員之間 84 { 85 int number;//(0) 86 Semaphore requested;//(0) 87 Semaphore Customers [10]; 88 Semaphore lock;//(1) 89 }; 90 91 92 void Cashier()//收銀員 93 { 94 for(int i = 0; i < 10; i++) 95 { 96 SemaphoreWait(line.requested); 97 Checkout(i); 98 SemaphoreSignal(line.customers[i]); 99 } 100 }