C++知识文档八_继承和派生

2024-06-05 14:58
文章标签 c++ 文档 知识 继承 派生

本文主要是介绍C++知识文档八_继承和派生,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

继承和派生一般性概念

继承:在一个已经存在的类的基础上建立一个新的类,新类从已有的类那里获取某些已有的特征,这种现象称为类的继承。

派生:从另一个角度说,从已有的类产生一个新的类,称为类的派生。

派生类的一般声明形式为:

class派生类名:[继承方式] 基类名

{

   派生类新增的成员

};

例1 继承和派生演示

1)建立一个控制台空工程,并加入实现文件ExampleOne.cpp。

#include<iostream>

usingnamespace std;

classCBase

{

public:

   int m_iBase;

   int BaseFunc()

   {

      return m_iBase;

   }

};

 

classCDerived:public CBase

{

public:

   int m_iDerived;

   int DerivedFunc()

   {

      return m_iDerived;

   }

};

 

intmain()

{

   CDerived *pd=new CDerived;

   int itest;

   itest=pd->DerivedFunc();

  

   return 0;

}

如上,CBase类派生了一个新的类CDerived,或者说类CDerived从类CBase继承而来,这样,类CDerived不但具有新增的成员m_iDerived和DerivedFunc(),还有从类CBase继承而来的成员m_iBase和BaseFunc(),从而类CDerived实际具有4个成员。


类的继承方式

派生类并非把基类的所有特性都继承下来。它还受两方面约束:

  不论何种方式,下面这些基类的特征是不能从基类继承下来的:

  构造函数

  析构函数

  用户重载的new 运算符

  用户重载的=运算符

  友员关系

  对基类的除上面以外的其他成员的继承受继承方式限制,有三种继承的方式:私有继承、保护继承和公有继承。不同的继承方式将导致从基类继承来的成员在派生类中具有不同的访问限制属性。

   三种继承方式一般形式为:

   class 派生类名:private 基类名    //私有继承

   {

      派生类新增成员

   };

   class 派生类名:protected 基类名     //保护继承

   {

      派生类新增成员

   };

   class 派生类名:public 基类名     //公有继承

   {

      派生类新增成员

   };

不同的继承方式下,各种成员的访问属性总结如下表所示。

(不同的继承方式下各种成员的访问属性列表)

继承方式

基类中的访问属性

派生类中的访问属性

公有继承

private

为基类私有

protected

保护类型

public

公有类型

保护继承

private

为基类私有

protected

保护类型

public

保护类型

私有继承

private

为基类私有

protected

私有类型

public

私有类型

 


派生类对基类成员的覆盖

如果基类和派生类中存在同名的成员数据或者成员函数,那么派生类的成员数据和成员函数将覆盖掉基类的成员数据和成员函数。而且与基类和派生类的访问属性无关;与基类和派生类的函数间的参数和返回类型无关。

例2 演示派生类对基类成员的覆盖

1)建立一个控制台空工程,并加入实现文件ExampleTwo.cpp。

#include<iostream>

usingnamespace std;

classCBase

{

protected:

   int m_i;

public:

   int Func(int i)

   {

      return m_i;

   }

};

classCDerived:public CBase

{

public:

   int m_i;         //覆盖掉基类的m_i;

   int Func()          //覆盖掉基类的Func

   {

      m_i=9;        //访问自身的成员

      CBase::m_i=22;      //通过作用域解析符访问基类的成员

      return m_i;

   }

};

voidmain()

{

   CDerived obj;

   obj.Func();

   //obj.Func(3);        //错误!基类的Func被覆盖

   obj.CBase::Func(3); //正确,通过作用域解析符访问基类的成员

}


派生类对象的初始化与清除

如果需要在初始化派生类成员时也初始化基类的成员,则可在派生类的构造函数的初始化成员列表中初始化基类,如果基类的构造函数均带有参数,则基类必须出现在派生类的初始化成员列表中。

派生类构造函数及初始化列表的一般形式为:

派生类名(构造函数参数列表): 基类名(参数列表),派生类成员1(参数列表1),,,派生类成员n(参数列表n)

例 3  演示派生类对象的初始化和清除

1)建立一个控制台空工程,加入文件ExmapleThree.cpp,文件内容如下:

#include<iostream>

usingnamespace std;

classCBase

{

protected:

   int m_iBase;

public:

   //CBase(){}

   CBase(int i){m_iBase=i;}

};

 

classCDerived:public CBase

{

public:

   int m_iDerived;

   CDerived(int i);

};

 

CDerived::CDerived(inti):CBase(0),m_iDerived(i)

{

   //注意派生类构造函数的写法,以及掌握怎样在派生类中访问基类的成员变量

}

intmain()

{

   CDerived *pd=new CDerived(1);

   delete pd;

   return 0;

}

执行派生类构造函数的顺序为:

1)  调用基类的构造函数,初始化基类的数据成员;

2)  初始化派生类的数据成员;

3)  执行派生类的构造函数本身。

派生类的析构函数相对简单,与无继承关系的普通类的析构函数形式相同。执行析构函数的顺序为:

1)  调用派生类的析构函数;

2)  调用基类的析构函数。

 


基类对象和派生类对象间的转换与赋值

  基类对象和派生类对象间的转换

正如整型数据可以自动转换成double型一样,基类对象与派生类对象间也存在赋值兼容的关系。假如有如下基类和派生类:

classCBase

{

   //...

};

classCDerived:public CBase

{

   //...

};

基类对象和派生类对象间的转换具体表现在以下几个方面:

  派生类对象可以向基类对象赋值;

     

      CBase base;

      CDerived derived;

      base=derived;       //将派生类对象的基类部分拷贝给基类对象

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

      CBase b1;

      CDerived d1;

      CBase &b1Alias=b1;//普通的引用

      b1Alias=d1;   //将d1的基类部分拷贝给b1Alias(即b1)

      CBase &b2=d1; //d1基类部分的引用

 

  如果函数的参数是基类对象或者基类对象的引用,则相应的实参可以是派生类对象。

      void Func1(CBase base)

      {

        //...

      }

      void Func2(CBase &base)

      {

        //...

      }

      CDerived derived;

      Func1(derived);  //将derived的基类部分拷贝给行参base

      Func2(derived)      //将derived的基类部分当作行参使用

 

  派生类对象的地址可以赋给基类类型的指针变量,或者说,基类型的指针可以指向派生类对象。

      CBase *pBase;

      CDerived derived;

      pBase=&derived;

  基类对象和派生类对象间的赋值

如果用户定义了基类的拷贝构造函数,而没有定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。

 

如果用户重载了基类的对象赋值运算符=,而没有定义派生类的对象赋值运算符,那么在用一个派生类对象给新的派生类对象赋值时,两对象间的派生类部分执行缺省的赋值行为,而两对象间的基类部分执行用户定义的重载赋值函数。

如果用户定义了派生类的拷贝构造函数或者重载了派生类的对象赋值运算符=,则在用已有派生类对象初始化新的派生类对象时,或者在派生类对象间赋值时,将会执行用户定义的派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,这时,通常需要用户在派生类的拷贝构造函数或者派生类的赋值函数中显式调用基类的相应函数。

例4 演示基类对象和派生类对象间的赋值

1)建立控制台空工程ExampleFour,并加入实现文件ExmapleFour.cpp。

#include<iostream>

usingnamespace std;

classCBase

{

protected:

   char *m_pszData;

public:

   CBase(const char *pszData)

   {

      m_pszData=new char[strlen(pszData)+1];

      strcpy(m_pszData,pszData);

   }

   CBase(const CBase &base)

   {

      m_pszData=newchar[strlen(base.m_pszData)+1];

      strcpy(m_pszData,base.m_pszData);

   }

   CBase &operator =(const CBase &base)

   {

      if(this==&base)

        return *this;

      m_pszData=new char[strlen(base.m_pszData)+1];

      strcpy(m_pszData,base.m_pszData);

      return *this;

   }

   ~CBase(){delete []m_pszData;}

};

classCDerived:public CBase

{

public:

   CDerived(const char*pszData):CBase(pszData){}

};

voidmain()

{                                                   

   CDerived d1("Hello!");

   //派生类使用缺省的拷贝构造函数、基类调用用户定义的拷贝构造函数

   CDerived d2=d1;

//派生类使用缺省的赋值操作,基类调用用户重载的赋值运算符

d1=d2;       

}


保护构造函数与私有构造函数

  保护构造函数

如果一个类的构造函数和析构函数是保护类型的,则不能在程序中实例化该类的对象(相当于从外部来调用该类的保护型成员,也就是非公有成员),这样的类称为抽象类。

由于保护的构造函数可以被派生类访问,所以我们通常将抽象类用于各种派生类的公共基类,使得该抽象类派生的类拥有共同的特性。

例5 保护构造函数演示

1)建立控制台空工程ExampleFive,并加入实现文件ExampleFive.cpp。

#include<iostream>

usingnamespace std;

classCAbstract

{

protected:

   CAbstract(){}         //基类保护类型的构造函数

   //...

};

 

classCDerived:public CAbstract

{

   //...

};

voidmain()

{

   //CAbstract objAbs;//错误,无法实例化,因为无法从类的外部来访问该类的非公有成员。那怎么访问?

   CDerived objDer; //答案:可以通过派生类的构造函数访问基类的保护型构造函数

}

  私有构造函数

如果类的构造函数是私有的,这样的类也是抽象类,但是因为派生类也无法调用它,从而无法用派生的方式初始化,但是可以用静态函数的方式实例化该类的对象。

例6 私有构造函数演示

1)建立控制台空工程ExampleSix,并加入实现文件ExampleSix.cpp。

#include<iostream>

usingnamespace std;

classCAbstract

{

private:

   CAbstract(){}         //保护类型的构造函数

   //...

public:

   static CAbstract *CreateInstance()

   {

      return new CAbstract();

   }

   static void ReleaseInstance(CAbstract *pThis)

   {

      delete pThis;

   }

};

 

voidmain()

{

   CAbstract *pObj=CAbstract::CreateInstance();

   //...

   CAbstract::ReleaseInstance(pObj);

}

回忆以前的单例模式(只能实例化一个对象的类的实现),跟以上例子有什么相同和不同之处。


多重继承和虚基类

  多重继承(Multiple Inheritance)

如果一个派生类同时有两个或者多个基类,派生类从两个和多个基类中继承所需的属性,这种继承方式称为多重继承(Multiple Inheritance)。

声明多重继承的一般形式为:

class派生类名:继承方式1 基类名1,继承方式2 基类名2,,,继承方式n 基类名n

{

   //... 派生类新增的成员。

};

例7 多重继承和成员二义性消除的简单方法演示

1)建立控制台空工程ExampleSeven,并加入实现文件ExampleSeven.cpp。

#include<iostream>

usingnamespace std;

classCBaseA

{

public:

   int m_i;

   void Func()

   {

      m_i++;

   }

};

classCBaseB

{

public:

   int m_i;

   void Func()

   {

      m_i++;

   }

};

classCDerived:public CBaseA,public CBaseB

{

public:

   int m_j;

};

intmain()

{

   return 0;

}

以上类的声明,相应的数据成员在内存中的布局形式如图所示。

(数据成员在内存中的布局形)

从图中很容易看出同样的成员m_i,在基类中有一份,在派生类中也有一份。可以想象一下,如果使用派生类对象来访问该成员的时候,就会出现二义性问题:到底访问的是基类中的m_i,还是访问派生类中的m_i?所以调用时必须注意防止二义性。具体用法见主函数体中相关语句和注释。

intmain()

{

   CDerived d;

   d.m_j=0; //正常

   //i=d.m_i; //引起二义性

//通过成员民限定消除二义性,访问从CBaseA中继承来的m_i d.CBaseA::m_i=1;

//通过成员民限定消除二义性,访问从CBaseB中继承来的m_i

   d.CBaseB::m_i=2;

   //d.Func();   //引起二义性

//通过成员民限定消除二义性,访问从CBaseA中继承来的成员函数Func()

   d.CBaseA::Func();  

//通过成员民限定消除二义性,访问从CBaseB中继承来的成员函数Func()

   d.CBaseB::Func();  

   CBaseA *pa;

   CBaseB *pb;

   pa=&d;        //系统会自动将pa指向对象d的CBaseA部分

   pb=&d;        //系统会自动将pb指向对象d的CBaseB部分

   return 0;

}

  二义性的另外一种消除方法-----虚基类

如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在生成派生类对象时,系统会为派生类对象生成共同基类数据成员的多份拷贝。

如果希望派生类对象中只包含一份共同基类的数据成员,则可以在声明派生类时,通过virtual加继承方式,使派生对象只保留共同基类的一份数据成员。

例8 虚基类使用方法演示

1)建立控制台空工程ExampleEight,并加入实现文件ExampleEight.cpp。

#include<iostream>

usingnamespace std;

classCBase

{

public:

   int m_i;

};

 

classCBaseA:public CBase

{

public:

   int m_ia;

};

 

classCBaseB:public CBase

{

public:

   int m_ib;

};

classCDerived: public CBaseA, public CBaseB

{

public:

   int m_d;

};

 

intmain()

{

   return 0;

}

按照以上类声明形式,则对应的数据成员内存布局形式如下图所示。

(数据成员内存布局形式图)

类体系结构如下图所示。

(类体系结构图)

从图中也可以看出,同样的数据成员在基类和派生类中都有,同样的道理,访问该成员的时候也会出现二义性。

下面介绍使用虚基类的方式来消除二义性。将以上类CBaseA和CBaseB的声明形式改为:

classCBaseA:virtual public CBase

{

public:

   int m_ia;

};

classCBaseB:virtual public CBase

{

public:

   int m_ib;

};

如此一来,这样派生类对象可以直接使用公共基类的成员,从而消除二义性。

intmain()

{

   CDerived d;

   d.m_i=9;

   return 0;

}

 可以在一个已经存在类的基础之上来产生一个新的类,对于已经存在类具有的方法和属性,新类就不用再重新添加。这样可以提高代码可重用性。新产生的类具有两种成员:新添加的成员和原来类具有的成员。这个已经存在的类叫做基类,产生的新类叫做派生类。类的继承方式有三种:公有、私有、保护继承。其中由于共有继承不改变基类成员在派生类中的访问属性,所以用得较多。当派生类中具有基类里边的同名的成员时,派生类的该同名成员会覆盖基类同名的成员。派生类的成员有两种,那么对应的派生类的构造函数对这些成员变量进行初始化的时候就要完成两部分工作。对本身新添加数据成员的初始化和从基类接收过来成员的初始化(调用基类构造函数)。派生类对象和基类对象由赋值兼容的关系,凡是该关系是单向的。当一个类的构造函数是私有的或者受保护型的,那么这样的类不能实例化本身,那么这样的类叫做抽象类。当一个派生类具有两个以上的基类时,就叫做多重继承,这种时候有可能产生成员的二义性,使用虚基类可以消除。


这篇关于C++知识文档八_继承和派生的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1033377

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

SpringBoot3集成swagger文档的使用方法

《SpringBoot3集成swagger文档的使用方法》本文介绍了Swagger的诞生背景、主要功能以及如何在SpringBoot3中集成Swagger文档,Swagger可以帮助自动生成API文档... 目录一、前言1. API 文档自动生成2. 交互式 API 测试3. API 设计和开发协作二、使用

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

基于C#实现将图片转换为PDF文档

《基于C#实现将图片转换为PDF文档》将图片(JPG、PNG)转换为PDF文件可以帮助我们更好地保存和分享图片,所以本文将介绍如何使用C#将JPG/PNG图片转换为PDF文档,需要的可以参考下... 目录介绍C# 将单张图片转换为PDF文档C# 将多张图片转换到一个PDF文档介绍将图片(JPG、PNG)转

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始