【C++面向对象程序设计】CH5 继承与派生

news2025/1/17 17:21:53

目录

前言

一、继承与派生的概念

二、派生类的声明方式

1.格式

2.【例】假定已经声明一个基类student,在它基础上通过单继承建立一个派生类student1。

三、派生类的构成

四、派生类成员的访问属性

前言

1.公有继承

(1)公有基类在派生类中的访问属性

(2)【例5.1】派生类访问公有继承的基类成员

(3)【例】公有继承举例

2.私有继承

(1)私有基类在派生类中的访问属性

(2)【例5.2】将例5.1中公有继承改为私有继承

3.保护成员和保护继承

(1)保护基类在派生类中的访问属性

(2)【例5.3】在派生类中访问保护成员

4.多级派生时的访问属性

(1)介绍

(2)【例5.4】多级派生类的访问属性。如果声明了以下的了类。

五、类型兼容规则

1.规则

2.类型兼容规则举例

3.基类与派生类的对应关系

4.多继承时派生类的声明

5.多继承举例

6.继承与派生的目的

六、派生类的构造函数和析构函数

前言——派生类的构造函数 

1.简单的派生类的构造函数

(1)介绍

(2)【例5.5】简单派生类的构造函数

(3)单一继承时的构造函数举例

2.有子对象的派生类的构造函数

(1)介绍

(2)【例】

3.多层派生时的构造函数

(1)介绍

(2)举例

4.派生类构造函数的特殊形式

5.派生类的析构函数

(1)介绍

(2)单一继承时构造函数、析构函数举例

七、多重继承

1.声明多重继承的方法

(1)介绍

(2)【例5.8】

2.多重继承派生类的构造函数

(1)派生类构造函数举例

(2)同名隐藏规则

(3)多继承同名隐藏举例

3.多重继承引起的二义性问题

(1)介绍

(2)二义性问题举例(一)

(3)二义性的解决方法

(4)二义性问题举例(二) 

(5)多重继承的同名问题


前言

        面向对象程序设计有四个主要特点:抽象、封装、继承和多态性。本文主要介绍有关继承的知识,在下一篇文章介绍多态性。

一、继承与派生的概念

        C++的继承机制实现软件可重用。有时两个类的内容基本相同或有一部分相同。例如已声明了类student:

class Student
{ 
	private :
	   int num;
	   string name;
	   char sex; 
    public:
	  	void display( )
	    {
			cout<<"num: "<<num<<endl;
	    	cout<<"name: "<<name<<endl;
	    	cout<<"sex: "<<sex<<endl;
		}
}; 

        如果另一个部门除了需要已有的数据外,还需要地址信息,你可以再声明另一个类:

class Student1
{
	private :
	    int num;
	    string name;
	    char sex;
	    char addr[20]; 
    public:
		void display()
	    { 
			cout<<"num: "<<num<<endl;
	        cout<<"name: "<<name<<endl;
	        cout<<"sex: "<<sex<<endl;
 	    	cout <<"address:"<< addr<<endl;
		}	
}; 

        可以看到新类中大部分成员是原来已有的。人们自然会想到能否利用原来声明的类student,加上新内容即可,以减少重复的工作。这就引出了C++的继承机制。

        所谓继承是在已存在的类A的基础上建立一个新类B。类A称为基类或父类,类B称为派生类或子类。子类从父类获得其已有的特性,这种现象称作类的继承。从另一个角度看从父类产生子类,称作类的派生

        一个基类可以派生出多个派生类,每个派生类又可以作为基类再派生新的派生类。一个派生类只从一个基类派生,称作单继承

        一个派生类也可从多个基类派生,也就是说一个派生类可以有两个或多个基类。一个派生类有两个或多个基类的称为多重继承。基类和派生类的关系可以表述为:派生类是基类的扩充,而基类是派生类的抽象

二、派生类的声明方式

1.格式

        使用派生类要先声明,声明的格式为:

class  派生类名: [继承方式] 基类名
{      
    派生类新增成员声明    
};

        继承方式包括:public、private、protected。如果省略,默认为private。

2.【例】假定已经声明一个基类student,在它基础上通过单继承建立一个派生类student1。

class Student1: public Student
{  
	private:
	    int age;
	    string addr;
    public:
	    void display_1()
	    {  
			cout <<"age: "<<age<<endl;
	    	cout <<"address: "<<addr<<endl;
		}
};

三、派生类的构成

        派生类中的成员包括从基类继承过来的成员和自己增加的成员。继承基类成员体现了同一基类的派生类都具有的共性,而新增加的成员体现了派生类的个性

  • 从基类接受成员:派生类将基类除构造函数和析构函数外的所有成员接收过来
  • 调整从基类接受的成员:一方面可以通过继承方式改变基类成员在派生类中的访问属性,另一方面可以在派生类中声明一个与基类成员同名的成员屏蔽基类的同名成员,注意如是成员函数不仅要函数名相同,而且函数的参数也要相同,屏蔽的含义是用新成员取代旧成员
  • 在声明派生类时增加成员:它体现了派生类对基类功能的补充
  • 在声明派生类时,还要自己定义派生类的构造函数

四、派生类成员的访问属性

前言

        派生类中包含了基类成员和派生类成员,就产生了这两部分成员的关系和访问属性的问题。这个关系由基类成员的访问属性和派生类的继承方式组合决定。

1.公有继承

(1)公有基类在派生类中的访问属性

        当派生类的继承方式为public(公有)属性时,在派生类中,基类的公有成员和保护成员在派生类中的访问属性没有变化,即分别作为派生类的公有成员和保护成员,派生类的成员可以直接访问它们。但是,派生类的成员无法直接访问基类的私有成员。保护私有成员是一条重要的原则。

(2)【例5.1】派生类访问公有继承的基类成员

class Student1: public Student
{
	private:
	    int age;
	    string addr; 
	public:
	   void get_value_1()
			{
				cin>>age>>addr;
			}
	   void display_1()
	    {  
			//cout<<"num: "<<num<<endl;      //  错误
	    	//cout<<"name: "<<name<<endl;   //  错误
	    	//cout<<"sex: "<<sex<<endl;          //  错误
	    	cout<<"age: "<<age<<endl;               //  正确
	    	cout<<"address: "<<addr<<endl;		//  正确
		}      
};

【解释】

        由于基类的私有成员对派生类说是不能访问的,所以派生类的成员函数display_1不能直接访问基类的私有成员,只能通过基类的公有成员函数访问基类的私有成员。

        因为是公有继承,基类的公有成员在派生类中仍是公有,所以派生类的对象可以通过基类的公有成员函数访问基类的私有数据成员,也可以在派生类的成员函数中调用基类的公有成员函数,访问基类的私有数据成员。

方法一:

int main()
{	
	Student1 stud1;
	…  … 
	stud1.display();
	stud1.display_1();
	return 0;
} 

方法二:

void display_1()
{ 	
	display();    //派生类成员调用基类公有成员
	cout<<"age: "<<age<<endl;              //  正确
	cout<<"address: "<<addr<<endl; 	//  正确
}   
int main()
{
	Student1 stud1;
	stud1.get_value();
	stud1.get_value_1();
	stud1.display_1();
	return 0;
} 

(3)【例】公有继承举例

class Point 
{	//基类Point类的声明
	public:	//公有函数成员
		void InitP(float xx = 0, float yy = 0) 
		{
			X = xx;
			Y = yy;
		}
		void Move(float xOff, float yOff) 
		{
			X += xOff;
			Y += yOff;
		}
		float GetX() 
		{
			return X;
		}
		float GetY() 
		{
			return Y;
		}
	private:	//私有数据成员
		float X, Y;
};

class Rectangle: public Point 
{ //派生类声明
	public:	//新增公有函数成员
		void InitR(float x, float y, float w, float h) 
		{
			InitP(x, y);    //调用基类公有成员函数
			W = w;
			H = h;
		}
		float GetH() 
		{
			return H;
		}
		float GetW() 
		{
			return W;
		}
	private:	//新增私有数据成员
		float W, H;
};
#include <iostream>
#include <cmath>
using namespace std;

int main() 
{
	Rectangle rect;
	rect.InitR(2, 3, 20, 10);
	//通过派生类对象访问基类公有成员
	rect.Move(3, 2);
	cout << rect.GetX() << ','
	     << rect.GetY() << ','
	     << rect.GetH() << ','
	     << rect.GetW() << endl;
	return 0;
}

2.私有继承

(1)私有基类在派生类中的访问属性

        在派生类中,基类的公有成员和保护成员作为派生类的私有成员,派生类的成员可以直接访问他们,而派生类的成员无法直接访问基类的私有成员。

        在派生类的外部,派生类的对象无法访问基类的全部对象。

        私有继承之后,全部基类成员在派生类中都成为了私有成员或不可访问的成员,无法进一步派生。私有成员一般很少使用。

(2)【例5.2】将例5.1中公有继承改为私有继承

class Student 
{
	private :
		int num;
		string name;
		char sex;
	public:
		void display( ) 
		{
			cout << "num: " << num << endl;
			cout << "name: " << name << endl;
			cout << "sex: " << sex << endl;
		}
};

class Student1: private Student 
{
	private:
		int age;
		string addr;
	public:
		void display_1() 
		{
			display();
			cout << "age: " << age << endl;       //  正确
			cout << "address: " << addr << endl;	//  正确
		}
};

int main() 
{
	Student1 stud1;
	stud1.display_1();
	return 0;
}

3.保护成员和保护继承

(1)保护基类在派生类中的访问属性

        当派生类的继承方式为protected继承属性时,在派生类中,基类的公有成员和保护成员均作为派生类的保护成员,派生类的成员可以直接访问他们,而派生类的成员无法访问基类的私有成员。

        在派生类的外部,派生类的对象无法访问基类的全部成员。

        如果基类只进行了一次派生,则保护继承和私有继承的功能完全相同,但保护继承可以进一步派生,而私有继承则不可以,两者具有实质性差别。

(2)【例5.3】在派生类中访问保护成员

class Student 
{              //    声明基类
	protected :                   //    基类保护成员
		int num;
		string name;
		char sex;
	public:                          //    基类公用成员
		void display( );
};

class Student1: protected Student 
{
	private:
		int age;
		string addr;
	public:
		void display1( );
};

void Student1::display1( ) 
{
	cout << "num: " << num << endl; //引用基类的保护成员
	cout << "name: " << name << endl;
	cout << "sex: " << sex << endl;
	cout << "age: " << age << endl;
	cout << "address: " << addr << endl;
}

        派生类的成员函数访问基类的保护成员是合法的。基类的保护成员对派生类的外界来说是不可访问的(例如,num是基类student的保护成员,由于派生类是保护继承,所以它在派生类中仍受保护,外界不能用stud1.num形式访问它)对照【例5.2】可以看到:保护成员和私有成员的区别在于把保护成员的访问范围扩展到派生类中

4.多级派生时的访问属性

(1)介绍

        以上介绍了只有一级派生的情况,实际上常常有多级派生的情况,如果有图5.9所示的派生关系:类A为基类,类B是类A的派生类,类C是类B的派生类,则类C也是类A的派生类。类B是类A的直接派生类,类C是类A的间接派生类。类A是类B的直接基类,是类C的间接基类

(2)【例5.4】多级派生类的访问属性。如果声明了以下的了类。

class A 
{             //    基类
	private:
		int ka;
	public:
		int ia;
	protected:
		void fa( );
		int ja;
};

class B: public A 
{         //   public方式
	private:
		int mb;
	public:
		void fb1( );
	protected:
		void fb2( );
};

class C: protected B 
{    //   protected方式
	private:
		int nc;
	public:
		void fc1( );
};

        类B公有继承类A,类C保护继承类B。各个成员在不同类中的访问属性如下:

五、类型兼容规则

1.规则

        一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止。具体表现在:

  • 派生类的对象可以被赋值给基类对象
  • 派生类的对象可以初始化基类的引用(向下兼容)
  • 指向基类的指针也可以指向派生类
  • 通过基类对象名、指针只能使用从基类继承的成员

2.类型兼容规则举例

#include <iostream>
using namespace std;

class B0 
{	//  基类B0声明
	public:      //  公有成员函数

		void display() 
		{
			cout << "B0::display()" << endl;
		}
};

class B1: public B0 
{
	public:
		void display() 
		{
			cout << "B1::display()" << endl;
		}
};

class D1: public B1 
{
	public:
		void display() 
		{
			cout << "D1::display()" << endl;
		}
};

void fun(B0 *ptr) 
{
	ptr->display();	//"对象指针->成员名"
}

int  main() 
{	//主函数
	B0 b0;	//声明B0类对象
	B1 b1;	//声明B1类对象
	D1 d1;	//声明D1类对象
	B0 *p;	//声明B0类指针
	p = &b0;	//  B0类指针指向B0类对象
	fun(p);
	p = &b1;	//  B0类指针指向B1类对象
	fun(p);
	p = &d1;	//  B0类指针指向D1类对象
	fun(p);
	return 0;
}

3.基类与派生类的对应关系

  • 单继承:派生类只能从一个基类派生
  • 多继承:派生类从多个基类派生
  • 多重派生:由一个基类派生出多个不同的派生类
  • 多层派生:派生类又作为基类,继续派生新的类

4.多继承时派生类的声明

class 派生类名:继承方式1  基类名1,继承方式2  基类名2,...
{
        成员声明;
}

 【注】每一个“继承方式”,只用于限制紧随其后的基类的继承

5.多继承举例

class A 
{
	public:
		void setA(int);
		void showA();
	private:
		int a;
};

class B 
{
	public:
		void setB(int);
		void showB();
	private:
		int b;
};

class C : public A, private B 
{
	public:
		void setC(int, int, int);
		void showC();
	private:
		int c;
};

void  A::setA(int x) 
{
	a = x;
}

void B::setB(int x) 
{
	b = x;
}

void C::setC(int x, int y, int z) 
{
	//派生类成员直接访问基类的
	//公有成员
	setA(x);
	setB(y);
	c = z;
}

//其它函数实现略
int main() 
{
	C obj;
	obj.setA(5);
	obj.showA();
	obj.setC(6, 7, 9);
	obj.showC();
// obj.setB(6);  错误
// obj.showB(); 错误
	return 0;
}

6.继承与派生的目的

  • 继承的目的:实现代码重用
  • 派生的目的:当新的问题出现,原有程序无法解决(或不能完全解决)时,需要对原有程序进行改造

六、派生类的构造函数和析构函数

前言——派生类的构造函数 

        基类的构造函数不被继承,派生类中需要声明自己的构造函数。

        声明构造函数时,只需要对本类中新增成员进行初始化,调用基类构造函数对继承来的基类成员初始化。

        派生类的构造函数需要给基类的构造函数传递参数。

1.简单的派生类的构造函数

(1)介绍

        简单派生类只有一个基类,而且只有一级派生,在派生类的数据成员中不包含基类的对象(即子对象)。

        在定义派生类的构造函数时除了对自己的数据成员进行初始化外,还必须用基类的构造函数初始化基类的数据成员。构造函数格式如下:

派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(基类参数表)
{
        本类成员初始化赋值语句;
};

        派生类名后的参数表分别列出基类和派生类构造函数的形参(有类型和形参变量)。

        基类参数表列出传递给基类构造函数的实参,是派生类构造函数总参数表中的参数。

        用派生类构造函数的形参作基类构造函数的实参。

(2)【例5.5】简单派生类的构造函数

#include <iostream>
#include <string.h>
using namespace std;

class Student 
{                            //声明基类
	public:                                  //公用部分
		Student(int n, string nam, char s ) 
		{   //基类构造函数
			num = n;
			name = nam;
			sex = s;
		}
		~Student( ) { }
	protected:                               //保护部分
		int num;
		string name;
		char sex ;                            //基类析构函数
};

class Student1: public Student 
{ //  声明公用派生类
	public:
		Student1(int n, string nam, char s, int a, char ad[ ] ): Student ( n, nam, s) 
		{ //   派生类构造函数
			age = a;     //  只对派生类新增的数据成员初始化
			addr = ad;
		}
		void show( );
	private:                 //  派生类的私有部分
		int age;
		string addr;
};

void Student1::show() 
{
	cout << "num: " << num << endl;
	cout << "name: " << name << endl;
	cout << "sex: " << sex << endl;
	cout << "age: " << age << endl;
	cout << "address: " << addr << endl;
}

int main( ) 
{
	Student1 stud1(10010, "Wang-li", 'f', 19, "115 Beijing Road, Shanghai");
	Student1 stud2(10011, "Zhang-fun", 'm', 21, "213 Shanghai Road,Beijing");
	stud1.show( );            // 输出第一个学生的数据
	stud2.show( );            // 输出第二个学生的数据
	return 0;
}

        在建立一个对象时,执行构造函数的顺序是:派生类构造函数先调用基类构造函数;再执行派生类构造函数本身(即派生类构造函数的函数体)。按上面的例子说,先初始化num,name,sex,然后再初始化age,addr。

        释放派生类对象时,先执行派生类析构函数,再执行其基类的析构函数。

(3)单一继承时的构造函数举例

#include <iostream>
using namespace std;

class B 
{
	private:
		int b;
	public:
		B();
		B(int i);
		void Print() const;
};

B::B() 
{
	b = 0;
	cout << "调用B的默认构造函数." << endl;
}

B::B(int i) 
{
	b = i;
	cout << "调用B的构造函数." << endl;
}

void B::Print() const 
{
	cout << b << endl;
}

class C: public B 
{
	private:
		int c;
	public:
		C();
		C(int i, int j);
		void Print() const;
};

C::C() 
{
	c = 0;
	cout << "调用C的默认构造函数." << endl;
}

C::C(int i, int j): B(i) 
{
	c = j;
	cout << "调用C的构造函数." << endl;
}

void C::Print() const 
{
	B::Print();
	cout << c << endl;
}

int main() 
{
	C obj(5, 6);
	obj.Print();
	return 0;
}

2.有子对象的派生类的构造函数

(1)介绍

        类的数据成员除了是标准类型或系统提供的类型如string,还可以是类类型,如声明一个类时包含类类型的数据成员:Student s1;Student是已声明过的类名,s1是该类的对象,我们称s1为子对象。以【例5.5】为例,除了可以在派生类student1中增加age、address成员外,还可以增加班长一项,而班长本身也是学生,它属于student类型,有学号和姓名等基本数据,班长这项就是派生类中的子对象。

        怎样对子对象初始化?由于类是一种数据类型,不能带具体的值,何况每个派生类对象的子对象一般是不同的(如学生A,B,C的班长是A,而学生D,E,F的班长是F)。所以不能在声明派生类时对子对象初始化,系统在建立派生类对象时调用派生类构造函数对子对象进行初始化。

        派生类构造函数的任务包括:

  • 对基类数据成员初始化
  • 对子对象的数据成员初始化
  • 对派生类的数据成员初始化

        派生类构造函数一般形式为:

派生类名::派生类名 (总参数表):基类名(实参表 ), 子对象名(参数表)
{
    派生类新增成员的初始化语句;
} 

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

  • 调用基类构造函数,初始化基类数据成员
  • 调用子对象构造函数,初始化子对象数据成员
  • 执行派生类构造函数,初始化派生类数据成员 

        编译系统在此根据参数名(而不是参数的顺序)决定各参数表中参数之间的传递关系。如果多个子对象,要逐个列出子对象及其参数表。

(2)【例】

#include <iostream>
#include <string.h>
using namespace std;

class Student 
{                            //声明基类
	public:                                  //公用部分
		Student(int n, string nam ) 
		{    //基类构造函数
			num = n;
			name = nam;
		}
		void display() 
		{
			cout << "学号:" << num << endl << "姓名:" <<  name << endl;
		}
	protected:                               //保护部分
		int num;
		string name;
		char sex ;                            //基类析构函数
};

class Student1: public Student 
{ //   public继承方式
	private:                           //  派生类的私有数据
		Student monitor;           //  定义子对象(班长)
		int age;
		string addr;
	public:
//下面是派生类构造函数
		Student1(int n, string nam, int n1, string nam1, int a, string ad)
			: Student(n, nam), monitor(n1, nam1) 
		{
			age = a;               //    在此处只对派生类
			addr = ad;            //    新增的数据成员初始化
		}


		void show( ) 
		{
			cout << "这个学生是:" << endl;
			display();            // 输出num和name
			cout << "年龄: " << age << endl;
			cout << "地址: " << addr << endl << endl;
		}
//  输出子对象的数据成员
		void show_monitor() 
		{
			cout << endl << "班长是:" << endl;
			monitor.display();  //调用基类成员函数
		}

};

int main( ) 
{
	Student1 stud1(10010, "王力", 10001, "李军", 19, "上海市北京路115号 ");
	stud1.show( );             //  输出第一个学生的数据
	stud1.show_monitor();     //  输出子对象的数据
	return 0;
}

3.多层派生时的构造函数

(1)介绍

        一个类可以派生出一个派生类,派生类和可以继续派生,形成派生的层次结构。多层派生时怎样写派生类的构造函数?现有如图所示的多层派生类:

        可以按照前面派生类构造函数的规则逐层写出各个派生类的构造函数。

        基类的构造函数首部:

Student(int n, string nam );

         派生类student1的构造函数首部:

Student1(int n,string nam,int a):Student(n,nam);

        派生类student2的构造函数首部:

Student2(int n,string nam,int a,int s):Student1(n,nam,a);

         写派生类构造函数的规则是:只须调用其直接基类的构造函数即可,不要列出每一层派生类的构造函数。在声明Student2类对象时,调用Student2构造函数,在执行Student2构造函数时,先调用Student1构造函数,在执行Student1构造函数时,先调用基类Student构造函数。初始化的顺序是:

  • 先初始化基类的数据成员num和name
  • 再初始化Student1的数据成员age
  • 最后初始化Student2的数据成员score

(2)举例

#include <iostream>
#include <string.h>
using namespace std;

class Student 
{                            //声明基类
	public:                                  //公用部分
		Student(int n, string nam ) 
		{          //基类构造函数
			num = n;
			name = nam;
		}
		void display() 
		{                         //输出基类数据成员
			cout << "num:" << num << endl;
			cout << "name:" << name << endl;
		}
	protected:                                //保护部分
		int num;                                //基类有两个数据成员
		string name;
};

class Student1: public Student 
{ //声明公用派生类Student1
	public:
		Student1(int n, string nam, int a): Student(n, nam)
//派生类构造函数
		{
			age = a;    //在此处只对派生类新增的数据成员初始化
		}
		void show( ) 
		{                 //输出num,name和age
			display();                      //输出num和name
			cout << "age: " << age << endl;
		}
	private:                                   //派生类的私有数据
		int age;                                  //增加一个数据成员
};

class Student2: public Student1

//声明间接公用派生类student2
{
	public:
		//下面是间接派生类构造函数
		Student2(int n, string nam, int a, int s): Student1(n, nam, a) 
		{
			score = s;
		}
		void show_all() 
		{   //  输出全部数据成员
			show();               //  输出num和name
			cout << "score:" << score << endl; //输出age
		}
	private:
		int score;                                   //增加一个数据成员
};

int main( ) 
{
	Student2 stud(10010, "李明", 17, 89);
	stud.show_all( );  //输出学生的全部数据
	return 0;
}

4.派生类构造函数的特殊形式

(1)当不需要对派生类新增成员进行初始化时,派生类构造函数的函数体可以为空。如【例5.6】程序中派生类Student1的构造函数改写成:

Student1(int n,string nam,int n1,string nam1 ) : Student(n,nam),monitor(n1,nam1)

        在调用派生类构造函数时不对派生类的数据成员初始化。

(2)如果在基类里没有定义构造函数,或定义了没有参数的构造函数,在定义派生类构造函数时可以不写基类的构造函数。因为此时派生类构造函数没有向基类构造函数传递参数的任务。在调用派生类构造函数时,系统地自动首先调用基类的默认构造函数。

(3) 如果在基类和子对象的类中都没有定义带参数的构造函数,也不需要对派生类自己的数据成员进行初始化,可以不定义派生类的构造函数

(4)如果在基类或子对象的类声明里定义了带参数的构造函数,就必须定义派生类构造函数,并在派生类构造函数中写出基类或子对象类的构造函数及其参数表

(5)如果在基类既定义了无参数的构造函数也定义了有参数的构造函数,在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数

5.派生类的析构函数

(1)介绍

        类派生时,派生类不能继承基类的析构函数,撤销派生类对象时,需要派生类的析构函数去调用基类的析构函数。首先执行派生类自己的析构函数,清理派生类新增加的成员,然后调用子对象类的析构函数清理子对象,最后调用基类析构函数清理基类的成员。

(2)单一继承时构造函数、析构函数举例

#include <iostream>
using namespace std;

class B 
{
	private:
		int b;
	public:
		B();
		B(int i);
		~B();
		void Print() const;
};

B::B() 
{
	b = 0;
	cout << "调用B的默认构造函数." << endl;
}

B::B(int i) 
{
	b = i;
	cout << "调用B的构造函数." << endl;
}

B::~B() 
{
	cout << "调用B的析构函数." << endl;
}

void B::Print() const 
{
	cout << b << endl;
}

class C: public B 
{
	private:
		int c;
	public:
		C();
		C(int i, int j);
		~C();
		void Print() const;
};

C::C() 
{
	c = 0;
	cout << "调用C的默认构造函数." << endl;
}

C::C(int i, int j): B(i) 
{
	c = j;
	cout << "调用C的构造函数." << endl;
}

C::~C() 
{
	cout << "调用C的析构函数." << endl;
}

void C::Print() const 
{
	B::Print();
	cout << c << endl;
}

int main() 
{
	C obj(5, 6);
	obj.Print();
	return 0;
}

七、多重继承

1.声明多重继承的方法

(1)介绍

        假定已声明了类A,类B和类C,由它们派生出新类D,声明的形式可以是:

Class D: public A,private B, protected C
	{  D类新增的成员声明 }

        多重继承派生类的构造函数在初始化表中包含多个基类构造函数,假定派生类有三个基类,它的构造函数形式是:

派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数表)
   { 派生类新增成员初始化语句 }

        各基类的排列顺序不分先后,系统调用基类构造函数的顺序就是声明派生类时基类的出现顺序。如在前面中声明派生类D时,基类出现的顺序是A,B,C。系统先调用A的构造函数,再掉用B的构造函数,最后调用C的构造函数。

(2)【例5.8】

声明一个教师类和一个学生类,用多重继承的方式声明一个研究生类。教师包括name,age,title数据成员,学生类包括name1,sex,score。在定义派生类对象时给出初始化数据,输出这些数据。

#include <iostream>
#include <string.h>
using namespace std;

class Teacher 
{                      //  声明Teacher(教师)类
	public:                                  //  公用部分
		Teacher(string nam, int a, string t) 
		{  //  构造函数
			name = nam;
			age = a;
			title = t;
		}
		void display() 
		{                        //  输出教师有关数据
			cout << "name:" << name << endl;
			cout << "age" << age << endl;
			cout << "title:" << title << endl;
		}
	protected:                               //  保护部分
		string name;
		int age;
		string title;                           //  职称
};

class Student 
{                          //  声明类Student(学生)
	public:
		Student(string nam, char s, float sco) 
		{
			name1 = nam;
			sex = s;
			score = sco;
		}                         //  构造函数
		void display1() 
		{                     //  输出学生有关数据
			cout << "name:" << name1 << endl;
			cout << "sex:" << sex << endl;
			cout << "score:" << score << endl;
		}
	protected:                               //  保护部分
		string name1;
		char sex;
		float score;                            //  成绩
};

class Graduate: public Teacher, public Student 
{
	public:
		Graduate(string nam, int a, char s, string t, float sco, float w):
			Teacher(nam, a, t), Student(nam, s, sco), wage(w) {}
		void show( ) 
		{                               //  输出人员的有关数据
			cout << "name:" << name << endl;
			cout << "age:" << age << endl;
			cout << "sex:" << sex << endl;
			cout << "score:" << score << endl;
			cout << "title:" << title << endl;
			cout << "wages:" << wage << endl;
		}
	private:
		float wage;                                  //  工资
};

int main( ) 
{
	Graduate grad1("Wang-li", 24, 'f', "assistant", 89.5, 1234.5);
	grad1.show( );
	return 0;
}

【注】从这个程序中可以发现,在多重继承时,从不同的基类中会继承重复的数据成员,例如本例中姓名就是重复的数据成员。 

2.多重继承派生类的构造函数

(1)派生类构造函数举例

#include <iostream>
using namespace std;

class B1 
{	//基类B1,构造函数有参数
	public:
		B1(int i) 
		{
			cout << "构造 B1 " << i << endl;
		}
};

class B2 
{	//基类B2,构造函数有参数
	public:
		B2(int j) 
		{
			cout << "构造 B2 " << j << endl;
		}
};

class B3 
{	//基类B3,构造函数无参数
	public:
		B3() 
		{
			cout << "构造 B3 *" << endl;
		}
};

class C: public B2, public B1, public B3 
{
	public:	//派生类的公有成员
		C(int a, int b, int c, int d):
			B1(a), B2(b), memberB1(c), memberB2(d)  {}
	private:	//派生类的私有对象成员
		B1 memberB1;
		B2 memberB2;
		B3 memberB3;
};

int main() 
{
	C obj(1, 2, 3, 4);
	return 0;
}

【注】从结果看到多重继承时,系统先调用基类的构造函数,再掉用子对象的构造函数,最后调用派生类的构造函数。

(2)同名隐藏规则

        当派生类与基类中有相同成员时:

  • 若未强行指明,则通过派生类对象使用的是派生类中的同名成员
  • 如要通过派生类对象访问基类中被覆盖的同名成员,应使用基类名限定

(3)多继承同名隐藏举例

#include <iostream>
using namespace std;

class B1 
{	//声明基类B1
	public:	//外部接口
		int nV;
		void fun()  
		{
			cout << "Member of B1" << endl;
		}
};

class B2 
{	//声明基类B2
	public:	//外部接口
		int nV;
		void fun() 
		{
			cout << "Member of B2" << endl;
		}
};

class D1: public B1, public B2 
{
	public:
		int nV;	//同名数据成员
		void fun() 
		{
			cout << "Member of D1" << endl;   //同名函数成员
		}
};

int main() 
{
	D1 d1;
	d1.nV = 1; //对象名.成员名标识, 访问D1类成员
	d1.fun();

	d1.B1::nV = 2;	//作用域分辨符标识, 访问基类B1成员
	d1.B1::fun();

	d1.B2::nV = 3;	//作用域分辨符标识, 访问基类B2成员
	d1.B2::fun();
	return 0;
}

3.多重继承引起的二义性问题

(1)介绍

        多继承最常见的问题是派生类继承基类同名成员而产生的二义性问题。

        在多重继承时,基类与派生类之间,或基类之间出现同名成员时,将出现访问时的二义性(不确定性)——采用虚函数或同名隐藏规则来解决。

        当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性——采用虚基类来解决。

(2)二义性问题举例(一)

class A
{
    public:
        void  f();
};
class B
{
    public:
        void f();
        void g()
};
class C: public A, piblic B
{         
    public:
           void g();
           void h();
};

        如果声明:C c1;则c1.f()具有二义性,而c1.g()无二义性(同名覆盖原则)

(3)二义性的解决方法

  • 解决方法一:用类名来限定c1.A::f()或c1.B::f()
  • 解决方法二:同名覆盖。在C中声明一个同名成员f(),f()再根据需要调用A::f()或B::f()

(4)二义性问题举例(二) 

#include <iostream>
using namespace std;

class B 
{
	public:
		B(int i ) 
		{
			b = i;
		}
		int b;
};

class B1 : public B 
{
	public:
		B1(int b, int bx): B(b) 
		{
			b1 = bx;
		}
	private:
		int b1;
};

class B2 : public B 
{
	public:
		B2(int b, int bx): B(b) 
		{
			b2 = bx;
		}
	private:
		int b2;
};

class C: public B1, public B2 
{
	public:
		C(int x1, int x2, int x3, int x4): B1(x1, x2), B2(x1, x3) 
		{
			d = x4;
		}
	private:
		int d;
};

int main(int argc, char *argv[]) 
{
	C cc(1, 2, 3, 4);
	cout << cc.B1::b << endl;
	cout << cc.B2::b << endl;
	//cout<<cc.b<<endl;// 错误语句
	return 0;
}

        派生类C的对象的存储结构示意图:

//有二义性
C cc;
cc.b;
cc.B::b;
//无二义性
cc.B1::b;
cc.B2::b;

(5)多重继承的同名问题

        如果类A和类B都有成员函数display和数据成员a,类c是类A和类B的直接派生类。分别讨论下下买你的三种情况:

  • 两个基类有同名成员,如上图所示
Class  A 
{
	public:
		int a;
		void display( );
};

Class  B 
{
	public:
		int a;
		void display( );
};
Class  C :public A, public B 
{
	public:
		int b;
		void show( );
};

        如果在main函数中定义C类对象c1,并写出如下语句:

C c1;
c1.a=3;
c1.display();

        由于基类A和基类B都有数据成员a和成员函数display,系统无法辨别要访问的是A类还是B类,程序编译报错。解决这个问题可以用基类名限定,如:

c1.A::a=3;
c1.A::display();

        如果在派生类C中通过派生类成员函数show访问A的display和a,可以这样写:

A::a=3;
A::display();

         为了清楚起见,上图应该用下图的形式表示:

  • 两个基类和派生类三者都有同名成员。将前面的C类声明改为:
Class  C : public A, public B
{ 
	public:
		int a;
		void display( );
};

        如果在main函数中定义C类对象c1:

C c1;
c1.a=3;
c1.display();

        程序可以通过编译,也能正常运行,在语法上认为这样访问的是派生类的成员a和成员函数display,规则是:派生类屏蔽基类的同名成员对于带参数的成员函数除了要函数名相同外,参数的个数和类型都要相同才符合屏蔽条件。如果在派生类外访问基类A的成员,要指明作用域A:。

c1.A::a=3;
c1.A::display();
  • 如果类A和类B又是从同一个基类派生的:
Class N 
{
	public :
		int a;
		Void display()
		{ 
			cout<<"N::a="<<a;
		}
};
class  A : public N
{ 
	public:
		int a1;
};
class  B:public N
{ 
	public:
		int a2;
};
class  C : public A, public B
{ 
	public:
		int a3;
		void show( )
		{ 
			cout<<"a3="<<a3;
		}
};
int main() {  C c1; … }

        类A和类B分贝从类N继承了数据成员a和成员函数display,在类A和类B中存在同名数据成员a和成员函数display。类A和类B中的同名数据成员a分别为不同的内存单元。在程序中类A和类B的构造函数调用基类N的构造函数分别对类A和类B的数据成员a初始化。对象c1怎样访问类A从基类N继承来的成员呢?访问格式如下:

c1.A::a=3;
c1.A::display();

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

r、 weka从决策树模型看员工为什么离职?

马云说&#xff1a;“员工离职的原因总是只有两个&#xff1a;钱&#xff0c;没有到位;心委屈了。” 现在很多老板都抱怨说&#xff0c;年轻人的流动率太高了&#xff0c;员工觉得老板的钱太少了&#xff0c;最后还是多指责。 最近我们被客户要求撰写关于员工离职的研究报告&…

黄佳《零基础学机器学习》chap2笔记

黄佳 《零基础学机器学习》 chap2笔记 第2课 数学和Python基础知识 文章目录黄佳 《零基础学机器学习》 chap2笔记第2课 数学和Python基础知识2.1 函数描述了事物间的关系机器学习中常用的一些函数2.2 捕捉函数的变化趋势2.3 梯度下降2.4 机器学习的数据结构--张量2.4.1 张量的…

Mysql5.7 基于Docker搭建主从复制集群

基础 参考&#xff1a;mysql5.7-Docker-安装-备份 拉取镜像 docker pull mysql:5.7 创建目录 mkdir /home/liangshijie/mysql-docker-file/conf mkdir /home/liangshijie/mysql-docker-file/logs mkdir /home/liangshijie/mysql-docker-file/data创建配置文件 cd /home…

GEE Python本地快速下载GEE数据(比网页版保存到网盘再下载快几十倍,尤其是在下载几十年的长时间系列数据时,速度提升更加明显)

前言 可根据研究区直接裁剪数据以及进行一些计算处理后再下载&#xff0c;GEE成为了大家下载数据的重要途径&#xff0c;然而直接通过官网网页将数据先保存到网盘再下载的下载方法速度太慢&#xff0c;新号速度还好&#xff0c;越用速度越来越慢&#xff0c;本文提供了一种直接…

IBM MQ通道接收端绑定步骤

不同类型的绑定 IBM MQ 支持应用程序可以连接的两种方式&#xff1a; 1.本地绑定&#xff1a;这是当应用程序和队列管理器在同一个操作映像上时。 CHLAUTH 与此类应用程序连接无关。 2. 客户端绑定&#xff1a;这是应用程序和队列管理器使用网络进行通信的时候。 应用程序和队列…

从Opencv之图像直方图源码,探讨高性能计算设计思想

前言 纸上得来终觉浅&#xff0c;绝知此事要躬行。学会算法的理论很重要&#xff0c;但是把理论高效的实现也是需要一点点练习的。 图像直方图的理论很简单&#xff0c;就是把一个图像的像素区间划分为几个子区间&#xff0c;然后统计图像中的像素包含在子区间内的个数。这里&a…

什么是网络变压器,它是干什么用的?作用/参数表/价格及型号体系有哪些?怎么测量网络变压器好坏

什么是网络变压器&#xff1f; 网络变压器是在以太网网络传输设备中&#xff0c;主板上芯片PHY与网口RJ45之间的一个黑黑的小方块&#xff0c;里面的结构是磁环及铜线组成&#xff0c;这个小方块就是我们俗称的网络变压器 有的变压器中心抽头接到地。而接电源时&#xff0c;电…

深度学习入门(五十七)循环神经网络——循环神经网络从零开始实现

深度学习入门&#xff08;五十七&#xff09;循环神经网络——循环神经网络从零开始实现前言循环神经网络——循环神经网络从零开始实现教材1 独热编码(one-hot)2 初始化模型参数3 循环神经网络模型4 预测5 梯度裁剪6 训练7 小结前言 核心内容来自博客链接1博客连接2希望大家多…

【Hack The Box】linux练习-- Pandora

HTB 学习笔记 【Hack The Box】linux练习-- Pandora &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月27日&#x1f334; &#x1f…

TensorFlow之文本分类算法-4

1 前言 2 收集数据 3 探索数据 4 选择模型 5 准备数据 6 模型-构建训练评估 机器学习主要包括模型构建、模型训练、模型评估&#xff0c;在选择模型章节中已经对如何根据S/W的比率进行模型的选择&#xff0c;其包括n-gram模型或者序列模型&#xff0c;本章节内容主要描述…

LeetCode-813-最大平均值和的分组

1、动态规划法 我们可以利用数组dp[i][j]dp[i][j]dp[i][j]来表示我们将数组中区间[0,i−1][0,i-1][0,i−1]的元素分为jjj组的平均值的总和。因此我们可以得到状态转化方程如下&#xff1a;{dp[i][j]∑r0i−1nums[r]i,j1dp[i][j]maxx≥j−1(dp[x][j−1]∑rxi−1nums[r]i−x),j&…

计算机五大部件是指什么

计算机五大部件&#xff1a; 计算机硬件系统由运算器、控制器、存储器&#xff08;内外存储器&#xff09;、输入设备和输出设备五部分组成。 1、控制器&#xff1a;控制器就是一个系统中枢&#xff0c;控制计算机如何运行、运转的基本单元。 每一个运算器都需一个单独的控制…

D-016 JTAG硬件电路设计

JTAG硬件电路设计1 简介1.1 JTAG分类1.2 ARM调试器2 设计要点3 电路实战3.1 Xilinx3.2 ALTERA ------USB blaster1 简介 JTAG(Join Test Action Group)的中文名称为联合测试工作组&#xff0c;是一种国际标准测试协议&#xff0c;主要用于芯片内部测试&#xff1a;验证设计和P…

Ubuntu16.04 完整版 Gym 安装及说明

Ubuntu16.04 完整版 Gym 安装及说明1. 安装前言2. Gym 开发环境说明3. 完整版 Gym 的安装3.1 Anaconda 创建虚拟环境3.2 安装 gym[box2d]3.2.1 采用官方 GitHub 教程进行安装的报错及解决方案3.2.2 采用 Anaconda 进行安装3.2.3 测试能否成功运行 LunarLander-v2 环境3.3 安装 …

【一文秒懂——YAML配置文件】

目录 1. YAML配置文件 2. 实例 2.1 将原application.properties改为application.yml&#xff1a; 2.2 将原application-dev.properties改为application-dev.yml&#xff1a; 1. YAML配置文件 在Spring Boot项目中&#xff0c;还可以使用YAML配置文件&#xff0c;这是一种使…

一文搞懂漏洞严重程度分析

漏洞的级别定义主要从两个维度进行判断&#xff1b; 1、可利用性 2、影响性 可利用性指标 可利用性指标组刻画脆弱性组件&#xff08;即包含漏洞的事物&#xff09;的特征&#xff0c;反映漏洞利用的难易程度和技术要求等。可利用性指标组包含四个指标&#xff0c;分别是攻击…

最新阿里云GPU服务器租用配置费用价格表出炉(多配置报价)

阿里云GPU服务器优惠3折GPU云服务器vgn6i优惠价469.46元/月起&#xff0c;GPU云服务器gn6i和GPU云服务器gn6v均有活动&#xff0c;NVIDIA T4及V100均有活动&#xff0c;主机教程网 来详细说下阿里云GPU云服务器收费标准价格表&#xff0c;再来说说GPU云服务器计费模式&#xff…

Python学习笔记(十八)——Pandas进阶

文章主要内容文件操作&#xff1a; • 文件读取: read_csv(csv),read_table(表格),read_excl(excel) • 文件写入&#xff1a;to_csv, to_excel 数据清洗&#xff1a; • 处理缺失值&#xff1a;isnull(识别), dropna (过滤), fillna(填充) • 数据转换&#xff1a;drop_duplic…

MVCC多版本并发控制

目录 1、数据库并发场景 2、当前读和快照读 3、MVCC的隔离级别 4、实现原理 4.1、隐藏字段 ​​​​​​​4.2、Undo_log&#xff08;版本链&#xff09; 4.3、Read View&#xff08;读视图&#xff09; 4.4、RC、RR隔离级别下的快照读有什么区别 MVCC&#xff0c;多版…

文献认证!Kamiya艾美捷抗酒石酸酸性磷酸酶TRAP染色试剂盒

人类及动物体骨组织不断地进行着重建&#xff0c;骨重建过程包括骨的分解吸收与新骨的形成。破骨细胞&#xff08;Osteoclast&#xff0c;OC&#xff09;负责骨分解与吸收&#xff0c;而成骨细胞&#xff08;osteoblast&#xff0c;OB&#xff09;负责新骨形成。破骨细胞贴附在…