一、概念
多态为C++中面向对象三大特征之一,通俗地讲,就是多种形态。作用为完成某个行为,当不同(具有继承关系)的对象去完成时会产生不同的状态。
二、虚函数
1、概念
在基类中被virtual修饰并且在派生类中重新定义的类成员函数。
2、重写(覆盖)
- 派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名字和参数列表完全相同,称派生类的虚函数重写(覆盖)了基类的虚函数。
- 在重写基类虚函数时,派生类的虚函数不加virtual关键字修饰虽然也可以构成重写。因为继承结束后,基类的虚函数被继承下来了,在派生类中依旧保持虚函数的属性。但是该种写法不是很规范,一般还是会加上virtual关键字进行修饰。
3、重载、覆盖(重写)、隐藏(重定义)的对比
(1)重载
- 两个函数在同一作用域。
- 函数名相同,参数(类型或者数量)不同。
(2)重写(覆盖)
- 两个函数分别在基类和派生类的作用域中。
- 函数名、参数、返回值都必须相同(协变除外)。
- 两个函数都必须是虚函数。
(3)隐藏(重定义)
- 两个函数分别在基类和派生类的作用域中。
- 函数名相同。
- 两个基类和派生类的同名函数不构成重写就是重定义。
三、多态的定义与使用
1、多态的构成条件
- 派生类必须继承基类。
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写(覆盖)。
2、示例代码
class Base
{
public:
virtual void Print()
{
cout << "Base::Print()" << endl;
}
};
class Derive :public Base
{
virtual void Print()
{
cout << "Derive::Print()" << endl;
}
};
//void Func(Base* b)
void Func(Base& b)
{
//b->Print();
b.Print();
}
int main()
{
Base b;
Derive d;
Func(b);
Func(d);
/*Func(&b);
Func(&d);*/
return 0;
}
3、运行结果
四、虚函数重写的两个例外
1、协变
(1)概念
当派生类重写基类虚函数时,重写的虚函数与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用。返回类型除了基类和派生类(或具有其他继承关系的基类和派生类)的指针或者引用,其他的类型都不构成协变。
(2)示例代码
class A{};
class B :public A{};
class Base
{
public:
virtual A* Func1()
{
return new A;
}
virtual Base* Func2()
{
return new Base;
}
};
class Derive :public Base
{
virtual B* Func1()
{
return new B;
}
virtual Derive* Func2()
{
return new Derive;
}
};
2、析构函数的重写
(1)概念
- 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
- 虽然基类与派生类的析构函数名字不同。但可以理解为编译器对它们的析构函数名称做了特殊处理,编译后它们的析构函数名称统一处理成destructor。
- 当基类指针指向派生类时,只有派生类Student的析构函数重写了Person的析构函数,delete该基类指针时,调用析构函数,才能构成多态。这样才能保证指针指向的对象正确的调用析构函数,否则它将调用基类的析构函数而不是调用派生类的析构函数。
(2)示例代码
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derive :public Base
{
public:
virtual ~Derive()
{
cout << "~Derive()" << endl;
}
};
int main()
{
Base* p1 = new Base;
Base* p2 = new Derive;
delete p1;
delete p2;
return 0;
}
(3)运行结果
五、override和final
1、override
(1)作用
检查用override修饰的派生类虚函数是否重写了基类的某个虚函数,如果没有重写则报错。
(2)代码
class Base
{
public:
virtual void Func()
{
cout << "Base::Func()" << endl;
}
};
class Derive :public Base
{
public:
virtual void Func1() override
{
cout << "Derive::Func()" << endl;
}
};
(3)编译器报错
2、final
(1)作用
修饰虚函数,表明该虚函数不能被重写。
(2)代码
class Base
{
public:
virtual void Func()final
{
cout << "Base::Func()" << endl;
}
};
class Derive :public Base
{
public:
virtual void Func()
{
cout << "Derive::Func()" << endl;
}
};
(3)编译器报错
六、抽象类
1、概念
- 在虚函数的后面写上 =0 ,则这个虚函数称为纯虚函数。包含纯虚函数的类称为抽象类,也称为接口类。
- 抽象类不能实例化出对象,派生类继承抽象类后也不能实例化出对象。只有派生类重写纯虚函数,派生类才能实例化出对象。
- 纯虚函数规范了派生类必须重写基类的纯虚函数,体现了接口继承。
2、接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用基类的函数,继承的是函数的实现。
- 纯虚函数的继承是一种接口继承,派生类继承的是基类该纯虚函数的接口,目的是为了重写它,达成多态。所以,如果不实现多态,不要把函数定义成纯虚函数。
3、代码
class Base
{
public:
virtual void Func() = 0;
};
class Derive :public Base
{
public:
virtual void Func()
{
cout << "Derive::Func()" << endl;
}
};
int main()
{
//Base b;
Derive d;
d.Func();
return 0;
}
4、运行结果
5、错误代码
class Base
{
public:
virtual void Func() = 0;
};
class Derive :public Base
{
public:
/*virtual void Func()
{
cout << "Derive::Func()" << endl;
}*/
};
int main()
{
Base b;
Derive d;
d.Func();
return 0;
}
6、编译器报错
七、虚函数表
1、代码
class Base
{
virtual void Func1()
{
cout << "Base::virtual void Func1()" << endl;
}
/*virtual void Func2()
{
cout << "Base::virtual void Func2()" << endl;
}
void Func3()
{
cout << "Base::void Func3()" << endl;
}*/
private:
int _b = 1;
};
//class Derive :public Base
//{
// virtual void Func1()
// {
// cout << "Derive::virtual void Func1()" << endl;
// }
//private:
// int _d = 2;
//};
int main()
{
Base b;
cout << sizeof(b) << endl;
//Derive d;
return 0;
}
2、运行结果与调试窗口查看
3、说明
- 在上方代码中,用sizeof函数计算具有虚函数的类时,得出的结果是16bytes(64位平台下的VS编译器)。
- 在调式窗口下,对象b中除了_b成员,还有一个__vfptr放在对象的前面(__vfptr在对象中的位置与编译器有关,此处为VS编译器下的情况)。
- 对象中的__vfptr指针称为虚函数表指针。其中,v代表virtual,f代表function。
- 一个含有虚函数的类中至少都有一个虚函数表指针(简称为虚表指针),因为虚函数的地址要被放到虚函数表中,虚函数表简称为虚表,虚表指针指向虚表。
4、将代码中的注释部分展开后的调试窗口查看
5、说明
- 派生类对象由两部分构成,一部分是从基类继承下来的成员,另一部分是自己的成员。派生类对象也有一个虚表指针。
- 基类b对象和派生类d对象的虚表是不一样的。在派生类中,Func1完成了重写,所以d的虚表中存的是派生类重写的Derive::Func1,即派生类的Func1函数覆盖了基类的Func1函数。所以虚函数的重写(语法层次)也称为覆盖(原理层次)。
- 因为基类中Func2函数被派生类继承下来后是虚函数,所以它会被放进派生类的虚表中。但因为派生类没有对它进行重写,所以Func2函数在该虚表中和在基类中的一致。
- 函数Func3也会被继承下来,但因为它不是虚函数,所以不会放进派生类的虚表中。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况下,这个指针数组最后面放了一个nullptr。
6、派生类虚表的生成
- 先将基类中的虚表内容拷贝到派生类虚表中,如果派生类重写了基类中的某个虚函数,就用派生类重写的虚函数覆盖虚表中基类的虚函数。
- 派生类新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后面。
7、虚函数与虚表
- 因为虚表中存的是指向虚函数的指针,而不是虚函数。所以,虚函数和普通函数一样,都存在代码段中。
- 对象中存的不是虚表,而是虚表指针。在VS编译器下,虚表存在代码段中。
八、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),指在程序编译期间就确定了程序的行为,也称为静态多态,如函数重载。
- 动态绑定又称后期绑定(晚绑定),指在程序运行期间,根据具体拿到的类型确定程序的具体行为,进而调用具体的函数,也称为动态多态。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕