参考文章:http://t.csdnimg.cn/Lfhpr
一、值传递不满足多态,主要是因为这种方式限制了对象类型的动态绑定(值传递中,接收到的对象具有具体的类型(而非基类类型))和原始对象的修改能力。还产生切片问题。
二、C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写
三、虚函数表
编译时,编译器会为每个包含虚函数的类自动生成一个虚函数表,当创建类的实例时会在对象内存中预留一个指针指向该虚函数表。
本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。
所以,如果继承了虚函数,那么
1 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。
2 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。
3 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。
四、多态的原理
编译器"看到"满足多态,就会根据指针找到对应对象虚函数表,然后查找虚函数表找到虚函数地址,然后访问虚函数。
五、多继承中的虚函数表
如果C自己有多余的虚函数,会按照继承顺序补在第一张虚表后面。
六、析构函数设为虚函数
析构函数要实现多态。But,析构函数名字天生不一样,怎么实现多态?
实际上,析构函数被编译器全部换成了Destructor,所以我们加上virtual就可以。
只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。
这也解释了为什么子类不写virtual可以构成重写,因为编译器怕你忘记析构。
一、如果基类的析构函数不是虚函数,那么通过基类指针或引用调用析构函数时只会调用基类的析构函数,而不会调用派生类的析构函数。导致派生类部分没有被正确清理。为了避免这种情况,我,我们通常在基类中将析构函数声明为虚函数。
#include <iostream>
using std::cout;
using std::endl;
class Base
{
public:
Base()
: _base(new int(10))
{
cout << "Base() " << endl;
}
virtual void display() const
{
cout << "*_base:" << *_base << endl;
}
virtual ~Base()
{
if (_base)
{
delete _base;
_base = nullptr;
}
cout << "~Base()" << endl;
}
private:
int *_base;
};
class Derived
: public Base
{
public:
Derived()
: Base(), _derived(new int(20))
{
cout << "Derived()" << endl;
}
virtual void display() const override
{
cout << "*_derived:" << *_derived << endl;
}
~Derived()
{
if (_derived)
{
delete _derived;
_derived = nullptr;
}
cout << "~Derived()" << endl;
}
private:
int *_derived;
};
int main()
{
Base *pbase = new Derived();
pbase->display();
delete pbase;
return 0;
}
运行结果:
当使用基类指针指向派生类对象并通过 delete 操作符删除该基类指针指向的对象时,如果基类的析构函数是虚函数,那么析构函数的多态性就会被触发。
在delete ptr;这一行会发生以下事情:
1、编译器首先会检查 ptr 所指向的对象的实际类型(在这种情况下是Derived类型)。
2、由于 Base 类的析构函数是虚函数,编译器会查找该对象的虚函数表(Vtable),以确定应该调用哪个析构函数。
3、编译器使用虚函数表找到Derived 类的析构函数,并首先调用它。
4、Derived 类的析构函数执行完毕后,会自动调用基类Base的析构函数。
这样,即使是通过基类指针ptr删除对象,也能确保Derived 类的析构函数被正确调用,从而避免了资源泄露等问题。
注:析构函数顺序:派生类析构函数的执行顺序与构造函数相反。首先调用派生类的析构函数,然后按照基类在继承列表中的顺序(如果有多个基类)或按照基类声明的顺序(如果有单个基类)调用基类的析构函数。这确保了在对象销毁时,首先清理派生类特有的资源,然后清理基类共有的资源。
七、虚函数的限制
1、构造函数不能设为虚函数
构造函数的作用是创建对象,完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,此时构造还未执行完,对象还没创建出来,存在矛盾。
2、静态成员函数不能设为虚函数
虚函数的实际调用:this->vfptr->vtable->virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr。
3、inline函数不能设为虚函数
因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以效果是冲突的,如果同时存在,inline失效。
4、普通函数不能设为虚函数
虚函数要解决的是对象多态的问题,与普通函数无关。
八、重载、隐藏、覆盖的区分
重载:发生在同一个类中,当函数名称相同时,函数参数类型、顺序、个数任一不同。.
隐藏:发生在基类派生类之间,函数名称相同时,就构成隐藏(参数不同也能构成隐藏)。它允许子类隐藏父类中的同名函数。
列如:
#include <iostream>
using std::cout;
using std::endl;
class A
{
public:
void print()
{
cout << "A" << endl;
}
};
class B
: public A
{
public:
void print(int x)
{
cout << "B" << endl;
}
};
int main()
{
B b;
b.print();
return 0;
}
运行结果:
在例子中,B类中的print(int x) 隐藏了A类中的print()。如果使用B类型的对象调用print(),编译器会报错,因为没有找到匹配的函数。
覆盖:发生在基类和派生类之间,基类和派生类中同时定义相同的虚函数,覆盖的是虚函数表的入口地址,并不是覆盖函数本身。
#include <iostream>
using std::cout;
using std::endl;
class A
{
public:
virtual void print() const
{
cout << "A" << endl;
}
};
class B
: public A
{
public:
void print() const override
{
cout << "B" << endl;
}
};
int main()
{
B b;
A *P = &b;
P->print();
return 0;
}
运行结果:
九、菱形继承
普通继承:派生类的内存布局中会直接包含基类的完整数据。虚继承:派生类通过虚基类指针间接访问基类数据,减少了内存占用并避免了数据冗余。
菱形继承是指当类A和类B分别继承自类C时,而类D又同时继承A和B,从而面临了一种形如菱形的继承结构。
为了解决菱形继承带来的问题,C++提供了虚继承(virtual inheritance)的机制。通过虚继承,可以确保在继承体系中,公共基类只被实例化一次,并被子类共享。具体做法是将公共基类声明为虚基类,在继承时使用virtual
关键字。
class C { // 公共基类
public:
void show() {
cout << "this is C" << endl;
}
};
class A : virtual public C { // A类虚继承C类
public:
void show() {
cout << "this is A" << endl;
}
};
class B : virtual public C { // B类虚继承C类
public:
void show() {
cout << "this is B" << endl;
}
};
class D : public A, public B { // D类继承A和B类
public:
void show() {
cout << "this is D" << endl;
}
};
通过虚继承,类D中只会有一个C类的实例,从而避免了数据冗余和访问二义性的问题。