淺談C語言的資料儲存(一)
程式由指令和資料組成,C語言程式亦是如此。開發者在編寫程式的時候往往需要根據不同資料的特點以及程式需求來選擇不同的資料儲存方式,那麼在C語言中資料的儲存分為哪些方式呢?
C程式大致來講可以分為四個資料區:常量區,靜態去,堆區,棧區。
其中常量區儲存了未被作為初始化使用的字串常量和被const修飾的全域性變數,其特點是隻可被訪問不可被寫入,生命週期同程式的執行過程。
靜態區儲存了全部的全域性變數,和所有被static修飾的變數(包括全域性和區域性),其特點是生命週期很長(為一次程式的執行過程)並且只被初始化一次(在編譯之後就已完成)。
棧區儲存了所有自動儲存(不加任何儲存型別關鍵字修飾或被auto
堆區是由作業系統負責維護的大片記憶體池,使用時需手動申請(呼叫malloc家族函式),但使用完畢後需手動釋放,否則會造成嚴重的記憶體洩漏,直到該程序退出後才會被作業系統回收。
下面詳細介紹每種儲存型別的特點。
一.常量區:
故名思意,常量區裡存放的是一些不可改變的量,比如字串常量。在實際的ELF(Executable and Linkable Format,可執行連線格式,是UNIX系統實驗室(USL)作為應用程式二進位制介面(Application
Binary Interface
常量區的資料被標記為只讀,也就是程式只有訪問權而沒有寫入權,因此如果開發者需要使用某些不希望被改變的資料時可以將其放入常量區。
在C語言中常量有很多種,比如常見的:
字元常量:‘a’, ’A’, ’*’。
字串常量:”helloworld”,”ilovechina”,”12345”。
整常量: 25,10,012,0x0a,0b00001010。
浮點常量: 3.14,123.456, 3.0E-23;
但是並不是所有的常量都會被編譯器放在常量區的,如圖1-1中程式碼所示:
圖1-1 定義一個變數並被常量初始化
圖中程式定義了一個整型區域性變數i,並且被初始化為10,其中i是變數,10是常量,但是編譯器並不將10放入常量區,而是在指令中直接通過立即數賦值(圖1-2)。
圖1-2 由圖1-1程式編譯生成的彙編程式碼
這是因為編譯器認為普通的整型、浮點型或字元型常量在使用的時候是可以通過立即數來實現的,沒有必要額外儲存到資料區,如此節省了儲存空間和執行時的訪問時間。那麼什麼樣的資料才將放入常量區呢?
1.字串常量
如圖1-1所示,在C程式中定義了一個區域性的字元指標變數p指向一個字串常量,其中p由於是區域性變數被放在棧區,而字串常量“helloworld”在彙編中被放入.rodata段(圖1-2),在編譯後生成的ELF格式檔案中也將被放入.rodata段(圖1-3):
圖1-3 C語言的示例程式定義指標變數指向字串常量。
圖1-4 由圖1-3中程式碼生成的彙編程式。
圖1-5 由圖1-3中程式編譯生成的可執行程式分析。
但是,當一個字元常量串被用來為陣列初始化的時候,那麼該字串常量將不會放入常量區,而是放入對應的陣列中,如圖1-6所示:
圖1-6 定義一個字元陣列並用字串常量初始化
編譯器會將該字串按照四位元組為一組轉換成對應的32位整數來為該陣列進行初始化,如圖1-7所示,其中第13行的10進位制整數1819043176轉換成16進製為0x6c6c6568,正好是字元‘l’,’l’,’e’,’h’:
圖1-7 圖1-6中C程式碼生成的彙編程式
因此在編譯生成的ELF格式檔案中的.rodata段也將不會儲存該字串常量:
圖1-8 圖1-6程式編譯後生成的可執行程式片段
2.被const修飾的全域性變數
a)
除了字串之外,其他常量也可以放在常量區,但是前提是該資料必須被存放在全域性變數的空間裡,並且被const關鍵字修飾。如圖1-9程式碼所示:
圖1-9 第4行定義了一個被const修飾的全域性變數
編譯生成的彙編程式比較:
圖1-10 生成的彙編程式比較
其中value0由於被const修飾所以放在了.rodata段也就是所謂的常量區,而value1是一個普通的全域性變數所以放在了.data段也就是所謂的靜態資料區。分析編譯生成的ELF格式可執行程式如下:
圖1-11 value0的存放位置
其中value0的資料被放在常量區(.rodata段)十六進位制顯示的0a對應了它的十進位制初始值10。
圖1-12 value1的存放位置
Value1的資料被放在靜態區(.data段)十六進位制顯示的14對應了它的十進位制初始值20。
b)
但是並不是所有被const修飾過的變數都放在常量區,事實上只有全域性變數才是如此,普通的區域性變數被const修飾後僅僅意味著在表示式上不能顯式地改變該變數值,否則編譯器會報語法錯誤,但該變數仍存放在棧區。而由於其儲存區域沒有發生本質的改變,因此仍然可以通過其他方式改變其值,比如指標。如圖1-13所示:
圖1-13 定義兩個區域性變數,其中一個被const修飾
儲存,編譯,結果如下:
圖1-14 編譯器發生編譯錯誤
由於 value1被const修飾,所以程式第9行的賦值語句將發生錯誤。
接著修改程式,通過指標去修改value1的值:
圖1-15 定義指標p指向value1並通過指標賦值。
編譯,執行:
圖1-16 編譯執行結果
由於定義的指標變數p和表示式&value1的型別不匹配(p是int *,而&value1的型別是const int *)所以在第7行賦值的時候編譯器會產生一個型別不匹配的警告,我們忽略該警告繼續執行,結果改變了value1的值。
3.由常量區引發的段錯誤
由於常量區的特性是隻讀,因此當程式試圖去向指向常量區的地址寫入資料的時候,作業系統處於安全考慮會發出一個段錯誤的訊號並且殺死該程序,以達到保護作業系統的目的。
圖1-17 示例程式碼,通過指標去向常量區寫資料。
第10行和11行的均可產生同樣的段錯誤,如圖1-18所示:
圖1-18 非法寫入引發的段錯誤