1. 程式人生 > >C語言高階篇 - 3.儲存類&作用域&生命週期&連結屬性

C語言高階篇 - 3.儲存類&作用域&生命週期&連結屬性

1.概念解析

1.1、儲存類

        (1)儲存類就是儲存型別,也就是描述C語言變數在何種地方儲存。

        (2)記憶體有多種管理方法:棧、堆、資料段、bss段、.text段······一個變數的儲存類屬性就是描述這個變數儲存在何種記憶體段中。

        (3)譬如:區域性變數分配在棧上,所以它的儲存類就是棧;顯式初始化為非0的全域性變數分配在資料段,顯式初始化為0和沒有顯示初始化(預設為0)的全域性變數分配在bss段。

 

1.2、作用域

        (1)作用域是描述這個變數起作用的程式碼範圍。

        (2)基本來說,C語言變數的作用域規則是程式碼塊作用域。意思就是這個變數起作用的範圍是當前的程式碼塊。程式碼塊就是一對大括號{}括起來的範圍,所以一個變數的作用域是:這個變數定義所在的{}範圍內從這個變數定義開始往後的部分。(這就解釋了為什麼變數定義總是在一個函式的最前面)

 

1.3、生命週期

        (1)宣告週期是描述這個變數什麼時候誕生(執行時分配記憶體空間給這個變數)及什麼時候死亡(執行時收回這個記憶體空間,此後再不能訪問這個記憶體地址,或者訪問這個記憶體地址已經和這個變數無關了)的。

        (2)變數和記憶體的關係,就和人(變數)去圖書館借書(記憶體)一樣。變數的生命週期就好象我人借書的這段週期一樣。

        (3)研究變數的生命週期可以我們理解程式執行的一些現象、理解C語言的一些規則。

 

1.4、連結屬性

        (1)大家知道程式從原始碼到最終可執行程式,經歷的過程:編譯、連結。

        (2)編譯階段就是把原始碼搞成.o目標檔案,目標檔案裡面有很多符號和程式碼段、資料段、bss段等分段。符號就是程式設計中的變數名、函式名等。執行時變數名、函式名能夠和相應的記憶體對應起來,靠符號來做連結的。

        (3).o的目標檔案連結生成最終可執行程式的時候,其實就是把符號和相對應的段給連結起來。

C語言中的符號有三種連結屬性:外連線屬性、內連結屬性、無連線屬性。

 

總結:以上4個概念,其實就是從4個不同角度來分析C語言的一些執行規則。綜合這4種分析角度能夠讓程式設計師完全掌握C語言程式的執行規則和方法。

 

2.linux下C程式的記憶體映像

2.1、程式碼段、只讀資料段

        (1)對應著程式中的程式碼(函式),程式碼段在linux中又叫文字段(.text)

        (2)只讀資料段就是在程式執行期間只能讀不能寫的資料,const修飾的常量有可能是存在只讀資料段的(但是不一定,const常量的實現方法在不同平臺是不一樣的)

 

2.2、資料段、bss段

        (1)資料段存:1、顯式初始化為非0的全域性變數;2、顯式初始化為非0的static區域性變數

        (2)bss段存:1、顯式初始化為0或者未顯式初始化的全域性變數;2、顯式初始化為0或未顯式初始化的static區域性變數。

 

2.3、堆

        (3)C語言中什麼樣變數存在堆記憶體中?C語言不會自動向堆中存放東西,堆的操作是程式設計師自己手工操作的。程式設計師根據需求自己判斷要不要使用堆記憶體,用的時候自己申請,自己使用,完了自己釋放。

 

2.4、檔案對映區

        (1)檔案對映區就是程序打開了檔案後,將這個檔案的內容從硬碟讀到程序的檔案對映區,以後就直接在記憶體中操作這個檔案,讀寫完了後在儲存時再將記憶體中的檔案寫到硬碟中去。

 

2.5、棧

        (1)棧記憶體區,區域性變數分配在棧上;函式呼叫傳參過程也會用到棧

 

2.6、核心對映區

        (1)核心對映區就是將作業系統核心程式對映到這個區域了。

        (2)對於linux中的每一個程序來說,它都以為整個系統中只有它自己和核心而已。它認為記憶體地址0xC0000000以下都是它自己的活動空間,0xC0000000以上是OS核心的活動空間。

        (3)每一個程序都活在自己獨立的程序空間中,0-3G的空間每一個程序是不同的(因為用了虛擬地址技術),但是核心是唯一的。

 

2.7、OS下和裸機下C程式載入執行的差異

        (1)C語言程式執行時環境有一定要求,意思是單獨個人寫的C語言程式沒法直接在記憶體中執行,需要外部一定的協助,這段協助的程式碼叫載入執行程式碼(或者叫構建C執行時環境的程式碼,這一段程式碼在作業系統下是別人寫好的,會自動新增到我們寫的程式上,這段程式碼的主要作用是:給全域性變數賦值、清bss段)。

        (2)ARM裸機第十六部分,寫shell時有一次定義了一個全域性變數初始化為0但是實際不為0,後來在裸機的start.S中加了清bss段程式碼就變0了。這就說明在裸機程式中沒人幫我們來做這一段載入執行時程式碼,要程式設計師自己做(start.S中的重定位和清bss段就是在做這個事);在作業系統中執行程式時程式設計師自己不用操心,會自動完成重定位和清bss,所以我們看到的現象:C語言中未初始化的全域性變數預設為0·····。

        (3)資料段的全域性變數或靜態區域性變數都是有非0的初值的,這些初值在main函式執行之前就已經被初始化了,是重定位期間完成的初始化。

 

3、儲存類相關的關鍵字1

3.1、auto

        (1)auto關鍵字在C語言中只有一個作用,那就是修飾區域性變數。

        (2)auto修飾區域性變數,表示這個區域性變數是自動區域性變數,自動區域性變數分配在棧上。(既然在棧上,說明它如果不初始化那麼值就是隨機的······)

        (3)平時定義區域性變數時就是定義的auto的,只是省略了auto關鍵字而已。可見,auto的區域性變數其實就是預設定義的普通的區域性變數。

 

3.2、static

        (1)static關鍵字在C語言中有2種用法,而且這兩種用法彼此沒有任何關聯、完全是獨立的。其實當年本應該多發明一個關鍵字,但是C語言的作者覺得關鍵字太多不好,於是給static增加了一種用法,導致static一個關鍵字竟然有兩種截然不同的含義。

        (2)static的第一種用法是:用來修飾區域性變數,形成靜態區域性變數。要搞清楚靜態區域性變數和非靜態區域性變數的區別。本質區別是儲存類不同(儲存類不同就衍生出很多不同):非靜態區域性變數分配在棧上,而靜態區域性變數分配在資料段/bss段上。

        (3)static的第二種用法是:用來修飾全域性變數,形成靜態全域性變數。要搞清楚靜態全域性變數和非靜態全域性變數的區別。區別是在連結屬性上不同,講到連結屬性時詳細講。

分析:

    1、靜態區域性變數在儲存類方面和全域性變數一樣。

    2、靜態區域性變數在生命週期方面和全域性變數一樣。

    3、靜態區域性變數和全域性變數的區別是:作用域、連線屬性。靜態區域性變數作用域是程式碼塊作用域(和普通區域性變數是一樣的)、連結屬性是無連線;全域性變數作用域是檔案作用域(和函式是一樣的)、連結屬性方面是外連線。

 

3.3、register

        (1)register關鍵字不常用,也只有一個作用,那就是:register修飾的變數。編譯器會盡量將它分配在暫存器中。(平時分配的一般的變數都是在記憶體中的)。分配在暫存器中一樣的用,但是讀寫效率會高很多。所以register修飾的變數用在那種變數被反覆高頻率的使用,通過改善這個變數的訪問效率可以極大的提升程式執行效率時。所以register是一種極致提升程式執行效率的手段。

        (2)uboot中用到了一個register型別的變數,gd這個變數是用來存uboot的全域性變數(gd就是global data)。因為這個全域性變數在整個uboot中到處都被訪問,所以定義成register的。

        (3)平時寫程式碼要被定義成register這種情況很少,一般慎用。

        (4)register編譯器只能承諾儘量將register修飾的變數放在暫存器中,但是不保證一定放在暫存器中。主要原因是因為暫存器數量有限,不一定有空用。

 

 

4.儲存類相關的關鍵字2

4.1、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關鍵字

 

4.2、volatile

        (1)volatile的字面意思:可變的、易變的。C語言中volatile用來修飾一個變數,表示這個變數可以被編譯器之外的東西改變。編譯器之內的意思是變數的值的改變是程式碼的作用,編譯器之外的改變就是這個改變不是程式碼造成的,或者不是當前程式碼造成的,編譯器在編譯當前程式碼時無法預知。譬如在中斷處理程式isr中更改了這個變數的值,譬如多執行緒中在別的執行緒更改了這個變數的值,譬如硬體自動更改了這個變數的值(一般這個變數是一個暫存器的值)

        (2)以上說的三種情況(中斷isr中引用的變數,多執行緒中共用的變數,硬體會更改的變數)都是編譯器在編譯時無法預知的更改,此時應用使用volatile告訴編譯器這個變數屬於這種(可變的、易變的)情況。編譯器在遇到volatile修飾的變數時就不會對改變數的訪問進行優化,就不會出現錯誤。

        (3)編譯器的優化在一般情況下非常好,可以幫助提升程式效率。但是在特殊情況(volatile)下,變數會被編譯器想象之外的力量所改變,此時如果編譯器沒有意識到而去優化則就會造成優化錯誤,優化錯誤就會帶來執行時錯誤。而且這種錯誤很難被發現。

        (4)volatile是程式設計師意識到需要volatile然後在定義變數時加上volatile,如果你遇到了應該加volatile的情況而沒有加程式可能會被錯誤的優化。如果在不應該加volatile而加了的情況程式不會出錯只是會降低效率。所以我們對於volatile的態度應該是:正確區分,該加的時候加不該加的時候不加,如果不能確定該不該加為了保險起見就加上。

 

4.3、restrict

        (1)c99中才支援的,所以很多延續c89的編譯器是不支援restrict關鍵字,gcc支援的。

        (2)restrict也是和編譯器行為特徵有關的。

        (3)restrict只用來修飾指標,不能修飾普通變數。

        (4)http://blog.chinaunix.net/uid-22197900-id-359209.html

        (5)memcpy和memmove的區別

 

4.4、typedef

        (1)之前講過了

        (2)typedef在C語言關鍵字歸類上屬於儲存類關鍵字,但是實際上和儲存類沒關係。

 

5.作用域詳解

5.1、區域性變數的程式碼塊作用域

        (1)程式碼塊基本可以理解為一對大括號{}括起來的部分。

        (2)程式碼塊不等於函式,因為if  while for都有{}。所以程式碼塊<=函式

        (3)區域性變數的作用域是程式碼塊作用域,也就是說一個區域性變數可以被訪問和使用的範圍僅限於定義這個區域性變數的程式碼塊中定義式之後的部分。

 

5.2、函式名和全域性變數的檔案作用域

        (1)檔案作用域的意思就是全域性的訪問許可權,也就是說整個.c檔案中都可以訪問這些東西。這就是平時所說的區域性和全域性,全域性就是檔案作用域。

        (2)詳細準確的說:函式和全域性變數的作用域是定義所在的整個.c檔案之內定義式之後的部分。

 

總結:

        (1)不管是區域性變數、全域性變數、函式,都要先定義才能使用

        (2)嚴格來說我們上面的總結是錯誤的。準確的說:全域性變數/函式的作用域都是自己所在的檔案,但是定義式之前的部分因為缺少宣告所以沒法用,解決方案是:1、把它定義到前面去;2、定義到後面但是在前面加宣告;區域性變數因為沒法宣告,所以只能定義在前面去。

        (3)在c89標準的編譯器中(現在很多編譯器還延續使用c89標準),所有的區域性變數必須先定義在最前面,在變數定義之前不能有一句執行程式碼。在c99標準的編譯器中(gcc相容c99標準)可以允許在程式碼塊內任意地方定義變數。但是允許定義的變數還是隻能使用在定義了之後,定義之前還是不能用的。

 

5.3、同名變數的掩蔽規則

        (1)問題:程式設計時,不可避免會出現同名變數。變數同名後不一定會出錯。

        (2)首先,如果兩個同名變數作用域不同且沒有交疊,這種情況下同名沒有任何影響。

        (3)其次,如果兩個同名變數作用域有交疊,C語言規定在作用域交疊範圍內,作用域小的一個變數會掩蔽掉作用域大的那個(縣官不如現管)。

 

 

6.變數的生命週期

6.1、研究變數生命週期的意義

        (1)研究變數生命週期,有助於理解變數的行為特徵。

6.2、棧變數的生命週期

        (1)區域性變數(棧變數)儲存在棧上,生命週期是臨時的。臨時的意思就是說:程式碼執行過程中按照需要去建立、使用、消亡的。

        (2)譬如一個函式內定義的區域性變數,在這個函式每一次被呼叫時都會建立一次,然後使用,最後在函式返回的時候消亡。

        (3)思考:一個函式內的區域性變數為什麼在函式外不能使用?

        (4)思考:區域性變數為什麼分配在棧上?或者說區域性變數為什麼是臨時生命週期?

 

6.3、堆變數的生命週期

        (1)首先要明白:堆記憶體空間是客觀存在的,是由作業系統維護的。我們程式只是去申請然後使用然後釋放。

        (2)我們只關心我們程式使用堆記憶體的這一段時間,因此堆變數也有了自己的生命週期,就是:從malloc申請時誕生,然後使用,直到free時消亡。

        (3)所以堆記憶體在malloc之前和free之後不能再去訪問,因此堆記憶體在實踐程式設計時都是被反覆的malloc和free的。

 

6.4、資料段、bss段變數的生命週期

        (1)全域性變數的生命週期是永久的。永久的意思就是在程式被執行時誕生,在程式終止時消亡。

        (2)全域性變數所佔用的記憶體是不能被程式自己釋放的,所以程式如果申請了過多的全域性變數會導致這個程式一直佔用大量記憶體。

        (3)如果說堆記憶體是圖書館借的書,那麼全域性變數就是自己買的書。

 

6.5、程式碼段、只讀段的生命週期

        (1)其實就是程式執行的程式碼,其實就是函式,它的生命週期是永久的。不過一般程式碼的生命週期我們並不關注。

        (2)有時候放在程式碼段的不只是程式碼,還有const型別的常量,還有字串常量。(const型別的常量、字串常量有時候放在rodata段,有時候放在程式碼段,取決於平臺)

 

7.連結屬性

7.1、C語言程式的組織架構:多個C檔案+多個h檔案

        (1)龐大、完整的一個C語言程式(譬如linux核心、uboot)由多個c檔案和多個h檔案組成的。

        (2)程式的生成過程就是:編譯+連結。編譯是為了將函式/變數等變成.o二進位制的機器碼格式,連結是為了將各個獨立分開的二進位制的函式連結起來形成一個整體的二進位制可執行程式。

 

7.2、編譯以檔案為單位、連結以工程為單位

        (1)編譯器工作時是將所有原始檔依次讀進來,單個為單位進行編譯的。

        (2)連結的時候實際上是把第一步編譯生成個單個的.o檔案整體的輸入,然後處理連結成一個可執行程式。

 

7.3、三種連結屬性:外連線、內連結、無連結

        (1)外連線的意思就是外部連結屬性,也就是說這傢伙可以在整個程式範圍內(言下之意就是可以跨檔案)進行連結,譬如普通的函式和全域性變數屬於外連線。

        (2)內連結的意思就是(c檔案內部)內部連結屬性,也就是說這傢伙可以在當前c檔案內部範圍內進行連結(言下之意就是不能在當前c檔案外面的其他c檔案中進行訪問、連結)。static修飾的函式/全域性變數屬於內連結。

        (3)無連線的意思就是這個符號本身不參與連結,它跟連結沒關係。所有的區域性變數(auto的、static的)都是無連線的

 

7.4、函式和全域性變數的同名衝突

        (1)因為函式和全域性變數是外部連結屬性,就是說每一個函式和全域性變數將來在整個程式中所有的c檔案都能被訪問,因此在一個程式中的所有c檔案中不能出現同名的函式/同名的全域性變數。

        (2)最簡單的解決方案就是起名字不要重複,但是很難做到。主要原因是一個很大的工程中函式和全域性變數名字太多了,而且一個大工程不是一個人完成的,是很多人協作完成,所以很難保證不會重名。解決方案呢?

        (3)現代高階語言中完美解決這個問題的方法是名稱空間namespace(其實就是給一個變數帶上各個級別的字首)。但是C語言不是這麼解決的。

        (4)C語言比較早碰到這個問題,當時還沒發明namespace概念,當時C語言就發明了一種不是很完美但是湊活能用的解決方案,就是三種連結屬性的方法。

        (5)C語言的連結屬性解決重名問題思路是這樣的:我們將明顯不會在其他c檔案中引用(只在當前c檔案中引用)的函式/全域性變數,使用static修飾使其成為內連結屬性,這樣在將來連線時即使2個c檔案中有重名的函式/全域性變數,只要其中一個或2個為內連結屬性就沒事。

        (6)這種解決方案在一定程度上解決了問題。但是沒有從根本上解決問題,留下了很多麻煩。所以這個就導致了C語言寫很大型的專案難度很大。

 

7.5、static的第二種用法:修飾全域性變數和函式

        (1)普通的(非靜態)的函式/全域性變數,預設的連結屬性是外部的

        (2)static(靜態)的函式/全域性變數,連結屬性是內部連結。

 

7.6、一般用法總結:

思考:為什麼static一個關鍵字可以有2種完全不同的意思?因為這兩種用法是互斥的。

 

8.最後的總結

        (1)普通(自動)區域性變數分配在棧上,作用域為程式碼塊作用域,生命週期是臨時,連線屬性為無連線。定義時如果未顯式初始化則其值隨機,變數地址由執行時在棧上分配得到,多次執行時地址不一定相同,函式不能返回該類變數的地址(指標)作為返回值。

        (2)靜態區域性變數分配在資料段/bss段(顯式初始化為非0則在資料段,顯式初始化為0或未顯示初始化則在bss段),作用域為程式碼塊作用域(人為規定的),生命週期為永久(天然的),連結屬性為無連線(天然的)。定義時如果未顯式初始化則其值為0(天然的),變數地址由執行時環境在載入程式時確定,整個程式執行過程中唯一不變;靜態區域性變數其實就是作用域為程式碼塊作用域(同時連結屬性為無連線)的全域性變數。靜態區域性變數可以改為用全域性變數實現(程式中儘量避免用全域性變數,因為會破壞結構性)。

        (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函式的連結屬性為無連線。