概念解析
存儲類
- 存儲類就是存儲類型,也就是描述C語言變量在何種地方存儲。
- 內存有多種管理方法:棧、堆、數據段、bss段、.text段······一個變量的存儲類屬性就是描述這個變量存儲在何種內存段中。
- 譬如:局部變量分配在棧上,因此它的存儲類就是棧;顯式初始化爲非0的全局變量分配在數據段,顯式初始化爲0和沒有顯示初始化(默認爲0)的全局變量分配在bss段。
做用域
- 做用域是描述這個變量起做用的代碼範圍。
- 基原本說,C語言變量的做用域規則是代碼塊做用域。意思就是這個變量起做用的範圍是當前的代碼塊。代碼塊就是一對大括號{}括起來的範圍,因此一個變量的做用域是:這個變量定義所在的{}範圍內從這個變量定義開始日後的部分。(這就解釋了爲何變量定義老是在一個函數的最前面)
生命週期
- 生命週期是描述這個變量何時誕生(運行時分配內存空間給這個變量)及何時死亡(運行時收回這個內存空間,此後再不能訪問這個內存地址,或者訪問這個內存地址已經和這個變量無關了)的。
- 變量和內存的關係,就和人(變量)去圖書館借書(內存)同樣。變量的生命週期就好象我人借書的這段週期同樣。
- 研究變量的生命週期能夠咱們理解程序運行的一些現象、理解C語言的一些規則。
連接屬性
- 你們知道程序從源代碼到最終可執行程序,經歷的過程:編譯、連接。
- 編譯階段就是把源代碼搞成.o目標文件,目標文件裏面有不少符號和代碼段、數據段、bss段等分段。符號就是編程中的變量名、函數名等。運行時變量名、函數名可以和相應的內存對應起來,靠符號來作連接的。
- .o的目標文件連接生成最終可執行程序的時候,其實就是把符號和相對應的段給連接起來。
- C語言中的符號有三種連接屬性:外鏈接屬性、內連接屬性、無鏈接屬性。
- 總結:以上4個概念,其實就是從4個不一樣角度來分析C語言的一些運行規則。綜合這4種分析角度可以讓程序員徹底掌握C語言程序的運行規則和方法。
linux下C程序的內存映像
代碼段 (.text)、只讀數據段( .ro.data )
- 代碼段對應着程序中的代碼(函數),代碼段在linux中又叫文本段(.text)
- 只讀數據段就是在程序運行期間只能讀不能寫的數據,const修飾的常量有多是存在只讀數據段的(可是不必定,const常量的實現方法在不一樣平臺是不同的)
數據段 (.data)、bss段
- 數據段:一、顯式初始化爲非0的全局變量;二、顯式初始化爲非0的static局部變量
- bss段:一、顯式初始化爲0或者未顯式初始化的全局變量;二、顯式初始化爲0或未顯式初始化的static局部變量。
堆(heap)
- C語言中什麼樣的變量存在堆內存中?C語言不會自動向堆中存放東西,堆的操做是程序員本身手工malloc操做的。程序員根據需求本身判斷要不要使用堆內存,用的時候本身申請,本身使用,完了本身釋放。
文件映射區
- 文件映射區就是進程打開了文件後,將這個文件的內容從硬盤讀到進程的文件映射區,之後就直接在內存中操做這個文件,讀寫完了後在保存時再將內存中的文件寫到硬盤中去。
棧 (stack)
- 棧內存區,局部變量分配在棧上;函數調用傳參過程也會用到棧
內核映射區
- 內核映射區就是將操做系統內核程序映射到這個區域了。
- 對於linux中的每個進程來講,它都覺得整個系統中只有它本身和內核而已。它認爲內存地址0xC0000000如下都是它本身的活動空間,0xC0000000以上是OS內核的活動空間。
- 每個進程都活在本身獨立的進程空間中,0-3G的空間每個進程是不一樣的(由於用了虛擬地址技術),可是內核是惟一的。
OS下和裸機下C程序加載執行的差別
- C語言程序運行時環境有必定要求,意思是單獨我的寫的C語言程序無法直接在內存中運行,須要外部必定的協助,這段協助的代碼叫加載運行代碼(或者叫構建C運行時環境的代碼,這一段代碼在操做系統下是別人寫好的,會自動添加到咱們寫的程序上,這段代碼的主要做用是:給全局變量賦值、清bss段)。
- ARM裸機第十六部分,寫shell時有一次定義了一個全局變量初始化爲0可是實際不爲0,後來在裸機的start.S中加了清bss段代碼就變0了。這就說明在裸機程序中沒人幫咱們來作這一段加載運行時代碼,要程序員本身作(start.S中的重定位和清bss段就是在作這個事);在操做系統中運行程序時程序員本身不用操心,操做系統會自動完成重定位和清bss,因此咱們看到的現象:C語言中未初始化的全局變量默認爲0·····。
- 數據段的全局變量或靜態局部變量都是有非0的初值的,這些初值在main函數運行以前就已經被初始化了,是重定位期間完成的初始化。
存儲類相關的關鍵字
auto
- auto關鍵字在C語言中只有一個做用,那就是修飾局部變量。
- auto修飾局部變量,表示這個局部變量是自動局部變量,自動局部變量分配在棧上。(既然在棧上,說明它若是不初始化那麼值就是隨機的······)
- 平時定義局部變量時就是定義的auto的,只是省略了auto關鍵字而已。可見,auto的局部變量其實就是默認定義的普通的局部變量。
static
- static關鍵字在C語言中有2種用法,並且這兩種用法彼此沒有任何關聯、徹底是獨立的。其實當年本應該多發明一個關鍵字,可是C語言的做者以爲關鍵字太多很差,因而給static增長了一種用法,致使static一個關鍵字居然有兩種大相徑庭的含義。
- static的第一種用法是:用來修飾局部變量,造成靜態局部變量。要搞清楚靜態局部變量和非靜態局部變量的區別。本質區別是存儲類不一樣(存儲類不一樣就衍生出不少不一樣):非靜態局部變量分配在棧上,而靜態局部變量分配在數據段/bss段上。
- static的第二種用法是:用來修飾全局變量,造成靜態全局變量。要搞清楚靜態全局變量和非靜態全局變量的區別。區別是在連接屬性上不一樣,講到連接屬性時詳細講。
- 第一種用法 分析:
- 靜態局部變量在存儲類方面和全局變量同樣(分配在.data/.bss段上)。
- 靜態局部變量在生命週期方面和全局變量同樣(存在於整個程序運行期間)。
- 靜態局部變量和靜態全局變量的區別是:做用域、鏈接屬性。靜態局部變量做用域是代碼塊做用域(和普通局部變量是同樣的)、連接屬性是無鏈接;靜態全局變量做用域是文件做用域(和函數是同樣的)、連接屬性方面是內鏈接。
- 第二種用法:
- 普通的(非靜態)的函數/全局變量,默認的連接屬性是外部的
- static(靜態)的函數/全局變量,連接屬性是內部連接。
register
- register關鍵字不經常使用,也只有一個做用,那就是:register修飾的變量,編譯器會盡可能將它分配在寄存器中。(平時分配的通常的變量都是在內存中的)。分配在寄存器中同樣的用,可是讀寫效率會高不少。因此register修飾的變量用在那種變量被反覆高頻率的使用,經過改善這個變量的訪問效率能夠極大的提高程序運行效率時。因此register是一種極致提高程序運行效率的手段。
- uboot中用到了1個register類型的變量,gd這個變量是用來存uboot的全局變量(gd就是global data)。由於這個全局變量在整個uboot中處處都被訪問,因此定義成register的。
- 平時寫代碼要被定義成register這種狀況不多,通常慎用。
- register編譯器只能承諾儘可能將register修飾的變量放在寄存器中,可是不保證必定放在寄存器中。主要緣由是由於寄存器數量有限,不必定有空用。
volatile
- volatile的字面意思:可變的、易變的。C語言中volatile用來修飾一個變量,表示這個變量能夠被編譯器以外的東西改變。編譯器以內的意思是變量的值的改變是代碼的做用,編譯器以外的改變就是這個改變不是代碼形成的,或者不是當前代碼形成的,編譯器在編譯當前代碼時沒法預知。譬如在中斷處理程序isr中更改了這個變量的值、多線程中在別的線程更改了這個變量的值、硬件自動更改了這個變量的值(通常這個變量是一個寄存器的值)
- 以上說的三種狀況(中斷isr中引用的變量,多線程中共用的變量,硬件會更改的變量)都是編譯器在編譯時沒法預知的更改,此時應用使用volatile告訴編譯器這個變量屬於這種(可變的、易變的)狀況。編譯器在遇到volatile修飾的變量時就不會對改變量的訪問進行優化,就不會出現錯誤。
- 編譯器的優化在通常狀況下很是好,能夠幫助提高程序效率。可是在特殊狀況(volatile)下,變量會被編譯器想象以外的力量所改變,此時若是編譯器沒有意識到而去優化則就會形成優化錯誤,優化錯誤就會帶來執行時錯誤。並且這種錯誤很難被發現。
- volatile是程序員意識到須要volatile而後在定義變量時加上volatile,若是你遇到了應該加volatile的狀況而沒有加程序可能會被錯誤的優化。若是在不該該加volatile而加了的狀況程序不會出錯只是會下降效率。因此咱們對於volatile的態度應該是:正確區分,該加的時候加不應加的時候不加,若是不能肯定該不應加爲了保險起見就加上。
- volatile的本意是「易變的」 因爲訪問寄存器的速度要快過RAM,因此編譯器通常都會做減小存取外部RAM的優化,但有可能會讀髒數據。當要求使用volatile 聲明的變量的值的時候,系統老是從新從它所在的內存讀取數據,即便它前面的指令剛剛從該處讀取過數據,並且讀取的數據馬上被保存。精確地說就是,優化器每次在用到這個變量時都必須當心地從新從內存裏讀取這個變量的值,而不是使用保存在寄存器裏的備份。
extern
- extern主要用來聲明全局變量,聲明的目的主要是在a.c中定義全局變量而在b.c中使用該變量。
- C語言中程序的編譯時以單個.c源文件爲單位的,所以編譯a.c時只考慮a.c中的內容(不會考了b.c的內容),這就致使a.c中使用了b.c中定義的變量時在編譯時報錯。解決方案是聲明
- 應該在a.c中使用g_b以前先聲明g_b,聲明就是告訴a.c我在別的文件中定義了g_b,而且它的原型和聲明的同樣,未來在連接的時候連接器會在別的.o文件中找到這個同名變量。聲明一個全局變量就要用到extern關鍵字
estrict
- c99中才支持的,因此不少延續c89的編譯器是不支持restrict關鍵字,gcc支持的。
- restrict也是和編譯器行爲特徵有關的。
- restrict只用來修飾指針,不能修飾普通變量。其特性與volatile相反
- estrict限制關鍵字用法
typedef
- 以前講過了
- typedef在C語言關鍵字歸類上屬於存儲類關鍵字,可是實際上和存儲類不要緊。
做用域詳解
局部變量的代碼塊做用域
- 代碼塊基本能夠理解爲一對大括號{}括起來的部分。
- 代碼塊不等於函數,由於if while for都有{}。因此代碼塊<=函數
- 局部變量的做用域是代碼塊做用域,也就是說一個局部變量能夠被訪問和使用的範圍僅限於定義這個局部變量的代碼塊中定義式以後的部分。
函數名和全局變量的文件做用域
- 文件做用域的意思就是全局的訪問權限,也就是說整個.c文件中均可以訪問這些東西。這就是平時所說的局部和全局,全局就是文件做用域。
- 詳細準確的說:函數和全局變量的做用域是定義所在的整個.c文件以內定義式以後的部分。
總結:
- 無論是局部變量、全局變量、函數,都要先定義才能使用
- 嚴格來講咱們上面的總結是錯誤的。準確的說:全局變量/函數的做用域都是本身所在的文件,可是定義式以前的部分由於缺乏聲明因此無法用,解決方案是:一、把它定義到前面去;二、定義到後面可是在前面加聲明;局部變量由於無法聲明,因此只能定義在前面去。
- 在c89標準的編譯器中(如今不少編譯器還延續使用c89標準),全部的局部變量必須先定義在最前面,在變量定義以前不能有一句執行代碼。在c99標準的編譯器中(gcc兼容c99標準)能夠容許在代碼塊內任意地方定義變量。可是容許定義的變量仍是隻能使用在定義了以後,定義以前仍是不能用的。
同名變量的掩蔽規則
- 問題:編程時,不可避免會出現同名變量。變量同名後不必定會出錯。
- 首先,若是兩個同名變量做用域不一樣且沒有交疊,這種狀況下同名沒有任何影響。
- 其次,若是兩個同名變量做用域有交疊,C語言規定在做用域交疊範圍內,做用域小的一個變量會掩蔽掉做用域大的那個(局部掩蓋全局,縣官不如現管)。
變量的生命週期
研究變量生命週期的意義
棧變量的生命週期
- 局部變量(棧變量)存儲在棧上,生命週期是臨時的。臨時的意思就是說:代碼執行過程當中按照須要去建立、使用、消亡的。
- 譬如一個函數內定義的局部變量,在這個函數每一次被調用時都會建立一次,而後使用,最後在函數返回的時候消亡。
- 思考:一個函數內的局部變量爲何在函數外不能使用?
- 思考:局部變量爲何分配在棧上?或者說局部變量爲何是臨時生命週期?
變量的生命週期
- 首先要明白:堆內存空間是客觀存在的,是由操做系統維護的。咱們程序只是去申請而後使用而後釋放。
- 咱們只關心咱們程序使用堆內存的這一段時間,所以堆變量也有了本身的生命週期,就是:從malloc申請時誕生,而後使用,直到free時消亡。
- 因此堆內存在malloc以前和free以後不能再去訪問,所以堆內存在實踐編程時都是被反覆的malloc和free的。
數據段、bss段變量的生命週期
- 全局變量的生命週期是永久的。永久的意思就是在程序被執行時誕生,在程序終止時消亡。
- 全局變量所佔用的內存是不能被程序本身釋放的,因此程序若是申請了過多的全局變量會致使這個程序一直佔用大量內存。
- 若是說堆內存是圖書館借的書,那麼全局變量就是本身買的書。
代碼段、只讀段的生命週期
- 其實就是程序執行的代碼,其實就是函數,它的生命週期是永久的。不過通常代碼的生命週期咱們並不關注。
- 有時候放在代碼段的不僅是代碼,還有const類型的常量,還有字符串常量。(const類型的常量、字符串常量有時候放在rodata段,有時候放在代碼段,取決於平臺)
連接屬性
C語言程序的組織架構:多個C文件+多個h文件
- 龐大、完整的一個C語言程序(譬如linux內核、uboot)由多個c文件和多個h文件組成的。
- 程序的生成過程就是:編譯+連接。編譯是爲了將函數/變量等變成.o二進制的機器碼格式,,連接是爲了將各個獨立分開的二進制的函數連接起來造成一個總體的二進制可執行程序。
編譯以文件爲單位、連接以工程爲單位
- 編譯器工做時是將全部源文件依次讀進來,以單個爲單位進行編譯的。
- 連接的時候其實是把第一步編譯生成個單個的.o文件總體的輸入,而後處理連接成一個可執行程序。
三種連接屬性:外鏈接、內連接、無連接
- 外鏈接的意思就是外部連接屬性,也就是說這傢伙能夠在整個程序範圍內(言下之意就是能夠跨文件)進行連接,譬如普通的函數和全局變量屬於外鏈接。
- 內連接的意思就是(c文件內部)內部連接屬性,也就是說這傢伙能夠在當前c文件內部範圍內進行連接(言下之意就是不能在當前c文件外面的其餘c文件中進行訪問、連接)。static修飾的函數/全局變量屬於內連接。
- 無鏈接的意思就是這個符號自己不參與連接,它跟連接不要緊。全部的局部變量(auto的、static的)都是無鏈接的
函數和全局變量的同名衝突
- 由於函數和全局變量是外部連接屬性,就是說每個函數和全局變量未來在整個程序中全部的c文件都能被訪問,所以在一個程序中的全部c文件中不能出現同名的函數/同名的全局變量。
- 最簡單的解決方案就是起名字不要重複,可是很難作到。主要緣由是一個很大的工程中函數和全局變量名字太多了,並且一個大工程不是一我的完成的,是不少人協做完成,因此很難保證不會重名。解決方案呢?
- 現代高級語言中完美解決這個問題的方法是命名空間namespace(其實就是給一個變量帶上各個級別的前綴)。可是C語言不是這麼解決的。
- C語言比較早碰到這個問題,當時還沒發明namespace概念,當時C語言就發明了一種不是很完美可是湊活能用的解決方案,就是三種連接屬性的方法。
- C語言的連接屬性解決重名問題思路是這樣的:咱們將明顯不會在其餘c文件中引用(只在當前c文件中引用)的函數/全局變量,使用static修飾使其成爲內連接屬性,這樣在未來鏈接時即便2個c文件中有重名的函數/全局變量,只要其中一個或2個爲內連接屬性就沒事。
- 這種解決方案在必定程度上解決了問題。可是沒有從根本上解決問題,留下了不少麻煩。因此這個就致使了C語言寫很大型的項目難度很大。
static的第二種用法:修飾全局變量和函數
- 普通的(非靜態)的函數/全局變量,默認的連接屬性是外部的
- static(靜態)的函數/全局變量,連接屬性是內部連接。
通常用法總結:
- 思考:爲何static一個關鍵字能夠有2種徹底不一樣的意思?由於這兩種用法是互斥的。(能夠明顯的區分開來)
最後的總結
- 普通(自動)局部變量分配在棧上,做用域爲代碼塊做用域,生命週期是臨時,鏈接屬性爲無鏈接。定義時若是未顯式初始化則其值隨機,變量地址由運行時在棧上分配獲得,屢次執行時地址不必定相同,函數不能返回該類變量的地址(指針)做爲返回值。
- static靜態局部變量分配在數據段/bss段(顯式初始化爲非0則在數據段,顯式初始化爲0或未顯示初始化則在bss段),做用域爲代碼塊做用域(人爲規定的),生命週期爲永久(自然的),連接屬性爲無鏈接(自然的)。定義時若是未顯式初始化則其值爲0(自然的),變量地址由運行時環境在加載程序時肯定,整個程序運行過程當中惟一不變;static靜態局部變量其實就是做用域爲代碼塊做用域(同時連接屬性爲無鏈接)的全局變量。靜態局部變量能夠改成用全局變量實現(程序中儘可能避免用全局變量,由於會破壞結構性)。
- 靜態全局變量/靜態函數和普通全局變量/普通函數的惟一差異是:static使全局變量/函數的連接屬性由外部連接(整個程序全部文件範圍)轉爲內部連接(當前c文件內)。這是爲了解決全局變量/函數的重名問題(C語言沒有命名空間namespace的概念,所以在程序中文件變多以後全局變量/函數的重名問題很是嚴重,將沒必要要被其餘文件引用的全局變量/函數聲明爲static能夠很大程度上改善重名問題,可是仍未完全解決)。
- 寫程序儘可能避免使用全局變量,尤爲是非static類型的全局變量。能肯定不會被其餘文件引用的全局變量必定要static修飾。
- 注意區分全局變量的定義和聲明。通常規律以下:若是定義的同時有初始化則必定會被認爲是定義;若是隻是定義而沒有初始化則有可能被編譯器認爲是定義,也可能被認爲是聲明,要具體分析;若是使用extern則確定會被認爲是聲明(實際上使用extern也能夠有定義,實際上加extern就是明確聲明這個變量爲外部連接屬性)。
- 全局變量應該定義在c文件中而且在頭文件中聲明,而不要定義在頭文件中(由於若是定義在頭文件中,則該頭文件被多個c文件包含時該全局變量會重複定義)。
- 在b.c中引用a.c中定義的全局變量/函數有2種方法:一是在a.h中聲明該函數/全局變量,而後在b.c中#include <a.h>;二是在b.c中使用extern顯式聲明要引用的函數/全局變量。其中第一種方法比較正式。
- 存儲類決定生命週期,做用域決定連接屬性
- 宏和inline函數的連接屬性爲無鏈接。