最近在重温c++基础知识,反正闲着也是没事干,以前只听说过这本书,但是从来没看过。毕业那会看的是面向对象编程,也是国外的某个牛人写的,但是是机械出版社出版的,读起来比较顺畅,但是里面有些细节还是不清楚。这本书是c++大牛写的,清华大学出版社翻译出来,刚开始读的时候觉得翻译挺别扭的,但是自己去看原文的时候,又碍于语言学的不好,发现很多翻译出来还没这本书好,就先姑且读之,以后慢慢再看英文原版。
读了这么长时间的书了,第一卷基本上要读完了,最后四章之前看得比较匆忙,没好好理解,现在又重新翻了一遍,很多东西跟我原来想的完全不一样,而且这本书读了之后可以让人知道为什么要这样做,从根本上剖析了做这件事的理由,我觉得读了之后挺有益的,就先把读书笔记贴出来。
今天分享一下new 和 delete两个操作符,面试的时候面试官很喜欢问,new 和 malloc有什么区别?当初只是知道new最重要的是要调用构造函数,而不知道为什么非要调用构造函数。
先来看一下如果没有new的话,用malloc()怎么在内存中分配一块区域给一个类对象,分配了之后,接下来应该怎么办。
下面是书中的一个例子,解答了上述的问题:
class Obj
{
int i,j,k;
enum{ sz = 100 };
char buf[sz];
public:
void initialize()
{
cout << "initializing Obj" << endl;
i = j = k;
memset(buf, 0 , sz);
}
void destroy()const
{
cout<< "destroying Obj" << endl;
}
};
int main()
{
Obj* obj = (Obj*)malloc(sizeof(Obj));
assert(obj != null);
obj->initialize();
obj->destroy();
return 0;
}
我们知道,在c++中,编译器一定要初始化一个类对象之后才能对其进行操作,使用malloc动态分配内存给对象之后,还要记得一定要初始化它,不然,这个对象就没办法用,而且很多人只关注类的功能,而不往往会忘掉初始化,这是bug的一个重要来源。所以c++想把分配内存和初始化这两份工作一块让编译器处理了,不用我们程序员惦记着。
所以new的作用就是,先为这个对象分配一块足够容纳这个对象的内存,然后调用其构造函数,初始化这块内存区域(注意分配内存和初始化的顺序,这对我们自理解重载new操作符有帮助)。delete的作用就是,先调用析构函数将内存中的参数清理掉(我理解是:指针清零,其他变量不管), 然后释放这块内存。
这里说一下前几天看的网易公开课中的《编程范式》里面讲的,其实我们分配堆上的内存的时候,哪块内存被分配了,哪块没被分配是记录在一块区域里面的,未被分配的空间都是链式连接起来的,被分配的空间也是链式连接起来的。第一块未被分配的空间最后四个字节指向了下一个未被分配的空间的地址,然后依次指向后面的未被分配的空间地址。被分配的空间亦是如此。那么,当我们调用new的时候,申请的这块内存的首地址就被写到前面一个的最后四个字节里面,说明这些空间是被占用的,不能再被分配了。当我们调用delete的时候,这块内存的首地址就会被写到未分配的字节中去。如果我们一块堆内存已经没有用了,但是没有释放掉,那么它的地址不会被写到未使用字节中去,那么你永远也用不了这块内存,这就是内存泄漏。
那如果我们重载new和delete操作符的时候,我们不可能显示地调用构造函数和析构函数,那我们怎么来重载呢?答案是,我们只负责分配内存的规则,调用构造函数和析构函数是编译器的事情。
如果我们用全局函数来重载new的话,那么原来的new就没用了,就完完全全被我们的new覆盖了。如果用成员函数来做的话,new只针对那个类。
new操作符会接受一个size_t类型的参数,标志着要分配的内存的大小,这个参数是编译器给我们的。编译器会针对这个类型判断该分配多少内存。new操作符的返回值是个void*,因为我们做的只是要分配一块内存,这块内存还没被初始化,所以返回值不是指向一个类的指针。
delete操作符接受一个指向new操作符分配的内存的void*指针,之所以是void*, 而不是类型的指针,是因为我们的delete是在析构函数调用完之后才开始回收空间的。所以我说要记住顺序啊。
好了,来点代码说说重载是怎么进行的吧。
#include <cstdlib> // malloc(), free()
#include <cstdio> // puts() , printf()
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()
{
int* p = new int(47);
delete p;
S* s = new S;
delete s;
S* sa = new S[3];// 这里我一直以为会new 3次,调用3次构造函数。但实际上是new 1次,
// 分配1204个字节,然后三次构造函数,多余的4个字节存放包含的对象的数量信息
delete sa; // 这里同样的,调用三次析构函数,delete操作符调用一次。
return 0;
}
注意:构造函数总是在new返回之后调用,如果new失败的话,构造函数不会被调用,且返回值是0。
另外需要注意的是,void*指针最好不能用delete释放,编了一下程序
void* a = new Obj(2);
delete a;
这段代码编译是成功的,但是运行时,既没有调用构造函数,也没有调用析构函数,而且程序出现假死现象。我分析原因是,当编译器看到第一行代码的时候,知道我只是要分配一个Obj这么大的一块内存空间,因为是void* ,所以我也不必为它初始化。在delete的时候,既然我不知道你这块内存中放的是什么类型的对象,那我就不知道应该调用哪个析构函数。情理上这么分析是合情合理的。
另外我学到的另一点是,重载new[]和重载new操作符原理上是一样的,它们接收的都是一个size_t的参数,来标识分配多少空间。delete[]与delete也基本相同。
大概就是这么多,在堆上分配最大的好处是可以被程序员自己控制,有些时候,在栈中分配内存不能满足我们的要求,就必须在堆上分配。另外,初始化是c++必须要保证的东西,非常重要。
最后,哪位大神能告诉我怎么排版?博客园自带的排版功能我怎么不太会用呢。。。囧
1
posted on 2012-04-20 21:07
Dino-Tech 阅读(220)
评论(0) 编辑 收藏 引用