C++編程思想學習筆記---第13章 動態創建對象

一個空中交通指揮系統需要處理多少架飛機?一個網絡中將會有多少個節點?爲了解決這個普通的問題,我們需要在運行時可以創建和銷燬對象是最基本的要求。當然C早就提供了動態內存分配 函數malloc()和free(),它們可以從堆中分配存儲單元。
然而這些函數將不能很好地運行,因爲構造函數不允許我們向他傳遞內存地址來進行初始化。如果這麼做了,我們可能:
1. 忘記了。則在c++中的對象初始化將會難以保證
2. 在對對象進行初始化之前做了其他事情,比如函數調用
3. 把錯誤規模的對象傳遞給它 
c++如何保證正確的初始化和清理,又允許我們在堆上動態創建對象呢?
答案是:使動態對象創建成爲語言的核心,malloc和free是庫函數,不在編譯器的控制範圍之內,而我們需要在程序運行之前就要保證所有的對象的構造函數和析構函數都會被調用。
我們需要兩個新的運算符new和delete

13.1 對象創建

當創建一個c++對象時,會發生兩件事情:
1. 爲對象分配內存
2. 調用構造函數來初始化那個內存
c++強迫第2步一定會發生,因爲不管對象存在於哪裏,構造函數總是要調用的。而步驟1可以用幾種方式發生:
1. 在靜態存儲區,在程序開始之前就分配內存
2. 在棧上,棧中存儲去的分配和釋放都由處理器內置的指令完成
3. 在堆上,程序員可以決定在任何時候分配內存及分配多少內存 

13.1.1 c從堆中獲取存儲單元的方法

爲了使用c在堆上動態創建一個類的實例,我們必須這樣做:

#include <cstdlib>
#include <cstring>
#include <cassert>
#include <iostream>
using namespace std;

class Obj
{
    int i, j, k;
    enum
    {
        sz = 100
    };
    char buf[sz];
public:
    void initialize()
    {
        cout << "initializing Obj" << endl;
        i = j = k = 0;
        memset(buf, 0, sz);
    }
    void destroy()
    {
        cout << "destroying Obj" << endl;
    }
};

int main()
{
    Obj* obj = (Obj*)malloc(sizeof(Obj));
    assert(obj != 0);
    obj->initialize();
    obj->destroy();
    free(obj);

    return 0;
}

查看以上代碼段,很發現幾個不太方便或者容易出錯的地方:
1. malloc()函數要求必須傳遞對象的大小
2. malloc()函數返回一個void*類型的指針,必須做類型轉換
3. malloc()可能分配失敗,從而返回(void*)0,所以必須檢查返回值
4. 手動調用obj->initialize()實在是不方便,而且很有可能忘記(destroy()函數也一樣)
c方式的動態內存分配函數太複雜,下面來看c++的方式。

13.1.2 operator new

使用new運算符它就在堆裏爲對象分配內存併爲這塊內存調用構造函數

MyType *fp = new MyType(1, 2);

在運行時,等價於調用malloc()分配內存,並且使用(1, 2)作爲參數表來爲MyType調用構造函數,this指針指向返回值的地址。這樣,我們只需一行代碼就可以安全,高效地動態創建一個對象了,它帶有內置的長度計算、類型轉換、安全檢查。

13.1.3 operator delete

new表達式的反面是delete表達式。delete表達式首先調用析構函數,然後釋放內存。

delete fp;

delete只用於刪除new創建的對象。如果用malloc創建一個對象,然後用delete刪除它,這個動作是未定義的。建議不要這樣做。如果刪除的對象的指針是0,將不發生任何事情。

13.1.4 一個簡單的例子

#ifndef TREE_H
#define TREE_H
#include <iostream>

class Tree
{
    int height;
public:
    Tree(int treeHeight): height(treeHeight) {}
    ~Tree() { std::cout << "destructor" << std::endl;}
    friend std::ostream&
    operator<<(std::ostream& os, const Tree* t)
    {
        return os << "Tree height is: " << t->height << std::endl;
    }
};
#endif//TREE_H
#include "Tree.h"
using namespace std;

int main()
{
    Tree *t = new Tree(40);
    cout << t;
    delete t;

    return 0;
}

這裏是通過調用參數爲ostream和Tree*類型的重載operator<<來實現這個運算的。

13.1.5 內存管理的開銷

  在棧上自動創建對象時,對象的大小和它們的生存期被準確地內置在生成的代碼裏,這是因爲編譯器知道確切的類型、數量、範圍。在堆上創建對象還包括另外的時間和空間開銷。
  調用malloc(),這個函數從堆裏申請一塊內存,搜索一塊足夠大的內存來滿足要求,這可以通過檢查按某種方式排列的映射或目錄來實現,這樣的映射或目錄用以顯示內存的使用情況。這個過程很快但可能要試探幾次,所以它可能是不確定的--即每次運行malloc()並不是花費了完全相同的時間。

13.2 重新設計前面的例子

下面我們來重寫設計本書前面的Stash和Stack的例子,在此之前先看一個對void*型指針進行delete操作的問題。

13.2.1 使用delete void*可能會出錯

//Deleting void pointers can cause memory leak
#include <iostream>
using namespace std;

class Object
{
    void* data;
    const int size;
    const char id;
public:
    Object(int sz, char c): size(sz), id(c) {
        data = new char [size];
        cout << "Constructing object " << id << ", size = " << size << endl;
    }
    ~Object(){
        cout << "Destructing object " << id << endl;
        delete []data;
    }
};

int main()
{
    Object* a = new Object(40, 'a');
    delete a;
    void* b = new Object(40, 'b');
    delete b;

    return 0;
}

程序運行的打印爲
Constructing object a, size = 40
Destructing object a
Constructing object b, size = 40
注意兩點:
1. 類Object包含一個void*指針,它指向“元”數據,即內建類型的數據,並沒有任何構造函數。在Object的析構函數中,對這個void*指針調用delete並不會發生什麼錯誤,僅僅釋放內存。
2. 但當delete b時,b定義爲一個void*類型的指針,delete沒有足夠的信息知道b是什麼類型,因此該操作不會調用類Object的析構函數,只是釋放內存,這樣就造成了內存泄漏

如果在程序中發現了內存泄漏的情況,一定要搜索delete語句並檢查被刪除的指針的類型。如果是void*類型,則可能發現了一種可能的原因。

13.2.2 對指針的清除責任

  爲了是Stash和Stack容器更具靈活性,需要使用void*類型的指針。這意味着當一個指針從Stash和Stack對象返回時,必須在使用之前把它轉換爲適當的類型;在刪除的時候也要轉換爲適當的類型,否則會丟失內存。
  解決內存泄漏的另一個工作在於確保容器中的每一個對象調用delete。而容器含有void*,因此必須又程序員來管理指針,用戶必須負責清理這些對象(這也是使用c++對程序員要求比較高的地方)

13.2.3 指針的Stash

  Stash的新版本叫做PStash。

//c13: ptash.h
//Holds pointer instead of objects
#ifndef PSTASH_H
#define PSTASH_H

class PStash
{
    int quantity;
    int next;
    void** storage;
    void inflate(int increase);
public:
    PStash(): quantity(0), next(0), storage(0) {}
    ~PStash();
    int add(void* element);
    void* operator[] (int index) const; //Fetch
    //Remove the reference from this PStash
    void* remove(int index);
    int count() const
    {
        return next;
    }
};

#endif //PSTASH_H

基本數據成員類似。quantity表示當前stash的最大存儲容量,next總是指向當前的空閒存儲區的首地址,storage是存儲區的首地址。析構函數刪除void指針本身,而不是試圖刪除它們所指向的內容。其他方面的變化是用operator[]代替了函數fetch()。

//c13: PStash.cpp
//Pointer Stash definitions
#include "pstash.h"
#include <cassert>
#include <iostream>
#include <cstring>
using namespace std;

int PStash::add(void* element)
{
    const int inflateSize = 10;
    if(next >= quantity)
    {
        inflate(inflateSize);
    }
    storage[next++] = element;
    return (next-1);
}

PStash::~PStash()
{
    for(int i = 0; i < next; i++)
    {
        //assert(storage[i] == 0);
        if(storage[i] != 0)
        {
            cout << "storage[" << i << "]  not cleaned up" << endl;
        }
    }
    delete []storage;
}

void* PStash::operator[] (int index) const
{
    if(index >= next)
    {
        return 0;// To indicate the end
    }
    return storage[index];
}

void* PStash::remove(int index)
{
    void* v = operator[](index);
    if(v)
    {
        storage[index] = 0;
    }
    return v;
}

void PStash::inflate(int increase)
{
    const int psz = sizeof(void*);
    void** st = new void*[quantity + increase];
    memset(st, 0, (quantity + increase) * psz);
    memcpy(st, storage, quantity * psz);
    quantity += increase;
    delete []storage;
    storage = st;
}
  • 在使用add()增加存儲內容時,僅僅存指針,而不是整個對象的拷貝。
  • inflate函數依然負責在存儲空間不夠時,增加內存
  • 爲了完全由客戶程序完全負責對象的清除,有兩種方法可以獲得PStash中的指針:其一是使用operator[],它簡單地返回作爲一個容器成員的指針。第二種方法是使用成員函數remove(),它返回指針,並通過置0的方式從容器中刪除該指針。

下面是一個測試程序:

//:C13: PStashTest.cpp
//Test of Pointer Stash
#include "pstash.h"
#include <cassert>
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main()
{
    PStash intStash;
    //'new' works with built-in types, too. Note
    //the pseudo-constructor syntax;
    for(int i = 0; i < 25; i++)
    {
        intStash.add(new int(i));
    }
    for(int j = 0; j < intStash.count(); j++)
    {
        cout << "intStash[" << j << "] = "
            << *(int*)intStash[j] << endl;
    }

    //clean up
    for(int k = 0; k < intStash.count(); k++)
    {
        delete intStash.remove(k);
    }

    ifstream in("pstashTest.cpp");
    if(in.is_open() == false)
    {
        cout << "open file failed...exit" << endl;
        return -1;
    }

    PStash stringStash;
    string line;
    while(getline(in, line))
    {
        stringStash.add(new string(line));
    }
    //Print out the string
    for(int u = 0; stringStash[u]; u++)
    {
        cout << "stringStash[" << u << "] = "
        << *(string*)stringStash[u] << endl;
    }
    //Clean up
    for(int v = 0; v < stringStash.count(); v++)
    {
        delete (string*)stringStash.remove(v);
    }

    return 0;
}
  • 注意第17行,intStash.add(new int(i)); 使用了僞構造函數形式,new運算符對內建類型仍然適用
  • 打印時,operator[]返回的值必須被轉換爲正確的正確的類型。這是使用void*的缺點
  • 第53行,使用delete時將remove函數返回的指針轉化爲string*,而在第28行卻並沒有又這麼做,因爲int類型只需要釋放內存而不用調用析構函數

以上測試程序的打印爲

intStash[0] = 0
intStash[1] = 1
intStash[2] = 2
intStash[3] = 3
intStash[4] = 4
intStash[5] = 5
intStash[6] = 6
intStash[7] = 7
intStash[8] = 8
intStash[9] = 9
intStash[10] = 10
intStash[11] = 11
intStash[12] = 12
intStash[13] = 13
intStash[14] = 14
intStash[15] = 15
intStash[16] = 16
intStash[17] = 17
intStash[18] = 18
intStash[19] = 19
intStash[20] = 20
intStash[21] = 21
intStash[22] = 22
intStash[23] = 23
intStash[24] = 24
stringStash[0] = //:C13: PStashTest.cpp
stringStash[1] = //Test of Pointer Stash
stringStash[2] = #include “pstash.h”
stringStash[3] = #include
stringStash[4] = #include
stringStash[5] = #include
stringStash[6] = #include
stringStash[7] = using namespace std;
stringStash[8] =
stringStash[9] = int main()
stringStash[10] = {
stringStash[11] = PStash intStash;
stringStash[12] = //’new’ works with built-in types, too. Note
stringStash[13] = //the pseudo-constructor syntax;
stringStash[14] = for(int i = 0; i < 25; i++)
stringStash[15] = {
stringStash[16] = intStash.add(new int(i));
stringStash[17] = }
stringStash[18] = for(int j = 0; j < intStash.count(); j++)
stringStash[19] = {
stringStash[20] = cout << “intStash[” << j << “] = ”
stringStash[21] = << (int)intStash[j] << endl;
stringStash[22] = }
stringStash[23] =
stringStash[24] = //clean up
stringStash[25] = for(int k = 0; k < intStash.count(); k++)
stringStash[26] = {
stringStash[27] = delete intStash.remove(k);
stringStash[28] = }
stringStash[29] =
stringStash[30] = ifstream in(“pstashTest.cpp”);
stringStash[31] = if(in.is_open() == false)
stringStash[32] = {
stringStash[33] = cout << “open file failed…exit” << endl;
stringStash[34] = return -1;
stringStash[35] = }
stringStash[36] =
stringStash[37] = PStash stringStash;
stringStash[38] = string line;
stringStash[39] = while(getline(in, line))
stringStash[40] = {
stringStash[41] = stringStash.add(new string(line));
stringStash[42] = }
stringStash[43] = //Print out the string
stringStash[44] = for(int u = 0; stringStash[u]; u++)
stringStash[45] = {
stringStash[46] = cout << “stringStash[” << u << “] = ”
stringStash[47] = << (string)stringStash[u] << endl;
stringStash[48] = }
stringStash[49] = //Clean up
stringStash[50] = for(int v = 0; v < stringStash.count(); v++)
stringStash[51] = {
stringStash[52] = delete (string*)stringStash.remove(v);
stringStash[53] = }
stringStash[54] =
stringStash[55] = return 0;
stringStash[56] = }
storage[1] not cleaned up

13.3 用於數組的new和delete

下面我們考慮用new在堆上創建對象數組。看看下面兩個語句有什麼不同:

MyType* fp = new MyType[100];
MyType* fp2 = new MyType;
  • 第一行用new創建了一個MyType類型的數組,併爲每一個對象都調用了構造函數,指針*fp指向數組的首地址
  • 第二行用new創建了一個MyType對象,指針*fp2指向該對象所在的內存地址

new好像沒有什麼問題,那麼delete的時候呢?

delete fp;
fp = 0;
delete fp2;
fp2 = 0;

  對delete的調用兩者的形式完全相同,那麼我們可以推測產生的結果應該也是完全相同的。fp2是常見的形式,只要分析出了對它調用delete會產生什麼結果,那麼對fp應該也是一樣的。
  delete fp2;delete先調用析構函數,然後釋放fp2所指向的內存地址。所以delete fp;也是先調用析構函數,然後釋放fp所指向的內存。這裏問題就出現了,fp後面還有99個對象沒有釋放內存,也沒有調用析構函數,內存泄漏就發生了。
  你有可能會想寫一個循環去逐個地做清理的工作,但更好的做法是如下:delete []fp; 它告訴編譯器產生一些代碼,該代碼的任務是將從數組創建時存放在某處的對象數量取回,併爲所有對象調用析構函數。這樣你並不需要知道數組的大小,就可以完成整個數組的清理工作。

13.4 耗盡內存

  當operator new()找不到足夠大的連續空間來安排對象的時候,一個 叫做new-handler的特殊函數將會被調用。new-handler的默認動作時產生一個異常,我們可以重新註冊一個異常函數,比如打印處錯誤信息,方便調試。
  包含new.h來替換new-handler,用set_new_handler()來註冊新的異常函數。

//C13: NewHandler.cpp
//Changeing the new-handler
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;

int count = 0;

void out_of_memory()
{
    cerr << "memory exhausted after " << count
    << "allocations!" << endl;
    exit(1);
}

int main()
{
    set_new_handler(out_of_memory);
    while(1)
    {
        count++;
        new int[1000];
    }

    return 0;
}

讀者注:這段程序在我的ubuntu14.04上並沒有收到意想的效果。電腦確實變得很卡,鼠標都動不了,但是最後只打印出了一個Killed,而不是out_of_memory裏的打印信息。另外大家就不要去嘗試,我運行程序之後重啓纔好的(^_^)

13.5 重載new和delete

  複習一下new的行爲:先分配內存,然後調用構造函數。delete先調用構造函數,然後釋放內存
  c++的內存分配系統是爲了通用目的設計的,而這種通用設計在保證兼容性的同時沒法保證效率。其中一個問題是堆碎片:分配不同大小的內存可能會在堆上產生很多碎片,雖然內存總量可能還足夠,但是無法得到一塊滿足需求的連續的內存空間而導致分配內存失敗。特別是在嵌入式系統中,設備內存資源本來就較小,但又要求能運行很長時間,這就要求程序員能定製自己的內存分配系統。
  當我們重載new和delete時,我們只是改變了原有的內存分配方法,並且可以選擇重載全局內存分配函數或者是針對特定類的分配函數。

13.5.1 重載全局new和delete

一個簡單的全局重載的代碼例子。

//:C13: GloableOperatorNew.cpp
#include <cstdio>
#include <cstdlib>
using namespace std;

void* operator new(size_t sz)
{
    printf("operator new: %d Bytes\n", sz);
    void* m = malloc(sz);
    if(!m) puts("out of memory");
    return m;
}

void operator delete(void* m)
{
    puts("operator delete");
    free(m);
}

class S
{
    int i[100];
public:
    S() { puts("S::S()"); }
    ~S() { puts("S::~S()"); }
};

int main()
{
    puts("creating & destroying an int");
    int* p = new int(47);
    delete p;
    puts("creating & destroying an s");
    S* s = new S;
    delete s;
    puts("creating & destroying S[3]");
    S* sa = new S[3];
    delete []sa;
}

程序創建和銷燬了一個int變量,一個對象,和一個對象數組,打印如下:

creating & destroying an int
operator new: 4 Bytes
operator delete
creating & destroying an s
operator new: 400 Bytes
S::S()
S::~S()
operator delete
creating & destroying S[3]
operator new: 1208 Bytes
S::S()
S::S()
S::S()
S::~S()
S::~S()
S::~S()
operator delete
  注意這裏的第8行並沒有使用iostream的對象來打印,因爲在初始化這些對象的時候也會調用我們重載過的new(因爲是全局的),會造成死鎖。所以我們選擇printf和puts這樣的庫函數,它們並不調用new來初始化自身。

13.5.2 對於一個類重載new和delete

  當編譯器看到使用new創建自己定義的類的對象時,它選擇成員版本的operator new()而不是全局版本。下面這個例子爲類Framis創建了一個非常簡單的內存分配系統,程序開始前先留下一塊靜態數據區域pool[],用alloc_map[]來表示第i塊內存是否被使用。

//: C13: Framis.cpp
//Local overloaded new & delete
#include <cstddef> //size_t
#include <fstream>
#include <iostream>
#include <new>
using namespace std;
ofstream out("Framis.out");

class Framis
{
    enum { sz = 100 };
    char c[sz];
    static unsigned char pool[];
    static bool alloc_map[];
public:
    enum { psize = 100 };
    Framis() { out << "Framis()\n"; }
    ~Framis() { out << "~Framis() ..."; }
    void* operator new(size_t) throw(bad_alloc);
    void operator delete(void*);
};

unsigned char Framis::pool[psize*sizeof(Framis)];
bool Framis::alloc_map[psize] = {false};

void*
Framis::operator new(size_t) throw(bad_alloc)
{
    for(int i = 0; i < psize; i++)
    {
        if(!alloc_map[i])
        {
            out << "using block " << i << " ... ";
            alloc_map[i] = true;
            return pool + (i * sizeof(Framis));
        }
    }
    out << "out of memory" << endl;
    throw bad_alloc();
}

void Framis::operator delete(void* m)
{
    if(!m) return;
    unsigned long block = (unsigned long)m - (unsigned long)pool;
    block /= sizeof(Framis);
    out << "freeing block " << block << endl;
    alloc_map[block] = false;
}

int main()
{
    Framis* f[Framis::psize];
    try
    {
        for(int i = 0; i < Framis::psize; i++)
        {
            f[i] = new Framis;
        }
        new Framis;
    }
    catch(bad_alloc)
    {
        cerr << "Out of memory!" << endl;
    }
    delete f[10];
    Framis* x = new Framis;
    delete x;
    for(int j = 0; j < Framis::psize; j++)
    {
        delete f[j];
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章