程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 【轉】C數據存儲(包括const存儲在哪,C++不同部分我在文中用紅字已指出)

【轉】C數據存儲(包括const存儲在哪,C++不同部分我在文中用紅字已指出)

編輯:關於C語言

非原創(文中紅字為自己見解,如有不對,請大神指點)

  程序由指令和數據組成,C語言程序亦是如此。開發者在編寫程序的時候往往需要根據不同數據的特點以及程序需求來選擇不同的數據存儲方式,那麼在C語言中數據的存儲分為哪些方式呢?

C程序大致來講可以分為四個數據區:常量區,靜態去,堆區,棧區。

  其中常量區存儲了未被作為初始化使用的字符串常量和被const修飾的全局變量,其特點是只可被訪問不可被寫入,生命周期同程序的運行過程。

  靜態區存儲了全部的全局變量,和所有被static修飾的變量(包括全局和局部),其特點是生命周期很長(為一次程序的運行過程)並且只被初始化一次(在編譯之後就已完成)。

  棧區存儲了所有自動存儲(不加任何存儲類型關鍵字修飾或被auto修飾)的局部變量,其特點是生命周期很短,僅僅是該變量所在函數的一次調用過程。運行時有操作系統分配並在函數結束後回收。

  堆區是由操作系統負責維護的大片內存池,使用時需手動申請(調用malloc家族函數),但使用完畢後需手動釋放,否則會造成嚴重的內存洩漏,直到該進程退出後才會被操作系統回收。

  下面詳細介紹每種存儲類型的特點。

一.常量區:

  故名思意,常量區裡存放的是一些不可改變的量,比如字符串常量。在實際的ELF(Executable and Linkable Format,可執行連接格式,是UNIX系統實驗室(USL)作為應用程序二進制接口(Application Binary Interface,ABI)而開發和發布的。Linux 作為類unix系統其程序仍然沿用了該格式。)的程序數據是分段存儲的,對應的常量區就是".rodata(只讀數據)段"。

  常量區的數據被標記為只讀,也就是程序只有訪問權而沒有寫入權,因此如果開發者需要使用某些不希望被改變的數據時可以將其放入常量區。

  在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修飾後僅僅意味著在表達式上不能顯式地改變該變量值,否則編譯器會報語法錯誤,但該變量仍存放在棧區。C++不同的地方就在此,C++鼓勵使用const來取代#define,因為C++對const進行了優化,如果該變量的值是常量表達式,在C++中就會進行常量折疊(const folding),何為const folding,百度一大堆,簡單來說就是在編譯期間任何出現該變量的地方都會被替換成常量表達式的值,正因此,這種情況下const定義的變量是可以用於定義數組的維度的,而C語言就沒有這個優化特性,所以C語言的const修飾的變量是無論如何都不能進行數組維度的定義的(注意,C++中不進行常量折疊的情況下,const修飾的仍無法進行數組定義,具體什麼時候常量折疊請參看我的博文http://www.cnblogs.com/yanqi0124/p/3795019.html)。而由於其存儲區域沒有發生本質的改變,因此仍然可以通過其他方式改變其值,比如指針。如圖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 非法寫入引發的段錯誤

二.靜態區:

  靜態區是一個抽象籠統的概念,在實際的Linux/C的可執行程序中並沒有靜態區這個區域,具體來講它主要由兩個段組成:.data段和.bss段。其中.data段就是程序的數據段,在采用段式內存管理的架構中,數據段(data segment)通常是指用來存放程序中已初始化且不為0的全局變量或靜態變量的一塊內存區域。相反,BSS(Block Started by Symbol)通常是指用來存放程序中未初始化的或初始化為0的全局變量或靜態變量的一塊內存區域。.data段在程序編譯期間其大小及數據被確定,而.bss段則沒有直接分配空間而是由編譯器在.data段之後為其預留空間,在程序裝載進內存時被正式分配。盡管靜態區由兩個不同的段組成,但是在程序鏈接並裝載進內存之後這兩段不做區分,因此我們在這裡不做分開討論。

  靜態區的變量擁有以下特征:

1) 生命周期長,直到該進程結束隨進程空間一起被系統回收。

2) 只初始化一次,它的空間數據在編譯期間被初始化,邏輯地址在鏈接期間固定。

那麼哪些變量將被放在靜態區呢?

1.全局變量:

  顧名思義它是全局的公用的,如果一個變量被定義為全局的,那麼在同一個程序中,任何函數都可以去訪問、存取該變量的數據。基於此,全局變量除擁有靜態區變量的全部特征之外還具有作用域廣的特點,其作用域在整個程序中(可以由多個源文件組成)全局可見。

2.靜態變量

  從字面上理解所謂靜態變量就是被static關鍵字修飾的變量,只要被static修飾為靜態變量那麼都將被編譯器分配在靜態區,其也就擁有了靜態區變量的全部特征。靜態變量分兩種:全局靜態變量和局部靜態變量。無論哪種只要被static修飾都將放在靜態區,擁有靜態區變量的全部特征。其區別僅在於作用域:如果是全局靜態變量,那麼該變量的作用域被限定只能在本源文件內使用(編譯之後該變量的符號將不允許對外鏈接,但是仍然可以通過指針去間接訪問);如果是局部變量則沒有變化(僅限函數內部使用)。

  下面給出一段示例代碼用以說明靜態變量的特性(圖2-1):

圖 2-1

  代碼中定義了一個全局變量gvalue和局部變量lvalue,並且經過兩次函數調用。gvalue由於是全局變量被編譯器分配在靜態區,而lvalue是局部變量放在棧區。由於靜態區的特寫導致gvalue經過兩次函數調用實現了累加,而局部變量lvalue則每次在函數調用時都被重新初始化。地址(圖2-2)。

圖 2-2

  下面更改程序,將局部變量修改為靜態局部變量(圖2-3):

圖 2-3

  則由於局部變量lvalue被static修飾放在了靜態區只初始化一次,因此也實現了累加(圖2-4)。

圖 2-4

原文出處:

淺談C語言的數據存儲(一):http://www.embedu.org/Column/Column540.htm

淺談C語言的數據存儲(二) :http://www.embedu.org/Column/Column558.htm

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved