高階C語言1

sleeeeeping發表於2024-05-05

一、程式的記憶體分段:(程序映像)

​ 當執行程式的執行命令後,作業系統會給程式分配它所需要的記憶體,並劃分成以下記憶體段供程式使用:

text 程式碼段:

​ C程式碼被翻譯成二進位制指令後儲存在可執行檔案中,當可執行檔案被作業系統執行時,它會把裡面的二進位制指令(編譯後的程式碼)載入到這個記憶體段,它裡面的內容決定了程式如何執行,為了避免程式被破壞、修改,所以它的許可權是隻讀。

​ 該記憶體段分為兩個部分:

​ r-x:二進位制指令 r--:常量資料

​ 注意:該記憶體段的內容如果被強制修改會產生段錯誤(非法使用記憶體)。

data 資料段:

​ 儲存的是初始化過(初始化的值非零)的全域性變數

​ 儲存在該記憶體段的變數,被const修飾後,就會改儲存到text記憶體段,變成真正的常量。

bss 靜態資料段:

​ 儲存的是未初始化的全域性變數

​ 作業系統把程式被載入到記憶體後,會把該記憶體段進行初始化,也就是所有位元組賦值為零,所以全域性變數的預設值不是隨機,而是零。

heap 堆:

​ 該記憶體段由程式設計師手動呼叫記憶體管理函式(malloc/free),進行分配、釋放,它的分配釋放受程式設計師的控制,適合儲存一些需要長期使用的資料。

​ 它的大小不受限制,理論上能達到物理的上限,所以適合儲存大量的資料。

​ 該記憶體段無法取名字,也就是無法與識別符號建立聯絡,必須與指標配合使用。

stack 棧:

​ 儲存的是區域性變數、塊變數

​ 該記憶體段會隨著程式的執行自動的分配(定義區域性變數、塊變數)、釋放(函式執行完畢自動釋放區域性變數、塊變數),雖然使用比較方便,但它的釋放不受程式設計師控制,長期使用的資料不能儲存在棧記憶體中。

​ 該記憶體的大小有限,在終端執行: ulimit -s 可以檢視當前系統棧記憶體的使用上限,我們使用虛擬機器ubuntu的棧記憶體使用上限是8192kb,一旦超過這個限制就會產生段錯誤。可以使用ulimit -s 命令設定棧記憶體的使用上限。

靜態記憶體:

​ 當程式完成編譯 text、data、bss 三個記憶體段的大小就確定,在程式執行期間大小不會有任何變化,可以使用size命令檢視程式的這三個記憶體段的大小。

sunll@:~/標準C語言$ size ./a.out
   text	   data	    bss	    dec	    hex	filename
   3884	    312	     96	   4292	   10c4	./a.out

動態記憶體:

​ heap、stack兩個記憶體段,會隨著程式的執行,而動態變化。

​ 當程式執行時,/proc/程式編號/maps 檔案裡記錄程式執行過程中記憶體的使用情況,程式執行結束這個檔案就消失了。

​ 使用ps aux 命令檢視所有程序的編號,getpid函式可以獲取當前程序的編號。

二、變數屬性和分類

變數的屬性

  • 作用域:變數的使用範圍。
  • 儲存位置:變數使用那個記憶體段儲存資料,決定了變數在執行期間能否被釋放(銷燬),能否被修改。
  • 生命週期:變數從定義、分配記憶體到記憶體銷燬的時間段。

全域性變數:

​ 定義在函式外的變數叫全域性變數。

  • 作用域:本程式內任何位置都可以使用。

  • 儲存位置:初始化的全域性變數使用的是data記憶體段,未初始化的全域性變數使用的是bss記憶體段。

  • 生命週期:從程式開始執行,到程式執行結束。

區域性變數:

​ 定義在函式內的變數叫區域性變數。

  • 作用域:只能在它所在的函式內使用(從定義的位置開始,到函式結束)。

  • 儲存位置:使用的是stack記憶體段。

  • 生命週期:當它所在的函式被呼叫後,執行到區域性變數的定義語句時區域性變數就會被建立(作業系統會給區域性變數的變數名分配一塊stack記憶體),當函式執行結束後,區域性變數就被銷燬了。

塊變數:

​ 定義在if、for、while、do while語句塊內的變數叫區域性變數,就是特殊的區域性變數。

  • 作用域:只能在它所在的語句塊內使用。
  • 儲存位置:使用的是stack記憶體段。
  • 生命週期:當它所在的函式被呼叫後,執行到塊變數的定義語句時塊變數就會被建立(作業系統會給塊變數的變數名分配一塊stack記憶體),當出了它所在的大括號,塊變數就被銷燬了。
int main() {
	for (int i = 0; i < 10; ++i) {   
        printf("%p\n",&i);
    }   
    //printf("%d\n",i);		//	 i已經被銷燬,無法使用
    for (int j = 0; j < 10; ++j) {   
        printf("%p\n",&j);
    }
}
	//	i j 的地址編號相同的,但是迴圈變數i離開了for迴圈後已經被銷燬了,j的地址相同只是剛好重新使用同一個記憶體而已
#include <stdio.h>
int num = 123;
int main() {
    printf("%d\n",num); 
    int num = 456;
    printf("%d\n",num);
    for (int i = 0;i < 1; ++i) {   
        printf("%d\n",num);
        int num = 789;
        printf("%d\n",num);
    }   
    printf("%d\n",num);                                                
}

注意:全域性變數、區域性變數、塊變數可以同名,不會造成命名衝突,區域性變數會遮蔽同名的全域性變數,塊變數會遮蔽同名的全域性變數、區域性變數。

解決: 一般為了解決全域性變數與區域性變數命名衝突問題,全域性變數一般首字母大寫,區域性變數一般全部小寫

全域性變數的優點和缺點:

優點:

​ 使用方便,避免了函式之間傳參產生的消耗,提高程式的執行速度。

缺點:

​ 程式執行期間全域性變數所佔用的記憶體不會被銷燬,可能會產生記憶體浪費。

​ 命名衝突的可能性比較大,可能會與其它檔案的全域性變數、函式、結構、聯合、列舉、宏命名衝突。

#include <stdio.h>

int scanf;	//	全域性變數,很容易起命名衝突
int main() {
    int scanf;	//	區域性變數 不容易起衝突
}
總結:

​ 全域性變數儘量少用,或者不用。

三、修飾變數的關鍵字——型別限定符

<型別限定符> 資料型別 變數名;

typedef

typedef int num;
num n1;	//n1 就是int型別

​ 變數名被typedef修飾後,就會變成定義它的資料型別,此時該名字不是變數名而是型別名,之後就可以使用這種新的資料型別定義變數、陣列了,該功能是為了給複雜的資料型別重新定義一個簡短的型別名。

​ 由於無符號整型使用比較麻煩,所以標準庫中為我們定義一些簡短的無符號整型的型別名,就使用typedef定義的,實現在stdint.h標頭檔案裡。

typedef signed char     int8_t;
typedef short int       int16_t;
typedef int         	int32_t;
typedef long long int   int64_t;

typedef unsigned char       uint8_t;
typedef unsigned short int  uint16_t;
typedef unsigned int        uint32_t;
typedef unsigned long long int  uint64_t;

注意:在之後的學習過程中,如果遇到一些xxx_t的資料型別,都使用typedef重定義,例如:time_t,size_t pid_t。

注意:後續在定義結構時,可以使用typedef縮短結構型別名

auto

auto int num;

​ 早期的C語言用它來修飾自動分配、釋放記憶體的變數,也就是區域性變數和塊變數,但由於程式碼使用的變數絕大多數都是區域性變數和塊變數,所以就約定,該關鍵字不加就程式碼加,所以該關鍵字已經沒有實用價值了。

​ 在C++11的語法標準中,auto有了新的功能,就是定義自動型別的變數,編譯器會根據變數的初始值,自動設定變數的資料型別。
​ auto num = 1234; // num int型別

​ auto f = 3.14; // f double型別

​ 編譯指令:g++ xxx.c -std=c++11

​ 注意:雖然auto關鍵字,已經不再使用,但基本功能還保留著,所以它不能修飾全域性變數。

const

const int num;

​ const的意思是常量,但實際它只是為變數提供一層保護,被它修飾的變數不能顯式修改,但可以隱式修改,也就被它修飾後並不能變成真正的常量。

#include <stdio.h>
int main() {
    const int num = 10; 
    int* p = (int*)&num;                                               
    *p = 88; 
    //num = 88;
    printf("%d\n",num);
    printf("%d\n",num);
}

​ 注意:儲存在data記憶體段的變數,被const修飾後就會變成真正的常量,儲存位置被修改為text,其實是修改了data段和text段的分界線。如果就算隱式修改也會段錯誤

static

​ static既可以修飾變數,也可以修飾函式,主要有三大功能:

限制作用域:

​ 預設情況下全域性變數、函式的作用域是整個程式都可以使用,被static修飾後,就只能在它所在的.c檔案內使用。

​ 該功能可以避免全域性變數、函式的命令衝突,也能防止全域性變數、函式被外部修改、呼叫,提高程式碼的安全性。

​ 普通全域性變數、函式也叫外部變數、外部函式,被static修飾後就叫做內部變數、內部函式、靜態全域性變數。

改儲存位置:

​ 區域性變數、塊變數被static修飾後,儲存位置就由stack改data、bss,稱呼為靜態區域性變數、靜態塊變數。

​ 靜態區域性變數、靜態塊變數的預設值不再是隨機的,而是零。

延長生命週期:

​ 由於靜態區域性變數、靜態塊變數的儲存位置由stack(動態分配、釋放)改為data、bss,所以靜態區域性變數、靜態塊變數不會隨著函式的執行結束而銷燬,而是和全域性變數的生成周期一樣。

注意:

​ static修飾區域性變數、塊變數,會改變它們的儲存、延長生命週期,但並不會改變它們的作用域。

volatile

int num1 = 10;
printf("%d\n",num1);
num1+10;
num1*100;
volatile int num;	//	告訴編譯器不要做取值最佳化

​ 在程式中使用到num變數時,系統會從記憶體中讀取該num的值交給CPU運算,如果之後num變數的值沒有發生明顯變化,再次使用變數時系統會直接使用上次讀取的舊值,而不會再從記憶體中讀取。這編譯器對變數讀值過程的最佳化。

​ volatile 關鍵字就告訴編譯器不要最佳化變數的讀值過程,每使用該變數時,都重新從記憶體中讀取它的值。

int num = 10;
if(num == num) {
    //	一定成立
}

volatile int num = 20;
if(num == num) {
    //	有可能不成立
}

​ 什麼情況下需要使用volatile關鍵字:

​ 變數被共享訪問,且有多個執行者可以修改它的值,這種情況下變數就應該被volatile修飾。

​ 情況1:多執行緒程式設計處理複雜問題時。

​ 情況2:裸機程式設計、驅動程式設計時,軟硬體共用的暫存器。

register

​ 計算機的儲存介質讀寫速度排序:機械硬碟->固態硬碟->記憶體條->高階快取->CPU暫存器

​ register關鍵字的作用是申請把變數的儲存介質由記憶體條改為CPU暫存器,一旦申請成功,變數的讀寫速度、運算速度會大大提高。

注意:CPU中的暫存器數量有限,申請不一定成功,只有需要長期大量運算的變數才適合用register關鍵字修飾。

注意:被register修飾過的變數,不能獲取變數的地址。

extern

​ 當使用其它.c檔案中的全域性變數時,需要像宣告函式一樣,對其它.c檔案全域性變數進行宣告。

extern 型別 變數名; 

注意:宣告變數只能解決編譯時的問題,如果目標檔案最終連結時,變數沒有定義,依然會報錯。

a.c:(.text+0x12):對‘num’未定義的引用,這種是連結時的錯誤。

相關文章