天天看點

【C prime Plus】存儲類别、連結和記憶體管理

文章目錄

  • ​​1. 存儲類别​​
  • ​​1.1 作用域​​
  • ​​1.1.1 塊作用域​​
  • ​​1.1.2 函數作用域​​
  • ​​1.1.3 函數原型作用域​​
  • ​​1.1.4 檔案作用域​​
  • ​​1.2 連結​​
  • ​​1.3 存儲期​​
  • ​​1.3.1 靜态存儲期​​
  • ​​1.3.2 線程存儲期​​
  • ​​1.3.3 自動存儲期​​
  • ​​1.4 自動變量​​
  • ​​1.5 寄存器變量​​
  • ​​1.6 塊作用域的靜态變量​​
  • ​​1.7 外部連結的靜态變量​​
  • ​​1.7.1 初始化外部變量​​
  • ​​1.7.2 使用外部變量​​
  • ​​1.7.3 外部名稱​​
  • ​​1.7.4 定義和聲明​​
  • ​​1.8 内部連結的靜态變量​​
  • ​​1.9 多檔案​​
  • ​​1.10 存儲類别說明符​​
  • ​​1.11 存儲類别和函數​​
  • ​​2. 配置設定記憶體:malloc() 和 free()​​
  • ​​2.1 free() 的重要性​​
  • ​​2.2 calloc() 函數​​
  • ​​2.3 動态記憶體配置設定和變長數組​​
  • ​​2.4 存儲類别和動态記憶體配置設定​​
  • ​​3. ANSI 類型限定符​​
  • ​​3.1 const 類型限定符​​
  • ​​3.1.1 在指針和形參聲明中使用 const​​
  • ​​3.1.2 對全局資料使用 const​​
  • ​​3.2 volatitle 類型限定符​​
  • ​​3.3 restrict 類型限定符​​
  • ​​3.4 _Atomic 類型限定符(C11)​​
【C prime Plus】存儲類别、連結和記憶體管理

1. 存儲類别

程式員通過 C 的記憶體管理系統指定變量的作用域和生命期,實作對程式的控制。

從軟體方面看,程式需要一種方法通路實體記憶體—— 聲明變量。

int entity = 3; // 使用辨別符 entity 指定記憶體區域      

變量名不是指定硬體記憶體中對象的唯一途徑。

int * pt = &entity; // pt 是一個辨別符,指定記憶體區域的位址,*pt 不是辨別符,與 entity 指定的對象相同。
int ranks[10];      

1.1 作用域

作用域描述程式中可通路辨別符的區域。一個 C 變量的作用域可以是塊作用域、函數作用域、函數原型作用域或檔案作用域。

1.1.1 塊作用域

塊是用一對花括号括起來的代碼區域,塊作用域變量的可見範圍是從定義處到包含該定義的塊的末尾。

double blocky(double cleo) // 塊作用域
{
  double patrick = 0.0; // 塊作用域
  ...
  return patrick;
}      
double blocky(double cleo) // 塊作用域
{
  double patrick = 0.0; // 塊作用域
  int i;
  for (i=0; i<10; i++)
  {
    double q = cleo * i;// q 的作用域開始,僅限于内層塊
    ...
    patrick *= q; // q 的作用域結束
  }
}      

以前,具有塊作用域的變量都必須聲明在塊的開頭。C99 标準放寬了這一限制,允許在塊中的任意位置聲明變量,即:

for (int i=0; i<10; i++)
  ...      

C99 把塊的概念擴充到 for 循環、while 循環、do while 循環和 if 語句所控制的代碼。

1.1.2 函數作用域

僅用于 goto 語句的标簽。即使一個标簽首次出現在函數的内層塊中,它的作用域也延伸至整個函數。

1.1.3 函數原型作用域

用于函數原型中的形參名:

​int mighty(int mouse, double large);​

函數原型作用域的範圍是從形參定義處到原型聲明結束,編譯器在處理函數原型中的形參時隻關心它的類型,而形參名無關緊要,即使有形參名,也不必與函數定義中的形參名相比對。隻有在變長數組中,形參名才有用。

1.1.4 檔案作用域

變量的定義在函數的外面,具有檔案作用域,具有檔案作用域的變量,從它的定義處到該定義所在檔案的末尾均可見。

#include <stdio.h>
int units=0;
void critic(void);
int main(void)
{
  ...
}
void critic(void)
{
  ...
}      

變量 units 具有檔案作用域,這樣的變量可用于多個函數,檔案作用域變量也稱為全局變量。

1.2 連結

C 語言有 3 中連結屬性:外部連結、内部連結和無連結。

  • 具有塊作用域、函數作用域或函數原型作用域的變量都是無連結變量(除了檔案作用域)。
  • 具有檔案作用域的變量可以是外部連結或内部連結。外部連結變量可以在多檔案程式中使用,内部連結變量隻能在一個翻譯單元(将 .c 檔案中的 .h 頭檔案中的内容替換後的文本)中使用。

内部連結的檔案作用域描述僅限于一個翻譯單元,用外部連結的檔案作用域描述可延伸其它翻譯單元的作用域。

内部連結的檔案作用域==檔案作用域

外部連結的檔案作用域==程式作用域、全局作用域

int giants = 5; //外部連結的檔案作用域
static int dodgers = 3; // 内部連結的檔案作用域
int main()
{
  ...
} 
...      

該檔案和同一程式的其它檔案都可以使用變量 giants,dodgers 屬于檔案私有,該檔案中的任意函數都可以使用它。

1.3 存儲期

作用域和連結描述了辨別符的可見性。存儲期描述了通過這些辨別符通路的對象的生存期。

4 種存儲期:靜态存儲期、線程存儲期、自動存儲期、動态配置設定存儲期。

1.3.1 靜态存儲期

如果對象具有靜态存儲期,那麼它在程式的執行期間一直存在。

檔案作用域具有靜态存儲期。對于檔案作用域變量,關鍵字 static 表明了其連結屬性,而非存儲期。 以 static 聲明的檔案作用域具有内部連結,但是無論外部/内部連結,所有的檔案作用域都具有靜态存儲周期。

1.3.2 線程存儲期

用于并發程式設計,程式執行可被分為多個線程。具有線程存儲期的對象,從被聲明到線程結束一直存在。以關鍵字 _Thread_local 聲明一個對象時,每個線程都獲得該變量的私有備份。

1.3.3 自動存儲期

塊作用域的變量通常具有自動存儲期。當程式進入定義這些變量的塊時,為這些變量配置設定記憶體;當退出這些塊時;釋放剛才為變量配置設定的記憶體。

變長數組稍有不同,它們的存儲期從聲明處到塊的末尾,而不是從塊的開始處到塊的末尾。

// number 和 index 每次調用 bore() 函數時建立,在離開函數時被銷毀;
void bore(int number)
{
  int index;
  for (idnex=0; index<number; index++)
    ...
}      

塊作用域變量也能具有靜态存儲期,加上 static 關鍵字即可。

void more(int number)
{
  int index;
  static int ct=0; // 存儲在靜态記憶體中,從程式被載入直到程式結束期間都存在,隻有在執行該函數時,程式才能使用 ct 通路它所指定的對象(也可以提供指針形參或傳回值來控制)
  ...
  return 0;
}      

作用域和連結描述了辨別符的可見性。存儲期描述了通過這些辨別符通路的對象的生存期。

5 種存儲類别

存儲類别 存儲期 作用域 連結 聲明方式
自動 自動 塊内
寄存器 自動 塊内,使用關鍵字 register
靜态外部連結 靜态 檔案 外部 所有函數外
靜态内部連結 靜态 檔案 内部 所有函數外,使用關鍵字 static
靜态無連結 靜态 塊内,使用關鍵字 static

1.4 自動變量

具有自動存儲期、塊作用域且無連結

預設情況下,聲明在塊或函數頭中的任何變量都屬于自動存儲類别。

在塊中,​

​auto int plox;​

​​ 和 ​

​int plox;​

​ 一樣。

​auto​

​ 是存儲類别說明符,如果編寫 C/C++ 相容的程式,最好不要使用 auto 作為存儲類别說明符。

塊作用域和無連結意味着隻有在變量定義所在的塊中才能通過變量名通路該變量。另一個函數可以使用相同名變量,但是該變量是儲存在不同記憶體位置上的另一個變量。

變量具有自動存儲期意味着,程式在進入該變量聲明所在的塊時變量存在,程式在退出該塊時變量消失,原來該變量占用的記憶體位置現在可做他用。

如果内層塊中聲明的變量與外層塊中的變量同名會怎樣?内層塊會隐藏外層塊的定義。

jiaming@jiaming-VirtualBox:~/Documents$ ./hiding.o 
x in outer block: 30 at 0x7ffc3b5fd390
x in inner block: 77 at 0x7ffc3b5fd394
x in outer block: 30 at 0x7ffc3b5fd390
x in while loop: 101 at 0x7ffc3b5fd394
x in while loop: 101 at 0x7ffc3b5fd394
x in while loop: 101 at 0x7ffc3b5fd394
x in outer block: 34 at 0x7ffc3b5fd390
jiaming@jiaming-VirtualBox:~/Documents$ cat hiding.c
#include <stdio.h>
int main()
{
  int x = 30;

  printf("x in outer block: %d at %p\n", x, &x);
  {
    int x = 77;
    printf("x in inner block: %d at %p\n", x, &x);
  }
  printf("x in outer block: %d at %p\n", x, &x);
  while (x++ < 33)
  {
    int x = 100;
    x++;
    printf("x in while loop: %d at %p\n", x, &x);
  }
  printf("x in outer block: %d at %p\n", x, &x);
  
  return 0;
}      

1.5 寄存器變量

由于寄存器變量儲存在寄存器而非記憶體中,是以無法擷取寄存器變量的位址,寄存器變量是塊作用域、無連結和自動存儲期。使用存儲類别說明符 ​

​register​

​ 可聲明寄存器變量。

register int quick;      

聲明 變量為 register 類别與直接指令相比更像是一種請求。編譯器必須根據寄存器或最快可用記憶體的數量衡量你的請求,或直接忽略你的請求,是以可能不會如你所願。

1.6 塊作用域的靜态變量

具有塊作用域、無連結,靜态存儲期。

靜态變量并不是一個不可變的變量,靜态的意思是該變量在記憶體中原地不動,并不是說它的值不變。具有檔案作用域的變量自動具有靜态周期。這些變量和自動變量一樣,具有相同的作用域,但是程式離開它們所在的函數後,這些變量不會消失。

jiaming@jiaming-VirtualBox:~/Documents$ ./loc_stat.o 
Here comes iteration 1:
fade = 1 and stay = 1
Here comes iteration 2:
fade = 1 and stay = 2
Here comes iteration 3:
fade = 1 and stay = 3
jiaming@jiaming-VirtualBox:~/Documents$ cat loc_stat.c
#include <stdio.h>
void trystat(void);

int main(void)
{
  int count;

  for (count=1; count<=3; count++)
  {
    printf("Here comes iteration %d:\n", count);
    trystat();
  }

  return 0;
}

void trystat(void)
{
  int fade = 1; // 每次調用都會初始化
  static int stay = 1; // 隻在編譯 strstat() 時被初始化一次,靜态變量和外部變量在程式被載入記憶體時已執行完畢。
  
  printf("fade = %d and stay = %d\n", fade++, stay++);
}      

不能在函數的形參中使用 static。

​​

​int wontwork(static int flu);// 不允許​

1.7 外部連結的靜态變量

具有檔案作用域、外部連結和靜态存儲期

該類别有時稱為外部存儲類别,屬于該類别的變量稱為外部變量。可以在函數中使用關鍵字 ​

​extern​

​ 聲明,也可以把變量的定義性聲明放在所有函數的外面便建立了外部變量。

int Errupt; // 外部定義的變量
double Up[100]; // 外部定義的數組
extern char Coal; // 如果 Coal 被定義在另一個檔案中,必須這樣聲明

void next(void);
int main()
{
  extern int Errupt; // 可選的聲明, int Errupt 将會隐藏外部定義的變量 Errupt,如果不得已要使用與外部同名的局部變量,可以在局部變量的聲明中使用 auto 存儲類别說明符明确表達這種意圖
  ...
  extern double Up[]; // 可選的聲明,僅為了說明 main 函數要使用改變量
}
void next(void)
{
  ...
}      
int Hocus;
int maigc();
int main(void)
{
  extern int Hocus; // 之前已聲明的外部變量
  ...
}
int magic()
{
  extern int Hocus; // 與上面的 Hocus 是同一個變量
  ...
}      
int Hocus; // 從聲明處到檔案結尾
int magic();
int main(void)
{
  int Hocus; // 預設是自動變量 auto int Hocus 也可
  ...
}
int Pocus;
int magic()
{
  auto int Hocus; // 把局部變量 Hocus 顯式聲明為自動變量
}      

extern 關鍵字詳解

一種變量聲明形式,不能進行變量初始化(不能定義)。如果一個檔案想使用另一個檔案中的某個變量,還是使用 extern 關鍵字更好。

int main()
{
  extern int num; // 不能在此處指派,原位置指派即可,之後可以改變num值,告訴編譯器num這個變量是存在的,但是不是在這之前聲明的,你到别的地方找找吧
  printf("%d\n", num);
}
int num = 4;      
// a.c
int main()
{
  extern int num; // 不能在此處指派,原位置指派即可,之後可以改變num值,告訴編譯器num這個變量是存在的,但是不是在這之前聲明的,你到别的地方找找吧
  printf("%d\n", num);
}
// b.c
int num = 4; // 全局變量      
// a.c
int main()
{
  extern void func(); // 還可以引用另一個檔案中的函數
  func();
}

// b.c
void func()
{
  ;
}      

1.7.1 初始化外部變量

外部變量與自動變量不同的是,如果未初始化外部變量,它們會被自動初始化為 0。這一原則也适用于外部定義的數組元素。隻能使用常量表達式初始化檔案作用域變量。

1.7.2 使用外部變量

jiaming@jiaming-VirtualBox:~/Documents$ ./global.o 
How many pounds to a firkin of butter?
14
No luck, my friend. Try again.
56
You must have looked it up!
jiaming@jiaming-VirtualBox:~/Documents$ cat global.c
#include <stdio.h>
int units = 0; // units 具有檔案作用域、外部連結和靜态存儲期。
void critic(void);

int main()
{
  extern int units;

  printf("How many pounds to a firkin of butter?\n");
  scanf("%d", &units);
  while(units != 56)
    critic();
  printf("You must have looked it up!\n");

  return 0;
}

void critic(void)
{
  printf("No luck, my friend. Try again.\n");
  scanf("%d", &units);
}      

main() 函數和 critic() 都可以通過辨別符 units 通路相同的變量。units 具有檔案作用域、外部連結和靜态存儲期。

1.7.3 外部名稱

C99 和 C11 标準都要求編譯器識别局部辨別符的前 63 個字元和外部辨別符的前 31 個字元。

1.7.4 定義和聲明

int tern = 1; // tern 被定義,為變量預留了存儲空間,該聲明構成了變量的定義,定義式聲明
main()
{
  extern int tern; // 使用在别處定義的 tern,隻是告訴編譯器使用之前已建立的 tern 變量,而不是定義 引用式聲明,關鍵字 extern 表明該聲明不是定義,因為它隻是編譯器去别處查詢其定義      
extern int tern;
int main(void)
{
// 編譯器會假設 tern 實際的定義在該程式的别處,也許在别的檔案中。該聲明并不會引起配置設定存儲空間。不要用 extern 來建立外部定義,隻用它來引用現有的外部定義      
// 外部變量隻能初始化一次,且必須在定義該變量時進行。
// file_one.c
char permis = 'N';

// file_two.c
extern char permis = 'Y'; // 錯誤
// file_two 中的聲明是錯誤的,因為 file_one.c 中的定義式聲明已經建立并初始化了 permis      

1.8 内部連結的靜态變量

靜态存儲期、檔案作用域、内部連結
static int svil = 1; // 靜态變量,内部連結
int main(){      

普通的外部變量可用于同一程式中任意檔案中的函數(​

​外部定義 int a;​

​​),但是内部連結變量隻能用于同一個檔案中的函數​

​外部定義 static int a;​

​。可以使用存儲類别說明符 extern,在函數中重複聲明任何具有檔案作用域的變量。

int traveler = 1;// 外部連結
static int stayhome = 1; // 内部連結
int main()
{
  extern int traveler; // 使用定義在别處的 traveler
  ...
}      

對于該程式的翻譯單元,traveler 和 stayhome 都具有檔案作用域,但隻有 traveler 可用于其它翻譯單元(因為它具有外部連結)。

1.9 多檔案

隻有當程式由多個翻譯單元組成時,才展現差別内部連結和外部連結的重要性。

複雜的 C 程式通常由多個單獨的源代碼檔案組成。有時,這些檔案可能要共享一個外部變量。C 通過在一個檔案中進行定義式聲明,然後在其他檔案中進行引用式聲明來實作共享。也就是說,除了一個定義式聲明外,其他聲明都要使用 extern 關鍵字。而且,隻有定義式聲明才能初始化變量。

如果外部變量定義在一個檔案中,那麼其他檔案在使用該變量之前必須先聲明它(用 extern 關鍵字)。在某檔案中對外部變量進行定義式聲明隻是單方面允許其他檔案使用該變量,其他檔案在用 extern 聲明之前不能直接使用它。

1.10 存儲類别說明符

關鍵字 ​

​static​

​​ 和 ​

​extern​

​​ 的含義取決于上下文。C 語言有 6 個關鍵字作為存儲類别說明符:​

​auto​

​​、​

​register​

​​、​

​static​

​​、​

​extern​

​​、​

​_Thread_local​

​​、​

​typedef​

​。

  • typedef:作為關鍵字與任何記憶體存儲無關,把它歸于此類有一些文法上的原因,在絕大數情況下,不能在聲明中使用多個存儲類别說明符,意味着不能使用多個存儲類别說明符作為 typedef 的一部分。
  • auto:表明變量是自動存儲期,隻能用于塊作用域的變量聲明中。由于在塊中聲明的變量本身就具有自動存儲期,是以使用 auto 主要是為了明确表達要使用與外部變量同名的局部變量的意圖。
  • register:隻用于塊作用域的變量,它把變量歸為寄存器存儲類别,請求最快速度通路改變量。同時,還保護了該變量的位址不被擷取。
  • static:建立的對象具有靜态存儲期,載入程式時建立對象,當程式結束時對象消失。如果 static 用于檔案作用域聲明,作用域受限于該檔案。如果 static 用于塊作用域聲明,作用域則受限于該塊。是以,隻要程式在運作對象就存在并保留其值,但是隻有在執行塊内的代碼時,才能通過辨別符通路。塊作用域的靜态變量無連結。檔案作用域的靜态變量具有内部連結。
  • extern:表明聲明的變量定義在别處。如果包含 extern 的聲明具有檔案作用域,則引用的變量必須具有外部連結。如果包含 extern 的聲明具有塊作用域,則引用的變量可能具有外部連結或内部連結。

自動變量具有塊作用域、無連結、自動存儲期。它們是局部變量,屬于其定義所在塊私有。寄存器變量的屬性和自動變量相同,但是編譯器會使用更快的記憶體或寄存器儲存它們。不能擷取寄存器變量的位址。

具有靜态存儲期的變量可以具有外部連結、内部連結或無連結。在同一個檔案所有函數的外部聲明的變量是外部變量,具有檔案作用域、外部連結和靜态存儲期。如果在這種聲明前面加上關鍵字 static,那麼其聲明的變量具有檔案作用域、内部連結和靜态存儲期。如果在函數中用 static 聲明一個變量,則該變量具有塊作用域、無連結、靜态存儲期。

具有自動存儲期的變量,程式在進入該變量的聲明所在塊時才為其配置設定記憶體,在退出該塊時釋放之前配置設定的記憶體。如果未初始化,自動變量中是垃圾值。程式在編譯時為具有靜态存儲器的變量配置設定記憶體,并在程式的運作過程中一直保留這塊記憶體,如果未初始化,這樣的變量會被設定為 0。

具有塊作用域的變量是局部的,屬于包含該聲明的塊私有。具有檔案作用域的變量對檔案中位于其聲明後面的所有函數可見。具有外部連結的檔案作用域變量,可用于該程式的其他翻譯單元,具有内部連結的檔案作用域變量,隻能用于其聲明所在的檔案内。

jiaming@jiaming-VirtualBox:~/Documents$ cc parta.c partb.c -o out.o
jiaming@jiaming-VirtualBox:~/Documents$ ./out.o 
Enter a positive integer (0 to quit): 5
loop cycle: 1
subtotal: 15; total: 15
Enter a positive integer (0 to quitt): 10
loop cycle: 2
subtotal: 55; total: 70
Enter a positive integer (0 to quitt): 2
loop cycle: 3
subtotal: 3; total: 73
Enter a positive integer (0 to quitt): 0
Loop executed 3 times

jiaming@jiaming-VirtualBox:~/Documents$ cat parta.c 
#include <stdio.h>
void report_count();
void accumulate(int k);
int count = 0; // 檔案作用域,外部連結
int main(void)
{
  int value;  // 自動變量
  register int i;// 寄存器變量
  printf("Enter a positive integer (0 to quit): ");
  while (scanf("%d", &value) == 1 && value > 0)
  {
    ++count; // 使用檔案作用域變量
    for (i = value; i >= 0; i--)
      accumulate(i);
    printf("Enter a positive integer (0 to quitt): ");
  }
  report_count();
  return 0;
}
void report_count()
{
  printf("Loop executed %d times\n", count);
}


jiaming@jiaming-VirtualBox:~/Documents$ cat partb.c 
#include <stdio.h>
extern int count;//引用式聲明,外部連結
static int total = 0;//靜态定義,内部連結
void accumulate(int k);//函數原型
void accumulate(int k)//k具有塊作用域,無連結
{
  static int subtotal = 0;//靜态,無連結
  if (k <= 0)
  {
    printf("loop cycle: %d\n", count);
    printf("subtotal: %d; total: %d\n", subtotal, total);
    subtotal = 0;
  }
  else
  {
    subtotal += k;
    total += k;
  }
}      

1.11 存儲類别和函數

函數也有存儲類别,可以是外部函數或靜态函數。C99 新增了第 3 種類别 —— 内聯函數。外部函數可以被其他檔案的函數通路,但是靜态函數隻能用于其定義所在的檔案。假設一個檔案中包含了以下函數原型:

double gamma(double);
static double beta(int, int);
extern double delta(double, int);      

在同一個程式中,其他檔案中的函數可以調用 gamma() 和 delta(),但是不能調用 beta(),因為以 static 存儲類别說明符建立的函數屬于特定子產品私有,避免了名稱沖突的問題。

保護性程式設計的黃金法則是:“按需知道”原則,盡量在函數内部解決該函數的任務,隻共享那些需要共享的變量。

#include <stdio.h>
extern int x;
int main()
{
  printf("%d\n", x);
}
int x = -1;      

2. 配置設定記憶體:malloc() 和 free()

之前是确定用哪種存儲類别後,根據已制定好的記憶體管理規則,将自動選擇其作用域和存儲期。

靜态資料在程式載入記憶體時配置設定,而自動資料在程式執行塊時配置設定,并在程式離開時該塊銷毀。

C 可以在運作時配置設定更多的記憶體。主要的工具是 ​

​malloc()​

​ 函數,該函數接受一個參數:所需的記憶體位元組數。malloc() 函數會找到合适的空閑記憶體塊,這樣的記憶體是匿名的,malloc() 配置設定記憶體,但是不會為其賦名。 它确實會傳回動态配置設定記憶體塊的首位元組位址。是以,可以把該位址賦給一個指針變量,并使用指針通路這塊記憶體。malloc() 的傳回類型通常被定義為指向 char 的指針。從 ANSI C 标準開始,C 使用一個新的類型:指向 void 的指針。該類型相當于一個“通用指針”。malloc() 函數可用于傳回指向數組的指針、指向結構的指針等。在 ANSI C 中,應該堅持使用強制類型轉換,提高代碼的可讀性。然而,把指向 void 的指針賦給任意類型的指針完全不用考慮類型比對的問題。如果 malloc() 配置設定記憶體失敗,将傳回空指針。

用 malloc() 建立一個數組,除了用 malloc() 在程式運作時請求一塊記憶體,還需要一個指針記錄這塊記憶體的位置。

double *ptd;
ptd = (double*)malloc(30 * sizeof(double));      

以上代碼為 30 個 double 類型的值請求記憶體空間,并設定 ptd 指向該位置。如果讓 ptd 指向這個塊的首元素,便可像使用數組名一樣使用它。也就是說,可以使用表達式 ptd[0] 通路該塊的首元素,ptd[1]通路第 2 個元素。

我們有 3 種建立數組的方法:

  • 聲明數組時,用常量表達式表示數組的次元,用數組名通路數組的元素。可以用靜态或自動記憶體建立這種數組。
  • 聲明變長數組(C99新增的特性)時,用變量表達式表示數組的次元,用數組名通路數組的元素。具有這種特性的數組隻能在自動記憶體中建立。
  • 聲明一個指針,調用 malloc(),将其傳回值賦給指針,使用指針通路數組的元素。該指針可以是靜态的或自動的。

在 C99 之前,不能這樣做:

double item[n];      

但是,可以這樣做:

ptd = (double *)malloc(n*sizeof(double));      

通常,malloc() 要與 free() 配套使用。free() 函數的參數是之前 malloc() 傳回的位址,該函數釋放之前 malloc() 配置設定的記憶體。是以,動态配置設定記憶體的存儲期從調用 malloc() 到 free() 釋放記憶體為止。二者的原型都定義在 stdlib.h。

使用 malloc(),程式可以在運作時才确定數組大小。

jiaming@jiaming-VirtualBox:~/Documents$ ./dyn_arr.o 
What is the maximum number of type double entries?
5
Enter the values (q to quit):
20 30 35 25 40 80
Here are your 5 entried:
  20.00  30.00  35.00  25.00  40.00
Done.
jiaming@jiaming-VirtualBox:~/Documents$ cat dyn_arr.c 
#include <stdio.h>
#include <stdlib.h>

int main()
{
  double *ptd;
  int max;
  int number;
  int i = 0;

  puts("What is the maximum number of type double entries?");
  if (scanf("%d", &max) != 1)
  {
    puts("Number not correctly entered -- bye.");
    exit(EXIT_FAILURE);
  }
  ptd = (double *)malloc(max * sizeof(double)); // 強制類型轉換在 C++ 中必須使用
  if (ptd == NULL)
  {
    puts("Memory allocation failed. Goodbye.");
    exit(EXIT_FAILURE);
  }
  puts("Enter the values (q to quit):");
  while (i < max && scanf("%lf", &ptd[i]) == 1)
    ++i;
  printf("Here are your %d entried:\n", number = i);
  for (i = 0; i < number; i++)
  {
    printf("%7.2f", ptd[i]);
    if (i % 7 == 6)
      putchar('\n');
  }
  if (i % 7 != 0)
    putchar('\n');
  puts("Done.");
  free(ptd);

  return 0;
}      

2.1 free() 的重要性

  • 靜态記憶體的數量在編譯時是固定的,在程式運作期間也不會改變。
  • 自動變量使用的記憶體數量在程式執行期間自動增加或減少。
  • 動态配置設定的記憶體數量隻會增加,除非用 free() 進行釋放。

耗盡所有記憶體稱之為記憶體洩漏,在函數末尾調用 free() 函數可避免這類問題發生。

2.2 calloc() 函數

另一個記憶體配置設定函數。

long * newmem;
newmem = (long*)calloc(100, sizeof(long));      

和 malloc() 類似,在 ANSI 之前,calloc() 也傳回指向 char 的指針,在 ANSI 之後,傳回指向 void 的指針。如果要存儲不同的類型,應該使用強制類型轉換運算符。

接受兩個無符号整數作為參數,第 1 個參數是所需的存儲單元數量,第 2 個參數是存儲單元的大小(bytes)。 它也會将塊中所有位都設定為 0。

2.3 動态記憶體配置設定和變長數組

變長數組是自動存儲類型,程式離開變長數組定義所在塊的時候,變長數組占用的記憶體空間會被自動釋放,不必使用 free()。

malloc() 建立的數組不必局限在一個函數内通路。free() 所用的指針變量可以與 malloc() 的指針變量不同,但是兩個指針必須存儲相同的位址,但是,不能釋放同一塊記憶體兩次。

對多元數組而言,使用變長數組更友善。

int n = 5;
int m = 6;
int ar2[n][m]; // nxm的變長數組(VLA)
int (*p2)[6];  // C99 之前的寫法, 表明 p2 指向一個内含 6 個 int 類型的數組。p2[i] 代表一個由 6 個整數構成的元素,p2[i][j] 代表一個整數
int (*p3)[m];  // 要求支援變長數組
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // nx6 數組
p3 = (int (*)[m]) malloc(n * m * sizeof(int));      

2.4 存儲類别和動态記憶體配置設定

記憶體可分為 3 個部分:

  • 一部分供具有外部連結、内部連結和無連結的靜态變量使用;
  • 一部分供自動變量使用;通常用棧來處理,這意味着新建立的變量按順序加入記憶體,然後以相反的順序銷毀。
  • 一部分供動态記憶體配置設定;通常使用堆來處理,程式員管理,記憶體可能會支離破碎,動态記憶體通常比使用棧記憶體慢。

3. ANSI 類型限定符

除了常用類型和存儲類别來描述一個變量,C90 新增了兩個屬性:恒常性(constancy)​

​const​

​​ 和易變性(volatility)​

​volatitle​

​​。以這兩個關鍵字建立的類型是限定類型。C99 還新增了第 3 個限定符:​

​restrict​

​​,用于提高編譯器優化。C11标準新增了第 4 個限定符:​

​_Atomic​

​。

C99 為類型限定符新增了一個屬性:幂等性。即:

const const const int n = 6; // 等價于 const int n = 6;      
typedef const int zip;
const zip q = 8;      

3.1 const 類型限定符

const int nochange; // 限定 nochange 的值不能被修改
nochange = 23; // 不允許      
const int nochange = 12; // 沒問題,讓 nochange 稱為隻讀變量,初始化後不能改變它的值      

3.1.1 在指針和形參聲明中使用 const

指針需要區分是限定指針本身為 const 還是限定指針指向的值為 const。

const float *pf;// pf 指向一個 float 類型的 const 值,pf 指向的值不能改變,而 pf 指向的位址可以改變

float * const pt; // 一個 const 指針,pt 本身的值不可改變,pt 必須指向同一個位址,但是它所指向的值可以改變

const float * const ptr; // ptr 既不能指向别處,所指向的值也不能改變      
float cont * pfc; // const float * pfc 相同      
const 放在 * 左側任意位置,限定了指針指向的資料不能改變;const 放在 * 的右側任意位置,限定了指針本身不能改變(位址不變)。

3.1.2 對全局資料使用 const

使用 const 限定符聲明全局資料結構很合理。

// file1.c
const double PI = 3.1415926;
const chat * MONTHS[12] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October",
  "November", "December"
};

// file2.c
extern const double PI;
extern const * MONTHS [];      
// 必須在頭檔案中使用 static 聲明全局 const 變量,如果去掉,将導緻每個包含該頭檔案的檔案中都有一個相同辨別符的定義式聲明
// constant.h
const double PI = 3.1415926;
const chat * MONTHS[12] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October",
  "November", "December"
};

// file.c
#include "constant.h"      

3.2 volatitle 類型限定符

volatitle 限定符告知計算機,代理(而不是變量所在的程式)可以改變該變量的值。 通常,它被用于硬體位址以及其他程式或同時運作的線程中共享資料。例如,一個位址上可能儲存着目前的時鐘時間,無論程式做什麼,位址上的值都随時間的變化而改變。

該限定符設計編譯器的優化。

val1 = x;
// 一些不使用 x 的代碼
val2 = x;      

智能的編譯器會注意到以上代碼使用了兩次 x,但并未改變它的值。于是編譯器把 x 的值臨時儲存在寄存器中,然後再 val2 需要使用 x 時,才從寄存器中(而不是原始記憶體位置上)讀取 x 的值,以節約時間。這個過程被稱為高速緩沖(caching)。但是如果一些其他代理在以上兩條語句之間改變了 x 的值,就不能這樣優化,如果沒有 volatitle 關鍵字,編譯器就不知道這種事情是否發生。

可以同時使用 const 和 volatitle 限定一個值,通常用 const 把硬體時鐘設定為程式不能更改的變量,但是可以通過代理改變,這時用 volatitle,二者順序不重要。

3.3 restrict 類型限定符

restrict 關鍵字運作編譯器優化某部分代碼以更好地支援計算。它隻能用于指針,表明該指針是通路資料對象的唯一且初始的方式。

舉例來說,

int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par;      

指針 restar 是通路 malloc() 所配置設定記憶體的唯一且初始的方式。是以,可以用 restrict 關鍵字限定它。而指針 par 既不是通路 ar 數組中資料的初始方式,也不是唯一方式,是以不用把它設定為 restrict。

for (n = 0; n < 10; n++)
{
    par[n] += 5;
    restar[n] += 5;
    ar[n] *= 2;
    par[n] += 3;
    restar[n] += 3;
}      

由于之前聲明了 restar 是通路它所指向的資料塊的唯一且初始的方式,編譯器可以把涉及 restar 的兩條語句替換成下面這條語句,效果相同:

restar[n] += 8;      

但是,如果把與 par 相關的兩條語句替換成下面的語句,将導緻計算錯誤:

par[n] += 8;      

因為 for 循環在 par 兩次通路相同的資料之前,用 ar 改變了該資料的值。

如果使用了 restrict 關鍵字,編譯器就可以選擇捷徑優化計算。

3.4 _Atomic 類型限定符(C11)

_Atomic int hogs; // 原子類型的變量
atomic_store(&hogs, 12);// stdatomic.h 中的宏
// 在 hogs 中存儲 12 是一個原子過程,其他線程不能通路 hogs.