C語言中儲存類別與內存管理的深入理解

儲存類別

C語言提供瞭多種儲存類別供我們使用,並且對應的有對應的內存管理策略,在瞭解C中的儲存類型前,我們先瞭解一下與儲存類型相關的一些概念。

1. 基礎概念

對象:不同於面向對象編程中的對象的含義,C語言是面向過程編程,不存在這樣對象的概念,這個對象指的是值儲存所占據物理內存空間。

左值:左值是可以指定對象的表達式,它的最簡單形式即為標識符,復雜的可以為為指針之類。一個表達式成為左值的前提是它確實指定瞭一塊作為對象的儲存空間,例如:

 int a = 1;//a作為標識符,也作基礎表達式,指定瞭一個對象,是左值
 int *pa = &a;//pa同a也指示瞭一個儲存地址對象,是一個左值;*pa是一個表示式,指示瞭a相同的對象也是一個左值
 int arr[5] = {0};
 arr+a*3;// 這段表達式就不是一個標識符,也不是一個左值瞭,因為它沒有指定內存上的任意位置
 *(arr + a * 3);// 不同於上面的,這也是一個左值,因為它確實指定瞭內存上的位置

左值分為可修改左值和不可修改左值。

我們通常用儲存期來描述對象,表明對象在內存中留存的時間。用標識符指定對象時,使用作用域和鏈接來描述標識符,其中作用域表明標識符可以可以被程序使用的范圍,鏈接表明程序的哪些其他文件也可以使用它。

不同的儲存類別之間的區別即在於它們的儲存期、作用域和連接形式的不相同。我們來分別瞭解他們一下。

儲存期:儲存期分為靜態儲存期,自動儲存期,線程儲存期和動態分配儲存期(線程儲存期暫時不多贅述),它們分別對應不同的在內存中的儲存位置,也有不同的特點。

靜態儲存期:對應靜態存儲位置,它在程序開始運行時就被分配,這段空間不可增加和減少,所以從程序開始運行到停止運行,靜態儲存期的數據一直存在。通常在函數外的變量和static表示的變量具有靜態儲存期。

自動儲存期:對應棧空間,它隨著程序的運行可以自動進行分配,增加或減少。程序進入到一個塊為其中的變量分配棧空間,退出一個塊後則會釋放相應的空間。一般的創建的變量都具有自動儲存期。

動態分配儲存期:對應堆空間,它需要通過特殊的語法進行申請,申請後也需要主動進行銷毀,存在時間為從申請內存開始到主動釋放內存為止。需要通過專門的語句來獲得具有動態分配儲存期的變量。

作用域:一個變量能被使用的范圍稱為作用域,作用域分為 塊作用域、函數作用域、函數原型作用域和 文件作用域。

塊作用域:由一個花括號開始到與之對應的花括號為止,其中的變量都具有塊作用域,一般情況下任何在塊內的定義的變量可以在塊內任何位置使用,但是不可以在塊外進行使用。(特例後面會舉出),而且對於內部塊也可以定義與外部塊同名的變量,這時候內部塊將隱去向內隱去外部塊的同名變量,在內部使用自己定義的該變量。

函數作用域:針對的是goto語句標簽,一個標簽首次出現在含糊內層,它的作用域將會延伸至整個函數,這表示我們不能使用同名的標簽。

函數原型作用域:對於函數的聲明,該作用域開始與形參定義處知道函數函數原型結束。編譯器隻註重形式參數的類型而不會註意具體的變量名,甚至可以不使用變量名。

文件作用域:聲明在函數外的變量具有文件作用域,他們可以在同一源文件下的任何塊和函數中使用,具有文件作用域的變量也被稱為全局變量。

對於分別屬於同類型作用域但是不同一個作用域的變量它們可以任意重名,例如不同塊的函數中變量屬於不同塊他們可以重名。對於具有文件作用域的變量它們對於所屬的文件塊都有作用,所以不建議塊中變量與全局變量重名,但是在重名後塊使用對應名稱變量時將以塊中自身定義的變量為準。

 #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
 * **/

我們發現在在外部a為在外部定義的值,a輸出為1;第一塊內部,a讀取的是內部的a的值,這一點沒有任何問題;然後我們到外部,我們再讓程序輸出a值,仍然為2,沒有問題;但是進入循環後,我們發現很奇怪的現象,通過輸出我們發現循環執行瞭4次,很明顯這是基於外部的a,但是內部的a在輸出時卻總是顯示內部的a,這一點是因為:內部循環定義的a作用域隻在塊內,並不會作用於循環條件判斷的部分,所以在進行循環條件判斷時始終使用外部的a。註意遞增條件一定要在循環判斷條件中,否則循環將變成死循環。但是,沒有必要使用同名變量。

鏈接:鏈接是程序中變量可以被其他文件使用的描述,有三種類型的鏈接:外部鏈接、內部鏈接和 無鏈接。

如果一個變量具有文件作用域它才可能具有外部鏈接和內部鏈接,其他變量都是無鏈接的。在具體瞭解內部鏈接和外部鏈接之前,我們先理解下 翻譯單元的概念。

翻譯單元:我們經常使用#include指令來包含頭文件,C通過預處理將對應頭文件內容直接替換掉該條命令,他們雖然表面上看起來不是一個文件但是被編譯器看做瞭一個文件,這個文件就被稱為一個翻譯單元,一個具有文件作用域的變量它的實際可見范圍就是整個翻譯單元。一個翻譯單元由一個源文件和多個它所包含的文件組成。

所以外部鏈接可以在多文件程序中使用,而內部鏈接隻可以在一個翻譯單元使用。區別二者在於是否使用瞭儲存類別說明符static,使用瞭static則為內部鏈接,反之則為外部鏈接。

 //main.c 文件
 #include <stdio.h>
 ​
 int main (void) {
 extern int a;// 聲明,讓編譯器在別處查找a的定義
 // extern int b;
 // printf("b = %d", b);這一段不可使用,因為b隻具有內部鏈接,不可在其他源文件訪問,運行
 // 報錯
 printf("a = %d", a);
 return 0;
 }
 ​
 /**
 a = 5
 * **/
 // 和它一同編譯的another.c文件
 int a = 5;// 具有外部鏈接,可以在多個源文件之間進行共享
 static int b = 2;// 具有內部鏈接,隻能在一個源文件內共享

在這裡我們使用瞭外部鏈接變量a,在兩個翻譯單元之間實現瞭變量的傳遞。其中main.c文件為瞭調用變量a必須有extern聲明語句,這段語句聲明瞭一個int型變量a但是並不會為它分配內存,使用它隻是為瞭告訴編譯器在別處尋找變量a的定義,這是必不可少的,否則程序將會報錯。

 // 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指令包含頭文件,編譯器將自動將對應文件內容替代到對應位置,它們屬於同一個翻譯單元,所以及時具有內部鏈接的變量仍可以使用。

2. 儲存類別分類

介紹瞭一些基礎概念後我們來根據這些基礎概念對於儲存類別進行分類:

儲存類別 儲存期 作用域 鏈接 聲明方式
自動 自動 塊內聲明
寄存器 自動 塊內聲明,加入關鍵字register
靜態外部鏈接 靜態 文件 外部 函數外
靜態內部鏈接 靜態 文件 內部 函數外,加入關鍵字static
靜態無鏈接 靜態 塊內聲明,計入關鍵字static

下面我呢來分別具體對於每種類型所對應的變量進行說明。

3. 自動變量

自動變量具有自動儲存期,塊作用域,無鏈接,在塊內進行聲明即可。自動儲存期意味著它在開始執行塊時被創建,在對應塊到結尾時被銷毀,不能再被通過任何途徑訪問;塊作用域表明隻能在塊中使用變量名對於變量進行訪問,但是在處於變量可使用的儲存期內(這點必要,因為我們無法控制編譯器的回收機制),我們也可以通過指針傳遞地址的方式來繼續使用;無鏈接表明不能再其他文件中對於該變量進行訪問。

對於自動變量,默認情況下聲明的變量都具有這樣的儲存類別,但是有時候為瞭更明顯的表現意圖,並且在外部具有同名變量時,為瞭更好覆蓋它,表明自己變量的自動儲存類型,可以使用關鍵字auto,例如:

 #include <stdio.h>
 ​
 int a = 1;
 ​
 int main (void) {
 auto int a;// int a;也是等價
 printf("a = %d", a);
 return 0;
 }

在外部有同名變量時,使用auto關鍵字還是用來標識a作為塊內的自動變量,覆蓋外部的a,即使不加auto也是可以的,但是使用後可以起到更好的標識性。

4. 寄存器變量

寄存器變量在多個方面與自動變量相同,不同在於自動變量通常儲存在計算機內存中,而寄存器變量儲存在計算機CPU的寄存器中,因此它具有高效的運算率,而且因為它在寄存器中所以無法獲得其地址。通過在變量定義中使用register修飾既可以聲明寄存器變量:register int a;

但是,值得註意的是,使用register企圖創建寄存器變量是一種請求而不是命令,編譯器很可能不會通過你的請求,而且寄存器也有可能沒有足夠大空間儲存double類型變量,所以可以聲明register的數據類型也是有限的。即使失敗,也能夠創建相應的自動變量,但是我們仍然不能獲得其地址。

5. 具有塊作用域的靜態變量

具有塊作用域的靜態變量,對應的儲存類別為靜態無鏈接,其具有靜態儲存期,塊作用域和無鏈接。它在程序開始運行時被創建,程序結束後被銷毀。隻在它被定義的塊內調用,無法被其他文件訪問。

相較於自動變量,它隻是擁有瞭靜態儲存期,所以我們使用static類別修飾符獲得該類型變量static int a;。值得註意的是,由於該變量具有靜態儲存期,所以它始終儲存在系統中的某一段內存空間中,我們可以利用指針在塊外的區域對於該變量進行訪問。

 #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
 * **/

通過上面的運行結果我們發現通過函數返回指向靜態變量的指針,我們可以對靜態變量進行訪問和修改,這使得我們在塊外對塊內無鏈接的靜態變量進行訪問。

6. 內部鏈接的靜態變量

內部鏈接的靜態變量對應儲存類別為靜態內部鏈接。它具有靜態作用期,文件作用域和內部鏈接。它在程序開始被創建,在同一個翻譯單元內可以任意訪問。在前面已經有它的用例。

7. 外部鏈接的靜態變量

外部鏈接的靜態變量對應儲存類別為靜態外部鏈接。它具有靜態作用器,文件作用域和外部鏈接。大體上與內部鏈接的靜態變量相同,但是它可以在多個翻譯單元(多個源文件之間)進行共享。

但是仍有一些事項註意:

聲明時可以顯示初始化,但是必須使用常量進行初始化(對於sizeof表達式也是常量),如果未進行初始化,無論如何它將被初始化為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關鍵字來實現,在變量聲明前加上關鍵字,編譯器就會明白根據指示在其他源文件中查找變量的聲明,這樣的聲明不會申請新的內存,也不可以進行賦值。

 // 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. 儲存類別說明符小結

C中儲存類別說明符有多個,並且不同說明符在不同位置也有不同意義。一共有一下說明符:auto、extern、register、static、_Thread_local和typedef。最後者被歸為此類屬於一些語法上的原因。它們大多都是單獨使用,但是_Thread_local可以和static和extern一起使用。

auto表明變量具有自動儲存期,在塊內的變量默認具有自動儲存期,使用auto隻是明確表示要使用與外部變量重名的局部變量的意圖。

register表明希望將變量儲存在寄存器中,希望以最快的速度讀取變量,同時不希望獲得該變量的地址。

static表明變量具有靜態儲存期,它並不改變塊內變量的鏈接類型,但是對於快外的變量,將會限制它的鏈接類型為內部鏈接。

extern表明該變量定義在別處,希望編譯器在別處查找,包含extern的聲明具有文件作用域,那麼變量的定義一定有外部鏈接;如果隻是具有塊作用域,那麼變量的定義可以有內部鏈接,也可以外部鏈接。

9. 儲存類別的選用

到最後瞭,我們來考慮下儲存類比的選用,一般情況下我們隻建議使用自動儲存類別,使用外部變量在程序間通信是方便的但同時也是危險的,所以我們希望盡力避免使用,同時我們要明白保護性程序設計的“按需知道”法則,盡量在函數內部解決該函數的任務,隻共享哪些必須共享的變量。

動態內存管理

C語言除瞭自身建立瞭自動儲存類型和靜態儲存類型來進行自主的內存管理來方便我們編程,同時也給我提供瞭一些工具是的我們能夠進行靈活的內存使用和管理,我們通過瞭解使用C語言的內存分配來具體瞭解。

1. 內存分配之malloc

malloc函數聲明在頭文件stdlib.h中,其函數原型為

void* malloc(size_t size);

我們通過參數size(單位字節)來獲得指定大小的一段空間同時返回指向該空間的空指針,我們可以通過強制類型轉換來獲得我們需要類型的指針,如果申請內存失敗,它就會返回空指針NULL。我們通過具體的用例來瞭解它的使用:

 #include <stdio.h>
 #include <stdlib.h>
 ​
 int main (void) {
 int *a = (int *)malloc(sizeof(int));// 創建一個int
 int *b = (int *)malloc(sizeof(int)*5);// 創建長度為5的數組
 *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
 * **/

我們發現可以通過內存申請可以靈活的創建變量和數組,然後對他們進行訪問和修改,但是千萬不要忘記調用free函數接受被分配空間的指針,來釋放對應空間。不然大量的空間將無法被再利用造成內存的浪費,同時一些操作系統在程序運行結束後可能不會自動釋放這些被分配的內存,甚至可能耗盡內存,產生可怕的 內存泄漏。

同時通過動態分配內存也有更加靈活的用途,例如創建變長數組:

 #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;
 }

通過這樣一段程序我們就實現瞭,創建用戶輸入數字大小的整形數組。

2. 內存分配值calloc

calloc函數與malloc函數功能大體相同,它的函數原型:

void *calloc(size_t num, size_t size);

接受兩個參數,第一個為希望分配的該大小的內存塊數,第二個為希望一個空間大小(單位字節)。同樣的我們要求在每次使用過後通過free函數將對應指針指向的分配的空間進行釋放。

儲存類別和動態內存分配

儲存類別和內存分配有著密切可分的關系,我們來講述一個理想中的模型:

程序將它的內存分為三個部分,一部分給靜態變量使用;一部分給自動變量使用,最後一部分提供給動態內存分配。為什麼這樣分配呢?

靜態變量使用的內存在程序編譯時確定,在程序運行的整個周期都可以別訪問,最後在程序結束時被銷毀,所以我們可以單獨使用一塊區域對於其進行管理。

自動變量在進入對應變量定義的塊時被分配空間,在離開塊後被銷毀。伴隨著函數的調用和結束,自動變量使用的內存數量也對應的增加和減少,這部分內存通常使用棧來管理,新建的變量將按順序入棧,銷毀時按照相反的方向進行銷毀。

使用動態分配內存的內容,他們創建於malloc或者calloc函數被調用的時候,在調用free函數被銷毀,這些內存完全依賴於程序員自身的管理,我們可以在一個函數中創建它然後在另一個函數中銷毀它。這樣就使得這樣的一部分內存被分配的支離破碎,有可能未分配的內存處於分配的內存之間,使用這樣的內存往往是比使用棧來的更慢的。

我們通過一個程序來更好瞭解變量處於的空間:

 #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
 * **/

可以發現不同類型的變量儲存在不同地址附近。

總結

對於C中的變量我們可以通過類型和儲存類別來描述,除此之外C新的標準也更新瞭一些特殊的修飾符const之類來修飾,靈活的使用他們能讓程序運行的更有效率,實現更多的功能。

到此這篇關於C語言中儲存類別與內存管理的文章就介紹到這瞭,更多相關C語言儲存類別與內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: