C語言內存模型相關

內存模型

內存中運行着很多程序,我們的程序只佔用一部分空間,這部分空間又可以細分爲以下的區域:

內存分區 說明
程序代碼區(code area) 存放函數體的二進制代碼
靜態數據區(data area) 也稱全局數據區,包含的數據類型比較多,如全局變量、靜態變量、一般常量、字符串常量。
其中:
全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。常量數據(一般常量、字符串常量)存放在另一個區域。
注意:靜態數據區的內存在程序結束後由操作系統釋放。
堆區(heap area) 一般由程序員分配和釋放,若程序員不釋放,程序運行結束時由操作系統回收。malloc()、calloc()、free() 等函數操作的就是這塊內存,這也是本章要講解的重點。
注意:這裏所說的堆區與數據結構中的堆不是一個概念,堆區的分配方式倒是類似於鏈表。
棧區(stack area) 由系統自動分配釋放,存放函數的參數值、局部變量的值等。其操作方式類似於數據結構中的棧。
命令行參數區 存放命令行參數和環境變量的值,如通過main()函數傳遞的值。

參考:永遠的UNIX 高質量C++-C編程指南 – 第7章 內存管理 (1)

7.4指針參數是如何傳遞內存的?

如果函數的參數是一個指針,不要指望用該指針去申請動態內存。示例7-4-1中,Test函數
的語句GetMemory(str, 200)並沒有使str獲得期望的內存,str依舊是NULL,爲什麼?

void GetMemory(char *p, int num)
{
    p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str, 100);    // str 仍然爲 NULL
    strcpy(str, "hello");   // 運行錯誤
}

示例7-4-1 試圖用指針參數申請動態內存

毛病出在函數GetMemory中。編譯器總是要爲函數的每個參數製作臨時副本,指針參數p的副本是 _p,編譯器使 _p =p。如果函數體內的程序修改了_p的內容,就導致參數p的內容作相應的修改。這就是指針可以用作輸出參數的原因。在本例中,_p申請了新的內存,只是把_p所指的內存地址改變了,但是p絲毫未變。所以函數GetMemory並不能輸出任何東西。事實上,每執行一次GetMemory就會泄露一塊內存,因爲沒有用free釋放內存。
如果非得要用指針參數去申請內存,那麼應該改用“指向指針的指針”,見示例7-4-2。

void GetMemory2(char **p, int num)
{
    *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
    char *str = NULL;
    GetMemory2(&str, 100); // 注意參數是 &str,而不是str
    strcpy(str, "hello");  
    cout<< str << endl;
    free(str);
}

示例7-4-2用指向指針的指針申請動態內存

由於“指向指針的指針”這個概念不容易理解,我們可以用函數返回值來傳遞動態內存。這種方法更加簡單,見示例7-4-3。

char *GetMemory3(int num)
{
    char *p = (char *)malloc(sizeof(char) * num);
    return p;
}
void Test3(void)
{
    char *str = NULL;
    str = GetMemory3(100);
    strcpy(str, "hello");
    cout<< str << endl;
    free(str);
}

示例7-4-3 用函數返回值來傳遞動態內存

用函數返回值來傳遞動態內存這種方法雖然好用,但是常常有人把return語句用錯了。這裏強調不要用return語句返回指向“棧內存”的指針,因爲該內存在函數結束時自動消亡,見示例7-4-4。

char *GetString(void)
{
    char p[] = "hello world";
    return p;   // 編譯器將提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的內容是垃圾
cout<< str << endl;
}

示例7-4-4 return語句返回指向“棧內存”的指針

用調試器逐步跟蹤Test4,發現執行str = GetString語句後str不再是NULL指針,但是str的內容不是“hello world”而是垃圾。
如果把示例7-4-4改寫成示例7-4-5,會怎麼樣?

char *GetString2(void)
{
    char *p = "hello world";
    return p;
}
void Test5(void)
{
    char *str = NULL;
    str = GetString2();
    cout<< str << endl;
}

示例7-4-5 return語句返回常量字符串

函數Test5運行雖然不會出錯,但是函數GetString2的設計概念卻是錯誤的。因爲GetString2內的“hello world”是常量字符串,位於靜態存儲區,它在程序生命期內恆定不變。無論什麼時候調用GetString2,它返回的始終是同一個“只讀”的內存塊。
14.函數參數的傳遞分爲:值傳送和引用(指針)傳送。前者將變量的複本傳給函數的形參,形參的改變不會引起變量原值得改變;後者將變量的地址傳給形參,形參的改變將引起變量的改變。

extern

注意:未初始化的全局變量的默認值是 0,而未初始化的局部變量的值卻是垃圾值(任意值)。
在所有的代碼塊(函數、if 塊、switch 塊等)之外定義的變量稱爲全局變量,它的作用範圍默認是整個程序,也就是所有的源文件,包括 .c 和 .h 文件。
如果你一直在編寫單個 .c 文件的程序,那麼請注意,全局變量的作用範圍不是從變量定義處到該文件結束,在其他文件中也有效。
雖然全局變量的作用範圍是整個程序,但是如果希望在 a.c 中使用 b.c 中的變量,也必須先進行聲明。聲明使用 extern 關鍵字,與其他變量不同,extern 變量有聲明和定義之分
extern 變量的定義格式爲:

extern type name = value;

不過 extern 可以省略(我們通常就是這麼做的),全局變量默認就是 extern 的.
聲明格式爲:

extern type name;

注意:

  • 在定義 extern 變量時不能省略 value,否則就變成了變量聲明。
  • 聲明 extern 變量時要指明數據類型(必須和定義時的數據類型一致)。
  • 聲明可以有多次,定義只能有一次。

但是,函數和變量的聲明有所不同,對於函數,你可以省略 extern。這是因爲,函數的定義和聲明區別很明顯,有函數體就是定義,沒有函數體就是聲明,所以有沒有 extern 都是函數聲明。但是變量不一樣,沒有 extern 就是變量定義,重複定義是錯誤的。

static

static 聲明的變量稱爲靜態變量,不管是全局變量還是局部變量,都存儲在靜態數據區(全局變量本來就存儲在靜態數據區,即使不加 static)。
靜態數據區的數據在程序啓動時就會初始化,直到程序運行結束;對於代碼塊中的靜態局部變量,即使代碼塊執行結束,也不會銷燬。
注意:靜態數據區的變量只能初始化(定義)一次,以後只能改變它的值,不能再被初始化,即使有這樣的語句,也無效。
總結起來,static 變量的主要作用有兩個:
1. 程序有多個源文件時,將全局變量或函數的作用範圍限制在當前文件,對其他文件隱藏。
2. 保持變量內容的持久化將局部變量存儲到靜態數據區。靜態數據區的內存在程序啓動時就已分配好(內存中所有的字節默認值都是0x00),直到程序運行結束。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章