C++的多态性:
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(){
cout << " Base::func()" <<endl;
}
};
class Son1 : public Base{
public:
virtual void fun() override{
cout << " Son1::func()" <<endl;
}
};
class Son2 : public Base{
};
int main()
{
Base* base = new Son1;
base->fun();
base = new Son2;
base->fun();
delete base;
base = NULL;
return 0;
}
// 运行结果
// Son1::func()
// Base::func()
例子: Base为基类,其中的函数为虚函数。子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性,那么为什么会出现多态性,其底层的原理是什么?
虚表和虚基表指针的概念:

上图中展示了虚表和虚表指针在基类对象和派生类对象中的模型,下面阐述实现多态的过程:
这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。
析构函数:
构造函数:
首先整理一下虚函数表的特征:
根据以上特征,虚函数表类似于类中静态成员变量,静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示:
由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区。
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。
register关键字: 这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。
举个例子:
#include <iostream>
using namespace std;
class A
{
public:
inline A() {
cout << "inline construct()" <<endl;
}
inline ~A() {
cout << "inline destruct()" <<endl;
}
inline virtual void virtualFun() {
cout << "inline virtual function" <<endl;
}
};
int main()
{
A a;
a.virtualFun();
return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct()
构造函数和析构函数声明为内联函数是没有意义的!
《Effective C++》中所阐述的是:
将虚函数声明为inline,要分情况讨论:
有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,而虚函数是运行期决定的。
即在不知道将要调用哪个函数的情况下,如何将函数内联呢?
上述观点看似正确,其实不然!
如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开。
综上,当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开;当是**
对象本身调用虚函数时,会内联展开**,当然前提依然是函数并不复杂的情况下。
1、编译器并不是把函数模板处理成能够处理任意类的函数;
2、因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误(头文件中需要有声明和定义)。
I、从存储空间角度
虚函数对应一个指向vtable虚函数表的vptr,这大家都知道,但是这个vptr事实上是存储在对象的内存空间的。
问题出来了,假设构造函数是虚的,就须要通过 vptr找到vtable里的虚函数地址来调用,但是对象还没有实例化,也就是内存空间还没有,没有vptr,找不到vtable呢?所以构造函数不能是虚函数。
II、从使用角度
III、从实现上看
C++中基类采用virtual虚析构函数是为了防止内存泄漏。
具体地说,如果派生类中申请了内存空间,需要在其析构函数中对这些内存空间进行释放。
如果基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。
那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。
析构函数 用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;
特点: 析构函数与构造函数同名,但该函数前面加~。
析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。
当撤销对象时,编译器也会自动调用析构函数。
每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。
一般析构函数定义为类的公有成员。
构造函数顺序:
析构函数顺序:
抛出异常:
- 抛出异常(也称为抛弃异常)即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常。
- 抛出异常实际是作为另一种返回值来使用的。 抛出异常的好处一是可以不干扰正常的返回值,另一个是调用者必须处理异常,而不像以前c语言返回一个整数型的错误码,调用者往往将它忽略了。
构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。