一、程式的記憶體分段:(程序映像)
當執行程式的執行命令後,作業系統會給程式分配它所需要的記憶體,並劃分成以下記憶體段供程式使用:
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*)#
*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’未定義的引用,這種是連結時的錯誤。