C++基础9:继承与派生

此专栏为移动机器人知识体系下的编程语言中的 C {\rm C} C++从入门到深入的专栏,参考书籍:《深入浅出 C {\rm C} C++》(马晓锐)和《从 C {\rm C} C C {\rm C} C++精通面向对象编程》(曾凡锋等)。



8.继承与派生
8.1 继承与派生基础
  • 继承与派生举例理解:

    14

    • 交通工具是总的分类,是对具体的交通工具最高层次的抽象分类,具有最普遍的一般性意义;
    • 下层的分类具有上层的特性,同时加入了自己的新特性,越往下层,新的特性越多;
    • 如果将每种交通工具都定义成类,则在这个类层次结构中,上层是下层的基类,下层是上层的派生;
  • 在面向对象程序设计中,以一个已经存在的类为基础声明一个新类的过程称为派生,这个已经存在的类称为基类或父类,由基类派生出来的类称为派生类,通过一定规则继承基类派生新类的过程称为继承和派生机制;

  • 派生类具有基类的属性,在派生过程中根据需要增加成员,既能重新利用基类的功能,又能开发新的功能,派生类可以作为基类而派生出新类,这样就形成了类的层次关系;

  • 派生类的声明,语法格式如下:

    class 派生类名:继承方式 基类名1,继承方式 基类名2,...
    {
        成员声明;
    };
    
    • 基类名:派生类所继承类的名称,这个类是已经存在的类,即基类;
    • 派生类名:派生的新类的名称,当派生类只继承一个类时,称为单继承,当派生类同时继承多个类,称为多继承;
    • 继承方式:派生类访问基类成员的权限,继承方式的关键字: p u b l i c 、 p r o t e c t e d 、 p r i v a t e {\rm public、protected、private} publicprotectedprivate,分别为:公有继承、保护继承、私有继承,没有显式说明继承方式,系统默认继承方式为私有继承;
  • 在派生的过程中派生出来的类同样可以作为基类再派生出新类,一个类可以派生出多个派生类,形成一系列相关联的类,称为类族;在类族中,如果类 A {\rm A} A直接派生出类 B {\rm B} B,则类 A {\rm A} A称为直接基类,如果类 A {\rm A} A派生出类 B {\rm B} B,类 B {\rm B} B又派生出类 C {\rm C} C,则类 A {\rm A} A C {\rm C} C类的间接基类。

  • 继承举例:通过继承学生类来定义研究生类。

    // 定义学生类;
    class CStudent
    {
        private:
        	string studentID;
        public:
        	CStudent();
        	virtual ~CStudent();
    };
    
    // 继承学生类定义研究生类;
    class CGraduateStu::CStudent
    {
        private:
        	string studentClass;
        	string studentID;
        public:
        	CGraduateStu();
        	virtual ~CGraduateStu();
    };
    
  • 派生的主要目的是实现代码重用,在类派生过程中主要有如下步骤:

    • 继承基类的成员,在继承类的过程中,派生类会继承基类中除了构造函数和析构函数的所有成员;
    • 改造基类成员,在派生类中,为了描述更具体的事物,需要将基类的一些属性加以控制和更改,改造主要是针对基类成员的访问控制和对基类成员的覆盖与重载;当派生类中定义了与基类中相同的成员(当为成员函数时,参数都相同)时,派生类的成员将覆盖基类中的成员;此时通过派生类或派生类对象只能访问派生类中的成员,称为同名覆盖,当派生类中声明了与基类中相同的函数名而参数不同时,称为重载;
    • 为派生类增加新成员,在派生类中增加新的成员是建立派生类的关键,保证了派生类在基类的功能基础上有所发展,开发者根据具体所要实现的功能在派生类中增加适当的数据和函数,实现一些新的功能;
  • 继承实例 ( I n h e r i t 1 ) ({\rm Inherit1}) (Inherit1)项目:

    • 项目需求:通过继承学生类来实现研究生类。

    • 学生类定义头文件代码 ( C S t u d e n t . h ) {\rm (CStudent.h)} (CStudent.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:学生类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      // 学生类,作为研究生类的基类;
      class CStudent {
      	protected:
      		/* 参数说明:
      		*  m_strName:		学生姓名;
      		*  nTotalCourse:	已修完课程总数;
      		*  fAveScore:		成绩平均分;
      		*  nTotalCrediHour:	总学分;
      		*/
      		string m_strName;
      		int nTotalCourse;
      		float fAveScore;
      		int nTotalCreditHour;
      	public:
      		CStudent(string strStuName = "No Name");		// 带有默认参数的构造函数;
      		virtual ~CStudent();							// 析构函数;
      		void AddCourse(int nCrediHour, float fScore);	// 增加已修课程函数,参数:课程学时和课程分数;
      		void ShowStuInfo();								// 显示学生信息;
      };
      
    • 学生类实现文件代码 ( C S t u d e n t . c p p ) ({\rm CStudent.cpp}) (CStudent.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:学生类实现文件。
       */
      
      #include "CStudent.h"
      
      CStudent::CStudent(string strStuName) {
      	m_strName = strStuName;
      	nTotalCreditHour = 0;
      	nTotalCourse = 0;
      	fAveScore = 0.0;
      }
      
      CStudent::~CStudent() {
      }
      
      void CStudent::AddCourse(int nCrediHour, float fScore) {
      	nTotalCreditHour += nCrediHour;
      	fAveScore = (fAveScore * nTotalCourse + fScore) / (nTotalCourse + 1);
      	nTotalCourse++;
      }
      
      void CStudent::ShowStuInfo() {
      	cout << "学生姓名:" << m_strName << endl;
      	cout << "学生总学分:" << nTotalCreditHour << endl;
      	cout << "已修完课程总数:" << nTotalCourse << endl;
      	cout << "学生平均分:" << fAveScore << endl;
      }
      
    • 导师类定义头文件代码 ( C T u t o r i a l . h ) ({\rm CTutorial.h}) (CTutorial.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:导师类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      // 导师类,定义导师的信息; 
      class CTutorial {
      	private:
      		string m_strTutorialName;							// 导师姓名; 
      	public:
      		CTutorial() {};
      		virtual ~CTutorial() {};
      		void SetTutorialName(string strTutorialName) {		// 设定导师名; 
      			m_strTutorialName = strTutorialName;
      		};
      		void ShowTutorialName() {							// 显示导师名; 
      			cout << "Name of tutor:" << m_strTutorialName << endl;
      		};
      };
      
    • 研究生类定义头文件代码 ( C G r a d u a t e S t u . h ) ({\rm CGraduateStu.h}) (CGraduateStu.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:研究生类定义头文件。
       */
      #pragma once
      #include "CStudent.h"
      #include "CTutorial.h"
      
      // 派生类,继承学生类; 
      class CGraduateStu: public CStudent {
      	protected:
      		CTutorial m_ctTutorial;		// 增加新成员,存储导师信息; 
      	public:
      		CGraduateStu() {};
      		virtual ~CGraduateStu() {};
      		CTutorial& GetTutorial() {
      			return m_ctTutorial;
      		};
      		void ShowStuInfo();			// 显示学生信息,改造了基类的函数成员; 
      };
      
    • 研究生类实现文件代码 ( C G r a d u a t e S t u . c p p ) ({\rm CGraduateStu.cpp}) (CGraduateStu.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:导师类实现文件。
       */
      #include "CGraduateStu.h"
      
      // 对基类中的函数进行重载; 
      void CGraduateStu::ShowStuInfo() {
      	this->CStudent::ShowStuInfo();
      	this->GetTutorial().ShowTutorialName();
      }
      
    • 程序主文件代码 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:程序主文件。
       */
      #include "CStudent.h"
      #include "CGraduateStu.h"
      
      int main() {
      	CStudent student1("Willard");		// 定义普通学生实例; 
      	CGraduateStu graduateStudent1;		// 定义研究生实例; 
      
      	student1.AddCourse(5, 99);
      	student1.AddCourse(2, 98);
      	student1.ShowStuInfo();
      
      	graduateStudent1.AddCourse(3, 97);
      	graduateStudent1.AddCourse(5, 98);
      	graduateStudent1.GetTutorial().SetTutorialName("My Tutorial");
      	graduateStudent1.ShowStuInfo();
      
      	return 0;
      }
      
8.2 继承中的访问控制
  • 派生类继承了基类中除构造函数和析构函数外的所有成员函数,根据继承方式的不同,这些成员函数在派生类中的访问权限不同,派生类继承基类的方式有:公有继承 ( p u b l i c ) ({\rm public}) (public)、私有继承 ( p r i v a t e ) ({\rm private}) (private)、保护继承 ( p r o t e c t e d ) ({\rm protected}) (protected)

  • 当派生类继承基类的方式是公有继承时,基类成员在派生类中的访问规则如下:

    • 基类的公有成员和保护成员的访问属性在派生类中不变,即基类的公有成员在派生类中是公有成员,基类中的保护成员在派生类中是保护成员;
    • 基类的私有成员不可访问;
  • 继承方式为公有继承时,派生类或其对象可以直接访问基类中的公有成员和保护成员,无法访问基类中的私有成员;

  • 继承实例 ( I n h e r i t 2 ) ({\rm Inherit2}) (Inherit2)项目:

    • 项目需求:已知存在一个点类( P o i n t {\rm Point} Point类),表示几何上的"点",通过继承点类来定义一个线段类 ( L i n e s e g m e n t ) ({\rm Linesegment}) (Linesegment)类;

    • 点类定义头文件代码 ( C P o i n t . h ) ({\rm CPoint.h}) (CPoint.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:点类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CPoint {
      	private:
      		double X, Y;
      	public:
      		CPoint() {};
      		void InitPoint(double x, double y) {
      			this->X = x;
      			this->Y = y;
      		};
      		virtual ~CPoint() {};
      		double GetX() {
      			return X;
      		};
      		double GetY() {
      			return Y;
      		};
      };
      
    • 线段类定义头文件代码 ( C L i n e s e g m e n t . h ) ({\rm CLinesegment.h}) (CLinesegment.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:线段类定义头文件。
       */
      #pragma once
      #include "CPoint.h"
      
      // 线段类继承点类,继承方式为公有继承; 
      class CLinesegment: public CPoint {
      	private:
      		double L;
      	public:
      		CLinesegment() {};
      		virtual ~CLinesegment() {};
      		void InitLinesegment(double x, double y, double l) {
      			InitPoint(x, y);		// 调用基类公有成员; 
      			this->L = l;			// 设定线段长度; 
      		};
      		double GetL() {
      			return L;				// 新增的私有成员; 
      		};
      };
      
    • 程序主文件代码 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:程序主文件。
       */
      #include "CPoint.h"
      #include "CLinesegment.h"
      
      using namespace std;
      
      int main() {
      	CLinesegment line;					// 定义线段类实例; 
      
      	line.InitLinesegment(0, 0, 5);
      	cout << "线段参数为:(" << line.GetX() << ","
      	     << line.GetY() << "," << line.GetL() << ")" << endl;
      
      	return 0;
      }
      
  • 派生类的继承方式为私有继承时,基类成员在派生类的访问规则如下:

    • 基类中的公有成员和保护成员在派生类中变为私有成员;
    • 基类中的私有成员在派生类中不可访问;
  • 继承方式为私有继承时,派生类可以访问基类中的公有成员和保护成员,不能访问基类中的私有成员,通过类的对象不能访问基类中的成员;

  • 私有继承实例 ( I n h e r i t 3 ) ({\rm Inherit3}) (Inherit3)项目:

    • 项目需求:通过私有继承实现线段类继承点类。

    • 点类定义头文件代码 ( C P o i n t . h ) ({\rm CPoint.h}) (CPoint.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:点类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CPoint {
      	private:
      		double X, Y;
      	public:
      		CPoint() {};
      		void InitPoint(double x, double y) {
      			this->X = x;
      			this->Y = y;
      		};
      		virtual ~CPoint() {};
      		double GetX() {
      			return X;
      		};
      		double GetY() {
      			return Y;
      		};
      };
      
    • 线段类定义头文件代码 ( C L i n e s e g m e n t . h ) ({\rm CLinesegment.h}) (CLinesegment.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:线段类定义头文件。
       */
      #pragma once
      #include "CPoint.h"
      
      // 线段类继承点类,继承方式为私有继承;
      class CLinesegment: private CPoint {
      	private:
      		double L;
      	public:
      		CLinesegment() {};
      		virtual ~CLinesegment() {};
      		void InitLinesegment(double x, double y, double l) {
      			InitPoint(x, y);		// 调用基类公有成员;
      			this->L = l;			// 设定线段长度;
      		};
      		// 通过重载基类函数实现外部接口; 
      		double GetX() {
      			return CPoint::GetX();
      		};
      		double GetY() {
      			return CPoint::GetY();
      		};
      		double GetL() {
      			return L;				// 新增的私有成员;
      		};
      };
      
    • 程序主文件代码 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:程序主文件。
       */
      #include "CPoint.h"
      #include "CLinesegment.h"
      
      using namespace std;
      
      int main() {
      	CLinesegment line;					// 定义线段类实例; 
      
      	line.InitLinesegment(0, 0, 5);
      	cout << "线段参数为:(" << line.GetX() << ","
      	     << line.GetY() << "," << line.GetL() << ")" << endl;
      
      	return 0;
      }
      
  • 派生类的继承方式为保护继承时,基类成员在派生类中访问规则如下:

    • 基类中公有成员和保护成员在派生类中变为保护成员;
    • 基类中的私有成员不可访问;
  • 继承方式为保护继承时,在派生类中可以访问基类中的公有成员和保护成员,通过类的对象无法访问,派生类或其对象,无法访问基类中的私有成员;

  • 保护继承实例 ( I n h e r i t 4 ) ({\rm Inherit4}) (Inherit4)项目:

    • 项目需求:通过保护继承实现线段类继承点类。

    • 点类定义头文件代码 ( C P o i n t . h ) ({\rm CPoint.h}) (CPoint.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:点类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CPoint {
      	protected:
      		double X, Y;
      	public:
      		CPoint() {};
      		void InitPoint(double x, double y) {
      			this->X = x;
      			this->Y = y;
      		};
      		virtual ~CPoint() {};
      		double GetX() {
      			return X;
      		};
      		double GetY() {
      			return Y;
      		};
      };
      
    • 线段类定义头文件代码 ( C L i n e s e g m e n t . h ) ({\rm CLinesegment.h}) (CLinesegment.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:线段类定义头文件。
       */
      #pragma once
      #include "CPoint.h"
      
      // 线段类继承点类,继承方式为保护继承;
      class CLinesegment: protected CPoint {
      	private:
      		double L;
      	public:
      		CLinesegment() {};
      		virtual ~CLinesegment() {};
      		void InitLinesegment(double x, double y, double l) {
      			InitPoint(x, y);		// 调用基类公有成员;
      			this->L = l;			// 设定线段长度;
      		};
      		double GetX() {
      			return X;
      		};
      		double GetY() {
      			return Y;
      		};
      		double GetL() {
      			return L;
      		};
      };
      
    • 程序主文件代码 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/17
       * 描述:程序主文件。
       */
      #include "CPoint.h"
      #include "CLinesegment.h"
      
      using namespace std;
      
      int main() {
      	CLinesegment line;					// 定义线段类实例; 
      
      	line.InitLinesegment(0, 0, 5);
      	cout << "线段参数为:(" << line.GetX() << ","
      	     << line.GetY() << "," << line.GetL() << ")" << endl;
      
      	return 0;
      }
      
8.3 派生类的构造函数和析构函数
  • 派生类中,构造函数和析构函数不被继承,在派生类中必须进行构造函数和析构函数的定义;

  • 派生类的数据成员由所有基类的数据成员与派生类新增的数据成员共同组成;如果派生类新增成员中包括其他类的对象,称为内嵌对象或子对象,派生类的数据成员中实际上间接包括了这些对象的数据成员;因此,构造派生类的对象时,必须对基类数据成员、新增数据成员和内嵌对象的数据成员进行初始化;

  • 派生类构造函数语法格式:

    派生类名::派生类名(参数表):基类名1(参数表1),...,基类名n(参数表n),
    					   内嵌对象名1(内嵌对象参数表1),...,内嵌对象名n(内嵌对象参数表n)
    {
             派生类构造函数体		// 派生类新增成员初始化;
    };
    
    • 对基类成员和内嵌对象成员初始化必须在成员初始化列表中进行,新增成员的初始化既可以在成员初始化列表中进行,也可以在构造函数体中进行;
    • 如果派生类的基类也是一个派生类,则每个派生类只需负责其直接基类的构造,依次上溯;
    • 如果基类中定义了默认构造函数或根本没有定义任何一个构造函数,在派生类构造函数的定义中可以省略对基类构造函数的调用,即省略"基类名(参数表)";内嵌对象情况与基类相同;
    • 当所有的基类和子对象的构造函数都可以省略时,可以省略派生类构造函数的成员初始化列表;
    • 如果所有基类和子对象的构造函数都不需要参数,派生类也不需要参数,可以不定义派生类构造函数;
  • 派生类构造函数实例 ( I n h e r i t 5 ) ({\rm Inherit5}) (Inherit5)项目:

    • 项目需求:通过继承学生类来实现研究生类,定义研究生类的构造函数对基类成员和派生类成员进行初始化;

    • 学生类定义头文件 ( C S t u d e n t . h ) ({\rm CStudent.h}) (CStudent.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:学生类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CStudent {
      	protected:
      		string m_strName;
      		int nTotalCourse;
      		float fAveScore;
      		int nTotalCrediHour;
      	public:
      		CStudent(string strStuName = "No Name");
      		virtual ~CStudent();
      		void AddCourse(int nCrediHour, float Score);
      		void ShowStuInfo();
      };
      
    • 学生类实现文件代码 ( C S t u d e n t . c p p ) ({\rm CStudent.cpp}) (CStudent.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:学生类实现文件。
       */
      #include "CStudent.h"
      
      CStudent::CStudent(string strStuName) {
      	m_strName = strStuName;
      	nTotalCrediHour = 0;
      	nTotalCourse = 0;
      	fAveScore = 0.0;
      }
      
      CStudent::~CStudent() {
      }
      
      void CStudent::AddCourse(int nCrediHour, float Score) {
      	nTotalCrediHour += nCrediHour;
      	fAveScore = (fAveScore * nTotalCourse + Score) / (nTotalCourse + 1);
      	nTotalCourse++;
      }
      
      void CStudent::ShowStuInfo() {
      	cout << "学生姓名:" << m_strName << endl;
      	cout << "学生总学分:" << nTotalCrediHour << endl;
      	cout << "已修完课程总数:" << nTotalCourse << endl;
      	cout << "成绩平均分:" << fAveScore << endl;
      }
      
    • 导师类定义头文件代码 ( C T u t o r i a l . h ) ({\rm CTutorial.h}) (CTutorial.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:导师类定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CTutorial {
      	private:
      		string m_strTutorialName;				// 导师姓名; 
      	public:
      		CTutorial() {};
      		virtual ~CTutorial() {};
      		void SetTutoriralName(string strTutorialName) {
      			m_strTutorialName = strTutorialName;
      		};										// 设定导师姓名; 
      		void ShowTutorialName() {
      			cout << "Name of tutor:" << m_strTutorialName;
      		};
      };
      
    • 研究生类定义头文件代码 ( C G r a d u a t e S t u . h ) ({\rm CGraduateStu.h}) (CGraduateStu.h)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:研究生类定义头文件。
       */
      #include "CStudent.h"
      #include "CTutorial.h"
      
      class CGraduateStu: public CStudent {
      	protected:
      		CTutorial m_ctTutorial;					// 内嵌对象,导师类; 
      	public:
      		// 派生类构造函数; 
      		CGraduateStu(string strName, CTutorial &tu): CStudent(strName), m_ctTutorial(tu) {};
      		virtual ~CGraduateStu() {};
      		CTutorial& GetTutorial() {
      			return m_ctTutorial;
      		};
      		void ShowStuInfo();
      };
      
    • 研究生类实现文件代码 ( C G r a d u a t e S t u . c p p ) ({\rm CGraduateStu.cpp}) (CGraduateStu.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:研究生类实现文件。
       */
      #include "CGraduateStu.h"
      
      void CGraduateStu::ShowStuInfo() {
      	this->CStudent::ShowStuInfo();				// 调用基类成员函数输出研究生基本信息; 
      	this->GetTutorial().ShowTutorialName();		// 输出研究生特有信息; 
      }
      
    • 程序主文件代码 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:程序主文件。
       */
      #include "CGraduateStu.h"
      #include "CTutorial.h"
      
      int main() {
      	CTutorial tu;							// 定义一个导师对象; 
      	CGraduateStu gStu("Willard", tu);		// 用导师对象和姓名初始化一个研究生对象; 
      
      	gStu.AddCourse(4, 99);
      	gStu.AddCourse(2, 98);
      	gStu.GetTutorial().SetTutoriralName("My tutor");
      	gStu.ShowStuInfo();						// 输出研究生信息; 
      }
      
  • 派生类和基类构造函数的执行顺序:

    • 调用基类构造函数,当派生类由多个基类时,处于同一层次的各个基类构造函数的调用顺序取决于定义派生类时声明的顺序,与在派生类构造函数的成员初始化列表中给出的顺序无关;
    • 调用子对象的构造函数,当派生类中有多个子对象时,各个子对象构造函数的调用顺序取决于在派生类中定义的顺序,与在派生类构造函数的成员初始化列表中给出的顺序无关;
    • 派生类的构造函数体;
  • 派生类的析构函数的执行过程:

    • 对派生类新增普通成员进行清理;
    • 调用成员对象析构函数,对派生类新增的成员对象进行清理;
    • 调用基类析构函数,对基类进行清理;
  • 多继承并含有内嵌对象情况下类析构函数实例 ( I n h e r i t 6 ) ({\rm Inherit6}) (Inherit6)项目:

    • 项目需求:多继承并含有内嵌对象的类的析构函数。

    • 程序文件:

      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:类1定义头文件。
       */
      #pragma once
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CClass1 {
      	public:
      		CClass1(int n1) {
      			cout << "CClass1构造函数." << endl;
      		};
      		virtual ~CClass1() {
      			cout << "CClass1析构函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:类2定义头文件。
       */
      #include <iostream>
      using namespace std;
      
      class CClass2 {
      	public:
      		CClass2(int n2) {
      			cout << "CClass2构造函数." << endl;
      		};
      		virtual ~CClass2() {
      			cout << "CClass2析构函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:类3定义头文件。
       */
      #include <iostream>
      
      using namespace std;
      
      class CClass3 {
      	public:
      		CClass3(int n3) {
      			cout << "CClass3构造函数." << endl;
      		};
      		virtual ~CClass3() {
      			cout << "CClass3析构函数." << endl;
      		};
      };
      
      #include "CClass1.h"
      #include "CClass2.h"
      #include "CClass3.h"
      
      class CClass: public CClass1, public CClass2, public CClass3 {
      	private:
      		CClass1 c1;
      		CClass2 c2;
      		CClass3 c3;
      	public:
      		CClass(int n1, int n2, int n3, int n4, int n5, int n6):
      			CClass1(n1), CClass2(n2), CClass3(n3), c1(n4), c2(n5), c3(n6) {}
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/18
       * 描述:程序主文件。
       */
      #include "CClass.h"
      
      int main() {
      	CClass c(1, 2, 3, 4, 5, 6);
      	
      	return 0;
      }
      
    • 程序执行过程:先执行基类的构造函数,接着执行成员对象的构造函数,再执行派生类的构造函数,然后执行派生类的析构函数;执行派生类的析构函数时,分别调用内嵌成员对象和基类的析构函数,在程序中是先执行对象的派生类的析构函数,然后执行成员对象的析构函数,最后执行基类的构造函数;

8.4 基类与派生类的相互作用
  • 通过派生机制,形成一个具有层次结构的类族,在类族中对各个类的访问需要一定的标识才能准确地访问到正确的成员,同时派生类可以和基类相互赋值,在类层次结构中根据赋值兼容规则进行必要的赋值和转换;

  • 在类中,有 4 4 4种不同的访问权限成员:不可访问成员、私有成员、保护成员和公有成员,在对派生类成员的访问中涉及如下问题:

    • 唯一标识成员问题,基类和派生类的成员可能同名;
    • 成员本身的可见性问题,会导致成员能否被正确地访问的问题;
  • 唯一标识成员问题和可见性问题通过作用域限定符和虚函数解决,其中作用域限定符" : : :: ::"可以用于限定要访问的成员所在类的名称,语法格式如下:

    基类名::成员名;				// 访问数据成员;
    基类名::成员函数名(参数表);	// 访问函数成员;
    
  • 如果存在两个或多个具有包含关系的作用域,且外层声明的标识符在内层没有声明同名标识符,则它在内层可见;如果内层声明了同名标识符,则外层标识符在内层不可见,即同名覆盖现象;

  • 在派生层次结构中,基类的成员和派生类非继承成员都具有类作用域,派生类在内层,基类在外层,如果在派生类中声明一个与基类成员一样的成员,则派生类的成员覆盖外层成员,如果直接用成员名访问,只能访问到派生类的成员;

  • 多继承并含有内嵌对象的类的析构函数实例 ( I n h e r i t 7 ) ({\rm Inherit7}) (Inherit7)项目:

    • 项目需求:多继承并含有内嵌对象的类的析构函数实例。

    • 项目代码:

      /**
       * 作者:罗思维
       * 时间:2024/03/19
       * 描述:类1定义头文件。
       */
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CTest1 {
      	public:
      		int nC;
      		void fun() {
      			cout << "CTest1的fun()成员函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/19
       * 描述:类2定义头文件。
       */
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CTest2 {
      	public:
      		int nC;
      		void fun() {
      			cout << "CTest2的fun()成员函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/19
       * 描述:类3定义头文件。
       */
      #include <iostream>
      #include <string>
      
      using namespace std;
      
      class CTest3 {
      	public:
      		int nC;
      		void fun() {
      			cout << "CTest3的fun()成员函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/19
       * 描述:类CTest定义头文件。
       */
      #include "CTest1.h"
      #include "CTest2.h"
      #include "CTest3.h"
      
      class CTest: public CTest1, public CTest2, public CTest3 {
      	public:
      		int nC;
      		void fun() {
      			cout << "CTest的fun()成员函数." << endl;
      		};
      };
      
      /**
       * 作者:罗思维
       * 时间:2024/03/19
       * 描述:程序主文件。
       */
      #include "CTest.h"
      
      int main() {
      	CTest c;
      	c.nC = 1;				// 对象名.数据成员名,只能访问派生类的成员; 
      	c.fun();				// 对象名.成员函数名,只能访问派生类的成员; 
      
      	c.CTest1::nC = 1;
      	c.CTest1::fun();		// 通过作用域限定符访问基类的成员; 
      
      	c.CTest2::nC = 2;
      	c.CTest2::fun();
      
      	c.CTest3::nC = 3;
      	c.CTest3::fun();
      
      	return 0;
      }
      
  • 基类和派生类的赋值规则:

    • 派生类的对象可以赋予基类对象;
    • 派生类的对象可以初始化基类的引用;
    • 派生类独享的地址可以赋予指向基类的指针;
8.5 多重继承特性
  • 一个派生类可以有多个基类,这样的继承方式称为多重继承 ( m u l t i p l e   i n h e r i t a n c e ) ({\rm multiple\ inheritance}) (multiple inheritance),多继承可以继承多个类的特性和功能,提供了软件的复用性,但多继承提供了程序的复杂性,可能引起大量的歧义性问题;

  • 在多继承中,如果两个基类具有同名或两个基类和派生类三者都有同名成员,可能造成对基类中某个成员的访问出现不唯一的情况,即产生二义性,解决二义性的三种方法:

    • 通过作用域运算符" : : :: ::"明确指出访问的哪一个基类中的成员,语法格式如下:

      对象名.基类名::成员名;				// 访问数据成员;
      对象名.基类名::成员函数名(参数表);	// 访问成员函数;
      
    • 在派生类中定义同名成员,根据同名覆盖原则,访问的是派生类的同名函数;

    • 引入虚基类 ( v i r t u a l   b a s e   c l a s s ) ({\rm virtual\ base\ class}) (virtual base class)

  • 如果一个派生类有多个直接基类,这些直接基类有一个公共的基类,在最终的派生类中会保留该间接基类数据成员的多份同名成员,使得对象存储空间占用过多造成内存浪费,使用虚基类解决此问题,采用虚基类的继承方式称为虚拟继承 ( v i r t u a l   i n h e r i t a n c e ) ({\rm virtual\ inheritance}) (virtual inheritance),虚基类语法格式如下:

    class 类名:virtual 继承方式 基类名
    
  • 采用虚拟继承可以消除基类成员的副本,还能消除对基类成员访问的二义性,利用虚拟继承解决类继承的二义性举例:

    class N
    {
        public:
        	int a;
        	void fun();
    };
    class A:virtual public N{};
    class B:virtual public N{};
    class C:public A,public B{};
    
  • 如果在虚基类中定义了带参数的构造函数,且没有定义默认构造函数,则在其所有派生类中,必须通过派生类的构造函数对虚基类进行初始化,虚基类初始化举例:

    class N
    {
        public:
        	N(int i);
    };
    class A:virtual public N
    {
        public:
        	A(int i):N(i){};
    };
    class B:virtual public N
    {
        public:
        	B(int i):N(i){};
    };
    class C:public A,public B
    {
        public:
        	C(int i):A(i),B(i),N(i){};
    };
    
  • 使用虚基类的注意事项:

    • 一个类可以在一个类族中既被用作虚基类,也可被用作非虚基类;
    • 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,非虚基类产生各自的子对象;
    • 虚基类子对象是由最后派生类的构造函数通过调用虚基类的构造函数进行初始化的;
    • 派生类构造函数的成员初始化列表中必须列出对虚基类构造函数的调用,若未列出,则表示使用该虚基类的默认构造函数;
    • 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行;
8.6 实战

项目需求 :编写一个操作日期(年、月、日)和时间(时、分、秒)的程序,要求该程序建立三个类:日期类 ( D a t e ) ({\rm Date}) (Date)、时间类 ( T i m e ) ({\rm Time}) (Time)、日期和时间类 ( D a t e T i m e ) ({\rm DateTime}) (DateTime),利用继承与派生机制生成 D a t e T i m e {\rm DateTime} DateTime类。

代码实现 ( I n h e r i t 8 ) ({\rm Inherit8}) (Inherit8)

  • 日期类 ( D a t e . h ) ({\rm Date.h}) (Date.h)头文件:

    /**
     * 作者:罗思维
     * 时间:2024/03/19
     * 描述:日期类(Date)定义头文件。
     */
    #include <iostream>
    #include <string>
    typedef char charArray[80];
    
    using namespace std;
    
    class Date {
    	protected:
    		int Year, Month, Day;
    	public:
    		Date() {};							// 默认构造函数; 
    		Date(int y, int m, int d) {			// 带参数构造函数; 
    			SetDate(y, m, d);
    		};
    		void SetDate(int y, int m, int d) {
    			Year = y;
    			Month = m;
    			Day = d;
    		};
    		charArray& GetStringDate(charArray &Date) {
    			sprintf(Date, "%d/%d/%d", Year, Month, Day);
    			
    			return Date;					// 返回格式化后的日期字符串; 
    		};
    };
    
  • 时间类 ( T i m e . h ) ({\rm Time.h}) (Time.h)头文件:

    /**
     * 作者:罗思维
     * 时间:2024/03/19
     * 描述:时间类(Time)定义头文件。
     */
    #include <iostream>
    #include <string>
    typedef char charArray[80];
    
    using namespace std;
    
    class Time {
    	protected:
    		int Hours, Minutes, Seconds;
    	public:
    		Time() {};
    		Time(int h, int m, int s) {
    			SetTime(h, m, s);
    		};
    		void SetTime(int h, int m, int s) {
    			Hours = h;
    			Minutes = m;
    			Seconds = s;
    		};
    		charArray& GetStringTime(charArray &Time) {
    			sprintf(Time, "%d:%d:%d", Hours, Minutes, Seconds);
    
    			return Time;
    		};
    };
    
  • 日期时间类 ( D a t e T i m e . h ) ({\rm DateTime.h}) (DateTime.h)头文件:

    /**
     * 作者:罗思维
     * 时间:2024/03/19
     * 描述:日期时间类(DateTime)定义头文件。
     */
    #include "Date.h"
    #include "Time.h"
    
    class DateTime: public Date, public Time {
    	public:
    		DateTime(): Date() {};
    		DateTime(int y, int mo, int d, int h, int mi, int s): Date(y, mo, d), Time(h, mi, s) {};
    		charArray& GetStringDT(charArray &DTstr) {
    			sprintf(DTstr, "%d/%d/%d %d:%d:%d", Year, Month, Day, Hours, Minutes, Seconds);
    
    			return DTstr;
    		};
    };
    
  • 程序主文件 ( m a i n . c p p ) ({\rm main.cpp}) (main.cpp)

    /**
     * 作者:罗思维
     * 时间:2024/03/19
     * 描述:程序主文件。
     */
    #include "DateTime.h"
    
    int main() {
    	DateTime date1, date2(1996, 8, 9, 23, 59, 48);
    	charArray Str;
    
    	date1.SetDate(1996, 7, 4);
    	date1.SetTime(22, 48, 48);
    	date1.GetStringDT(Str);
    
    	cout << "date1日期为:" << date1.GetStringDate(Str) << endl;
    	cout << "date1时间为:" << date1.GetStringTime(Str) << endl;
    	cout << "date2日期时间为:" << date2.GetStringDT(Str) << edl;
    
    	return 0;
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/484711.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++函数参数传递

目录 传值参数 指针形参 传引用参数 使用引用避免拷贝 使用引用形参返回额外信息 const形参和实参 指针或引用形参与const 数组形参 管理指针形参 使用标记指定数组长度 使用标准库规范 显式传递一个表示数组大小的形参 数组形参和const 数组引用形参 传递多维数…

mysql基础1sql分类

mysql基础 [rootvm ~]# docker run -itd -p 3306:3306 -e "MYSQL_ROOT_PASSWORD123456" mysql:5.7.26通用语法 1). SQL语句可以单行或多行书写&#xff0c;以分号结尾。 2). SQL语句可以使用空格/缩进来增强语句的可读性。 3). MySQL数据库的SQL语句不区分大小写…

(ES6)前端八股文修炼Day2

1. let const var 的区别 var&#xff1a; var 是在 ES5 中引入的声明变量的关键字。 具有函数作用域&#xff0c;而不是块作用域&#xff0c;这意味着使用 var 声明的变量在函数内部是可见的。 变量可以被重复声明&#xff0c;而且变量的值可以在声明前使用&#xff0c;这可能…

阿里云服务器租用价格表,2024年1个月和一年优惠价格表

2024年腾讯云服务器优惠价格表&#xff0c;一张表整理阿里云服务器最新报价&#xff0c;阿里云服务器网整理云服务器ECS和轻量应用服务器详细CPU内存、公网带宽和系统盘详细配置报价单&#xff0c;大家也可以直接移步到阿里云CLUB中心查看 aliyun.club 当前最新的云服务器优惠券…

每日一题 --- 反转链表[力扣][Go]

反转链表 题目&#xff1a;206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&a…

AtCoder Regular Contest 174 A~E

A.A Multiply&#xff08;贪心&#xff09; 题意&#xff1a; 给你一个长度为 N N N 、 A ( A 1 , A 2 , … , A N ) A(A_1,A_2,\dots,A_N) A(A1​,A2​,…,AN​) 和整数 C C C 的整数序列。 在进行最多一次以下操作后&#xff0c;求 A A A 中元素的最大可能和&#xff…

为什么安装了4GB的内存条,却显示只有3.8GB?

朋友们&#xff0c;对于计算机而言&#xff0c;其基本包含三部分。 第一&#xff0c;CPU; 第二&#xff0c;存储器&#xff08;内存、物理内存&#xff09;&#xff1b;第三&#xff0c;输入设备、输出设备。 32位的地址总线&#xff0c;其地址范围就是 CPU 访问内存&#xf…

小车倒立摆系统极点配置,LQR闭环控制

在之前直流电机控制仿真里有讲过状态控制的基本架构&#xff0c;有兴趣的同学可以再回去看看&#xff0c;链接如下好玩的直流电机调速实验、PID、极点配置、LQR、观测器&#xff1b;不讲大道理_lqr控制器观测器-CSDN博客 在专栏的前三篇文章 小车倒立摆物理建模与simulink仿真…

【消息队列开发】 设计网络通信协议

文章目录 &#x1f343;前言&#x1f38d;明确需求&#x1f340;设计应用层协议&#x1f384;定义 Request / Response&#x1f334;定义参数父类与返回值父类&#x1f332;定义其他参数类&#x1f333;定义其他返回类⭕总结 &#x1f343;前言 本次开发任务 设计网络通信协议…

带3090显卡的Linux服务器上部署SDWebui

背景 一直在研究文生图&#xff0c;之前一直是用原始模型和diffuser跑SD模型&#xff0c;近来看到不少比较博主在用 SDWebui&#xff0c;于是想着在Linux服务器上部署体验一下&#xff0c;谁知道并没有想象的那么顺利&#xff0c;还是踩了不少坑。记录一下过程&#xff0c;也许…

基于springboot+vue的高校专业实习管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

数据结构面试常见问题之串的模式匹配(KMP算法)系列-大师改进实现以及原理

&#x1f600;前言 KMP算法是一种改进的字符串匹配算法&#xff0c;由D.E.Knuth&#xff0c;J.H.Morris和V.R.Pratt提出的&#xff0c;因此人们称它为克努特—莫里斯—普拉特操作&#xff08;简称KMP算法&#xff09;。KMP算法的核心是利用匹配失败后的信息&#xff0c;尽量减少…

CentOS7配置静态ip

CentOS7进入root账户,点击右上角 点击有线设置 点击有线设置,查看目前ip地址,记住,点击ipv4 点击手动,在下面的地址写你自己想要的IP地址,注意只有最后的100可以改,146改成你自己的网段,网关和DNS最后一位都写2就行了 注意DNS一定要写,要不然让他自动找不到,会断网 在这个位置…

云计算安全分析

目录 一、概述 二、《云计算服务安全指南》的云安全风险分析 2.1 客户对数据和业务系统的控制能力减弱 2.2 客户与云服务商之间的责任难以界定 2.3 可能产生司法管辖权问题 2.4 数据所有权保障面临风险 2.5 数据保护更加困难 2.6 数据残留 2.7 容易产生对云服务商的过度…

手撕算法-删除链表的倒数第 N 个结点

描述 思路 快慢指针&#xff0c;快指针先走N步&#xff0c;走不够N步返回空。慢指针和快指针一起走&#xff0c;当快指针到达终点&#xff0c;即快指针为null时&#xff0c;慢指针到达倒数第N个节点。因为要删除倒数第N个&#xff0c;所以要记录之前的节点pre&#xff0c;假设…

JavaScript原型、原型对象、原型链系列详解(二)

(二)、JavaScript原型对象 原型对象 JavaScript 的原型机制是一种非常强大和灵活的面向对象编程概念&#xff0c;它使用一个对象作为另一个对象的模板&#xff0c;从而实现继承。在 JavaScript 中&#xff0c;每个对象都有一个原型对象&#xff0c;它定义了该对象的属性和方法&…

力扣100热题[哈希]:最长连续序列

原题&#xff1a;128. 最长连续序列 题解&#xff1a; 官方题解&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;题解&#xff0c;最长连续序列 &#xff1a;哈希表 官方解题思路是先去重&#xff0c;然后判断模板长度的数值是否存在&#xff0c;存在就刷新&#xff0c…

【4XVR】win11局域网共享3D影片给quest3

准备工作 首先要有一个路由器&#xff0c;使电脑和quest3处于同一个局域网下 一.创建一个离线账户 打开设置选择账户 添加账户 二.共享文件 选择要共享的文件夹&#xff0c;右键打开属性&#xff0c;点击共享 选择刚刚创建的用户&#xff0c;点击共享即可 三.使用quest观影 …

AttributeError: ‘_MSDataLoaderIter‘ object has no attribute ‘_put_indices‘

问题描述 复现代码过程中遇到错误&#xff1a;AttributeError: _MSDataLoaderIter object has no attribute _put_indices 解决方案 出错的原因是代码中使用了不存在的属性"_put_indices"。这个错误可能与你使用的版本不兼容有关。在pytorch1.x版本中&#xff0c;&q…