乐趣区

关于c++:C多态

什么是多态

  • 多态是指调用同一个函数时,呈现不同的成果。

多态分类

  • 动态的多态:函数重载就是动态的多态
  • 动静的多态:用父类的指针或援用调用虚函数

虚函数

  • 虚函数是指:类的成员函数被 virtual 润饰的函数(一般函数不能用 virtual 润饰)
  • 留神:

    • 内联函数理论是没有地址的,然而如果内联函数被 virtual 润饰,则编译器会疏忽函数的内联属性,把它当做失常函数应用。
    • 动态成员函数是不能作为虚函数的,因为动态函数调用时不会隐式传入 this 指针,所以动态成员函数无奈放入虚函数表。
    • 构造函数不能是虚函数,因为构造函数阶段,虚表还没有初始化,所以构造函数不能是虚函数。
  • 有虚函数的类,编译器会主动减少一个成员变量(虚表指针,指向虚表 (函数指针数组))

    • 例如下列类,sizeof(A) == 12
    • 理论的成员变量为:1 个指针类型,一个 int 类型,一个 char 类型
    • 依据对齐规定,所以类的大小为 12 字节。
class A
{
public:
    virtual void test()   // 这个就是虚函数
    {}
    int a;
    char b;
}

继承中形成多态的条件

  • 必须通过父类的指针或援用调用虚函数
  • 被调用的函数必须是虚函数,且子类必须对虚函数进行笼罩。

    • 留神:虚函数的笼罩,必须是返回类型、函数名、参数列表都雷同才行。
    • 如果返回值是父类、子类的指针或援用时,返回值不同也能够形成虚函数笼罩(协变)
    • 如果父类的析构函数为虚函数,则子类的析构函数只有定义(无论是否 virtual 润饰),都与父类的虚析构函数形成笼罩。(析构函数自身函数名就不雷同)
class A
{
public:
    virtual void test()   // 这个就是虚函数
    {}}

class B
{
  public:
    virtual void test()   // 笼罩父类的虚函数
    {}}

虚函数的重写(笼罩)

  • 子类会继承父类的虚函数,如果子类有父类虚函数的重写时,就会将重写的虚函数地址笼罩父类虚函数的地址。
  • 留神:虚函数的重写,是从新实现父类的虚函数,所以重写的函数参数要应用父类的参数。
  • 下列代码运行后果为:B->0
class A
{
public:
    virtual void test1(int a = 0)
    {cout << "A->" << a << endl;}
    virtual void test()   // 父类函数被子类调用
    {test1(); // 这里其实隐含了一个 this,即 this->test1()
          // 子类调用 test,传递指针 p,所以这里是 p->test1()}
    
};

class B :public A
{
public:
    virtual void test1(int a = 1)
    {
        cout << "B->" << a << endl; 
              // 实践上这里应该是输入 B ->1
              // 然而这个函数重写父类的函数,是实现父类的函数
              // 所以 a 在这里要应用父类的 a = 0
    }
};


int main()
{
    B* p = new B;  // 创立子类
    p->test();    // 子类调用父类成员
    
    return 0;
}

虚析构函数的笼罩

  • 下列 类 A 的析构函数被申明为虚函数,子类的析构函数与父类形成笼罩
class A
{
public: 
    virtual ~A()    // 父类的析构函数申明为虚函数
    {cout << "~A" << endl;}
};

class B :public A
{
public:
    ~B()   // 与父类的虚析构函数形成笼罩
    {cout << "~B" << endl;}
};

int main()
{
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2;
    return 0;
}
  • 形成笼罩后,delete p2 时,会先调用子类的析构函数,再调用父类的析构函数。
  • 如果没有形成笼罩,则 delete p2 时只会调用父类的析构函数。

关键字 final 和 override

  • final:润饰虚函数,使虚函数不能被笼罩。(加在父类中)

    • final 润饰类时,示意这个类不能被继承。
  • override:润饰虚函数,检测是否正确笼罩。(加在子类中)
class A
{
public:
    virtual void test()final   // 使该虚函数不能笼罩
    {}}

class B: public A
{
public:
    virtual void test()override // 查看虚函数是否正确笼罩
    {}}

重载、笼罩、暗藏的比照

  • 重载:

    • 两个函数在同一作用域
    • 函数名雷同,参数不同
  • 笼罩:

    • 两个函数别离在父类和子类中
    • 函数名、参数、返回值必须雷同(协变除外)
    • 两个函数必须是虚函数
  • 暗藏:

    • 父类和子类领有同名成员,当调用这些同名成员时,无奈确定调用的是父类还是子类
    • 所以子类不能间接调用父类的成员,必须指明这个成员属于哪个类。
    • 即子类成员屏蔽了父类成员的间接拜访(这就是暗藏)

    纯虚函数(抽象类)

  • 在虚函数前面加上 =0,则这个函数为纯虚函数。
  • 蕴含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
  • 子类继承抽象类后也不能实例化出对象,只有笼罩(重写)纯虚函数,子类能力实例化出对象。
  • 纯虚函数强制子类必须笼罩函数,更加体现出接口继承。
  • 纯虚函数不须要实现性能,只须要申明(重写时再实现性能)
class A  // 抽象类
{
public:
    virtual void test() = 0;   // 纯虚函数}

虚表与虚表指针

  • 虚表全称虚函数表,用来寄存类中所有虚函数的指针,一个类只会有一份虚表(无论创立多少个对象)
  • 虚表会在编译阶段生成,在运行阶段应用。
  • 虚表指针全称虚函数表指针,指向虚函数表。
  • 虚表指针是编译器主动创立的,在程序运行时,会依据对象类型去初始化虚表指针,从而让虚表指针指向所属类的虚表。
  • 留神:对象的前四个字节就是虚表的地址,虚表寄存在常量区(虚表是不能人为更改的)

多态的示例

class A
{
public:
    virtual void test() // 父类虚函数
    {cout << "A 的类" << endl;}
};

class B :public A
{
public:
    virtual void test()   // 虚函数的笼罩
    {cout << "B 的类" << endl;}
};

void f1(A& p)  // 应用援用
{p.test();
}

void f2(A p)
{p.test();
}

int main()
{
    A aa;
    B bb;
    f1(aa);   // 输入 A 的类
    f1(bb);   // 输入 B 的类

    f2(aa);   // 输入 A 的类
    f2(bb);   // 输入 A 的类
    return 0;
}
  • void f1(A& p) 应用父类的援用,援用无需拷贝,间接将传递的参数拿来应用,所以能够别离调用父类的 test() 和子类的 test()。
  • void f2(A p) 应用父类的对象,相当于值传递,须要拷贝出一个长期变量,而拷贝时是不会拷贝虚表指针,虚表指针在程序运行时依据对象类型去初始化(对象 p 的类型是 A,所以虚表指针指向 A 的虚表)
  • 所以 void f2(A p) 函数中,无论传递哪种子类的对象,最终调用的虚函数都是会是 A 父类的虚函数。
退出移动版