【C++基础】多态

C++ 多态分类及实现:

  • 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
  • 子类型多态(Subtype Polymorphism,运行期):虚函数
  • 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
  • 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转

一、静态多态(编译期/早绑定)

函数重载

class A{
public:
    void do(int a);
    void do(int a, int b);
};

二、动态多态(运行期期/晚绑定)

  • 虚函数:用 virtual 修饰成员函数,使其成为虚函数
  • 动态绑定:当使用基类的引用或指针调用一个虚函数时将发生动态绑定(基类的指针指向派生类对象)

注意:

  • 可以将派生类的对象赋值给基类的指针或引用,反之不可
  • 普通函数(非类成员函数)不能是虚函数
  • 静态函数(static)不能是虚函数
  • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
  • 内联函数不能是表现多态性时的虚函数,(内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联)
class Shape{                     // 形状类
public:
    virtual double calcArea(){
        ...
    }
    virtual ~Shape();
};
class Circle : public Shape     // 圆形类
{
public:
    virtual double calcArea();
    ...
};
class Rect : public Shape       // 矩形类
{
public:
    virtual double calcArea();
    ...
};
int main()
{   
    //基类的指针指向派生类对象
    Shape * shape1 = new Circle(4.0);
    Shape * shape2 = new Rect(5.0, 6.0);
    shape1->calcArea();         // 调用圆形类里面的方法
    shape2->calcArea();         // 调用矩形类里面的方法
    delete shape1;
    shape1 = nullptr;
    delete shape2;
    shape2 = nullptr;
    return 0;
}

三、虚函数的延伸内容

1、虚函数和纯虚函数的区别

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

  • 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。

  • 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义

  • 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  • 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  • 虚函数的定义形式:virtual{};纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

2、虚函数指针和虚函数表

虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
虚函数表:在程序只读数据段(.rodata section,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

3、虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。

底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

4、抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类。纯虚函数在基类有声明而没有定义,所以这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
    抽象类有如下几个特点:
    1)抽象类只能用作其他类的基类,不能建立抽象类对象
    2)抽象类不能用作参数类型、函数返回类型或显式转换的类型
    3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
  • 所有成员都是 public
  • 没有定义任何构造函数
  • 没有类内初始化
  • 没有基类,也没有 virtual 函数

5、虚继承和虚函数

1、相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
2、不同之处:

虚继承

  • 虚基类依旧存在继承类中,只占用存储空间
  • 虚基类表存储的是虚基类相对直接继承类的偏移

虚函数

  • 虚函数不占用存储空间
  • 虚函数表存储的是虚函数地址

五、关于内联函数

内联函数是一种以空间换时间的策略,可以减少调用函数的额外时间开销,但将各处内联函数展开增加了实际代码量,从而也就增加了空间的开销
函数设定为内联,这只是个请求,而编译器可以忽略它。编译器可能会展开内联函数调用,也可能不展开

虚函数(virtual)可以是内联函数(inline)吗?

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

虚函数内联使用

#include   
using namespace std;
class Base{
public:
	inline virtual void who(){
		cout << "I am Base\n";
	}
	virtual ~Base() {}
};
class Derived : public Base{
public:
	inline void who()  // 不写inline时隐式内联
	{
		cout << "I am Derived\n";
	}
};

int main()
{
	// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
	Base b;
	b.who();

	// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
	Base *ptr = new Derived();
	ptr->who();

	// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
	delete ptr;
	ptr = nullptr;

	system("pause");
	return 0;
} 

你可能感兴趣的:(C++,c++,算法,开发语言)