C語言中儲存類別與內(nèi)存管理的深入理解
儲存類別
C語言提供了多種儲存類別供我們使用,并且對應(yīng)的有對應(yīng)的內(nèi)存管理策略,在了解C中的儲存類型前,我們先了解一下與儲存類型相關(guān)的一些概念。
1. 基礎(chǔ)概念
對象:不同于面向?qū)ο缶幊讨械膶ο蟮暮x,C語言是面向過程編程,不存在這樣對象的概念,這個對象指的是值儲存所占據(jù)物理內(nèi)存空間。
左值:左值是可以指定對象的表達(dá)式,它的最簡單形式即為標(biāo)識符,復(fù)雜的可以為為指針之類。一個表達(dá)式成為左值的前提是它確實指定了一塊作為對象的儲存空間,例如:
int a = 1;//a作為標(biāo)識符,也作基礎(chǔ)表達(dá)式,指定了一個對象,是左值 int *pa = &a;//pa同a也指示了一個儲存地址對象,是一個左值;*pa是一個表示式,指示了a相同的對象也是一個左值 int arr[5] = {0}; arr+a*3;// 這段表達(dá)式就不是一個標(biāo)識符,也不是一個左值了,因為它沒有指定內(nèi)存上的任意位置 *(arr + a * 3);// 不同于上面的,這也是一個左值,因為它確實指定了內(nèi)存上的位置
左值分為可修改左值和不可修改左值。
我們通常用儲存期來描述對象,表明對象在內(nèi)存中留存的時間。用標(biāo)識符指定對象時,使用作用域和鏈接來描述標(biāo)識符,其中作用域表明標(biāo)識符可以可以被程序使用的范圍,鏈接表明程序的哪些其他文件也可以使用它。
不同的儲存類別之間的區(qū)別即在于它們的儲存期、作用域和連接形式的不相同。我們來分別了解他們一下。
儲存期:儲存期分為靜態(tài)儲存期,自動儲存期,線程儲存期和動態(tài)分配儲存期(線程儲存期暫時不多贅述),它們分別對應(yīng)不同的在內(nèi)存中的儲存位置,也有不同的特點。
靜態(tài)儲存期:對應(yīng)靜態(tài)存儲位置,它在程序開始運(yùn)行時就被分配,這段空間不可增加和減少,所以從程序開始運(yùn)行到停止運(yùn)行,靜態(tài)儲存期的數(shù)據(jù)一直存在。通常在函數(shù)外的變量和static表示的變量具有靜態(tài)儲存期。
自動儲存期:對應(yīng)??臻g,它隨著程序的運(yùn)行可以自動進(jìn)行分配,增加或減少。程序進(jìn)入到一個塊為其中的變量分配棧空間,退出一個塊后則會釋放相應(yīng)的空間。一般的創(chuàng)建的變量都具有自動儲存期。
動態(tài)分配儲存期:對應(yīng)堆空間,它需要通過特殊的語法進(jìn)行申請,申請后也需要主動進(jìn)行銷毀,存在時間為從申請內(nèi)存開始到主動釋放內(nèi)存為止。需要通過專門的語句來獲得具有動態(tài)分配儲存期的變量。
作用域:一個變量能被使用的范圍稱為作用域,作用域分為 塊作用域、函數(shù)作用域、函數(shù)原型作用域和 文件作用域。
塊作用域:由一個花括號開始到與之對應(yīng)的花括號為止,其中的變量都具有塊作用域,一般情況下任何在塊內(nèi)的定義的變量可以在塊內(nèi)任何位置使用,但是不可以在塊外進(jìn)行使用。(特例后面會舉出),而且對于內(nèi)部塊也可以定義與外部塊同名的變量,這時候內(nèi)部塊將隱去向內(nèi)隱去外部塊的同名變量,在內(nèi)部使用自己定義的該變量。
函數(shù)作用域:針對的是goto語句標(biāo)簽,一個標(biāo)簽首次出現(xiàn)在含糊內(nèi)層,它的作用域?qū)由熘琳麄€函數(shù),這表示我們不能使用同名的標(biāo)簽。
函數(shù)原型作用域:對于函數(shù)的聲明,該作用域開始與形參定義處知道函數(shù)函數(shù)原型結(jié)束。編譯器只注重形式參數(shù)的類型而不會注意具體的變量名,甚至可以不使用變量名。
文件作用域:聲明在函數(shù)外的變量具有文件作用域,他們可以在同一源文件下的任何塊和函數(shù)中使用,具有文件作用域的變量也被稱為全局變量。
對于分別屬于同類型作用域但是不同一個作用域的變量它們可以任意重名,例如不同塊的函數(shù)中變量屬于不同塊他們可以重名。對于具有文件作用域的變量它們對于所屬的文件塊都有作用,所以不建議塊中變量與全局變量重名,但是在重名后塊使用對應(yīng)名稱變量時將以塊中自身定義的變量為準(zhǔn)。
#include <stdio.h> void showA(int a, int type) { switch (type) { case 1: printf("outer : "); break; case 2: printf("inter : "); break; case 3: printf("circle : "); default: ; } printf("a = %d\n", a); } int main (void) { int a = 1; showA(a, 1); { int a = 2; showA(a, 2); } showA(a, 1); while (a++ < 5) { int a = 5; showA(a, 3); } showA(a, 1); return 0; } /** outer : a = 1 inter : a = 2 outer : a = 1 circle : a = 5 circle : a = 5 circle : a = 5 circle : a = 5 outer : a = 6 * **/
我們發(fā)現(xiàn)在在外部a為在外部定義的值,a輸出為1;第一塊內(nèi)部,a讀取的是內(nèi)部的a的值,這一點沒有任何問題;然后我們到外部,我們再讓程序輸出a值,仍然為2,沒有問題;但是進(jìn)入循環(huán)后,我們發(fā)現(xiàn)很奇怪的現(xiàn)象,通過輸出我們發(fā)現(xiàn)循環(huán)執(zhí)行了4次,很明顯這是基于外部的a,但是內(nèi)部的a在輸出時卻總是顯示內(nèi)部的a,這一點是因為:內(nèi)部循環(huán)定義的a作用域只在塊內(nèi),并不會作用于循環(huán)條件判斷的部分,所以在進(jìn)行循環(huán)條件判斷時始終使用外部的a。注意遞增條件一定要在循環(huán)判斷條件中,否則循環(huán)將變成死循環(huán)。但是,沒有必要使用同名變量。
鏈接:鏈接是程序中變量可以被其他文件使用的描述,有三種類型的鏈接:外部鏈接、內(nèi)部鏈接和 無鏈接。
如果一個變量具有文件作用域它才可能具有外部鏈接和內(nèi)部鏈接,其他變量都是無鏈接的。在具體了解內(nèi)部鏈接和外部鏈接之前,我們先理解下 翻譯單元的概念。
翻譯單元:我們經(jīng)常使用#include指令來包含頭文件,C通過預(yù)處理將對應(yīng)頭文件內(nèi)容直接替換掉該條命令,他們雖然表面上看起來不是一個文件但是被編譯器看做了一個文件,這個文件就被稱為一個翻譯單元,一個具有文件作用域的變量它的實際可見范圍就是整個翻譯單元。一個翻譯單元由一個源文件和多個它所包含的文件組成。
所以外部鏈接可以在多文件程序中使用,而內(nèi)部鏈接只可以在一個翻譯單元使用。區(qū)別二者在于是否使用了儲存類別說明符static,使用了static則為內(nèi)部鏈接,反之則為外部鏈接。
//main.c 文件 #include <stdio.h> int main (void) { extern int a;// 聲明,讓編譯器在別處查找a的定義 // extern int b; // printf("b = %d", b);這一段不可使用,因為b只具有內(nèi)部鏈接,不可在其他源文件訪問,運(yùn)行 // 報錯 printf("a = %d", a); return 0; } /** a = 5 * **/ // 和它一同編譯的another.c文件 int a = 5;// 具有外部鏈接,可以在多個源文件之間進(jìn)行共享 static int b = 2;// 具有內(nèi)部鏈接,只能在一個源文件內(nèi)共享
在這里我們使用了外部鏈接變量a,在兩個翻譯單元之間實現(xiàn)了變量的傳遞。其中main.c文件為了調(diào)用變量a必須有extern聲明語句,這段語句聲明了一個int型變量a但是并不會為它分配內(nèi)存,使用它只是為了告訴編譯器在別處尋找變量a的定義,這是必不可少的,否則程序?qū)箦e。
// main.c文件 #include <stdio.h> #include "main.h" int main (void) { extern int a; printf("a = %d", a); return 0; } /** a = 7 * **/ // main.h文件 static int a = 7;
不同于源文件,對于頭文件,通過#include指令包含頭文件,編譯器將自動將對應(yīng)文件內(nèi)容替代到對應(yīng)位置,它們屬于同一個翻譯單元,所以及時具有內(nèi)部鏈接的變量仍可以使用。
2. 儲存類別分類
介紹了一些基礎(chǔ)概念后我們來根據(jù)這些基礎(chǔ)概念對于儲存類別進(jìn)行分類:
儲存類別 | 儲存期 | 作用域 | 鏈接 | 聲明方式 |
---|---|---|---|---|
自動 | 自動 | 塊 | 無 | 塊內(nèi)聲明 |
寄存器 | 自動 | 塊 | 無 | 塊內(nèi)聲明,加入關(guān)鍵字register |
靜態(tài)外部鏈接 | 靜態(tài) | 文件 | 外部 | 函數(shù)外 |
靜態(tài)內(nèi)部鏈接 | 靜態(tài) | 文件 | 內(nèi)部 | 函數(shù)外,加入關(guān)鍵字static |
靜態(tài)無鏈接 | 靜態(tài) | 塊 | 無 | 塊內(nèi)聲明,計入關(guān)鍵字static |
下面我呢來分別具體對于每種類型所對應(yīng)的變量進(jìn)行說明。
3. 自動變量
自動變量具有自動儲存期,塊作用域,無鏈接,在塊內(nèi)進(jìn)行聲明即可。自動儲存期意味著它在開始執(zhí)行塊時被創(chuàng)建,在對應(yīng)塊到結(jié)尾時被銷毀,不能再被通過任何途徑訪問;塊作用域表明只能在塊中使用變量名對于變量進(jìn)行訪問,但是在處于變量可使用的儲存期內(nèi)(這點必要,因為我們無法控制編譯器的回收機(jī)制),我們也可以通過指針傳遞地址的方式來繼續(xù)使用;無鏈接表明不能再其他文件中對于該變量進(jìn)行訪問。
對于自動變量,默認(rèn)情況下聲明的變量都具有這樣的儲存類別,但是有時候為了更明顯的表現(xiàn)意圖,并且在外部具有同名變量時,為了更好覆蓋它,表明自己變量的自動儲存類型,可以使用關(guān)鍵字auto,例如:
#include <stdio.h> int a = 1; int main (void) { auto int a;// int a;也是等價 printf("a = %d", a); return 0; }
在外部有同名變量時,使用auto關(guān)鍵字還是用來標(biāo)識a作為塊內(nèi)的自動變量,覆蓋外部的a,即使不加auto也是可以的,但是使用后可以起到更好的標(biāo)識性。
4. 寄存器變量
寄存器變量在多個方面與自動變量相同,不同在于自動變量通常儲存在計算機(jī)內(nèi)存中,而寄存器變量儲存在計算機(jī)CPU的寄存器中,因此它具有高效的運(yùn)算率,而且因為它在寄存器中所以無法獲得其地址。通過在變量定義中使用register修飾既可以聲明寄存器變量:register int a;
但是,值得注意的是,使用register企圖創(chuàng)建寄存器變量是一種請求而不是命令,編譯器很可能不會通過你的請求,而且寄存器也有可能沒有足夠大空間儲存double類型變量,所以可以聲明register的數(shù)據(jù)類型也是有限的。即使失敗,也能夠創(chuàng)建相應(yīng)的自動變量,但是我們?nèi)匀徊荒塬@得其地址。
5. 具有塊作用域的靜態(tài)變量
具有塊作用域的靜態(tài)變量,對應(yīng)的儲存類別為靜態(tài)無鏈接,其具有靜態(tài)儲存期,塊作用域和無鏈接。它在程序開始運(yùn)行時被創(chuàng)建,程序結(jié)束后被銷毀。只在它被定義的塊內(nèi)調(diào)用,無法被其他文件訪問。
相較于自動變量,它只是擁有了靜態(tài)儲存期,所以我們使用static類別修飾符獲得該類型變量static int a;。值得注意的是,由于該變量具有靜態(tài)儲存期,所以它始終儲存在系統(tǒng)中的某一段內(nèi)存空間中,我們可以利用指針在塊外的區(qū)域?qū)τ谠撟兞窟M(jìn)行訪問。
#include <stdio.h> int* fun(); int main (void) { int *p = fun(); *p += 2; fun(); return 0; } int *fun() { static int a = 1; printf("a = %d\n", a); return &a; } /** a = 1 a = 3 * **/
通過上面的運(yùn)行結(jié)果我們發(fā)現(xiàn)通過函數(shù)返回指向靜態(tài)變量的指針,我們可以對靜態(tài)變量進(jìn)行訪問和修改,這使得我們在塊外對塊內(nèi)無鏈接的靜態(tài)變量進(jìn)行訪問。
6. 內(nèi)部鏈接的靜態(tài)變量
內(nèi)部鏈接的靜態(tài)變量對應(yīng)儲存類別為靜態(tài)內(nèi)部鏈接。它具有靜態(tài)作用期,文件作用域和內(nèi)部鏈接。它在程序開始被創(chuàng)建,在同一個翻譯單元內(nèi)可以任意訪問。在前面已經(jīng)有它的用例。
7. 外部鏈接的靜態(tài)變量
外部鏈接的靜態(tài)變量對應(yīng)儲存類別為靜態(tài)外部鏈接。它具有靜態(tài)作用器,文件作用域和外部鏈接。大體上與內(nèi)部鏈接的靜態(tài)變量相同,但是它可以在多個翻譯單元(多個源文件之間)進(jìn)行共享。
但是仍有一些事項注意:
聲明時可以顯示初始化,但是必須使用常量進(jìn)行初始化(對于sizeof表達(dá)式也是常量),如果未進(jìn)行初始化,無論如何它將被初始化為0。
#include <stdio.h> int a; int b = 3; int c = 3 * sizeof b; // int d = 3 * a;非常量無法初始化 char d; int main (void ) { printf("a = %d\n", a); printf("b = %d\n", b); printf("c = %d\n", c); printf("d = %d d = %c\n", d, d); return 0; } /** a = 0 b = 3 c = 12 d = 0 d = * **/
如何跨文件使用具有外部鏈接的變量?正常情況下直接使用將會報錯,我們需要通過引用性聲明來使用,通過extern關(guān)鍵字來實現(xiàn),在變量聲明前加上關(guān)鍵字,編譯器就會明白根據(jù)指示在其他源文件中查找變量的聲明,這樣的聲明不會申請新的內(nèi)存,也不可以進(jìn)行賦值。
// main.c #include <stdio.h> int main (void ) { // extern int a = 1;是不能賦值的 extern int a; printf("a = %d\n", a); return 0; } /** a = 5 * **/ // anothor.c int a = 5;
8. 儲存類別說明符小結(jié)
C中儲存類別說明符有多個,并且不同說明符在不同位置也有不同意義。一共有一下說明符:auto、extern、register、static、_Thread_local和typedef。最后者被歸為此類屬于一些語法上的原因。它們大多都是單獨使用,但是_Thread_local可以和static和extern一起使用。
auto表明變量具有自動儲存期,在塊內(nèi)的變量默認(rèn)具有自動儲存期,使用auto只是明確表示要使用與外部變量重名的局部變量的意圖。
register表明希望將變量儲存在寄存器中,希望以最快的速度讀取變量,同時不希望獲得該變量的地址。
static表明變量具有靜態(tài)儲存期,它并不改變塊內(nèi)變量的鏈接類型,但是對于快外的變量,將會限制它的鏈接類型為內(nèi)部鏈接。
extern表明該變量定義在別處,希望編譯器在別處查找,包含extern的聲明具有文件作用域,那么變量的定義一定有外部鏈接;如果只是具有塊作用域,那么變量的定義可以有內(nèi)部鏈接,也可以外部鏈接。
9. 儲存類別的選用
到最后了,我們來考慮下儲存類比的選用,一般情況下我們只建議使用自動儲存類別,使用外部變量在程序間通信是方便的但同時也是危險的,所以我們希望盡力避免使用,同時我們要明白保護(hù)性程序設(shè)計的“按需知道”法則,盡量在函數(shù)內(nèi)部解決該函數(shù)的任務(wù),只共享哪些必須共享的變量。
動態(tài)內(nèi)存管理
C語言除了自身建立了自動儲存類型和靜態(tài)儲存類型來進(jìn)行自主的內(nèi)存管理來方便我們編程,同時也給我提供了一些工具是的我們能夠進(jìn)行靈活的內(nèi)存使用和管理,我們通過了解使用C語言的內(nèi)存分配來具體了解。
1. 內(nèi)存分配之malloc
malloc函數(shù)聲明在頭文件stdlib.h中,其函數(shù)原型為
void* malloc(size_t size);
我們通過參數(shù)size(單位字節(jié))來獲得指定大小的一段空間同時返回指向該空間的空指針,我們可以通過強(qiáng)制類型轉(zhuǎn)換來獲得我們需要類型的指針,如果申請內(nèi)存失敗,它就會返回空指針NULL。我們通過具體的用例來了解它的使用:
#include <stdio.h> #include <stdlib.h> int main (void) { int *a = (int *)malloc(sizeof(int));// 創(chuàng)建一個int int *b = (int *)malloc(sizeof(int)*5);// 創(chuàng)建長度為5的數(shù)組 *a = 4; for (int i = 0; i < 5; ++i) b[i] = i*i; printf("*a = %d\n", *a); for (int i = 0; i < 5; ++i) printf("b[%d] = %d\n", i, b[i]); free(a); free(b); return 0; } /** *a = 4 b[0] = 0 b[1] = 1 b[2] = 4 b[3] = 9 b[4] = 16 * **/
我們發(fā)現(xiàn)可以通過內(nèi)存申請可以靈活的創(chuàng)建變量和數(shù)組,然后對他們進(jìn)行訪問和修改,但是千萬不要忘記調(diào)用free函數(shù)接受被分配空間的指針,來釋放對應(yīng)空間。不然大量的空間將無法被再利用造成內(nèi)存的浪費,同時一些操作系統(tǒng)在程序運(yùn)行結(jié)束后可能不會自動釋放這些被分配的內(nèi)存,甚至可能耗盡內(nèi)存,產(chǎn)生可怕的 內(nèi)存泄漏。
同時通過動態(tài)分配內(nèi)存也有更加靈活的用途,例如創(chuàng)建變長數(shù)組:
#include <stdio.h> #include <stdlib.h> int main (void) { int len; scanf("input the len: %d", &len); int *arr = (int *)malloc(sizeof(int) * len); return 0; }
通過這樣一段程序我們就實現(xiàn)了,創(chuàng)建用戶輸入數(shù)字大小的整形數(shù)組。
2. 內(nèi)存分配值calloc
calloc函數(shù)與malloc函數(shù)功能大體相同,它的函數(shù)原型:
void *calloc(size_t num, size_t size);
接受兩個參數(shù),第一個為希望分配的該大小的內(nèi)存塊數(shù),第二個為希望一個空間大?。▎挝蛔止?jié))。同樣的我們要求在每次使用過后通過free函數(shù)將對應(yīng)指針指向的分配的空間進(jìn)行釋放。
儲存類別和動態(tài)內(nèi)存分配
儲存類別和內(nèi)存分配有著密切可分的關(guān)系,我們來講述一個理想中的模型:
程序?qū)⑺膬?nèi)存分為三個部分,一部分給靜態(tài)變量使用;一部分給自動變量使用,最后一部分提供給動態(tài)內(nèi)存分配。為什么這樣分配呢?
靜態(tài)變量使用的內(nèi)存在程序編譯時確定,在程序運(yùn)行的整個周期都可以別訪問,最后在程序結(jié)束時被銷毀,所以我們可以單獨使用一塊區(qū)域?qū)τ谄溥M(jìn)行管理。
自動變量在進(jìn)入對應(yīng)變量定義的塊時被分配空間,在離開塊后被銷毀。伴隨著函數(shù)的調(diào)用和結(jié)束,自動變量使用的內(nèi)存數(shù)量也對應(yīng)的增加和減少,這部分內(nèi)存通常使用棧來管理,新建的變量將按順序入棧,銷毀時按照相反的方向進(jìn)行銷毀。
使用動態(tài)分配內(nèi)存的內(nèi)容,他們創(chuàng)建于malloc或者calloc函數(shù)被調(diào)用的時候,在調(diào)用free函數(shù)被銷毀,這些內(nèi)存完全依賴于程序員自身的管理,我們可以在一個函數(shù)中創(chuàng)建它然后在另一個函數(shù)中銷毀它。這樣就使得這樣的一部分內(nèi)存被分配的支離破碎,有可能未分配的內(nèi)存處于分配的內(nèi)存之間,使用這樣的內(nèi)存往往是比使用棧來的更慢的。
我們通過一個程序來更好了解變量處于的空間:
#include <stdio.h> #include <stdlib.h> static int a = 1; static int b = 2; int main (void) { static int c = 3; int d = 4; int e = 5; int f = 6; int *p1 = (int *)malloc(sizeof(int)); int *p2 = (int *)malloc(sizeof(int)); int *p3 = (int *)malloc(sizeof(int)); printf("static: %p\n", &a); printf("static: %p\n", &b); printf("static: %p\n", &c); printf("auto: %p\n", &d); printf("auto: %p\n", &e); printf("auto: %p\n", &f); printf("dynasty: %p\n", p1); printf("dynasty: %p\n", p2); printf("dynasty: %p\n", p3); return 0; } /** static: 00405004 static: 00405008 static: 0040500C auto: 0061FF10 auto: 0061FF0C auto: 0061FF08 dynasty: 00791930 dynasty: 007918B8 dynasty: 007918C8 * **/
可以發(fā)現(xiàn)不同類型的變量儲存在不同地址附近。
總結(jié)
對于C中的變量我們可以通過類型和儲存類別來描述,除此之外C新的標(biāo)準(zhǔn)也更新了一些特殊的修飾符const之類來修飾,靈活的使用他們能讓程序運(yùn)行的更有效率,實現(xiàn)更多的功能。
到此這篇關(guān)于C語言中儲存類別與內(nèi)存管理的文章就介紹到這了,更多相關(guān)C語言儲存類別與內(nèi)存管理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言進(jìn)階輸入輸出重定向與fopen函數(shù)使用示例詳解
這篇文章主要為大家介紹了C語言進(jìn)階輸入輸出重定向與fopen函數(shù)的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02C/C++ Qt 基本文件讀寫的基本使用(2種實現(xiàn))
文件的讀寫是很多應(yīng)用程序具有的功能,本文主要介紹了兩種實現(xiàn)方法,第一種使用QFile類的IODevice讀寫功能直接讀寫,第二種是利用 QFile和QTextStream結(jié)合起來,用流的方式進(jìn)行文件讀寫2021-11-11c++動態(tài)內(nèi)存管理與智能指針的相關(guān)知識點
為了更容易同時也更安全地使用動態(tài)內(nèi)存,新的標(biāo)準(zhǔn)庫提供了兩種智能指針(smart pointer)類型來管理對象,下面這篇文章主要給大家介紹了關(guān)于c++動態(tài)內(nèi)存管理與智能指針的相關(guān)知識點,需要的朋友可以參考下2022-03-03C語言實現(xiàn)職工工資管理系統(tǒng)的示例代碼
這篇文章主要為大家詳細(xì)介紹了C語言如何實現(xiàn)職工工資管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08