一个空中交通指挥系统需要处理多少架飞机?一个网络中将会有多少个节点?为了解决这个普通的问题,我们需要在运行时可以创建和销毁对象是最基本的要求。当然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