C++ 继承与多态

虚函数和非虚函数都支持继承和重写,唯一的区别在于动态绑定/静态绑定,然后实现方面也有区别罢了



0. 继承与切片

  • 在子类对象本身(不是指针!!!)被强制类型类型转换为父类对象时,所有子类独有的东西都会被drop掉!!!!!

  • 同时这个对象指向虚函数表的指针会改为指向父类的虚函数表!!!

  • eg:

    1
    2
    3
    B b(1, 2);
    A a = b; //此时a的虚函数表是父类的表,不是子类 B 的!!!

  • 注:那怎样才能安全呢?这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换(以后详谈).



I. 父类指针与子类指针

  • 1.为了实现多态,C++ 中父类指针指向子类对象是自动转换的!!

  • 2.但是子类指针指向父类对象可以强制类型转换(但是非常不安全,万一访问了父类虚函数表中没有的东西会报错)

  • 3.指针类型本质上只会限制访问虚函数表,而且是编译器限制,技术上是完全可以访问的

    • 比如以下例子:一个父类指针A* pa指向一个子类对象b, 那么除非刻意绕过限制,pa是不能访问b中独有的虚函数的,即使pa始终是根据B类型的虚函数表在调用(意味着如果pa调用的是b中被重写的虚函数,则还是会访问被重写的虚函数)


II. 虚函数继承与实现


一. 虚函数表(vtable)与虚函数的储存位置

  • 1.虚函数本身和非虚函数,正常的类外的函数一样都是储存在代码段中的

  • 1.5. 补充: 虚函数和非虚函数都有一个隐藏的 *this 指针参数

  • 2.虚函数表:

    • 0.虚函数表本体是储存在C++程序的只读数据段中的(意味着运行时每个类的虚函数表不变!!
    • 1.虚函数表本身是一个函数指针数组,指向代码段中的虚函数。
    • 2.虚函数在编译时创建一个类只有一个虚函数表
    • 3.类实例化后的对象本身只有一个隐藏指针指向所属类的虚函数表,所以所有的同类对象共用一张虚函数表!!!

二. 派生类的虚函数表的生成

  • 1.C++在编译时

  • 2.创建的时候首先复制一份基类的虚函数表

  • 3.如果有重写的虚函数,那么改变虚函数表指针,指向重写后的虚函数(i.e.,原来基类的虚函数直接被覆盖了)

  • 3.5. 补充: 重写只有同名同参数才叫重写,同名不同参数是算作重载的子类独有的虚函数(和原本继承来的函数是两个函数)

  • 4.扩展自己独有的虚函数

  • 5.特别注意,由于虚函数表在编译时创建,所以运行时子类对象根本不知道被覆写的虚函数是什么!!!!


三. 总结: C++虚函数是动态绑定的

  • i.e., 根据动态类型(对象本身的类型)而不是静态类型(指针的类型)决定调用的!!

III. 非虚函数(注意与静态成员区分)


一. 非虚函数的储存位置

  • 非虚函数本质上就是一个有一个隐藏了(*this)指针参数的函数

  • 1.非虚函数不存在重写的概念,如果你基类和派生类中有同名,同参数表的函数也会被视作两个完全不同的函数

  • 2.同样的,非虚函数本身也是储存在代码段中的


静态绑定是在编译时由编译器确定的,不是运行时确定的!!

实际上,C++ 中的函数查找机制首先在对象的静态类型对应的类中查找函数,如果找不到,才会沿着继承链向上查找。

  • 本质上还是 *this 指针类型不同导致的重载(沿着继承链向上查找也是有道理的,因为子类的*this指针能指向父类的*this指针)

对于多重继承和同名函数,C++ 有明确的规则来处理这种情况:

多重继承中的同名非虚函数

  • 1.首先查找静态类型的类:当你通过一个对象或指针调用一个函数时,编译器首先在该对象或指针的静态类型对应的类中查找匹配的函数。如果找到了,就使用这个函数。

  • 2.多重继承导致的歧义:如果一个派生类继承自多个基类,并且这些基类有同名的非虚函数,那么直接调用这个同名函数会导致歧义。编译器无法决定应该调用哪个基类的函数,因为它们在派生类中都是有效的候选项。

  • 3.解决歧义:为了解决这种歧义,你需要在派生类中明确指定使用哪个基类的函数,或者在派生类中重定义该函数。你可以使用作用域解析运算符(::)来指定要调用的基类函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Base1 {
    public:
    void func();
    };

    class Base2 {
    public:
    void func();
    void test();
    };

    class Derived : public Base1, public Base2 {
    // ...
    void test();
    };

    Derived d;
    Base2* p2 = &d;
    p2->test(); // 这种就是静态绑定,调用的依然是Base2的test()
    d.Base1::func(); // 明确调用 Base1 中的 func
    d.Base2::func(); // 明确调用 Base2 中的 func
  1. 隐藏规则:如果派生类重定义了同名的非虚函数,那么这个函数会隐藏所有基类中的同名函数。在这种情况下,即使是通过作用域解析运算符也无法访问被隐藏的基类函数。

总结

在C++的多重继承中,如果存在同名的非虚函数,你需要明确指定希望调用哪个基类的函数,以避免歧义。编译器总是首先在对象的静态类型的类中查找函数,如果有必要,再沿着继承链向上查找。这种机制确保了函数调用的明确性和一致性,但同时也要求程序员对继承关系和函数覆盖(或隐藏)规则有清晰的理解。


三. 总结:C++非虚函数是静态绑定的!!

i.e., 根据静态类型(指针的类型)而不是动态类型(对象本身的类型)决定调用的!!



IV 静态函数与友元函数的区别

  • 同其他语言的静态函数一样, C++静态函数没有 *this 指针。派生类对象可以访问基类的静态函数

是的,静态函数(Static Functions)和友元函数(Friend Functions)在C++中有一些相似之处,但它们的用途和行为是不同的。下面是它们的主要区别和用途:

  1. 静态函数:
  • 静态函数属于类,而不是类的对象。这意味着它们没有*this指针。
  • 静态函数只能访问类的静态成员(静态变量和静态函数)。
  • 它们主要用于当函数的行为不依赖于对象状态时。
  • 静态函数可以通过类名直接调用,不需要创建类的实例。
  1. 友元函数:
  • 友元函数不是类的一部分,它可以是一个全局函数、另一个类的成员函数或者一个静态函数。
  • 友元函数虽然不是类的成员,但它可以访问类的所有私有(private)和保护(protected)成员。
  • 友元函数的主要用途是允许某些非成员函数或其他类的成员函数访问私有成员。
  • 友元函数的声明在类定义中,但其实现在类外部。

总结来说,静态函数是类的一部分,主要用于访问静态成员,而友元函数则允许外部函数访问类的私有和保护成员。虽然它们看起来可能相似,但它们服务于不同的目的。



V 成员变量的继承

对于成员变量的继承,在C++中的情况有所不同,不像成员函数那样有静态、虚拟等区分。成员变量的继承主要取决于它们的访问权限(即public, protected, 和 private)以及继承类型(public, protected, 或 private继承)。这里是一些关键点:

  1. 公有继承(Public Inheritance):
  • 父类的公有成员变量在子类中保持公有。
  • 父类的保护成员变量在子类中保持保护。
  • 父类的私有成员变量也会被继承,但不能在子类中直接访问。
  1. 保护继承(Protected Inheritance):
  • 父类的公有和保护成员变量在子类中都变成保护成员变量。
  • 私有成员变量的规则与公有继承相同。
  1. 私有继承(Private Inheritance):
  • 父类的公有和保护成员变量在子类中都变成私有成员变量。
  • 私有成员变量的规则与公有继承相同。
  1. 成员变量遮蔽(Shadowing):
  • 如果子类声明了一个与父类同名的成员变量,这会在子类中遮蔽掉父类的同名成员变量。
  1. 静态成员变量:
  • 静态成员变量属于类本身,而非类的实例。
  • 在基类和所有派生类中,静态成员变量是共享的。
  • 如果派生类声明了一个与基类同名的静态成员变量,这会隐藏基类的静态成员变量,但并不常见或推荐这样做。

成员变量的继承更多关注于访问控制和继承类型,而不像成员函数那样有静态和虚拟的区别。重要的是理解继承类型和访问修饰符如何影响子类对父类成员变量的访问。



VI 父类指针?子类对象?

非常简单,记住指针类型是静态类型,指向的对象类型是动态类型就行了

具体原理也不难,C++检索指针调用的步骤是:

  • 首先,在编译时C++会为每个类创建一个类似 “属性表” (在代码段里面)的东西,记录这个类的变量,方法等,和虚函数表一道被创建,且运行时不会变。

  • 接着,同样在编译时,进行非虚函数的绑定

    • 由于class的非虚函数func(class* this)
    • 首先C++ 把 class->func() 变为 func(class), 然后当做普通函数重载与调用(其中重载是虚函数继承的实现方法(子类指针可以传入父类指针参数,如果没有更好的参数表的话))
  • 然后,虚函数运行调用的就是指向对象的虚函数表了。