不同对象完成同样的事,所能能达到的结果不同。
1.在继承的前提下构成多态的条件:
a.调用函数的对象必须是指针或者引用;
b.被调用的函数必须是虚函数(在类成员函数前加virtual关键字的),且完成了虚函数的重写。
2.虚函数重写:
若派生类中有跟基类函数名、参数、返回值都相同的虚函数,则称子类的虚函数重写基类的虚函数,也称作虚函数的覆盖。
eg:
class A
{
public:
virtual void func()
{
cout << "基类虚函数" << endl;
}
};
class B : public A
{
public :
virtual void func()
{
cout << "子类虚函数" << endl;
}
};
void test(A& a)//引用的调用
{
a.func();
}
void main()
{
A a;
B b;
test(a);//父类调用
test(b);//子类调用
}
运行结果:
a.协变(虚函数重写的一个例外):
重写的虚函数的返回值可以不同,但是必须分别是1.基类指针和派生类指针2.基类
引用和派生类引用,这俩种情况也构成重写。
eg:
//协变,返回类型为基类指针和派生类指针
class A{};
class B :public A{};
class X{
public:
virtual A* f(){
return new A;
}
};
class Y :public X{
public:
virtual B* f(){
return new B;
}
};
b.不规范的重写:
派生类重写的成员函数前,可以不加virtual关键字也是构成重写的。但尽量不要这样写。
原因:派生类继承了基类的虚函数,因此其依旧保持虚函数属性。
//不规范的重写
class A{
public:
virtual void f(){}
};
class B :public A{
public:
void f(){}//没有写virtual关键字
};
c.析构函数的重写问题:
如果基类的析构函数是虚函数,那么派生类的析构函数就会重写基类的析构函数。
原因:虽然他们的函数名不同,但编译器对其进行了特殊处理,在编译后,他们的名称都会被处理为destructor,因此就构成了重写。
注意: 有基于此,若基类的析构函数为虚函数,那么也要将派生类的析构函数写成虚函数。
//析构函数重写问题
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
//virtual ~Student() { cout << "~Student()" << endl; }
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多
//态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
运行截图:
左边为不重写的运行截图,右边为重写之后的截图。
由此可见,当基类析构函数为虚函数时,只有重写了才满足多态,而且不满足多态的析构还会造成内存泄漏(Student的析构函数没有被调到,因此没有清理资源)。
d.接口继承与实现继承的区别:
实现继承:
普通函数的继承,即:派生类继承了基类的函数,可以使用函数,继承的是函数的实现。
接口继承:
虚函数的继承,即:派生类继承了基类的虚函数,其实继承的是接口,为了形成重写,已以达成多态。这样会有一定的副作用,例如:派生类的虚函数参数列表中的缺省参数就毫无意义。
注意: 有基于接口继承的特性,如果不实现多态,就不要把函数定义为虚函数。
class A
{
public:
virtual void func(int val = 1)
{
cout << "A:" << val << endl;
}
virtual void test()
{
func();
}
};
class B :public A
{
public:
virtual void func(int val = 0)//接口继承//因此此处的缺省参数会使用基类的
{
cout << "B:" << val << endl;
}
};
void main()
{
A* b = new B;//构成多态,指向的是B,因此调用的为B的函数
(*b).func();
}
运行结果:
e.重载、覆盖(重写)、隐藏(重定义)概念的对比:
在虚函数后边写上=0,则这个函数称为纯虚函数。包含纯虚函数的类叫做抽象类(或接口类),抽象类不能实例化出对象,派生类继承后也不能实例化处对象,只有将纯虚函数重写,派生类才能实例化出对象。
抽象即在现实中找不到对应的实体。
纯虚函数规范了派生类必须重写,更体现了接口继承。
//抽象类
class A
{
public:
virtual void f() = 0;
};
class B :public A
{
public:
//如果不重写,B就不可以实例化出对象//B b;这句就会报错
virtual void f(){
cout << "我重写了" << endl;
}
};
void main()
{
B b;
b.f();
}
这是俩个C++11提供的用来修饰虚函数的关键字。
建议使用虚函数+override的方式来强制写虚函数。
原因:虚函数的意义就是为了实现多态,如果没有重写,那虚函数也就没有了意义。
用法示例:
final关键字:用其修饰的虚函数不能被派生类重写
override关键字:修饰派生类虚函数强制完成重写,如果没有重写,则编译报错
class A{
public:
virtual void f(int a){}
};
class B :public A{
public:
virtual void f(int b)override{}
};
void main()
{
B b;
b.f(1);
}
5.1.虚函数表
一个含有虚函数的类中至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中(简称虚表)。本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
//x86平台下运行
class A{
public:
virtual void F(){}//含有一个虚函数表指针
private:
int _b;
};
void main(){
cout<<sizeof(A)<<endl;
//输出结果:8
}
派生类的虚表: a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基
类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写更符合语法的叫法,覆盖符合原理层的叫法。
注意:
1. 虚表存的是虚函数指针,不是虚函数,虚函数存在代码段(与普通函数一样)
2. 虚函数指针指向的是这个虚函数的地址
3. 对象中存的不是虚表,而是虚表指针,虚表存储在代码段(VS平台下)
5.2 多态的原理:
多态是在运行时到指向对象的虚表中查找要调用的虚函数地址来进行调用的。这样就满足了多态的指向谁就调用谁的虚函数的目的。
如下图所示:
5.3 动态绑定与静态绑定
静态绑定: 又称前期绑定(早绑定),在程序编译期间确定了函数地址,也称为静态多态,比如:函数重载
动态绑定: 又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型去到虚表中找到虚函数的地址,从而调用具体的函数,也称为动态多态。