多态分为编译时多态和运行时多态。编译时多态就是在编译阶段就能绑定要执行的那个函 数。运行时多态要等到运行到调用的那条语句时,根据指针/引用所绑定的对象,来决定执行哪
个函数,我们要讲的虚函数就是运行时多态,是 C++中非常重要的一个东西。
讲虚函数之前,我们先来脑洞讨论一下,我们都知道继承了,那么我们来设计一个王者荣耀的英雄,,首先每个英雄都是一个人物,它们都可以行走,都可以释放技能,那么就可以设计一个人物基类,基类的成员我们定义英雄公有的一些属性,例如血量值,移动速度,攻击力等等。那么这个时候我们就想一个问题,每个英雄类都去继承这个人物基类,那么相对于每个英雄的一些行为,按理来说形式都是一样的,不过方式不一样,例如,每个游戏都可以释放1,2,3,技能,这个是每个英雄都会的一个行为形式,不过不同英雄的技能方式会不一样,那我们可不可以将这个这个公有行为放在人物基类中呢?然后当游戏类去继承我的人物基类时,只需要重新去实现我这个行为就好了。虚函数就可以完成这样的一个工作。
虚函数定义语法:
class 类名
{
virtual 函数返回值 函数名(函数参数列表);
}
例:
class A
{
public:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
virtual void fun() //虚函数
{
cout << "a:" << a << "b:" << b << endl;
}
};
设置为虚函数后,那么在类的头部就会生成一个虚表指针,它指向一张虚表,虚表中保存了各虚函数的地址,也就是说,上面的类 A 在内存中的情况如下
纯虚函数
纯虚函数就是在定义虚函数的时候加上 = 0;那么该函数就是一个纯虚函数,语法如下:
class 类名
{
virtual 函数返回值 函数名(函数参数列表) = 0;
}
示例:
class A
{
public:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
virtual void fun() = 0; //纯虚函数
};
定义纯虚函数需要注意的地方:
1.有纯虚函数就是抽象类,这样的类只能做基类,因为它不能实例化对象
2.纯虚函数不需要在本类中实现,子类继承了该抽象类一定要去实现这些纯虚函数,不然继承下来的子类也将变成抽象类。
虚函数与继承
当基类中有虚函数时,子类继承它他时,同样也会继承它的虚表指针,对于虚函数,子类可以重写他(纯虚函数必须重写),重写了虚函数后,那么子类继承下来的虚表中将会替换被重写的虚函数地址。例如:
class A
{
public:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
virtual void fun() //虚函数
{
cout << "A" << endl;
}
};
class B:public A
{
private:
int c;
int d;
public:
B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
virtual void fun() //重写虚函数
{
cout << "B" << endl;
}
};
上面类 B 继承了类 A,如果类 B 不去对虚函数 fun 进行重写的话,那么继承下来的虚表地址中保存虚函数地址应该是 A 中实现的 fun,但是如果类 B 对 fun 进行了重写,那么虚表地址中保存的虚函数地址就是 B 中实现的这个 fun.
当然,重写基类虚函数时,子类中加不加 virtual 关键字都是一样的,基类加就好了。
补充:
需要注意的是,当基类定义虚函数为私有成员的时候,子类继承它也会进行对他进行重写,但是就无法通过基类指针去动态访问这个虚函数了,但是如果基类的虚函数不是私有的,而派生类重写的虚函数是私有的,这个时候是可以通过基类指针去动态访问这个虚函数的,这个也很好理解,虚函数是存在在虚表中的,所以不管这个虚函数是什么类型的成员都会被子类重写,毕竟子类都会继承虚表,但是对于访问而言,通过函数名字去访问虚函数,就需要访问权限,打个比喻,基类虚函数为私有成员,派生类重写的私有成员为公有成员,那么不能通过基类指针去访问这个虚函数,因为这个虚函数在基类中是私有的,但是可以通过派生类指针去访问这个虚函数,因为这个虚函数在派生类中是公有的。同理,如果基类的虚函数是公有的,派生类继承过去的虚函数是私有的,那么可以通过基类指针去访问这个虚函数,而且基类指针指向派生类,那么访问的虚函数内容就是派生类中的内容,因为不是直接通过函数名去访问派生类中这个虚函数的,而是因为虚表替换了虚函数内容,通过虚表访问的,所以尽管派生类中这个虚函数是私有的,通过基类指针也是可以访问的。
例:
class A
{
public:
int a;
int b;
public:
A(int x,int y):a(x),b(y){}
virtual void fun() //公有虚函数
{
std::cout << "A" << std::endl;
}
};
class B:public A
{
private:
int c;
int d;
public:
B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
private:
virtual void fun() //私有重写虚函数
{
std::cout << "B" << std::endl;
}
};
int main(int argc, char const *argv[])
{
B b(1,2,3,4);
A *p = &b;
p->fun();
return 0;
}
虚函数与析构函数
构造函数和析构函数是不能被继承的,因为构造函数和析构函数负责的是他们对应的那一个类成员的初始化和销毁。对于构造函数而言,构造函数是用来初始化对象的,不应该将构造
函数指定为虚函数,原因如下:
(
1
)当子类创建对象时,会首先调用子类的构造函数,然后再调用父类的构造函数(即子类不继承父类的构造函数),因此,构造函数为虚函数没有意义。
(
2
)虚函数的调用,要用到虚函数表指针,指针属于类的对象上,实例化对象需要调用构造函数,如果构造函数为虚函数,前后相矛盾。
而
基类的析构函数
应当被定义为虚函数
,
原因是:
(
1
)可以保证当我们
new
一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
例如下面例子,A 是基类,B 是继承 A 类。
A *p = new B(1,2,3,4);
delete p;
当我们用基类指针去 new 子类时,如果基类析构函数不是虚函数的话,那么释放对象的时 候,就自会调用基类的析构函数,不会调用子类的析构函数,当我们将基类的析构函数定义为虚函数之后,这个时候我们释放对象的时候就会先调用子类的析构函数再调用基类的析构函数
不能被定义为虚函数的元素:
1.构造函数
2.静态成员函数
3.友元函数
虚函数的使用
其实,如果你没有真正接触 C++程序的话,你并不会明白 C++中的虚函数到底用处在哪里,但是不得不说的是虚函数在 C++中真的是非常的重要,一个 C++程序的设计好坏就主要看虚函数的设计。
c++中的虚函数之所以能实现运行时多态是因为当我们子类继承了基类后,我们不直接使用子类去做一些操作,因为使用子类去做操作的话,那么你在编译阶段就已经绑定了这个子类的操作函数,就不会是运行时多态了。真正的用法是用基类指针去操作,那么我这个基类指针指向哪个子类,就会运行哪个子类中的操作函数,例如:
class Person //人物类
{
private:
int m_blood; //血量
int m_person; //蓝量
public:
Person(int x,int y):m_blood(x),m_person(y){}
virtual ~Person(){};
virtual void skill1() = 0;//1 技能
virtual void skill2() = 0;//2 技能
virtual void skill3() = 0;//3 技能
};
//英雄类
class JiaLuo:public Person //伽罗
{
public:
JiaLuo(int x,int y):Person(x,y){}
~JiaLuo(){}
virtual void skill1()//1 技能
{
cout << "伽罗释放渡灵之箭" << endl;
}
virtual void skill2()//2 技能
{
cout << "伽罗释放静默之箭" << endl;
}
virtual void skill3()//3 技能
{
cout << "伽罗释放纯净之域" << endl;
}
};
class SunCe:public Person //孙策
{
public:
SunCe(int x,int y):Person(x,y){}
~SunCe(){}
virtual void skill1()//1 技能
{
cout << "船夫释放劈风斩浪" << endl;
}
virtual void skill2()//2 技能
{
cout << "船夫释放惊涛骇浪" << endl;
}
virtual void skill3()//3 技能
{
cout << "船夫释放长帆破浪" << endl;
}
};
void fun(Person *P) //释放技能,具体释放谁的技能要看参数传什么对象,这样就做
到了运行时多态
{
P->skill1();
P->skill2();
P->skill3();
}
int main(int argc, char const *argv[])
{
Person *p = new JiaLuo(1290,1270); //创建一个伽罗类
Person *q = new SunCe(3450,2370); //创建一个孙策类
fun(p); //伽罗放技能
fun(q); //船夫放技能
return 0;
}
通过虚表访问虚函数
我们知道,当类中含有虚函数时,就会在类的头部生成一个虚表指针,虚表指针指向的虚表的地址,虚表中保存着虚函数的地址,那么知道这些我们就可以通过虚表指针来访问虚函数,例如:
class A
{
public:
virtual void fun1()
{
cout << "fun1" << endl;
}
virtual void fun2()
{
cout << "fun2" << endl;
}
virtual void fun3()
{
cout << "fun3" << endl;
}
};
int main(int argc, char const *argv[])
{
A a;
typedef void (*p)();
p f1,f2,f3;
//&a ---虚表指针地址
//*(unsigned long *)(&a) ---虚表地址
//*(unsigned long *)(*(unsigned long *)(&a)) ---第一个虚函数地址
//(p)((unsigned long *)(*(*(unsigned long *)(&a)) + n) ---第 n-1 个虚函数地址
//通过虚表指针访问 fun1
f1 = (p)(*(unsigned long *)(*(unsigned long *)(&a)));
f1();
//通过虚表指针访问 fun2
f2 = (p)(*((unsigned long *)(*(unsigned long *)(&a))+1));
f2();
f3 = (p)(*((unsigned long *)(*(unsigned long *)(&a))+2));
f3();
return 0;
}