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

你可能感兴趣的:(C++)