此专栏为移动机器人知识体系下的编程语言中的 C {\rm C} C++从入门到深入的专栏,参考书籍:《深入浅出 C {\rm C} C++》(马晓锐)和《从 C {\rm C} C到 C {\rm C} C++精通面向对象编程》(曾凡锋等)。
8.继承与派生
8.1 继承与派生基础
-
继承与派生举例理解:
- 交通工具是总的分类,是对具体的交通工具最高层次的抽象分类,具有最普遍的一般性意义;
- 下层的分类具有上层的特性,同时加入了自己的新特性,越往下层,新的特性越多;
- 如果将每种交通工具都定义成类,则在这个类层次结构中,上层是下层的基类,下层是上层的派生;
-
在面向对象程序设计中,以一个已经存在的类为基础声明一个新类的过程称为派生,这个已经存在的类称为基类或父类,由基类派生出来的类称为派生类,通过一定规则继承基类派生新类的过程称为继承和派生机制;
-
派生类具有基类的属性,在派生过程中根据需要增加成员,既能重新利用基类的功能,又能开发新的功能,派生类可以作为基类而派生出新类,这样就形成了类的层次关系;
-
派生类的声明,语法格式如下:
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} public、protected、private,分别为:公有继承、保护继承、私有继承,没有显式说明继承方式,系统默认继承方式为私有继承;
-
在派生的过程中派生出来的类同样可以作为基类再派生出新类,一个类可以派生出多个派生类,形成一系列相关联的类,称为类族;在类族中,如果类 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; }