目錄
- 前言
- 一、存盤類&作用域&生命周期&鏈接屬性的概念決議
- 二、linux下C程式 的記憶體映像
- 三、存盤類相關的關鍵字
- 四、作用域詳解
- 五、變數的生命周期
- 七、鏈接屬性
前言
本篇文章將會為大家介紹一些變數相關的存盤屬性、作用域、生命周期以及鏈接屬性的一些知識,有助于大家更好地理解程式,分析程式,
一、存盤類&作用域&生命周期&鏈接屬性的概念決議
1、存盤類
(1)存盤類就是存盤型別,也就是描述C語言變數在何種地方存盤,
(2)記憶體有多種管理方法:堆疊、堆、資料段、bss段、.text段······一個變數的存盤類屬性就是描述這個變數存盤在何種記憶體段中,
(3)譬如:區域變數分配在堆疊上,所以它的存盤類就是堆疊;顯式初始化為非0的全域變數分配在資料段,顯式初始化為0和沒有顯示初始化(默認為0)的全域變數分配在bss段,
2、作用域
(1)作用域是描述這個變數起作用的代碼范圍,
(2)基本來說,C語言變數的作用域規則是代碼塊作用域,意思就是這個變數起作用的范圍是當前的代碼塊,
代碼塊就是一對大括號{}括起來的范圍,所以一個變數的作用域是:這個變數定義所在的{}范圍內從這個變數定義開始往后的部分,(這就解釋了為什么變數定義總是在一個函式的最前面)/*
3、生命周期
(1)宣告周期是描述這個變數什么時候誕生(運行時分配記憶體空間給這個變數)及什么時候死亡(運行結束時識訓這個記憶體空間,此后再不能訪問這個記憶體地址,或者訪問這個記憶體地址已經和這個變數無關了)的,
(2)變數和記憶體的關系,就和人(變數)去圖書館借書(記憶體)一樣,變數的生命周期就好象我人借書的這段周期一樣,
(3)研究變數的生命周期可以我們理解程式運行的一些現象、理解C語言的一些規則,
4、鏈接屬性
(1)大家知道程式從源代碼到最終可執行程式,經歷的程序:編譯、鏈接,
(2)編譯階段就是把源代碼變成.o目標檔案,目標檔案里面有很多符號和代碼段、資料段、bss段等分段,符號就是編程中的變數名、函式名等,運行時變數名、函式名能夠和相應的記憶體對應起來,靠符號來做鏈接的,
(3).o的目標檔案鏈接生成最終可執行程式的時候,其實就是把符號和相對應的段給鏈接起來,
C語言中的符號有三種鏈接屬性:外鏈接屬性、內鏈接屬性、無鏈接屬性,
總結:以上4個概念,其實就是從4個不同角度來分析C語言的一些運行規則,綜合這4種分析角度能夠讓程式員完全掌握C語言程式的運行規則和方法,
二、linux下C程式 的記憶體映像
1、代碼段、只讀資料段
(1)對應著程式中的代碼(函式),代碼段在Linux中又叫文本段(.text)
(2)只讀資料段就是在程式運行期間只能讀不能寫的資料,const修飾的常量有可能是存在只讀資料段的(但是不一定,const常量的實作方法在不同平臺是不一樣的)
2、資料段、bss段
(1)資料段存:1、顯式初始化為非0的全域變數;2、顯式初始化為非0的static區域變數
(2)bss段存:1、顯式初始化為0或者未顯式初始化的全域變數;2、顯式初始化為0或未顯式初始化的static區域變數,
3、堆
C語言中什么樣變數存在堆記憶體中?
C語言不會自動向堆中存放東西,堆的操作是程式員自己手工操作的,程式員根據需求自己判斷要不要使用堆記憶體,用的時候自己申請,自己使用,完了自己釋放,
4、檔案映射區
檔案映射區就是行程打開了檔案后,將這個檔案的內容從硬碟讀到行程的檔案映射區,以后就直接在記憶體中操作這個檔案,讀寫完了后在保存時再將記憶體中的檔案寫到硬碟中去,
5、堆疊
堆疊記憶體區,區域變數分配在堆疊上;函式呼叫傳參程序也會用到堆疊,
6、內核映射區
(1)內核映射區就是將作業系統內核程式映射到這個區域了,
(2)對于linux中的每一個行程來說,它都以為整個系統中只有它自己和內核而已,以32位系統為例,它認為記憶體地址0xC0000000以下都是它自己的活動空間,0xC0000000以上是OS內核的活動空間,
(3)每一個行程都活在自己獨立的行程空間中(看不到有其他的行程存在,以為3G都是我的,上面的1G是內核空間,自己只能通過API介面來訪問內核空間),0-3G的空間每一個行程是不同的(因為用了虛擬地址技術),但是內核是唯一的,
7、OS下和裸機下C程式加載執行的差異
(1)C語言程式運行時環境有一定要求,意思是單獨個人寫的C語言程式沒法直接在記憶體中運行,需要外部一定的協助,這段協助的代碼叫加載運行代碼(或者叫構建C運行時環境的代碼,這一段代碼在作業系統下是別人寫好的,會自動添加到我們寫的程式上,這段代碼的主要作用是:給全域變數賦值、清bss段),
(2)資料段的全域變數或靜態區域變數都是有非0的初值的,這些初值在main函式運行之前就已經被初始化了,是重定位期間完成的初始化,(關于重定位這個概念大家可以百度了解一下,我在之后ARM裸機文章中也會講)
三、存盤類相關的關鍵字
1、auto
(1)auto關鍵字在C語言中只有一個作用,那就是修飾區域變數,
(2)auto修飾區域變數,表示這個區域變數是自動區域變數,自動區域變數分配在堆疊上,(既然在堆疊上,說明它如果不初始化那么值就是隨機的······)
(3)平時定義區域變數時就是定義的auto的,只是省略了auto關鍵字而已,可見,auto的區域變數其實就是默認定義的普通的區域變數,
2、static
(1)static關鍵字在C語言中有2種用法,而且這兩種用法彼此沒有任何關聯、完全是獨立的,其實當年本應該多發明一個關鍵字,但是C語言的作者覺得關鍵字太多不好,于是給static增加了一種用法,導致static一個關鍵字竟然有兩種截然不同的含義,
(2)static的第一種用法是:用來修飾區域變數,形成靜態區域變數,要搞清楚靜態區域變數和非靜態區域變數的區別,本質區別是存盤類不同(存盤類不同就衍生出很多不同):非靜態區域變數分配在堆疊上,而靜態區域變數分配在資料段/bss段上,
(3)static的第二種用法是:用來修飾全域變數,形成靜態全域變數,要搞清楚靜態全域變數和非靜態全域變數的區別,區別是在鏈接屬性上不同,下面講到鏈接屬性時詳細講,
分析:
<1>靜態區域變數在存盤類方面和全域變數一樣,
<2>靜態區域變數在生命周期方面和全域變數一樣,
<3>靜態區域變數和全域變數的區別是:作用域、連接屬性,靜態區域變數作用域是代碼塊作用域(和普通區域變數是一樣的)、鏈接屬性是無連接;全域變數作用域是檔案作用域(和函式是一樣的)、鏈接屬性方面是外連接,
3、register
(1)register關鍵字不常用,也只有一個作用,那就是:register修飾的變數,編譯器會盡量將它分配在暫存器中,(平時分配的一般的變數都是在記憶體中的),分配在暫存器中一樣的用(與在記憶體中的用法相同),但是讀寫效率會高很多,所以register修飾的變數用在那種變數被反復高頻率的使用,通過改善這個變數的訪問效率可以極大的提升程式運行效率時,所以register是一種極致提升程式運行效率的手段,
(2)uboot中用到了一個register型別的變數,比如gd這個變數是用來存uboot的全域變數(gd就是global data),因為這個全局變數在整個uboot中到處都被訪問,所以定義成register的,(關于uboot有興趣的同學可自行了解,其本質上是一個龐大且復雜的裸機程式,裸機程式即不在作業系統上運行的程式)
(3)平時寫代碼要被定義成register這種情況很少,一般慎用,
(4)register編譯器只能承諾盡量將register修飾的變數放在暫存器中,但是不保證一定放在暫存器中,主要原因是因為暫存器數量有限,不一定有空間使用,
4、extern
(1)extern主要用來宣告全域變數,宣告的目的主要是在a.c中定義全域變數可在b.c中使用該變數,
(2)C語言中程式的編譯是以單個.c源檔案為單位的,因此編譯a.c時只考慮a.c中的內容(不會考了b.c的內容),這就導致a.c中使用了b.c中定義的變數時在編譯時報錯,解決方案是宣告,
(3)應該在a.c中使用g_b之前先宣告g_b,宣告就是告訴a.c我在別的檔案中定義了g_b,并且它的原型和宣告的一樣,將來在鏈接的時候聯結器會在別的.o檔案中找到這個同名變數,宣告一個全域變數就要用到extern關鍵字,宣告時不可以初始化,否則會報錯,將這個宣告當作定義,但另一個檔案中也定義了這個變數,就會報錯顯示重復定義,鏈接時的錯誤
在本檔案中定義一個全域變數,在本檔案再用extern宣告不會報錯,因為定義兼有宣告,但宣告沒有定義,有了宣告才可以用,
extern int g_X;
extern void func(int a, int b);
5、volatile(面試常考)
(1)volatile的字面意思:可變的、易變的,C語言中volatile用來修飾一個變數,表示這個變數可以被編譯器之外的東西改變,編譯器之內的意思是變數的值的改變是代碼的作用,編譯器之外的改變就是這個改變不是代碼造成的,或者不是當前代碼造成的,編譯器在編譯當前代碼時無法預知,譬如在中斷處理程式isr中更改了這個變數的值,譬如多執行緒中在別的執行緒更改了這個變數的值,譬如硬體自動更改了這個變數的值(一般這個變數是一個暫存器的值)
(2)以上說的三種情況(中斷isr中參考的變數,多執行緒中共用的變數,硬體會更改的變數)都是編譯器在編譯時無法預知的更改,此時應用使用volatile告訴編譯器這個變數屬于這種(可變的、易變的)情況,編譯器在遇到volatile修飾的變數時就不會對改變數的訪問進行優化,就不會出現錯誤(有時優化會造成錯誤),
(3)編譯器的優化在 一般情況下非常好,可以幫助提升程式效率,但是在特殊情況(volatile)下,變數會被編譯器想象之外的力量所改變,此時如果編譯器沒有意識到而去優化則就會造成優化錯誤,優化錯誤就會帶來執行時錯誤,而且這種錯誤很難被發現,
int a, b, c;
a = 3;
b = a;
c = b;
//優化的情況下,編譯器一次性取出a,b,c,然后分別賦值為3;
//在不優化時,分三次取出a,b,c,然后分三次賦值為3
(4)volatile是程式員意識到需要volatile然后在定義變數時加上volatile,如果你遇到了應該加volatile的情況而沒有加程式可能會被錯誤的優化,如果在不應該加volatile而加了的情況程式不會出錯只是會降低效率,所以我們對于volatile的態度應該是:正確區分,該加的時候加不該加的時候不加,如果不能確定該不該加為了保險起見就加上,
6、restrict
(1)c99中才支持的,所以很多延續c89的編譯器是不支持restrict關鍵字,gcc支持的,
(2)restrict也是和編譯器行為特征有關的,
(3)restrict只用來修飾指標,不能修飾普通變數,
(4)該關鍵字用于告知編譯器,所有修改其修飾的指標所指向內容的操作全部都是基于(base on)該指標的,即不存在其它進行修改操作的途徑;這樣的后果是幫助編譯器進行更好的代碼優化,生成更有效率的匯編代碼,
(5)參考學習:http://blog.chinaunix.net/uid-22197900-id-359209.html
7、typedef
typedef在C語言關鍵字歸類上屬于存盤類關鍵字,但是實際上和存盤類沒關系,
四、作用域詳解
1、區域變數的代碼塊作用域
(1)代碼塊基本可以理解為一對大括號{}括起來的部分,
(2)代碼塊不等于函式,因為if while for都有{},所以代碼塊<=函式
(3)區域變數的作用域是代碼塊作用域,也就是說一個區域變數可以被訪問和使用的范圍僅限于定義這個區域變數的代碼塊中定義式之后的部分,
2、函式名和全域變數的檔案作用域
(1)檔案作用域的意思就是全域的訪問權限,也就是說整個.c檔案中都可以訪問這些東西,這就是平時所說的區域和全域,全域就是檔案作用域,
(2)詳細準確的說:函式和全域變數的作用域是定義所在的整個.c檔案之內定義式之后的部分,
總結:
(1)不管是區域變數、全域變數、函式,都要先定義才能使用
(2)嚴格來說我們上面的總結是錯誤的,準確的說:全域變數/函式的作用域都是自己所在的檔案,但是定義式之前的部分因為缺少宣告所以沒法用,解決方案是:1、把它定義到前面去;2、定義到后面但是在前面加宣告;區域變數因為沒法宣告(extern int a;),所以只能定義在前面去,
(3)在c89標準的編譯器中(現在很多編譯器還延續使用c89標準),所有的區域變數必須先定義在最前面,在變數定義之前不能有一句執行代碼,在c99標準的編譯器中(gcc兼容c99標準)可以允許在代碼塊內任意地方定義變數,但是允許定義的變數還是只能使用在定義了之后地方,定義之前還是不能用的,
C99支持:
for(int i = 0; i< 100; i++)
{
//在for回圈的陳述句中定義變數
}
3、同名變數的掩蔽規則
(1)問題:編程時,不可避免會出現同名變數,變數同名后不一定會出錯,
(2)首先,如果兩個同名變數作用域不同且沒有交疊,這種情況下同名沒有任何影響,
(3)其次,如果兩個同名變數作用域有交疊,C語言規定在作用域交疊范圍內,作用域小的一個變數會掩蔽掉作用域大的那個(縣官不如現管),
五、變數的生命周期
1、研究變 量生命周期的意義
研究變數生命周期,有助于理解變數的行為特征,
2、堆疊變數的生命周期
(1)區域變數(堆疊變數)存盤在堆疊上,生命周期是臨時的,臨時的意思就是說:代碼執行程序中按照需要去創建、使用、消亡的,
(2)譬如一個函式內定義的區域變數,在這個函式每一次被呼叫時都會創建一次(但每次的地址都不同了),然后使用,最后在函式回傳的時候消亡,
(3)思考:一個函式內的區域變數為什么在函式外不能使用?生命周期就是那么短
3、堆變數的生命周期
(1)首先要明白:堆記憶體空間是客觀存在的,是由作業系統維護的,我們程式只是去申請然后使用然后釋放,
(2)我們只關心我們程式使用堆記憶體的這一段時間,因此堆變數也有了自己的生命周期,就是:從malloc申請時誕生,然后使用,直到free時消亡,
(3)所以堆記憶體在malloc之前和free之后不能再去訪問,因此堆記憶體在實踐編程時都是被反復的malloc和free的,
4、資料段、bss段變數的生命周期
(1)全域變數的生命周期是永久的,永久的意思就是在程式被執行時誕生,在程式終止時消亡,
(2)全域變數所占用的記憶體是不能被程式自己釋放的,所以程式如果申請了過多的全域變數會導致這個程式一直占用大量記憶體,
(3)如果說堆記憶體是圖書館借的書,那么全域變數就是自己買的書,
5、代碼段、只讀段的生命周期
(1)其實就是程式執行的代碼,其實就是函式,它的生命周期是永久的,不過一般代碼的生命周期我們并不關注,
(2)有時候放在代碼段的不只是代碼,還有const型別的常量,還有字串常量,(const型別的常量、字串常量有時候放在rodata段,有時候放在代碼段,取決于平臺)
七、鏈接屬性
一個大一些的工程往往不是只有一個程式檔案,經常由好多C程式檔案構成,有的時候里面個別程式可能還用的其他語言,編碼完成后常常分別編譯,編譯完成再鏈接到一起,某個C程式需要用到其他程式中定義過的變數,一般都加extern前綴,編譯時編譯器會預留訪問鏈接的空位,等到鏈接階段再在整個工程的其他C編譯結果中去對號,把訪問鏈接填上,這就是外部鏈接,如果你程式全寫在一個檔案里,那永遠都不會有外部鏈接,
內部鏈接常指一個程式檔案中全域變數,可以被程式檔案內各個子程式訪問,這在編譯程序中處理,和鏈接階段不發生關系,如果變數前加了static,那么它永遠不會被外部程式訪問,它不會被編譯程式寫入目標代碼的鏈接區,
1、C語言程式的組織架構:多個C檔案+多個h檔案
(1)龐大、完整的一個C語言程式(譬如linux內核、uboot)由多個c檔案和多個h檔案組成的,
(2)程式的生成程序就是:編譯+鏈接,編譯是為了將函式/變數等變成. o二進制的機器碼格式,鏈接是為了將各個獨立分開的二進制的函式鏈接起來形成一個整體的二進制可執行程式,
2、編譯以檔案為單位、鏈接以工程為單位
(1)編譯器作業時是將所有源檔案依次讀進來,單個為單位進行編譯的,
(2)鏈接的時候實際上是把第一步編譯生成個單個的.o檔案整體的輸入,然后處理鏈接成一個可執行程式,
3、三種鏈接屬性:外鏈接、內鏈接、無鏈接
(1)外鏈接的意思就是外部鏈接屬性,也就是說這家伙可以在整個程式范圍內(言下之意就是可以跨檔案)進行鏈接,譬如普通的函式和全域變數屬于外連接,
(2)內鏈接的意思就是(c檔案內部)內部鏈接屬性,也就是說這家伙可以在當前c檔案內部范圍內進行鏈接(言下之意就是不能在當前c檔案外面的其他c檔案中進行訪問、鏈接),static修飾的函式/全域變數屬于內鏈接,
(3)無連接的意思就是這個符號本身不參與鏈接,它跟鏈接沒關系,所有的區域變數(auto的、static的)都是無連接的
4、函式和全域變數的同名沖突(經過測驗,二者不可重名)
(1)因為函式和全域變數是外部鏈接屬性,就是說每一個函式和全域變數將來在整個程式中所有的c檔案都能被訪問,因此在一個程式中的所有c檔案中不能出現同名的函式/同名的全域變數,
(2)最簡單的解決方案就是起名字不要重復,但是很難做到,主要原因是一個很大的工程中函式和全域變數名字太多了,而且一個大工程不是一個人完成的,是很多人協作完成,所以很難保證不會重名,解決方案呢?
(3)現代高級語言中完美解決這個問題的方法是命名空間namespace(其實就是給一個變數帶上各個級別的前綴),但是C語言不是這么解決的,
(4)C語言比較早碰到這個問題,當時還沒發明namespace概念,當時C語言就發明了一種不是很完美但是炊訓能用的解決方案,就是三種鏈接屬性的方法,
(5)C語言的鏈接屬性解決重名問題思路是這樣的:我們將明顯不會在其他c檔案中參考(只在當前c檔案中參考)的函式/全域變數,使用static修飾使其成為內鏈接屬性,這樣在將來連接時即使2個c檔案中有重名的函式/全域變數,只要其中一個或2個為內鏈接屬性就沒事,
(6)這種解決方案在一定程度上解決了問題,但是沒有從根本上解決問題,留下了很多麻煩,所以這個就導致了C語言寫很大型的專案難度很大,
5、static的第二種用法:修飾全域變數和函式
(1)普通的(非靜態)的函式/全域變數,默認的鏈接屬性是外部的
(2)static(靜態)的函式/全域變數,鏈接屬性是內部鏈接,
6、一般用法總結:
思考:為什么static一個關鍵字可以有2種完全不同的意思?
因為這兩種用法是互斥的,
7、最后的總結
(1)普通(自動)區域變數分配在堆疊上,作用域為代碼塊作用域,生命周期是臨時,鏈接屬性為無連接,定義時如果未顯式初始化則其值隨機,變數地址由運行時在堆疊上分配得到,多次執行時地址不一定相同,函式不能回傳該類變數的地址(指標)作為回傳值,傳回去在外面訪問,出了函式區域變數就死了,即使有地址也是無意義的 ,
(2)靜態區域變數分配在資料段/bss段(顯式初始化為非0則在資料段,顯式初始化為0或未顯示初始化則在bss段),作用域為代碼塊作用域(人為規定的),生命周期為永久(天然的),鏈接屬性為無連接(天然的),定義時如果未顯式初始化則其值為0(天然的),變數地址由運行時環境在加載程式時確定,整個程式運行程序中唯一不變;靜態區域變數其實就是作用域為代碼塊作用域(同時鏈接屬性為無連接)的全域變數,靜態區域變數可以改為用全域變數實作(程式中盡量避免用全域變數,因為會破壞結構性),
其實BSS本質是資料段
(3)靜態全域變數/靜態函式和普通全域變數/普通函式的唯一差別是:static使全域變數/函式的鏈接屬性由外部鏈接(整個程式所有檔案范圍)轉為內部鏈接(當前c檔案內),這是為了解決全域變數/函式的重名問題(C語言沒有命名空間namespace的概念,因此在程式中檔案變多之后全域變數/函式的重名問題非常嚴重,將不必要被其他檔案參考的全域變數/函式宣告為static可以很大程度上改善重名問題,但是仍未徹底解決),
(4)寫程式盡量避免使用全域變數,尤其是非static型別的全域變數,能確定不會被其他檔案參考的全域變數一定要static修飾,
(5)注意區分全域變數的定義和宣告,一般規律如下:如果定義的同時有初始化則一定會被認為是定義;如果只是定義而沒有初始化則有可能被編譯器認為是定義,也可能被認為是宣告,要具體分析;如果使用extern則肯定會被認為是宣告(實際上使用extern也可以有定義,實際上加extern就是明確宣告這個變數為外部鏈接屬性),
(6)全域變數應該定義在c檔案中并且在頭檔案中宣告,而不要定義在頭檔案中(因為如果定義在頭檔案中,則該頭檔案被多個c檔案包含時該全域變數會重復定義),
(7)在b.c中參考a.c中定義的全域變數/函式有2種方法:一是在a.h中宣告該函式/全域變數,然后在b.c中#include <a.h>;二是在b.c中使用extern顯式宣告要參考的函式/全域變數,其中第一種方法比較正式,
(8)存盤類決定生命周期,作用域決定鏈接屬性
(9)宏和inline函式的鏈接屬性為無連接,
注:本資料大部分由朱老師物聯網大講堂課程筆記整理而來,如有侵權,聯系洗掉!水平有限,如有錯誤,歡迎各位在評論區交流,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/293424.html
標籤:其他
