详解C++中基类与派生类的转换以及虚基类

C++基类与派生类的转换
在公用继承、私有继承和保护继承中,只有公用继承能较好地保留基类的特征,它保留了除构造函数和析构函数以外的基类所有成员,基类的公用或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公用成员函数访问基类的私有成员。因此,公用派生类具有基类的全部功能,所有基类能够实现的功能, 公用派生类都能实现。而非公用派生类(私有或保护派生类)不能实现基类的全部功能(例如在派生类外不能调用基类的公用成员函数访问基类的私有成员)。因此,只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

不同类型数据之间在一定条件下可以进行类型的转换,例如整型数据可以赋给双精度型变量,在赋值之前,把整型数据先转换成为双精度型数据,但是不能把一个整型数据赋给指针变量。这种不同类型数据之间的自动转换和赋值,称为赋值兼容。现在要讨论 的问题是:基类与派生类对象之间是否也有赋值兼容的关系,可否进行类型间的转换?

回答是可以的。基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面。

1) 派生类对象可以向基类对象赋值

可以用子类(即公用派生类)对象对其基类对象赋值。如

  A a1; //定义基类A对象a1
  B b1; //定义类A的公用派生类B的对象b1
  a1=b1; //用派生类B对象b1对基类对象a1赋值

在赋值时舍弃派生类自己的成员。也就是“大材小用”,如图

实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

请注意,赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。假设age是派生类B中增加的公用数据成员,分析下面的用法:
    a1.age=23;  //错误,a1中不包含派生类中增加的成员
    b1.age=21;  //正确,b1中包含派生类中增加的成员

应当注意,子类型关系是单向的、不可逆的。B是A的子类型,不能说A是B的子类型。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,理由是显然的,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。

2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化

如已定义了基类A对象a1,可以定义a1的引用变量:

  A a1; //定义基类A对象a1
  B b1; //定义公用派生类B对象b1
  A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化

这时,引用变量r是a1的别名,r和a1共享同一段存储单元。也可以用子类对象初始化引用变量r,将上面最后一行改为

  A& r=b1; //定义基类A对象的引用变量r,并用派生类B对象b1对其初始化

或者保留上面第3行“A& r=a1;”,而对r重新赋值:

  r=b1; //用派生类B对象b1对a1的引用变量r赋值

注意,此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。

3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。

如有一函数:

  fun: void fun(A& r) //形参是类A的对象的引用变量
  {
    cout<<r.num<<endl;
  } //输出该引用变量的数据成员num

函数的形参是类A的对象的引用变量,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参:

   fun(b1);

输出类B的对象b1的基类数据成员num的值。

与前相同,在fun函数中只能输出派生类中基类成员的值。

4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。

[例] 定义一个基类Student(学生),再定义Student类的公用派生类Graduate(研究生), 用指向基类对象的指针输出数据。本例主要是说明用指向基类对象的指针指向派生类对象,为了减少程序长度,在每个类中只设很少成员。学生类只设num(学号),name(名字)和score(成绩)3个数据成员,Graduate类只增加一个数据成员pay(工资)。程序如下:

#include <iostream>
#include <string>
using namespace std;
class Student//声明Student类
{
public:
  Student(int, string,float); //声明构造函数
  void display( ); //声明输出函数
private:
  int num;
  string name;
  float score;
};
Student::Student(int n, string nam,float s) //定义构造函数
{
  num=n;
  name=nam;
  score=s;
}
void Student::display( ) //定义输出函数
{
  cout<<endl<<"num:"<<num<<endl;
  cout<<"name:"<<name<<endl;
  cout<<"score:"<<score<<endl;
}
class Graduate:public Student //声明公用派生类Graduate
{
public:
 Graduate(int, string ,float,float); //声明构造函数
 void display( ); //声明输出函数
private:
 float pay; //工资
};
//定义构造函数
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
void Graduate::display() //定义输出函数
{
  Student::display(); //调用Student类的display函数
  cout<<"pay="<<pay<<endl;
}
int main()
{
  Student stud1(1001,"Li",87.5); //定义Student类对象stud1
  Graduate grad1(2001,"Wang",98.5,563.5); //定义Graduate类对象grad1
  Student *pt=&stud1; //定义指向Student类对象的指针并指向stud1
  pt->display( ); //调用stud1.display函数
  pt=&grad1; //指针指向grad1
  pt->display( ); //调用grad1.display函数
}

下面对程序的分析很重要,请大家仔细阅读和思考。

很多读者会认为,在派生类中有两个同名的display成员函数,根据同名覆盖的规则,被调用的应当是派生类Graduate对象的display函数,在执行Graduate::display函数过程中调用Student::display函数,输出num,name,score,然后再输出pay的值。

事实上这种推论是错误的,先看看程序的输出结果:

num:1001
name:Li
score:87.5

num:2001
name:wang
score:98.5

前3行是学生stud1的数据,后3行是研究生grad1的数据,并没有输出pay的值。

问题在于pt是指向Student类对象的指针变量,即使让它指向了grad1,但实际上pt指向的是grad1中从基类继承的部分。

通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。所以pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数,所以只输出研究生grad1的num,name,score3个数据。

如果想通过指针输出研究生grad1的pay,可以另设一个指向派生类对象的指针变量ptr,使它指向grad1,然后用ptr->display()调用派生类对象的display函数。但这不大方便。

通过本例可以看到,用指向基类对象的指针变量指向子类对象是合法的、安全的,不会出现编译上的错误。但在应用上却不能完全满足人们的希望,人们有时希望通过使用基类指针能够调用基类和子类对象的成员。如果能做到这点,程序人员会感到方便。后续章节将会解决这个问题。办法是使用虚函数和多态性。

C++虚基类详解
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如非常经典的菱形继承层次。如下图所示:

类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自 A-->B-->D 这一路,另一份来自 A-->C-->D 这一条路。

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突,而且很少有这样的需求。

为了解决这个问题,C++提供了虚基类,使得在派生类中只保留间接基类的一份成员。

声明虚基类只需要在继承方式前面加上 virtual 关键字,请看下面的例子:

#include <iostream>
using namespace std;
class A{
protected:
  int a;
public:
  A(int a):a(a){}
};
class B: virtual public A{ //声明虚基类
protected:
  int b;
public:
  B(int a, int b):A(a),b(b){}
};
class C: virtual public A{ //声明虚基类
protected:
  int c;
public:
  C(int a, int c):A(a),c(c){}
};
class D: virtual public B, virtual public C{ //声明虚基类
private:
  int d;
public:
  D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
  void display();
};
void D::display(){
  cout<<"a="<<a<<endl;
  cout<<"b="<<b<<endl;
  cout<<"c="<<c<<endl;
  cout<<"d="<<d<<endl;
}
int main(){
  (new D(1, 2, 3, 4)) -> display();
  return 0;
}

运行结果:

a=1
b=2
c=3
d=4

本例中我们使用了虚基类,在派生类D中只有一份成员变量 a 的拷贝,所以在 display() 函数中可以直接访问 a,而不用加类名和域解析符。

请注意派生类D的构造函数,与以往的用法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

有的读者会提出:类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

最后请注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。

可以看到:使用多重继承时要十分小心,经常会出现二义性问题。上面的例子是简单的,如果派生的层次再多一些,多重继承更复杂一些,程序员就很容易陷人迷 魂阵,程序的编写、调试和维护工作都会变得更加困难。因此很多程序员不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。也正由于这个原因,C++之后的很多面向对象的编程语言(如Java、Smalltalk、C#、PHP等)并不支持多重继承。

(0)

相关推荐

  • 实例讲解C++编程中的虚函数与虚基类

    虚函数 ① #include "stdafx.h" #include <iostream> using namespace std; class B0//基类B0声明 { public: void display(){cout<<"B0::display()"<<endl;}//公有成员函数 }; class B1: public B0//公有派生类B1声明 { public: void display(){cout<<

  • C++中基类和派生类之间的转换实例教程

    本文实例讲解了C++中基类和派生类之间的转换.对于深入理解C++面向对象程序设计有一定的帮助作用.此处需要注意:本文实例讲解内容的前提是派生类继承基类的方式是公有继承,关键字public.具体分析如下: 以下程序为讲解示例: #include<iostream> using namespace std; class A { public: A(int m1, int n1):m(m1), n(n1){} void display(); private: int m; int n; }; voi

  • 浅谈C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Object.如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异.另外,一定要保证基类的完整性.实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object.举个栗子: class A { int b; char c; }

  • 详谈C++中虚基类在派生类中的内存布局

    今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的原理如下所示: 在C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.mem和p->mem在实现和效率上有什么不同. 答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件: (1).obj 是一个虚拟继承的派生类的对象 (2).mem是从虚拟基类派

  • 详解 Java中日期数据类型的处理之格式转换的实例

    详解 Java中日期数据类型的处理之格式转换的实例 概要: 日期以及时间格式处理,在Java中时间格式一般会涉及到的数据类型包括Calendar类和Date类. Date类: 1.Date类型转String类型(以时间格式1970-01-01 01:01:01为例) //yyyy-MM-dd HH:mm:ss表示24时间进制 SimpleDateFormat sDateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); String

  • 详解IDEA中Debug的使用和进制转换问题

    1.Debug模式 1.1 什么是Debug模式 是供程序员使用的程序调试工具,它可以用于查看程序的执行流程,也可以用于追踪程序执行过程来调试程序. 1.2 Debug介绍与操作流程 如何加断点 选择要设置断点的代码行,在行号的区域后面单击鼠标左键即可 如何运行加了断点的程序 在代码区域右键Debug执行 看哪里 看Debugger窗口 看Console窗口 点哪里 点Step Into (F7)这个箭头,也可以直接按F7 如何删除断点 选择要删除的断点,单击鼠标左键即可 如果是多个断点,可以每

  • 详解C++中基类与派生类的转换以及虚基类

    C++基类与派生类的转换 在公用继承.私有继承和保护继承中,只有公用继承能较好地保留基类的特征,它保留了除构造函数和析构函数以外的基类所有成员,基类的公用或保护成员的访问权限在派生类中全部都按原样保留下来了,在派生类外可以调用基类的公用成员函数访问基类的私有成员.因此,公用派生类具有基类的全部功能,所有基类能够实现的功能, 公用派生类都能实现.而非公用派生类(私有或保护派生类)不能实现基类的全部功能(例如在派生类外不能调用基类的公用成员函数访问基类的私有成员).因此,只有公用派生类才是基类真正的

  • 详解C++中String类模拟实现以及深拷贝浅拷贝

    详解C++中String类模拟实现以及深拷贝浅拷贝 在C语言中/C++中,字符串是一个应用很广泛的类型,也是很基础的类型,C语言并没有直接处理字符串的操作而是采用字符指针和字符串数组进行操作,而在C++中标准库为我们封装了一个字符串的类供我们使用,使用需要#inlcude <string>头文件.我们也可以自己模拟实现一个简单的String类. 在模拟实现String类的过程中,不可避免的会遇到深拷贝浅拷贝的问题,下面就深拷贝浅拷贝做一个简介.所谓深拷贝浅拷贝,简单来说就是浅拷贝只是简单的将值

  • 详解Java 中的嵌套类与内部类

    详解Java 中的嵌套类与内部类 在Java中,可以在一个类内部定义另一个类,这种类称为嵌套类(nested class).嵌套类有两种类型:静态嵌套类和非静态嵌套类.静态嵌套类较少使用,非静态嵌套类使用较多,也就是常说的内部类.其中内部类又分为三种类型: 1.在外部类中直接定义的内部类. 2.在函数中定义的内部类. 3.匿名内部类. 对于这几种类型的访问规则, 示例程序如下: package lxg; //定义外部类 public class OuterClass { //外部类静态成员变量

  • 详解JAVA中使用FTPClient工具类上传下载

    详解JAVA中使用FTPClient工具类上传下载 在Java程序中,经常需要和FTP打交道,比如向FTP服务器上传文件.下载文件.本文简单介绍如何利用jakarta commons中的FTPClient(在commons-net包中)实现上传下载文件. 1.写一个javabean文件,描述ftp上传或下载的信息 实例代码: public class FtpUseBean { private String host; private Integer port; private String us

  • 详解Java中的日期类

    Java 编程语言中时间的处理类有 Date类与 Calendar类.目前官方不推荐使用 Date类,因为其不利于国际化:而是推荐使用 Calendar类,并使用 DateFormat 类做格式化处理. 一.Date 类介绍 Date 表示特定的瞬间,精确到毫秒. 在 JDK 1.1 之前,类 Date 有两个其他的函数.它允许把日期解释为年.月.日.小时.分钟和秒值.它也允许格式化和解析日期字符串. 不过,这些函数的 API 不易于实现国际化.从 JDK 1.1 开始,应该使用 Calenda

  • 详解MySQL中的数据类型和schema优化

    最近在学习MySQL优化方面的知识.本文就数据类型和schema方面的优化进行介绍. 1. 选择优化的数据类型 MySQL支持的数据类型有很多,而如何选择出正确的数据类型,对于性能是至关重要的.以下几个原则能够帮助确定数据类型: 更小的通常更好 应尽可能使用可以正确存储数据的最小数据类型,够用就好.这样将占用更少的磁盘.内存和缓存,而在处理时也会耗时更少. 简单就好 当两种数据类型都能胜任一个字段的存储工作时,选择简单的那一方,往往是最好的选择.例如整型和字符串,由于整型的操作代价要小于字符,所

  • 详解Java中String JSONObject JSONArray List<实体类>转换

    JSON使用阿里的fastJson为依赖包 gradle依赖管理如下: compile group: 'com.alibaba', name: 'fastjson', version:'1.2.41' 1.String转JSONObject 前言:String 是JSONObject格式的字符串 eg: JSONObject jSONObject = JSONObject.parseObject(String); 2.String转JSONArray 前言:String 是JSONArray格式

  • 详解Java中CountDownLatch异步转同步工具类

    使用场景 由于公司业务需求,需要对接socket.MQTT等消息队列. 众所周知 socket 是双向通信,socket的回复是人为定义的,客户端推送消息给服务端,服务端的回复是两条线.无法像http请求有回复. 下发指令给硬件时,需要校验此次数据下发是否成功. 用户体验而言,点击按钮就要知道此次的下发成功或失败. 如上图模型, 第一种方案使用Tread.sleep 优点:占用资源小,放弃当前cpu资源 缺点: 回复速度快,休眠时间过长,仍然需要等待休眠结束才能返回,响应速度是固定的,无法及时响

随机推荐