C++中的多态与多重继承实现与Java的区别

多态问题

笔者校招面试时被问到了著名问题「C++ 与 Java 如何实现多态」,然后不幸翻车。过于著名反而没有去准备,只知道跟虚函数表有关。面试之后比较了 C++ 和 Java 多态的实现的异同,一并记录在这里。

C++ 多态的虚指针实现

首先讨论 C++. 多态也即子类对父类成员函数进行了重写 (Override) 后,将一个子类指针赋值给父类,再对这个父类指针调用成员函数,会调用子类重写版本的成员函数。简单的例子:

class Parent1 {
  public:
  virtual void sayHello() { printf("Hello from parent1!\n"); }
};

class Child : public Parent1 {
  public:
  virtual void sayHello() { printf("Hello from child!\n"); }
};

int main() {
  Parent1 *p = new Child();
  p->sayHello();  // get "Hello from child!"
}

首先需要明白,对于底层实现而言,成员函数就是第一个参数为对象指针的函数,编译器自动将对象指针添加到函数参数中并命名为 this 指针,除此之外与普通函数并无本质不同。对于非多态的成员函数调用,与非成员函数调用过程基本是一致的,根据参数列表(参数列表中包含对象指针类型)和函数名在编译时确定实际调用的函数。

为了实现多态,不能只根据对象指针类型推断函数签名,也即例子中,p->sayHello() 这一行代码在执行时不能只根据 p 的类型确认调用的函数应该是 Parent::sayHello 还是 Child:sayHello。在多态机制下,每个类父类和子类都需要在其数据结构中多携带一个指针,这个指针指向该类的虚函数表。

类的虚函数表也即所有可能发生重写的函数指针表,对象创建时根据其实际类型决定其虚函数指针指向的虚函数列表。如在上文的例子中,Parent1 和 Child 类的虚函数列表都只有一个函数,分别是 Parent1::sayHello Child::sayHello. 编译器在编译时将会把函数调用翻译为「引用虚函数表中的第 N 个函数」这样的指令,比如本例中翻译为「引用虚函数表中第一个函数」。在运行时读取虚函数表中真正的函数指针。运行时 CPU 代价基本是一次指针解引用和一次下表访问。

Parent1 和 Child 对象都没有自定义的数据结构。运行以下代码能够确认 Parent1 和 Child 对象的真实数据结构大小都是 8 字节,也即只有虚函数列表指针。把 Parent1 和 Child1 对象作为 64 位整数输出,可以看到 p1, p2 的值相同,p3 与前两者不同。这个值也即相应类的虚函数表地址。

Parent1* p1 = new Parent1();
Parent1* p2 = new Parent1();
Parent1* p3 = new Child();
printf("sizeof Parent1: %d, sizeof Child: %d\n",
  sizeof(Parent1), sizeof(Child));
printf("val on p1: %lld\n", *(int64_t*)p1);
printf("val on p2: %lld\n", *(int64_t*)p2);
printf("val on p3: %lld\n", *(int64_t*)p3);

C++ 多态与多重继承

有一个非常有意思的问题:C++ 发生多重继承时,如何支持多态。刚刚提到,多态的原理是编译器将成员函数调用编译为「引用虚函数表中第 N 个函数」,虚函数表在对象数据结构中的位置和要调用虚函数列表中的第几个函数在编译时都是需要确定的。多重继承对象如果只有一个虚函数列表,那不同父类的虚函数列表中的位置就要发生冲突。如果有多个虚函数列表,编译时就难以确定虚函数列表指针在数据结构中的位置。C++ 采取了非常精妙的做法:将所有父类的数据结构(包括虚指针列表)在该对象的数据结构上依次排列,该对象的指针正常指向数据结构起始位置。当指针发生类型转换时,C++ 编译器会对指针的值尽可能的进行调整,使其指向该指针类型应该对应的位置。指针的值在这个过程中发生了变化

比如,Child 类继承了 Parent1, Parent2 两个类,则在 Child 指针转换为 Parent1 指针时,不对指针的值进行调整,因为 Parent1 是 Child 的第一个父类。但将 Child 转换为 Parent2 时,需要将指针指增加 Parent1 数据结构长度的值,使指针指向对应 Parent2 数据结构开始位置。在本例子中,Parent1 数据结构只有虚函数列表指针,在 64 位机器上长度为 8. 因此,在 Child 指针转换为 Parent2 指针时,其值增加了 8.

class Parent1 {
  public:
  virtual void sayHello() { printf("Hello from parent1!\n"); }
};

class Parent2 {
  public:
  virtual void sayHi() { printf("Hi from Parent2!\n"); }
};

class Child : public Parent1, public Parent2 {
  public:
  virtual void sayHello() { printf("Hello from child!\n"); }
  virtual void sayHi() { printf("Hi from child!\n"); }
};

int main() {
  Child *p = new Child();
  printf("size of Child: %d", sizeof(Child));
  printf("pointer val as Child*: %lld\n", int64_t(p));
  printf("pointer val as Parent1*: %lld\n", int64_t((Parent1*)p));
  printf("pointer val as Parent2*: %lld\n", int64_t((Parent2*)p));
}

运行这段代码,会发现 Child 数据结构大小增长到 16,也即两个指针。并且指针的值在后两次类型转换时是不同的,在 64 位机器上相差 8 个字节,也即 Parent1 的数据结构大小。另外如果将 p 转换成 Void 指针再转换为 Parent 指针,此时编译器就不能正确推断这个偏移量,此时就会发生未定义行为。

这个特性其实说明了一个非常有意思的事实:C++ 编译器在编译时能够推断指针的偏移量,那么编译器也应该可以推断该指针指向对象的真实类型。那么,既然可以编译时推断对象真实类型,那要虚函数表又有何用?直接推断正确的函数调用不就可以了吗?问题在于,如果真的在编译时推断多态函数调用,就意味着要为不同类型的对象生成不一样的二进制代码。同一行代码,根据指针值的不同,产生的函数调用不同。这样一来也意味第三方库需要提供源代码,来进行相关的推断,类似于模板库。这都是不可接受的,因此虚函数列表仍然有必要。借助虚函数列表,使用指针的代码能够生成一致的机器码。
从另一个角度理解,编译器在编译一个完整的 App 时确实能够推断所有变量的真实类型,但这需要联系过多上下文。编译一段代码却需要这段代码输入参数的除类型之外的上下文信息,并根据上下文信息生成不同的二进制文件,这是不可接受的。

Java 多态比较

由于 Java 的多态机制比 C++ 简单,理论上可以使用 C++ 的机制实现 Java 多态。但 C++ 跟 Java 有一点决定性的不同:C++ 要求父类成员方法必须有 Virtual 关键字修饰时才能被重写。这就意味着编译器在编译父类时就能确认那些函数可能被重写,于是可以对不可能重写的函数直接在编译时决定调用的具体函数,而对可能重写的函数使用虚指针表处理。而 Java 的方法默认都是可以重写的,因此可以认为 Java 方法调用都需要经过查询虚函数列表的过程,会比 C++ 不重写函数多一点开销。

Java 不支持多重继承,但 Java 支持接口 Interface, 接口跟多重继承有相似之处,不能简单的使用一个虚函数表查找。类需要为其实现的每个 Interface 生成一个虚函数列表,跟 C++ 的情况类似。OpenJDK 文档指出,在类定义中找到 Interface 的虚函数列表的办法是很粗暴的:在类实现的所有 Interface 列表中遍历查找。文档中指出,真正的多重继承是罕见的,通常可以归结为单继承。对此遍历过程可能有各种优化,笔者没有深入了解。

思考 Java 和 C++ 的一点不同:C++ 没有运行时类型,由编译器在编译时尽力保证指针指向的位置有对象正确的数据结构。将子类指针赋值给父类指针变量时,编译器尽力对其进行调整,但如果发生了 Void 指针赋值等,则编译器无法保证指针指向的位置有正确的对象数据结构。这一步只要语法上没有错误,就不会立即报错,编译器也无法确认是否会发生问题,一定要等到该指针实际进行解引用等发生异常才会报错。Java 有运行时类型,在将对象赋值给不同的类型的变量时,会在运行时进行类型检查,如果没有正确的类型继承关系,会在赋值时报错。

另外,对比 Java 的 Interface 和 C++ 的多重继承,会发现 Interface 的运行时时间开销要比 C++ 多重继承大得多。但是 C++ 多重继承需要为每个父类附加一个指针,并且编译器在编译时需要完成更多的工作。Java 相对于 C++ 是更加「强类型」的语言。

到此这篇关于C++中的多态与多重继承实现与Java的区别的文章就介绍到这了,更多相关C++ 多态与多重继承内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++多重继承引发的重复调用问题与解决方法

    本文实例讲述了C++多重继承引发的重复调用问题与解决方法.分享给大家供大家参考,具体如下: 前面简单介绍了一个C++多重继承功能示例,这里再来分析一个多重继承引发的重复调用问题,先来看看问题代码: #include "stdafx.h" #include<stdlib.h> #include<iostream> using namespace std; class R//祖先类 { private: int r; public: R(int x = 0):r(x

  • C++多重继承与虚继承分析

    本文以实例形式较为全面的讲述了C++的多重继承与虚继承,是大家深入学习C++面向对象程序设计所必须要掌握的知识点,具体内容如下: 一.多重继承 我们知道,在单继承中,派生类的对象中包含了基类部分 和 派生类自定义部分.同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义成员的子对象.下面是一个多重继承关系图: class A{ /* */ }; class B{ /* */ }; class C : public A { /* */ }

  • 深入解析C++中类的多重继承

    C++类的多继承 在前面的例子中,派生类都只有一个基类,称为单继承.除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类. 多继承容易让代码逻辑复杂.思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java.C#.PHP 等干脆取消了多继承.想快速学习C++的读者可以不必细读. 多继承的语法也很简单,将多个基类用逗号隔开即可.例如已声明了类A.类B和类C,那么可以这样来声明派生类D: class D: public A, private B, protected C{ //类D新

  • C++实现的多重继承功能简单示例

    本文实例讲述了C++实现的多重继承功能.分享给大家供大家参考,具体如下: 多重继承 1. 多重继承即一个类继承了多个基类的属性. 2. 多重继承下派生类的构造函数必须同时负责所有基类构造函数的调用, 3. 派生类构造函数的参数个数,必须满足多个基类初始化的需要. 4. 在多重继承下,当建立派生类对象时,系统首先调用各个基类的构造函数,调用顺序与定义派生类时指定的基类顺序一致. 多重继承范例: #include <iostream> /* run this program using the c

  • C++ 多重继承和虚拟继承对象模型、效率分析

    一.多态 C++多态通过继承和动态绑定实现.继承是一种代码或者功能的传承共享,从语言的角度它是外在的.形式上的,极易理解.而动态绑定则是从语言的底层实现保证了多态的发生--在运行期根据基类指针或者引用指向的真实对象类型确定调用的虚函数功能!通过带有虚函数的单一继承我们可以清楚的理解继承的概念.对象模型的分布机制以及动态绑定的发生,即可以完全彻底地理解多态的思想.为了支持多态,语言实现必须在时间和空间上付出额外的代价(毕竟没有免费的晚餐,更何况编译器是毫无感情): 1.类实现时增加了virtual

  • C++中的多态与多重继承实现与Java的区别

    多态问题 笔者校招面试时被问到了著名问题「C++ 与 Java 如何实现多态」,然后不幸翻车.过于著名反而没有去准备,只知道跟虚函数表有关.面试之后比较了 C++ 和 Java 多态的实现的异同,一并记录在这里. C++ 多态的虚指针实现 首先讨论 C++. 多态也即子类对父类成员函数进行了重写 (Override) 后,将一个子类指针赋值给父类,再对这个父类指针调用成员函数,会调用子类重写版本的成员函数.简单的例子: class Parent1 { public: virtual void s

  • PHP5中实现多态的两种方法实例分享

    在PHP5中,变量的类型是不确定的,一个变量可以指向任何类型的数值.字符串.对象.资源等.我们无法说PHP5中多态的是变量. 我们只能说在PHP5中,多态应用在方法参数的类型提示位置.一个类的任何子类对象都可以满足以当前类型作为类型提示的类型要求.所有实现这个接口的类,都可以满足以接口类型作为类型提示的方法参数要求.简单的说,一个类拥有其父类.和已实现接口的身份. 通过实现接口实现多态 复制代码 代码如下: <?phpinterface User{ // User接口    public fun

  • Go语言实现类似c++中的多态功能实例

    前言 Go语言作为编程语言中的后起之秀,在博采众长的同时又不失个性,在注重运行效率的同时又重视开发效率,不失为一种好的开发语言.在go语言中,没有类的概念,但是仍然可以用struct+interface来实现类的功能,下面的这个简单的例子演示了如何用go来模拟c++中的多态的行为. 示例代码 package main import "os" import "fmt" type Human interface { sayHello() } type Chinese s

  • 从汇编看c++中的多态详解

    在c++中,当一个类含有虚函数的时候,类就具有了多态性.构造函数的一项重要功能就是初始化vptr指针,这是保证多态性的关键步骤. 构造函数初始化vptr指针 下面是c++源码: class X { private: int i; public: X(int ii) { i = ii; } virtual void set(int ii) {//虚函数 i = ii; } }; int main() { X x(1); } 下面是对应的main函数汇编码: _main PROC ; 16 : in

  • C++中的多态与虚函数的内部实现方法

    1.什么是多态 多态性可以简单概括为"一个接口,多种行为". 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可以用自己的方式去响应共同的消息.所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数.这是一种泛型技术,即用相同的代码实现不同的动作.这体现了面向对象编程的优越性. 多态分为两种: (1)编译时多态:主要通过函数的重载和模板来实现. (2)运行时多态:主要通过虚函数来实现. 2.几个相关概念 (1)覆盖.

  • 进一步理解Java中的多态概念

    多态性有两种: 1)编译时多态性 对于多个同名方法,如果在编译时能够确定执行同名方法中的哪一个,则称为编译时多态性. 2)运行时多态性 如果在编译时不能确定,只能在运行时才能确定执行多个同名方法中的哪一个,则称为运行时多态性. 方法覆盖表现出两种多态性,当对象获得本类实例时,为编译时多态性,否则为运行时多态性,例如: XXXX x1 = new XXXX(参数列表); //对象获得本类实例,对象与其引用的实例类型一致 XXX xx1 = new XXX(参数列表); x1.toString();

  • Java中的多态用法实例分析

    本文实例讲述了Java中的多态用法.分享给大家供大家参考.具体分析如下: 多态,是面向对象的程序设计语言最核心的特征.封装性.继承性都比较简单,所以这里只对多态做一个小小的笔记... 1.什么是多态? 多态意味着一个对象可以多重特征,可以在特定的情况下,表现出不同的状态,从而应对不同的属性和方法.在Java中,多态的实现指的是使用同一个实现接口,以实现不同的对象实例. 例如,我们定义一个Parent类,再定义一个getName()方法返回一个字符串,定义一个形参为Parent类型的成员方法doS

  • Java编程—在测试中考虑多态

    面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承是为了重用父类代码.两个类若存在IS-A的关系就可以使用继承.,同时继承也为实现多态做了铺垫.那么什么是多态呢?多态的实现机制又是什么?请看我一一为你揭开: 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底

  • C#中的多态深入理解

    封装.继承.多态,面向对象的三大特性,前两项理解相对容易,但要理解多态,特别是深入的了解,对于初学者而言可能就会有一定困难了.我一直认为学习OO的最好方法就是结合实践,封装.继承在实际工作中的应用随处可见,但多态呢?也许未必,可能不经意间用到也不会把它跟"多态"这个词对应起来.在此抛砖引玉,大家讨论,个人能力有限,不足之处还请指正. 之前看到过类似的问题:如果面试时主考官要求你用一句话来描述多态,尽可能的精炼,你会怎么回答?当然答案有很多,每个人的理解和表达不尽相同,但我比较趋向这样描

  • JS中的多态实例详解

    多态在面向对象编程语言中是十分重要的.在JAVA中是通过继承来得到多态的效果.如下: public abstract class Animal { abstract void makeSound(); // 抽象方法 } public class Chicken extends Animal{ public void makeSound(){ System.out.println( "咯咯咯" ); } } public class Duck extends Animal{ publi

随机推荐