多态和虚函数
什么是多态?
同一种事物在不同场景下表现出的所中形态(可以比喻为一个会说话的人,见人说人话,见鬼说鬼话)
例如:*
的处境、函数的重载(静态的多态)
静态的多态:程序在编译期间就确定程序的行为
- 函数重载
- 泛型编程
动态的多态:程序在运行期间确定程序的行为
实现多态的条件
- 基类中必须包含虚函数(在成员函数之前加上
virutal
关键字),并且在派生类中对基类中的虚函数进行重写 - 在派生类中重写的函数在基类中必须是虚函数(派生类中可以加
virtual
关键字,不加的时候仍然会保持虚函数特性,但是建议加上) - 派生类中虚函数必须与基类中虚函数的原型保持一致(返回值,函数名,参数列表和返回值都要相同,如果不同则会构成同名隐藏)
- 例外:析构函数(函数名不同)
- 例外:协变:基类的虚函数返回基类的引用或指针,派生类的虚函数返回派生类的引用或指针
- 基类与派生类中函数的访问权限可以不同,不过基类中虚函数必须是public权限
- 通过基类的指针或者引用来调用虚函数
- 静态成员函数不能定义为虚函数
重写和同名隐藏的异同
共同点:
- 都是在继承体系中
不同点:
同名隐藏:
- 继承体系中,基类与派生类中具有相同名称的成员(成员变量和成员函数,只需名称相同即能形成同名隐藏)
- 当通过派生类对象调用相同名称的成员时,优先调用派生类的成员。要调用基类中的成员时必须加作用域限定。
重写:
- 基类中函数必须为虚函数
- 派生类中虚函数原型必须域基类中的虚函数原型保持一致(除过例外)
抽象类
相当于上述的基类,抽象类专门用来存放多态的接口,它是所有要实现多态的类的基类。
写法:只有public的成员函数,并且只是声明,而且声后面加上= 0
,此函数叫做纯虚函数。
以纯虚函数作为成员函数的类叫做抽象类,抽象类不能直接实例化,但是抽象类的指针是可以定义的。
一般的抽象类只是为了构成多态,并且其中的纯虚函数只是为了在其它的类种进行重写。
如果一个类继承于一个虚函数,那么这个函数种必须要重写抽象类中的纯虚函数。
class Fun//这是一个抽象类
{
public:
virtual void TestFunc() = 0;
};
class D1 : public Fun//D1继承于抽象类
{
public:
virtual void TestFunc()
{
cout << "D1::TestFunc()" << endl;
}
};
class D2 : public Fun//D2继承于抽象类
{
public:
virtual void TestFunc()
{
cout << "D2::TestFunc()" << endl;
}
};
void Test(Fun &rf)
{
rf.TestFunc();
}
int main()
{
Fun f;//这样是错误的
D1 d1;
D2 d2;
Test(d1);//将会输出"D1::TestFunc()"
Test(d2);//将会输出"D2::TestFunc()"
system("pause");
return 0;
}
重载、重写与同名隐藏的区别
重载
- 在同一作用域
- 函数名相同、参数可以不同、返回值可以不同
重写
- 在不同的作用域
- 函数名相同、参数相同、返回值相同(协变和析构函数除外)
- 基类函数必须有virtual关键字
- 访问修饰符可以不同
同名隐藏
- 在不同的作用域中
- 函数名相同
- 在基类和派生类的中只要不构成重写就是重定义
多态的调用原理
虚函数(类里面)在内存中的布局
单继承
- 构造了一个对象之后,对象的前四个字节是一个地址
- 此地址指向的是一个函数地址表,表里面存的是每个虚函数的地址(按照虚函数的声明顺序)。
- 此类的派生类在构造对象的时候,也会将这个表给派生类的对象拷贝一份(注意是拷贝表,并不是拷贝表的地址)
- 如果派生类重写了基类中的某个虚函数,将虚表中相同偏移量位置的基类虚函数替换为重写之后的虚函数的地址
- 将派生类自己新增加的虚函数按照其在类中声明的先后次序,放在虚表的最后位置
虚表
- 含有虚函数的类构造的对象前四个字节是一个指针,此指针是一个函数指针数组的指针
- 虚表中的虚函数表的顺序和虚函数在类中声明的顺序相同,并且基类的虚函数优先在前
- 所指向的是此类中所有的虚函数的地址,在调用虚函数的时候都是通过此指针找到对应的函数,再进行调用
- 如果派生类中对某个虚函数进行了重写,那么虚函数表中的此函数的地址将会被改为重写之后的虚函数的地址
虚表的位置是在代码段,不可以修改的
class Base { public: virtual void TestFunc() { cout << "D1::TestFunc()" << endl; } virtual Base& GetObj() { cout << "Base::GetObj()" << endl; return *this; } int _b; }; class Derived : public Base { public: virtual void TestFunc() { cout << "D2::TestFunc()" << endl; } virtual Derived& GetObj() { cout << "Derived::GetObj()" << endl; return *this; } int _d; }; void Test(Base &rb) { rb.GetObj(); } int main() { Base b; Derived d; Test(d); Test(b); system("pause"); return 0; }
以上的类的虚表可以图示为:
虚函数的调用过程
- 取虚表的地址
- 传递this指针
- 取虚函数的地址
- 调用该虚函数
多继承
- 派生类自己的虚函数按照其在类中的声明次序,放在第一个虚表的后面
菱形虚拟继承中的虚函数
菱形继承的派生类对象内存模型
- 前四个字节是虚函数表的地址
- 接着是虚基表的地址(偏移量表)
- 派生类的成员
- 基类的成员
虚拟继承和虚函数同时存在的情况
虚拟继承和虚函数同时存在的时候,虚函数表在前,虚基表在其后面
虚拟继承部分的派生类在前,基类在后
D类的对象中回将C1类和C2类继承于B类的成员变变量和虚函数进行重合,只生成一个,并且放在最后的位置
class B
{
public:
virtual void TestFunc1()
{
cout << "B::TestFunc1()" << endl;
}
virtual void TestFunc2()
{
cout << "B::TestFunc2()" << endl;
}
int _b;
};
class C1 : virtual public B
{
public:
virtual void TestFunc1()
{
cout << "C1::testFunc1()" << endl;
}
virtual void TestFunc3()
{
cout << "C1::testFunc3()" << endl;
}
int _c1;
};
class C2 : virtual public B
{
public:
virtual void TestFunc1()
{
cout << "C2::TestFunc2()" << endl;
}
virtual void TestFunc4()
{
cout << "C2::TestFunc4()" << endl;
}
int _c2;
};
class D : public C1, public C2
{
public:
virtual void TestFunc1()
{
cout << "D::TestFunc1()" << endl;
}
virtual void TestFunc3()
{
cout << "D::TestFunc3()" << endl;
}
virtual void TestFunc4()
{
cout << "D::TestFunc4()" << endl;
}
virtual void TestFunc5()
{
cout << "D::TestFunc5()" << endl;
}
int _d;
};
d类对象的内存布局
一些问题
那些函数不能作为虚函数,为什么?
- 不能被继承的函数
- 不能被重写的函数
- 构造函数(拷贝构造函数 )(虚函数调用的时候是通过对象的前四个字节找到虚函数表,但是构造函数没有执行完成的时候对象都没有构造完成,所以无法取到虚函数表)
- 静态成员函数(静态成员函数不通过对象调用,而虚函数表是在对象的前四个字节所指的虚函数表中)
- 内联函数(在编译期间都展开了)
- 友元函数和普通函数(不在继承体系中)
调用普通函数和调用虚函数的效率?
- 调用普通函数是直接进行调用,但是调用虚函数的时候还要先找虚函数表,然后才能调用,所以效率相对来说就会慢一点
- 抽象类能不能定义变量?
- 抽象类是可以定义变量的,但是抽象类不能构造对象,所以定义的变量也就没有什么用
注意:
- 最好将基类中的析构函数设置为虚函数
- 如果基类析构函数需要释放资源,那么一定要将基类的析构函数定义为虚函数,以防止内存泄漏。