類和動態內存分配

程序對內存的使用:


1.  棧區 (stack) - 程序運行時由編譯器自動分配,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。程序結束時由編譯器自動釋放。

2.  堆區 (heap) - 在內存開闢另一塊存儲區域。一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。

3.  全局區 (靜態區) (static) - 編譯器編譯時即分配內存。全局變量和靜態變量的存儲是放在一塊的。初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。- 程序結束後由系統釋放。

4.  文字常量區 - 常量字符串就是放在這裏的。程序結束後由系統釋放

5. 程序代碼區-存放函數體的二進制代碼。


定義靜態成員變量:

      可以在類聲明中定義靜態成員變量,使用 static 修飾。不過,雖說是成員變量,但是不屬於這個類的任何一個對象。它們是是分開存儲的。

  因爲對所有對象,這個變量的值都是一樣的,存儲上也只用存一份就好。訪問的時候,使用 "className::varName" 即可。絕大多數語言中可以定義靜態變量,只是法上稍有不同。Java中的靜態變量,既可以通過對象來訪問,也可以通過類來訪問。C++中就只能通過類名來訪問。不過,Java通過對象來訪問靜態變量,實質上是通過類名來訪問的。好吧,這個問題無關痛癢。

  其次,C++不允許在類聲明中初始化靜態成員變量。而且初始化的時候要使用作用域運算符,"className::varName"。一種"內部"的感覺。

 

 

在類中定義常量:

  

1. 編譯時確定的常量

  存儲: 對所有對象而言,這個常量都是一樣的。因此和對象分開存儲,僅保留一份副本。

  實現: 1. 枚舉: 如 enum {SIZE = 100 }; 這就定義了一個枚舉常量 SIZE = 100。

       當然,你可以定義多個,並給定類型名。

     2. 靜態成員變量: 如 const static int a = 5;

     3. 用const限定並初始化, 如聲明成員 const int id = 5 (C++ 11 拓展)
  

2. 運行時確定的常量
  存儲: 不同對象,可以有不同常量,屬於對象的普通成員

  實現: 聲明用const修飾的成員,然後用構造函數的成員初始化列表.


#include <iostream>

class Student
{
public:
    const int id;
    Student(int ID) : id(ID) {
    }
};

int main() {
    Student a(10);    // a的id常量爲10
    Student b(20);    // b的id常量爲20
    std::cout << a.id << " " << b.id << std::endl;
}


      成員初始化列表的初始化工作,是在對象創建後,構造函數函數體的代碼執行前做的。對於內置類型成員的初始化,不管是放在初始化列表中初始化,還是放在函數體中初始化,效率是一樣的。不過,對於對象成員來說,使用初始化列表來初始化,效率更高。暫且不提。要注意的一點是: 成員初始化列表只能用於構造函數

 

 

複製構造函數 與 賦值運算符:

 

函數原型:

 

copy constructor:    className (const className &)

assignment operator:  className& operator=(const className &)

 

當定義的類,有指針成員,且使用new初始化的時候,需要定義"深拷貝"的複製構造函數和賦值運算。(暫不考慮定位new,因爲常規new申請的內存位於堆中,需要程序員手動delete。而定位new申請的內存地址是自行指定的,如果定在堆,則情況相同。如果定義在靜態內存中,那就沒我們的事兒了。交給OS 吧)

 

基本概念:


深拷貝:

  將一個對象拷貝給另一個對象的時候,被賦值的對象存儲賦值對象的一個額外副本。若類成員中含有指針成員,且用new初始化的時候,被賦值的成員,會申請一塊內存,將賦值對象的指針成員所指的內存的內容複製到這塊內存中。兩個指針各自指向自己申請的內存

 

淺拷貝:

  和深拷貝相似,淺拷貝對於非指針成員都是直接賦值。但是當類成員中含指針成員,且用new初始化的時候,被賦值的成員指針並不會額外申請一塊內存,而僅僅是將自己指向賦值對象的指針成員所指的那塊內存。兩個指針指向同一塊內存

 

      當我們沒有定義類的複製構造函數和賦值運算符時,編譯器會生成默認的版本,它們使用淺拷貝

回到上面所說的,爲什麼我們需要定義"深拷貝"的複製構造函數和賦值運算法捏 ? 難道,是因爲默認的淺拷貝會導致錯誤 ?

      沒錯!  我們知道,如果定義的類中含指針成員,如果它將會使用new申請新內存。在析構函數中,我們會用delete釋放相應的內存佔用。

 

 

 

考慮兩種情況:

 

1.   一個對象使用另外一個已有對象初始化,這樣將調用默認複製構造函數(有可能還會調用賦值操作符,視編譯器而定)。由於使用淺拷貝,就會存在這兩個對象的指針成員指向同一塊內存的情況,當這兩個對象棄用時,會調用它們的析構函數。這樣會出現同一塊內存被釋放兩次的情況,出現未知的錯誤。

      類似地,如果你定義了一個返回對象的函數,也會造成同一塊內存釋放兩次的情況,爲啥 ? 因爲這還將調用複製構造函數,按值傳遞意味着創建原始變量的一個副本。caller和這個函數(callee)中的對象的指針指向同一塊內存。當函數返回的時候,函數中的這個對象要被kill掉,調用析構函數了,釋放掉佔用的內存... 放心,這些都不會告訴你的。嗯,當caller中的那個對象析構時,那塊內存又被釋放了一次... 仍然是不可預知的錯誤。類似地,創建臨時對象的時候,也會調用複製構造函數,這將發生同樣的趣事--同樣的奇怪的錯誤。

 

2.   兩個已有對象之間的賦值,這將調用默認賦值運算符函數。後面的情況和1相同,都是淺拷貝鬧的--兩個對象的指針指向同一塊內存,然後被釋放兩次。

 

      囉嗦一句,“當定義的類中含有指針成員,且使用常規new(或定位new,定位在申請的堆內存中)初始化的時候,需定義深拷貝的複製構造函數和賦值操作符”,不然會被外星人抓走。

 

 

其他的的內存分配、回收問題

 

將涉及定位new的使用。(不考慮內存不夠用的情況)

 

. 如果使用定位new運算符,定位在靜態內存中,就不必釋放了 (交給OS吧)

. 如果先用常規new運算符,申請了一塊堆內存。然後,再使用定位new運算符在這塊堆內存中爲我們的對象申請內存捏 ?

 

  這種情況下,你卻不能delete這些對象。因爲,對對象指針執行delete操作,不僅會調用析構函數,而後還會回收成員所佔用的內存。你如果delete了這個對象,然後又delete那塊堆內存,就會造成某些內存被釋放兩次的情況 (正是原來存放對象成員的內存)。

  但是! 也因爲你沒有delete這些對象,這些對象是不會調用析構函數的。萬一調用析構函數是必須的 (比如: 對象中有一個指針成員,該指針成員指向了一塊用常規new申請的另外一塊堆內存,不調用析構函數,這一塊內存不就無法回收了嗎 ?  飄渺孤鴻影~  寂寞開無主~  又恨又愛的孤島內存~ )

  但是! 解決方法還是有的,我們可以顯式調用析構函數啊 ! 像這樣: p->~className(); 這樣,對象就會調用它的析構函數,且不會回收成員所佔的內存了。

 

一個簡單的例子:

#include <iostream>
#include <string>
#include <new>
using namespace std;

class Student
{
private:
    string name;
public:
    Student(const string& s): name(s) {
    }
    ~Student() {
        cout << name << " destroyed\n";
    }
};

int main() {
    double * buffer = new double[512];

    Student *s1 = new (buffer) Student("Peter");
    Student *s2 = new (buffer + sizeof(Student)) Student("Tom");

    /* 下面兩條語句將引發錯誤,後面delete[] buffer,
     * 導致同一塊內存被釋放兩次*/
    //delete s1;
    //delete s2;

    /*顯式調用析構函數, 這裏按棧的順序了,其實都行,不走尋常路 o_O */
    s2->~Student();
    s1->~Student();
    delete[] buffer;

    return 0;
}



References:


[1]《C++ Primer Plus 6th Edition》

發佈了36 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章