虚函数
1.多态
编译时的多态又叫早期绑定
//如
int max(int a,int b){ return a>b?a:b; }
double max(double a,double b){ return a>b?a:b; }
char max(char a,char b){ return a>b?a:b; }
int main()
{
max(1,2);
max(12.2,11,1);
max('z','e');
return 0;
}
运行时的多态又叫晚绑定
需要用到虚函数
//如
#define PI 3.1415
class point
{
private:
float x;
float y;
public:
point(float x=0,float y=0) : x{x},y{y} { }
virtual void Area()const = 0;
};
class circle :public point
{
private:
float r;
public:
circle(float r=1,float x=0,float y=0) : point(x,y),r{r} { }
virtual void Area()const{
cout<<"Cirlce Area: "<<r*r*PI<<endl;
}
};
void fun(point& p)
{
p.Area();
}
int main()
{
circle a;
fun(a);
return 0;
}
注意:运行时的多态性: 公有继承 + 虚函数 + (指针或引用调用虚函数)。
2.定义虚函数的规则
-
派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型(三同)。否则被认为是 同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个 例外(协变)。
-
只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和 全局函数也不能作为虚函数。
-
静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
-
内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
-
构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
-
析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例 化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义 为虚函数,实现撤消对象时的多态性。
-
实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的 对象,并通过该指针指向虚函数,才能实现运行时的多态性。
-
在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函 数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价, 但通用性是一个 更高的目标。
-
如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必 须不包括virtual。
eg:
//协变示例->基类中返回基类指针,派生类中返回派生类指针 class base { int a = 1; public: virtual const base* getobj() const { cout <<"base::a :"<< a << endl; return this; } }; class object : public base { int b = 12; public: virtual const object* getobj() const { cout << "object::b :" << b << endl; return this; } }; void fun(base* p) { cout << p->getobj()<< endl; } int main() { base a; object b; fun(&a); fun(&b); return 0; }
运行结果:
3.运行时的多态的原理(过程)
代码在预编译阶段扫描到有关键字 virtual 修饰的函数,则会为每个类创建虚函数指针表简称虚表,虚表里面存储了当前类的虚函数地址。每个对象都有虚表指针,当进入该类的构造函数中,构造函数中,会首先设置虚表指针指向该类的虚表,当进入析构函数时,首先将虚表指针重新指向该类的虚表。
即 构造函数设置虚表指针,析构函数重置虚表指针。
重点:派生类有n个父类,就有n个虚表,其对象就有n个虚表指针。
- 单一继承
//单一继承的原理图解
#define PI 3.1415
class point
{
private:
float x;
float y;
public:
point(float x = 0, float y = 0) : x{ x }, y{ y } { }
virtual void Area() = 0;
virtual void show() { cout << "point:: " << endl; }
};
class circle: public point
{
private:
float r;
public:
circle(float r = 1, float x = 0, float y = 0) : point(x, y), r{ r } { }
virtual void Area() {
cout << "Cirlce Area: " << r * r * PI << endl;
}
virtual void print() { cout << "hello world! " << endl; }
};
int main()
{
point* a;
circle b;
a = &b;
return 0;
}
1.预编译阶段创建各类的虚表(vftable)
派生类首先继承基类虚表,然后将自己新增的虚函数,重写基类的虚函数更新到虚表中。
指向type_info的指针:
每一个虚函数表前都有一个指针指向type_info,负责对RTTI(runtime type interpret)的支持。
基类构造函数
2.运行阶段
进入circle的构造函数,跳转到基类 point的构造函数,然后将虚表指针vfptr指向 point::vftable,
初始化 x,y;回到circle 构造函数,设置虚表指针vfptr指向 circle::vftable ,构初始化 r;
如下图:
3当使用基类指针或引用调动虚函数时,会对此时虚表指针指向的虚表查找,从而实现动态链接(多态)。
- 多重继承
class Base1
{
private:
int num1;
public:
Base1(int x = 0) : num1(x){}
virtual void fun(){}
virtual void fun1(){}
virtual ~Base1(){}
};
class Base2
{
private:
int num2;
public:
Base2(int x = 1) : num2(x){}
virtual void fun(){}
virtual void fun2(){}
virtual ~Base2(){}
};
class Object : public Base1,public Base2
{
private:
int val;
public:
Object(int x = 5) : Base1(x+10),Base2(x+5),val(x) {}
virtual void fun(){}
virtual void fun1(){}
virtual void fun2(){}
virtual ~Objecct(){}
}
int main()
{
Object obj;
return 0;
}
知识点:派生类先继承谁,它新添加的虚函数就加入从它那继承来的虚表中
图解:
4.联编
- C++语言中,使用类类型的引用或指针调用虚函数(成员选择符“->”),则程序在运行时选择虚函
数的过程,称为动态联编. - C++语言中,使用对象名加点“.”成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译
和链接时确定。(称为静态联编)。 - 函数重载和函数模板也是静态联编。
-
5.纯虚函数和抽象类
纯虚函数的概念:
纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一 个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。 定义纯虚函数的一般格式为: virtual 返回类型 函数名(参数表)= 0;
“=0”表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明; 函数的声明是为了在虚函 数表中保留一个位置。“=0”本质上是将指向函数体的指针定义为nullptr。
抽象类的概念:
含有纯虚函数的类是抽象类。
抽象类的使用规则:
(1)抽象类只能用作其他类的基类,不能创建抽象类的对象。
(2)抽象类不能用作参数类型、函数返回类型或显式类型转换。
(3)可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现
运行时多态。