【C++】影响动态多态的静态联编与对象切割

简介: 在使用C++动态多态时,有时候会出错误,这里讲述其中的两个原因

【C++】影响动态多态的静态联编与对象切割

I - 动态多态


多态就是利用虚函数和继承将多种功能实现从架构中独立出来,方便维护和修改。

多态的表现形式

  • 重载:同一个类中,相同的方法名对应着不同的方法实现
  • 重写:子类重写父类的方法
  • 抽象类:在面向对象语言中,一个类的方法只给出了标准,而没有给出具体的方法实现,这样的类就是抽象类
  • 接口:抽象类组成的集合就是接口

1.1 - 动态多态的原理


实现 C++ 的多态,利用了动态绑定的技术,该技术的核心是 虚函数表

虚函数表 是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。编译器会为每个包含虚函数的类维护一个虚函数表。普通的成员函数(非虚函数)调用不需要经过虚表,所以需表中的元素不包括普通函数的指针。

虚表是属于类的,而不是属于某个具体的对象,一个类只有一个虚表。同一个类实例化的不同对象实际上指向同一张虚表。

为了让每个包含虚表的类的对象都拥有⼀个虚表指针,编译器在类中添加了⼀个指针,*__vptr ,⽤来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会⾃动被设置为指向类的虚表。

所以实例化的对象,不要轻易使用

memset(&myobject, 0, sizeof(MyClass));

会将虚表指针置空,在运行时容易出现异常退出。

1.2 - 问题代码示例


看下列一段代码,补充两处空缺,使得输出为:

A::Fun
C::Do

#include <iostream>
using namespace std;

class A 
{
   
private:
    int nVal;
public:
      void Fun(){
    cout << "A::Fun" << endl;} 
      void Do(){
    cout << "A::Do" << endl;} 
};

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

class C : public B 
{
   
public:
    void Do(){
    cout << "C::Do" << endl; } 
    void Fun() {
    cout << "C::Fun" << endl; } 
};

void Call( /* 填空 1 */ )
{
    
    p.Fun();
    p.Do(); 
}

int main(int argc, char * argv[])
{
   
    C c;
    /* 填空 2 */; 
    return 0;
}

II - 题目解析

通常可能认为题目出错了 ,如下两种想法。

2.1 - 常规思路

无非就是 9 种排列组合,但是均无法获得题目中需要的结果。

// 填空 - 1
void Call(A p);
void Call(B p);
void Call(C p);
// 以及 填空 - 2
Call(c);  
Call((B)c);  
Call((A)c);

影响动态多态的有两种情况:静态联编 与 对象切割

静态联编 (static binding) :函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,此时就算是使用向上类型转换 (派生类的对象),受到调用的依旧是基类方法。静态联编可通过添加 virtual 关键字恢复动态多态。

静态联编问题,代码示例

class Base
{
   
// ...
public:
    void Run() {
    std::cout << "Base::Run" << std::endl; }
// ...
};

class Derivated : public Base
{
   
// ...
public:
    void Run() {
    std::cout << "Derivated::Run" << std::endl; }
// ...
}

int main(int argc, char * argv[])
{
   
    Base* obj = new Derivated;
    obj->Run(); // Base::Run
}

示例代码,需要将基类 Base 的 run 函数,声明为 virtual 即可解决问题。

强转会造成对象切割,派生类强转为基类对象时会发生对象切割,使之仍然调用基类方法。此处属于强转造成的对象切割。

  1. C++与C相同,是静态编译型语言

  2. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象:
    所以编译器认为基类指针指向的是基类对象。

  3. 由于程序没有运行,所以不可能知道基类指针指向的具体是基类对象还是
    派生类对象,从程序安全的角度,编译器假设基类指针只指向基类对象,因此编
    译的结果为调用基类的成员雨数。这种特性就是静态联编。

  4. 多态的发生是动态联编,在程序执行的时候判断具体基类指针应该调用的方法。

2.2 - 动态多态

或可能认为题目在考察虚函数和动态多态的问题,动态多态的调用是 基类指针指向派生类对象,这里 Call 里边的常理实现应该是使用指针

void Call(A * p)
{
   
    p->Fun();
    p->Do();
}

int main()
{
   
    C c;
    Call(&c);
}

因为在 B中 Do 函数被声明为 virtual 所以 C 中即便没有写关键字 virtual 也为虚函数,基类指针指向子类对象,就会调用 子类的实现,即 C::Do
而 Fun 函数一直都不为虚函数,则会一直调用指针对应的函数。即 A::Fun

III - 正确答案

题目没有出错,这个是在考察引用不会造成对象切割的问题。

void Call(B & p); //1
Call(c); //2

参数为父类引用,传递时不会造成对象实现被切割,虚函数指针指向无误 即 C::Do,但是造成父类函数覆盖子类函数。即 A::Fun。

这也是为何我们要求在继承标准库异常 std::exception ,实现自定义异常时, try ... catch 的参数使用异常的基类类型引用传递,而不使用基类类型值传递。

错误示例

try 
{
   
    // ...
}
catch (std::exception ex)
{
   
    // handle exception
}

正确使用,需要使用引用,防止造成对象切割。

try
{
   
    // ...
}
catch (std::exception & ex)
{
   
    // handle exception
}
目录
相关文章
|
24天前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
32 0
|
19天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
21 4
|
19天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
18 4
|
20天前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
15 0
|
24天前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
28 0
|
24天前
|
编译器 C++
C++之类与对象(3)(下)
C++之类与对象(3)(下)
30 0
|
24天前
|
编译器 C++
C++之类与对象(3)(上)
C++之类与对象(3)
16 0
|
24天前
|
编译器 C++
C++之类与对象(2)
C++之类与对象(2)
28 0
|
24天前
|
存储 编译器 C++
C++之类与对象(1)(下)
C++之类与对象(1)(下)
25 0
|
24天前
|
存储 C++
C++之类与对象(1)(上)
C++之类与对象(1)(上)
21 0