由简单的代码出发,边看边分析:
第一个例子:
#include <iostream>
using namespace std;
class A
{
public:
A( )
{
cout << "constructor" << endl ;
}
~A( ) {
cout << "destructor" << endl;
}
void printA( ) {
cout << "print A" << endl;
}
virtual void printB( ) {
cout << "print B" << endl;
}
};
class B {
public:
B(){cout<<"constructor B"<<endl;
};
~B(){cout<<"destructor B"<<endl;
}
};
int main( ) {
A a ; //在栈区
B b; //作用:看空类占用的内存,以及看是否会自动调用c的析构函数。
A *c = new(&a)A; //这一句是在a的位置(栈区)重新实例化A的一个对象c。
//虽然在栈区,但main()函数结束时(其他函数也同理),栈区释放内存时,在栈区释放这块内存调用的是a的析构函数。
//不会析构c,虽然用delete c ;会自动调用c的析构函数并释放那块内存,但不能用delete c ;会触发异常。
//可以最后显式调用c的析构函数 c->~A();告诉编译器,析构了c。
A *d=new A;
cout << sizeof(a )<<"\t"<< sizeof( b ) << endl;
(&a)->printA( );
(&a)->printB( );
c->printA( );
c->printB( );
cout<<endl;
d->printB( );//如果这句放到delete d;后面,会触发异常。
delete d; //如果删了,那输出结果中的printB后面那句destructor将不会打印出来,
//因为在运行期间,这块内存一直被占用,没被回收。程序运行结束,也没打印。系统回收进程所有占用的资源(包括内存)在进程结束之后。
d->printA( );//即使放到delete d;后面, 也能正常运行。可以看第二个例子。
}
以上代码能正常运行,运行结果:
constructor //a
constructor B //b
constructor //c
constructor //d
4 1 //空类B的对象 默认占用1个字节,类A的实例a中第一个数据为一个指向虚函数表的指针__vfptr,所以a占用4个字节。
print A
printB
print A
printB
printB
destructor// d 在堆区,所以要用delete 释放那块内存。如果删了delete d;这句没有。
print A //destructor B 前面没有 c 的 destructor。
destructor B //b 先构造的后析构,可以看出,没有调用c的析构函数,但是不能通过用delete c;这句
//调用c的析构函数,因为同时也把a的内存释放了,main()函数结束时,栈区释放不存在的a的内存时会触发异常。
//可以最后显式调用比如 c->~A();
destructor //a 这是运行即将结束,系统释放栈区的a 占用的那块内存时,调用的析构函数,系统回收
//内存时告诉编译器这块内存,被我回收了。栈区实例化的对象内存的回收机制和普通变量差不多,
//只不过会调用下析构函数,即使析构函数是空的。
第二个例子:
把main()中的 A a ;替换为 A *a=NULL;
int main( ) {
A *a=NULL ;
cout << sizeof( *a )<<endl;
a->printA( );
a->printB( );
}
输出结果:
4
print A
//下面进入异常,不会打印print B
printA()函数,因为在代码区,在a->printA( ); 这句中能调用成功。
构造函数没调用,到虚函数printB()那显示异常;
析构函数也没调用;
虚函数是由对象内第一个数据(一个指向虚函数表的指针__vfptr)指向虚函数表,由于NULL内不能写数据。当对象没有构造时,没有数据,也就是没有指向虚函数表的指针__vfptr,关于这点可参考
C++虚函数表,虚表指针,内存分布。
这个博主写的比较透彻,受益匪浅。