1.不要把析构函数声明为纯虚函数
因为如果这个类被继承了,那么子类在析构时,是会调用父类的析构函数的,而如果纯虚函数被调用,就会终止程序(这个不同编译器不一样)
2.指针取地址上的内容和指针指向对应地址的区别
在看书过程中看到这样一段代码:
本来我以为delete是有问题的,因为我理解成了这样:
heap=&local
但是这两者是有区别的。书上那样的写法,其实是使用逐位拷贝或者"="运算符函数,将local对象中复制到heap指针指向的内存中。而我的理解则是heap指针指向local内存所在地址。如果按我这样理解,heap原来指向的内存就属于僵尸内存了,delete也就有问题了。
3.编译器自动生成或者扩充的的构造函数的具体任务
①初始化列表会转换成代码放入构造函数中,并且以类成员声明的顺序排列
②如果有类对象成员没有再初始列表中,但是又有构造函数的,就会调用这个类对象的构造函数
③如果该类中有vptr的,一个或者多个(多重继承情况下,就有可能有多个),会设定vptr的初值,指向适当的虚函数表
④所有父类的构造函数被调用
⑤所有父虚类的构造函数被调用
4.不是有用的拷贝构造函数和“=”函数都会被编译器默认定义
如果整个程序中没有使用到拷贝构造,或者“=”函数,那么编译器就不会定义这一类函数,也就是说这一类操作函数是在程序中真正使用到了才会被编译器定义。
比如:程序中有class b=a;拷贝构造才会被编译器定义
又比如有a=b;(a,b都是类对象),“=”函数才会被定义。
5.构造函数执行顺序
①构造函数会先调用它父类以及虚继承类的构造函数。
②在第一步结束以后,初始化vptr指针(可能有多个),指向对应的虚函数表。
注意:每一个类只有一份虚函数表,同一个类的多个对象的vptr指针都是指向同一张表的。
③如果有初始化列表的话,就在构造函数中展开,并调用。
注意:如果初始化列表中有父类或者类对象成员的初始化函数,那么这个初始化会放在第一步中,而不会在这一步中执行,具体例子可以看书p217页的例子
④最后执行程序员所提供的构造函数代码
注意:对于上面这个过程,有以下几点说明,以一个例子来说
class father
{
public:
father()
{
cout << "constructor ";
show();
}
virtual void show()
{
cout << "father\n";
}
virtual ~father()
{
cout << "distructor ";
show();
}
};
class child : public father
{
public:
child()
{
cout << "constructor ";
show();
}
virtual void show()
{
cout << "child\n";
}
virtual ~child()
{
cout << "distructor ";
show();
}
};
//int point3d::a = 10;
int main()
{
{
child a;
}
system("pause");
}
结果图
可以看到这里面先调用父类的构造函数,在调用子类的构造函数。其实父类的构造函数是在步骤①中调用的,而子类的构造函数是在步骤④中调用的。
这里有一个规则就是:在一个class的构造函数(以及析构函数中),经由构造函数来调用的虚成员函数,这个虚函数实例应该是这个构造函数所在类的虚函数,就比如上面程序中,在构造函数中,先调用父类构造函数,此时show虚函数的实例按照规则是father::show()虚函数。就是父类的构造函数中调用的虚函数,如果在父类中有对应的虚函数,一定使用的是这个父类的虚函数。
正因为是这个规则,所以在创建一个子对象时,会先创建一个父对象,然后再在父对象的基础上创建一个子对象。
6.“=”操作符函数
在每一个类中,“=”操作符函数在一些情况下,也是会由编译器默认生成。
在类对象的操作中,如果有等号,那等号背后的运算情况可能分为三种:
①使用逐位拷贝法,在编译器认为不需要拷贝构造函数或者“=”操作符函数时,该使用这两种函数的地方都会使用逐位拷贝法
②使用拷贝构造函数,在初始化时使用等号
③使用“=”操作符函数,在其他类对象用到等号地方使用等号
7.哪些类中“=”操作符函数会被编译器生成
①类中含有类对象成员,并且该类中含有“=”操作符函数
②该类的基类中含有“=”操作符函数
③当类中有虚函数或者父类中有虚函数
④当该类虚继承了一个类
注意:书上建议不要允许任何一个虚继承的父类拥有拷贝函数。
禁止“=”操作符函数的方法就是把“=”操作符函数设置为私有,就禁止了。
8.析构函数执行流程
①自定的析构函数代码先执行
②如果有类对象成员,并且该类有析构函数,就执行这个类对象的析构函数,并且不会执行的顺序和声明成员的顺序相反
③如果类对象中有vptr指针,就会指向父类的虚函数表,为了父类析构函数能够调用自己的虚函数。
④调用非虚继承的父类析构函数
⑤调用虚继承的父类析构函数
9.析构函数的产生条件
只有在编译器认为必要,或者程序员自定义的析构函数才会有析构函数。
析构函数编译器自动生成的类如下:
①该类有类对象
②父类有析构函数
③有虚函数
④有虚继承
10.为什么析构函数一般为虚函数
主要是为了防止出现下面的情况:
class father
{
public:
father()
{
cout << "consturctor father\n";
}
~father()
{
cout << "distructor father\n";
}
};
class child : public father
{
public:
child()
{
cout << "consturctor child\n";
}
~child()
{
cout << "distructor child\n";
}
};
int main()
{
father *ptr = new child;
delete ptr;
system("pause");
}
其结果是:
可以看到,最后的析构函数并没有执行子类的析构函数,这明显是不对的。
从原理上来分析,这是属于子类的析构函数把父类的析构函数给隐藏起来了,情况类似我的博客中第五点中的第一种情况https://blog.csdn.net/qq_34489443/article/details/102502830 。当调用子类对象或者子类指针时,使用的是子类方法,但是调用父类指针时,使用的是父类方法。所以这里是调用父类指针指向的析构函数,所以只调用父类的方法(也就是父类析构函数)
如果在析构函数前加上虚函数,程序如下:
using namespace std;
class father
{
public:
father()
{
cout << "consturctor father\n";
}
virtual ~father()
{
cout << "distructor father\n";
}
};
class child : public father
{
public:
child()
{
cout << "consturctor child\n";
}
virtual ~child()
{
cout << "distructor child\n";
}
};
int main()
{
father *ptr = new child;
delete ptr;
system("pause");
}
结果如图所示:
从原理上来分析,这属于https://blog.csdn.net/qq_34489443/article/details/102502830 博客第五点中的第四种情况,虚函数重写,即便是父类指针,仍然调用的是子类的虚函数,并且由于子类析构函数会自动扩展,加入父类的析构函数,所以这样子类和父类的析构函数都调用了。