【C++】浅谈C++多态

发布于:2025-06-13 ⋅ 阅读:(27) ⋅ 点赞:(0)

目录

1 多态的概念

2 多态

2.1 多态的条件

2.2 虚函数

 2.3 虚函数的覆盖

 2.4 虚函数的协变

2.5 override 和 final关键字

2.6 重载 / 覆盖 / 隐藏的对比

3 纯虚函数和抽象类

3.1 纯虚函数

3.2 抽象类

4 浅谈多态的原理

4.1 虚函数表指针

4.2 原理

4.2.1 多态是如何运行的

4.2.2 动态绑定和静态绑定

 4.3 虚函数表


1 多态的概念


在继承体系中同一个方法在子类和父类中的行为是不一样的。方法的行为因该取决于调用该方法的对象。这种复杂的行为称为多态——具有多种形态,及同一个方法的行为随调用者而改变

多态分为 编译时多态和运行时多态
(1)编译时多态                                                                                                                                

编译时多态主要包括函数的重载和函数模板。它们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态多态

(2)运行时多态                                                                                                                                

运行时多态存在于继承体现中,同一个方法可以传递不同的对象然而产生的的行为是不一样的。这种复杂的行为称为运行时多态

举例:

//父类
class zhangsan
{
public:	
    virtual void talk()  {
 cout << "haha" << endl; 
}
   
};

//子类
class lisi : public zhangsan
{
public:
    virtual void talk()
 {
 cout << "hehe" << endl; 
}
};

//多态的体现
void test(zhangsan& p)
{
    // 同一个方法在子类和父类中的行为是不一样的
    // 方法的行为取决于调用该方法的对象
    p.talk();
}

int main() 
{
    lisi l;
    zhangsan z;
    
    test(l); // hehe
    test(z); // haha
    return 0;
}

2 多态


2.1 多态的条件

实现多态必须满足一下两个条件:

1. 必须是基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,并且完成了虚函数重写 / 覆盖

说明:

要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到

2.2 虚函数

虚函数是C++实现多态性的核心机制。类成员函数前面加 virtual 修饰,那么这个成员函数被称为虚函数

注意:非成员函数不能加 virtual 修饰 

class test
{
public:
    //类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数
    virtual void talk()
    {
        cout << "haha" << endl;
    }
};

 2.3 虚函数的覆盖

虚函数的覆盖规则:

1. 子类中有⼀个跟父类完全相同的虚函数即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数覆盖了父类的虚函数

2. 在子类覆盖父类的过程中,子类的虚函数使用的是父类虚函数的声明和子类虚函数的定义

注意:

在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成覆盖因为继承后父类的虚函数被继承下来了在子类依旧保持虚函数属性),但是这样写不规范 

//父类
class zhangsan
{
public:	
    virtual void talk()  {cout << "haha" << endl;}
};

//子类
class lisi : public zhangsan
{
public:
    // 子类的虚函数在不加virtual关键字时,也可以构成覆盖
    // 不加virtual不规范
    virtual void talk(){cout << "hehe" << endl;}
};

//多态的体现
void test(zhangsan& p)
{
    // 同一个方法在子类和父类中的行为是不一样的
    // 方法的行为取决于调用该方法的对象
    p.talk();
}

int main() 
{
    lisi l;
    zhangsan z;
    
    test(l); // hehe
    test(z); // haha
    return 0;
}

 2.4 虚函数的协变

虚函数的协变是指派生类中重写的虚函数可以返回与基类虚函数不同的返回类型,但这些返回类型必须是指针或引用,并且派生类的返回类型必须是基类返回类型的派生类

class A
{
public:
    //协变
    virtual A* func() { cout << "A" << endl; return nullptr; }
    //构成多态
    virtual void test() { func(); }
};
class B : public A
{
public:
    //协变
    virtual B* func() { cout << "B" << endl; return nullptr; }
};
int main(int argc, char* argv[])
{
    A* a = new A;
    B* b = new B;
    a->test(); // A
    b->test(); // B
    return 0;
}

2.5 override 和 final关键字

C++11提供了override可以帮助我们检测是否重写

class A
{
public:
    void func() { cout << "A" << endl; };    
};
class B : public A
{
public:
	//报错,使用 override 声明的函数不能重写父类的成员
        //父类的 func 没有被 virtual 修饰         
	virtual void func() override { cout << "B" << endl; }
};

 如果我们不想让子类重写这个虚函数,那么可以用 final 去修饰

class A
{
public:
    //此时这个虚函数不可以被覆盖
    virtual void func() final { cout << "A" << endl; };   
};
class B : public A
{
public:
    //报错,无法覆盖被final修饰的虚函数
    virtual void func() { cout << "B" << endl; }
};

2.6 重载 / 覆盖 / 隐藏的对比

3 纯虚函数和抽象类


3.1 纯虚函数

虚函数的后面写上 = 0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现,只要声明即可

class A
{
public:
    //纯虚函数
    virtual void func() = 0;
};

3.2 抽象类

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果子类继承后不重写纯虚函数,那么子类也是抽象类

//抽象类
class A
{
public:
    //纯虚函数
    virtual void func() = 0;
};

//子类继承后不重写纯虚函数,那么子类也是抽象类
class B : public A
{
public:
};

int main()
{
    //报错,抽象类不允许实例化对象
    A a;
    //报错,抽象类不允许实例化对象
    B b;
    return 0;
}

4 浅谈多态的原理


4.1 虚函数表指针

⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

1. 虚函数表(vtable)

  • 每个包含虚函数的类都有一个虚函数表
  • 表中存储了该类所有虚函数的地址
  • 编译器在编译时创建虚函数表

2. 虚函数表指针(vptr)

  • 每个包含虚函数的类的对象都有一个隐藏的vptr成员
  • vptr指向该对象所属类的虚函数表
  • 在对象构造时自动设置正确的vptr值
class A
{
public:
    //虚函数
    virtual void func() { cout << "A" << endl; }
private:
    int a;
};

int main()
{
    A a;
    cout << sizeof(a);//8
    return 0;
}

4.2 原理

4.2.1 多态是如何运行的

class A
{
public:
    virtual void func() { cout << "A" << endl; }
	
};
class B : public A
{
public:
    virtual void func() { cout << "B" << endl; }
};

//构成多态
void test(A* a) { a->func(); }

int main()
{
    A* a = new A;
    B* b = new B;
    
    test(a);//A
    test(b);//B
    return 0;
}

⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,在继承过程中父类的虚函数表也会被继承,当子类的虚函数满足重写时,就会覆盖掉从父类继承下来的虚函数表中的指定虚函数地址

总之多态在运行时,到指定的对象的虚函数表中找到对应的虚函数的地址,进行调用

4.2.2 动态绑定和静态绑定

对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定

class A
{
public:
    void func() { cout << "A" << endl; }

};

void test(A* a) { a->func(); }

int main()
{
    A* a = new A;
    test(a);
    return 0;
}

底层汇编: 

     a->func(); 
     //这里的func不是虚函数,不满足多态的条件
     //这里就是静态绑定,编译器直接调用函数的地址
00782681  mov         ecx,dword ptr [a]  
00782684  call        A::func (078150Fh) 

满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定

class A
{
public:
    virtual void func() { cout << "A" << endl; }

};

void test(A* a) { a->func(); }

int main()
{
    A* a = new A;
    test(a);
    return 0;
}

底层汇编:

    a->func(); 
    //a是指针+func是虚函数满⾜多态条件
    //这⾥就是动态绑定,编译在运⾏时到a指向对象的虚函数表中确定调⽤函数地址
006E1E91  mov         eax,dword ptr [a]  
006E1E94  mov         edx,dword ptr [eax]  
006E1E96  mov         esi,esp  
006E1E98  mov         ecx,dword ptr [a]  
006E1E9B  mov         eax,dword ptr [edx]  
006E1E9D  call        eax  
006E1E9F  cmp         esi,esp  
006E1EA1  call        __RTC_CheckEsp (06E1311h) 

 4.3 虚函数表

基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表

派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的

派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址

派⽣类的虚函数表中包含,(1)基类的虚函数地址,(2)派⽣类重写的虚函数地址完成覆盖,派⽣类⾃⼰的虚函数地址三个部分

虚函数表本质是⼀个存虚函数指针的指针数组

问:虚函数存在哪的?

虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中

问:虚函数表存在哪的?

这个问题严格说并没有标准答案C++标准并没有规定。(VS是存在常量区)