C++核心编程——继承与派生

C++核心编程——继承与派生

  • 继承与派生的概念
    • 继承派生的概念
    • 派生类的声明方式
    • 派生类的构成
  • 派生类成员的访问属性
    • 公用继承
    • 私有继承
    • 保护继承
  • 派生类的构造函数与析构函数
    • 简单派生类构造函数
    • 有子对象的派生类的构造函数
    • 多层派生时的构造函数
  • 多重继承
    • 声明多重继承的方法
    • 多重继承引起的二义性问题
    • 虚基类
  • 基类与派生类的转换

继承与派生的概念

继承派生的概念

  • 在C++中所谓“继承”:就是在一个已存在的类的基础上建立一个新的类。
  • 一个新类从已有的类那里获得其已有特性,这种现象称为类的继承;从另一角度来说,从已有的类(父类)产生一个新的子类,称为类的派生
  • 派生类继承了基类的所有数据成员和成员函数,并可以对成员作必要的增加或调整
  • 一代一代地派生下去,就形成类的继承层次结构;一个派生类只从一个基类派生,这称为单继承,一个派生类有两个或多个基类的称为多重继承
  • 基类综合了派生类的公共特征,派生类则在基类的基础上增加某些特性,把抽象类变成具体的、实用的类型。
  • 派生类是基类的具体化,而基类则是派生类的抽象

派生类的声明方式

class Student1:public Student

在class后面的Student1是新建的类名。冒号后面的Student表示是已声明的基类。在Student之前有一关键字public用来表示基类Student中的成员在派生类Student1中的继承方式。基类名前面有public的称为“公用继承
声明派生类的一般形式为:

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

继承方式包括:public(公用的),private(私有的)和protected(受保护的)。继承方式是可选的,如果不写此项,则默认为 private(私有的)。

派生类的构成

  • 从基类继承的成员体现了派生类从基类继承而获得的共性,而新增加的成员体现了派生类的个性
  • 派生类分为两大部分:一部分是从基类继承来的成员,另一部分是在声明派生类时增加的部分。每一部分均分别包括数据成员和成员函数
  • 在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的。
  • 因此派生类是抽象基类的具体实现

派生类成员的访问属性

(1)公用继承: 基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有
(2)私有继承: 基类的公有成员和保护成员在派生类中成了私有成员。其私有成员仍为基类私有。
(3)保护继承: 基类的公有成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。保护成员的意思是,不能被外界引用,但可以被派生类的成员引用

公用继承

在定义一个派生类时将基类的继承方式指定为 public的,称为公用继承,用公用继承方式建立的派生类称为公用派生类,其基类称为公用基类
公用基类的成员在派生类中的访问属性:

在基类的访问属性继承方式在派生类中的访问属性
private(私有)public(公用)不可访问
public(公用)public(公用)public(公用)
protected(保护)public(公用)protected(保护)

程序设计,访问共有基类的成员:

#include<iostream>
using namespace std;//基类 
class Student{
public:void get_value(){cout << "please input num、name、sex: ";cin >> num >> name >> sex;}void diaplay(){cout << "num : " << num << endl;cout << "string: " << name << endl;cout << "sex: " << sex << endl;}
private:int num;string name;char sex;
};class Student1:public Student{
public:void get_value1(){cout << "please input age、addr: ";cin >> age >> addr;}void diaplay1(){diaplay();cout << "age: " << age << endl;cout << "addr: " << addr << endl;//错误,派生类不能访问基类私有成员 //cout << "name: " << name << endl;		} private:int age;string addr; 
};int main()
{Student1 stu1;stu1.get_value();stu1.get_value1();//stu1.diaplay();stu1.diaplay1();return 0;
}

私有继承

在声明一个派生类时将基类的继承方式指定为private的,为私有继承,用私有继承方式建立的派生类称为私有派生类,其基类称为私有基类
  私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们。私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们。
私有基类在派生类中的访问属性:

在基类的访问属性继承方式在派生类中的访问属性
private(私有)private(私有)不可访问
public(公用)private(私有)private(私有)
protected(保护)private(私有)private(私有)

既然声明为私有继承,就表示将原来能被外界引用的成员隐藏起来,不让外界引用,因此私有基类的公用成员和保护成员理所当然地成为派生类中的私有成员。私有基类的私有成员按规定只能被基类的成员函数引用,在基类外当然不能访问它们,因此它们在派生类中是隐蔽的,不可访问的。
  程序设计:将上述代码的继承方式改为私有继承方式(基类Student不变)

class Student1:private Student{
public:void get_value1(){get_value();	//通过公用函数接口,初始化基类属性 cout << "please input age、addr: ";cin >> age >> addr;}void diaplay1(){diaplay(); cout << "age: " << age << endl;cout << "addr: " << addr << endl;} private:int age;string addr; 
};int main()
{Student1 stu1;//stu1.get_value();	//错误,私有基类的公用函数在派生类中为私有函数  stu1.get_value1();//stu1.diaplay();	//错误,私有基类的公用函数在派生类中为私有函数 stu1.diaplay1();//stu1.age = 18;	//错误,外界不能引用派生类的私有成员 return 0;
}

结论:

  • 不能通过派生类对象(如stud1)引用从私有基类继承过来的任何成员【如stu1.diaplay()或stu1.age】
  • 派生类的成员函数不能访问私有基类的私有成员,但可以访问私有基类的公用成员(如stu1.diaplay1函数可以调用基类的公用成员函数display,但不能引用基类的私有成员age)
  • main函数访问基类的私有成员的方式
    1. 在main函数中调用派生类的公用成员函数
    2. 派生类的公用成员函数调用基类的公用成员函数
    3. 基类的公用成员函数引用基类的数据成员
void diaplay1()
{//派生类成员函数访问基类的公用成员函数display,从而访问基类的私有成员变量diaplay(); 	cout << "age: " << age << endl;cout << "addr: " << addr << endl;
} 

由于私有派生类的限制太多,使用不方便,一般不常使用。

保护继承

由protected 声明的成员称为“受保护的成员”,或简称“保护成员”受保护成员不能被类外访问,这点和私有成员类似,可以认为保护成员对类的用户来说是私有的。从类的用户角度来看,保护成员等价于私有成员。但有一点与私有成员不同,保护成员可以被派生类的成员函数引用
保护继承的特点是:保护基类的公有成员和保护成员在派生类中都成了保护成员其私有成员仍为基类私有,也就是把基类原有的公有成员也保护起来,不让类外任意访问。
保护基类在派生类中的访问属性:

在基类的访问属性继承方式在派生类中的访问属性
private(私有)protected(保护)不可访问
public(公用)protected(保护)protected(保护)
protected(保护)protected(保护)protected(保护)

基类成员在派生类中的访问属性汇总:

在基类的访问属性继承方式在派生类中的访问属性
private(私有)public(公用)不可访问
private(私有)private(私有)不可访问
private(私有)protected(保护)不可访问
public(公用)public(公用)public(公用)
public(公用)private(私有)private(私有)
public(公用)protected(保护)protected(保护)
protected(保护)public(公用)protected(保护)
protected(保护)private(私有)private(私有)
protected(保护)protected(保护)protected(保护)

分析:

  • 保护基类的所有成员在派生类中都被保护起来,类外不能访问,其公用成员和保护成员可以被其派生类的成员函数访问,私有成员则不可访问。
  • 基类的私有成员被派生类继承,派生类中的一切成员均无法访问它们
  • 在派生类中,成员有4种不同的访问属性
    (1)公用的,派生类内类外都可以访问
    (2)受保护的,派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问
    (3)私有的派生类内可以访问,派生类外不能访问
    (4)不可访问的派生类内类外都不能访问

在上述基础上,修改继承方式为protected保护类型:

#include<iostream>
using namespace std;//基类 
class Student{
public:		//基类无公用成员 protected:int num;string name;char sex;
};//用 protected 方式声明派生类 Student1
class Student1:protected Student{	
public:void get_value1()			//定义派生类公用成员函数 {cout << "please input num、name、sex: ";cin >> num >> name >> sex;cout << "please input age、addr: ";cin >> age >> addr;}void diaplay1(){cout <<  "num: " << num  << endl;	//引用基类的保护成员 cout << "name: " << name << endl;cout <<  "sex: " << sex  << endl;cout <<  "age: " << age  << endl;	//引用派生类私有成员 cout << "addr: " << addr << endl;} private:int age;string addr; 
};int main()
{Student1 stu1;stu1.get_value1();stu1.diaplay1();return 0;
}

可以看到:无论哪一种继承方式,在派生类中是不能访问基类的私有成员,私有成员只能被本类的成员函数所访问;保护成员只能派生类访问,不能类外访问。
在实际中,常用的是公用继承

派生类的构造函数与析构函数

简单派生类构造函数

#include<iostream>
using namespace std;//基类 
class Student{
public:	//基类构造函数 Student(int numT, string nameT, char sexT){num  = numT;name = nameT;sex  = sexT;	} protected:int num;string name;char sex;
};class Student1:public Student{	
public://派生类构造函数 Student1(int n, string nam, char s, int a, string add):Student(n, nam, s){age = a;addr = add;}void display();		//实现同上述程序 private:int age;string addr; 
};int main()
{Student1 stu1(1001,"zhangsan",'m',22,"Beijing");Student1 stu2(1002,"lisi",'w',20,"shanghai");stu1.display();stu2.display();return 0;
}

注意派生类构造函数的首行写法:

Student1(int n, string nam, char s, int a, string add):Student(n, nam, s)

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

派生类构造函数名 (总参数表):基类构造函数表(参数表)
{
  派生类中新增数据成员初始化
}

派生类构造函数名(Student1)后面括号内的参数表中包括参数的类型和参数名(如int n),而基类构造函数名后面括号内的参数表列只有参数名而不包括参数类型(如n,nam,s),因为在这里不是定义基类构造函数,而是调用基类构造函数,因此这些参数是实参而不是形参。
在main函数中定义对象stud1时指定了5个实参。它们按顺序传递给派生类构造函数Student1的形参(n,nam,s,a,add)。然后,派生类构造函数将前面3个(n,nam,s)传递给基类构造函数的形参。
在上例中也可以将派生类构造函数在类外面定义,而类体中只写该函数的声明

Student1(int n, string nam, char s, int a, string add);

在类外定义派生类构造函数:

Student1::Student1(int n, string nam, char s, int a, string add):Student(n, nam, s)
{age  = a;addr = add;
}

请注意:在类中对派生类构造函数作声明时,不包括上面给出的一般形式中的“基类构造函数名(参数表)”部分,即Student(n,nam,s)。只在定义函数时才将它列出。

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

实际上,类中的数据成员中还可以包含类对象,如可以在声明一个类时包含这样的数据成员:
//Student是已声明的类名,sl是Student类的对象这时,sl就是类对象中的内嵌对象,称为子对象(subobiect),即对象中的对象。
那么,在对数据成员初始化时怎样对子对象初始化呢,编写案例程序,注意派生类构造函数的写法

#include <iostream>
#include <string>
using namespace std;//基类 
class Student{
public:	//基类构造函数 Student(int numT, string nameT){num  = numT;name = nameT;} void display(){cout << "num=" << num << endl << "name=" << name <<  endl;} protected:int num;string name;
};class Student1:public Student{	
public://声明派生类构造函数Student1(int n, string nam, int n1, string nam1, int a, string add);void show();					//显示学生信息 void show_monitor()	{		//显示班长信息 cout << "monitor Info:" << endl;monitor.display();}private:Student monitor;	//派生类的子对象 monitor int age;string addr; 
};int main()
{Student1 stu1(1021,"zhangsan",1001,"lihua",22,"Beijing");stu1.show();				//显示stu1学生信息 stu1.show_monitor();		//显示stu1学生的班长信息 return 0;
}//派生类构造函数实现 
Student1::Student1(int n, string nam, int n1, string nam1, int a, string add):Student(n, nam),monitor(n1, nam1)
{age = a;addr = add;
}void Student1::show()
{cout << "This student is:" << endl;display();cout <<  "age: " << age  << endl;	//引用派生类私有成员 cout << "addr: " << addr << endl;
} 

派生类构造函数的任务应包括3个部分:

  • 对基类数据成员初始化
  • 对子对象数据成员初始化
  • 对派生类数据成员初始化
//派生类构造函数实现 
Student1::Student1(int n, string nam, int n1, string nam1, int a, string add):Student(n, nam),monitor(n1, nam1)
{age = a;addr = add;
}

在上面的构造函数中有6个形参,前两个是作为基类构造函数的参数,第3, 4个是作为子对象构造函数的参数,第5, 6个是用作派生类数据成员初始化。
归纳起来
定义派生类构造函数的一般形式为:

派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{
  派生类中新增数据成员初始化语句
}

多层派生时的构造函数

多级派生情况下派生类的构造函数定义例程:

#include <iostream>
#include <string>
using namespace std;//基类 
class Student{
public:	//基类构造函数 Student(int numT, string nameT){num  = numT;name = nameT;} void showBase();protected:int num;string name;
};class Student1:public Student{	
public://定义派生类构造函数Student1(int n, string nam, int a):Student(n, nam){age = a;}void show1();		//派生类1显示函数 private:int age;
};class Student2:public Student1{	
public://定义派生类的派生类构造函数Student2(int n, string nam, int a, int s):Student1(n, nam, a){score = s;}void show_all();	//派生类2显示函数 private:int score;
};int main()
{Student2 stu(1021,"zhangsan",22,98);stu.show_all();		//显示stu学生的全部信息 return 0;
}

程序分析

  • 基类的构造函数首部:
    Student(int n, string nam)
    
  • 派生类Student1的构造函数首部:
    Student1(int n, string nam, int a):Student(n, nam)
    {age = a;
    }
    
  • 派生类Student2的构造函数首部:
    Student2(int n, string nam, int a, int s):Student1(n, nam, a)
    {score = s;
    }
    

派生类构造函数只须写出其上一层派生类(即它的直接基类)的构造函数即可。在声明Student2类对象时调用Student2构造函数;在执行Student2构造函数时先调用Student1构造函数;在执行 Student1构造函数时,先调用基类 Student 构造函数。
初始化的顺序是:

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

多重继承

前面讨论的是单继承,即一个类是从一个基类派生而来的。实际上,常常有这样的情况:一个派生类有两个或多个基类,派生类从两个或多个基类中继承所需的属性。
C++为了适应这种情况,允许一个派生类同时继承多个基类。这种行为称为多重继承

声明多重继承的方法

如果已声明了类A、类B和类C,可以声明多重继承的派生类D:
class D:public A,private B,protected C
  {类D新增加的成员}
D是多重继承的派生类,它以公用继承方式继承类 A,以私有继承方式继承类 B,以保护继承方式继承类C。D按不同的继承方式的规则继承ABC的属性,确定各基类的成员在派生类中的访问权限。
声明多重继承的一般方法
派生类构造函数名(总参数表):基类1构造函数(参数表),基类2构造函数(参数表),基类3构造函数(参数列表)
  {派生类中新增数据成员初始化语句}

典例:声明一个教师(Teacher)类和一个学生(Student)类,用多重继承的方式声明一个在职研究生(Graduate)派生类。教师类包括name(姓名) age(年龄) title(职称)。学生类包括name1(姓名)age(性别)、score(成绩)。

#include <iostream>
#include <string>
using namespace std;class Teacher{
public://利用初始化成员列表初始化Teacher(string nam, int a, string t):name(nam),age(a),title(t){} void showTeacher();protected:string name;int age;string title;	
}; //学生基类 
class Student{
public:	//初始化成员列表 Student(string nam1, char s, float sco):name1(nam1), sex(s), score(sco){}void showStudent();protected:string name1;char sex;float score;	//成绩 
};class Graduate: public Teacher, public Student
{
public:Graduate(string nam, int a, string t,char s, float sco, float w): Teacher(nam, a, t), Student(nam, s, sco){wage = w;}void showInfo();private:float wage;
}; int main()
{Graduate grad1("zhangsan",28,"phd",'m',87.5,600);grad1.showInfo();return 0;
}//基类显示函数 
//教师基类 打印函数 
void Teacher::showTeacher()
{cout <<  "name: " << name << "\nage: " << age << "\ntitle: "<< title << endl;	
}
//学生基类 打印函数 
void Student::showStudent()
{cout <<  "name1: " << name1 << "\nsex: " << sex << "\nscore: " << score << endl;
} 
//打印研究生信息 
void Graduate::showInfo()
{showTeacher();cout <<  "sex: " << sex << "\nscore: " << score << endl;cout << "wage: " << wage << endl;
}

在两个基类中分别用name和name1来代表姓名,其实这是同一个人的名字,从Graduate类的构造函数中可以看到总参数表中的参数nam分别传递给两个基类的构造函数,作为基类构造函数的实参。

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

在上述代码中,两个基类能否用同一个名字name来代表?答案是在程序中只作这样的修改是不行的,因为在同一个派生类中存在着两个同名的数据成员,在用派生类的成员函数showInfo中引用name时就会出现二义性,编译系统无法判定应该选择哪一个基类的name。其中最常见的问题也就是继承的成员同名而产生的二义性问题。
如果类A和类B中都有成员函数display和数据成员a,类C是类A和类B的直接派生类,下面讨论三种情况。
(1)两个基类有同名成员

在这里插入图片描述

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并调用数据成员a和成员函数display:

int main()
{C c1;//c1.a=10;//c1.display(); return 0;
}

由于基类A和基类B都有数据成员a 和成员函数display,编译系统无法判别要访问的是哪一个基类的成员,因此程序编译出错
通过基类名限定:

//in main()
C c1;
c1.A::a=10;
c1.A::display(); 

如果派生类C中的成员函数show访问基类A的display和a,可以不必写对象名而直接写:

// in class C
A::a=10;
A::display(); 

(2)两个基类和派生类都有同名成员
派生类C的声明如下,与两个基类有同名成员:

class C: public A, public B
{
public:int a;void display();
};

在这里插入图片描述

main函数调用

int main()
{C c1;c1.a=10;c1.display(); return 0;
}

当基类和派生类都有同名成员时,派生类新增加的同名成员覆盖了基类中的同名成员。因此如果在定义派生类对象的模块中通过对象名访问同名的成员,则访问的是派生类的成员
请注意:不同的成员函数,只有在函数名和参数个数相同、类型相匹配的情况下才发生同名覆盖,如果只有函数名相同而参数不同,不会发生同名覆盖,而属于函数重载。

(3)类A和类B都是从同一个基类的派生(菱形继承)
在这里插入图片描述

//基类N 
class N{
public:int a;void display(){cout << "A::a=" << a << endl;}
}; //派生类 A 
// a a1 display
class A:public N 
{
public:int a1;
}; //派生类 B
// a a2 display
class B:public N 
{
public:int a2;}; //派生类继承A, B
//a1, a2, a3  A::a  B::a  A::display   B::display  
class C: public A, public B
{
public:int a3;void show(){cout << "a3=" << a3 << endl;}
};

在类A和类B分别从类N继承了数据成员a和成员函数display,这样在类A类B中同时存在着两个同名的数据成员a和成员函数display,并将2个同名的数据成员a和成员函数display同时继承给派生类C。
在这里插入图片描述
类C应该通过类N的直接派生类名来指出要访问的是类N的哪一个派生类中的基类成员。如:

int main()
{C c1;//要访问的是类N的派生类A中的基类成员c1.A::a=10;c1.B::display(); return 0;
}

虚基类

1. 虚基类的作用
在一个类中保留间接共同基类的多份同名成员,虽然有时是有必要的,可以分别存放不同的数据,也可以通过构造函数分别进行初始化。但是在大多数情况下,这种现象是人们不希望出现的。不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错。而且在实际上,并不需要有多份拷贝。
C++提供虚基类的方法,使得在继承间接共同基类时只保留一份成员
class A          //声明基类A
{…};
class B:virtual public A
  //声明类B是A的公用派生类,A是B的虚基类
{…};
class C:virtual public A
  //声明类C是A的公用派生类,A是C的虚基类
{…};
注意:虚基类并不是在声明基类时声明的,而是在声明派生类时指定继承方式时声明的
声明虚基类的一般形式为:
  class 派生类名:virtual 继承方式 基类名
当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次
需要注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。
2. 虚基类初始化

class A{		//定义基类A 
public: A(int i){}	//基类构造函数 一个参数 //...
}; class B:virtual public A	//A作为B的虚基类 
{
public:B(int n):A(n){}			//B类构造函数,在初始化列表中对基类初始化 //... 	
}; class C:virtual public A	//A作为C的虚基类 
{
public:C(int n):A(n){}			//C类构造函数,在初始化列表中对基类初始化 //... 	
}; class D:public B, public C
{	
public:D(int n):A(n),B(n),C(n){}	//D类构造函数,在初始化表中对所有基类初始化 //... 
};

在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
类D的构造函数通过初始化表调用了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?
C++编译系统执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

典例: 在上述案例中增加一个共同的基类Person,作为人员的一些基本数据都放Person中,在Teacher 类和Student 类中再增加一些必要数据。
在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;class Person{
public://构造函数 Person(string nam, char s, int a):name(nam), sex(s), age(a){}protected:string name;char sex;int age;
};class Teacher:virtual public Person
{
public://利用初始化成员列表初始化Teacher(string nam, char s, int a, string t):Person(nam,s,a),title(t){}protected:string title;	
}; //学生基类 
class Student:virtual public Person
{
public:	//初始化成员列表 Student(string nam, char s, int a, float sco):Person(nam,s,a),score(sco){}protected:float score;	//成绩 
};class Graduate: public Teacher, public Student
{
public://构造函数 Graduate(string nam, char s, int a, string t, float sco, float w): Person(nam,s,a), Teacher(nam, s,a, t), Student(nam, s, a, sco){wage = w;}void showInfo();private:float wage;		//津贴 
}; int main()
{Graduate grad1("zhangsan", 'm', 22, "phd", 95.5, 600);grad1.showInfo();return 0;
}//输出与研究生有关的信息 
void Graduate::showInfo()
{cout << "name: " << name << endl;cout << "sex: "  << sex << endl;cout << "age: " << age << endl;cout << "title: " << title << endl;cout << "score: " << score << endl;cout << "wage: " << wage << endl;
}

程序分析:
(1)注意各类的构造函数初始化。在Teacher和Student类的构造函数中,按规定要在初始化表中包含对基类的初始化。在最后的派生类Graduate的构造函数中,既包括对虚基类构造函数的调用,也包括对其直接基类的初始化。
(2)在Graduate类中只保留了一份基类的成员,因此可以用Graduate 类中的show函数引用Graduate类对象中的公共基类Person的数据成员name,sex,age的值,不需要加基类名和域运算符,不会产生二义性。

不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,如果能用单一继承解决的问题不要使用多重继承。

基类与派生类的转换

只有公有派生类才是基类真正的子类型,它完整地继承了基类的功能。
不同类型数据之间的自动转换和赋值,称为赋值兼容
基类和派生类对象之间有赋值兼容关系,可以将派生类的值赋给基类对象。

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

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

在赋值时舍弃派生类自己的成员,也就是“大材小用”
在这里插入图片描述
只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值。理由是显然的,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。

(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对象bl对其初始化

注意此时r并不是b1的别名,也不是与 bl1共享同一段存储单元。它只是 bI1中基类部分的别名,r与b1中基类部分共享同一段存储单元,与bl1具有相同的起始地址。
(3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象
如有一函数fun():

void fun(A &r)
{cout << r.num << endl;
}

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

fun( b1);

输出B类对象b1的基类数据成员num的值。与前面相同,在fun函数中只能输出派生类中基类成员的值。

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

典例:修改上述代码,声明一个基类Student(学生),声明Student类的公用派生类Graduate,用指向基类对象的指针输出数据。

#include <iostream>
#include <string>
using namespace std;//学生基类 
class Student{
public:	//初始化成员列表 Student(string nam, int n, float sco):name(nam),num(n),score(sco){}void display(){cout << "name: " << name << "\nnum: " << num << "\nscore:" << score << endl; }private:string name;int num; float score;
};class Graduate: public Student
{
public://构造函数 Graduate(string nam, int n, float sco, float w): Student(nam,n,sco),wage(w){}void display(){Student::display();cout << "wage: " << wage << endl;	}private:float wage;		//津贴 
}; int main()
{Student stu1("zhangsan", 1003, 95.5);Graduate grad1("lisi", 1004, 76.5, 600);Student *pt = &stu1;	//定义指向基类的指针ptpt->display();			//通过指针访问基类display函数pt = &grad1;			//指针指向grad1pt->display();			//通过指针访问派生类的display函数,但不能访问派生类增加的成员 wagereturn 0;
}

通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11K+ Star!图解计算机网络、操作系统、计算机组成、数据库!

大家好&#xff0c;我是 Java陈序员。 俗话说得好&#xff0c;面试造火箭&#xff0c;入职拧螺丝。我们在工作中&#xff0c;其实很少用到一些计算机底层知识&#xff0c;往往只要编码完事。但是&#xff0c;知其然还要知其所以然&#xff0c;我们不仅要做一个合格的“CV 工程…

第21章总结 网络通信

21.1 网络程序设计基础 网络程序设计编写的是与其他计算机进行通信的程序。Java已经将网络程序所需要的元素封装成不同的类&#xff0c;用户只要创建这些类的对象&#xff0c;使用相应的方法&#xff0c;即使不具备有关的网络知识&#xff0c;也可以编写出高质量的网络通信程序…

excel做预测的方法集合

一. LINEST函数 首先&#xff0c;一元线性回归的方程&#xff1a; y a bx 相应的&#xff0c;多元线性回归方程式&#xff1a; y a b1x1 b2x2 … bnxn 这里&#xff1a; y - 因变量即预测值x - 自变量a - 截距b - 斜率 LINEST的可以返回回归方程的 截距(a) 和 斜…

DouyinAPI接口开发系列丨商品详情数据丨视频详情数据

电商API就是各大电商平台提供给开发者访问平台数据的接口。目前&#xff0c;主流电商平台如淘宝、天猫、京东、苏宁等都有自己的API。 二、电商API的应用价值 1.直接对接原始数据源&#xff0c;数据提取更加准确和完整。 2.查询速度更快&#xff0c;可以快速响应用户请求实现…

麒麟系统系统添加路由

系统添加路由 一、路由的解释&#xff1a; 路由工作在OSI参考模型第三层——网络层的数据包转发设备&#xff08;TCP/IP&#xff09;路由器根据收到数据包中的网络层地址以及路由器内部维护的路由表决定输出端口以及下一跳地址&#xff0c;并且重写链路层数据包头实现转发数据…

Linux基础——进程初识(一)

1. 硬件 ①冯诺依曼体系 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。其详细结构如下图所示 在这里有几点要说明 1. 这里的储存器实际上指的是内存 2. 输入设备与输出设备都属于外设 常见的输入设备…

uniapp实战 —— 轮播图【数字下标】(含组件封装,点击图片放大全屏预览)

组件封装 src\components\SUI_Swiper2.vue <script setup lang"ts"> import { ref } from vue const props defineProps({config: Object, })const activeIndex ref(0) const change: UniHelper.SwiperOnChange (e) > {activeIndex.value e.detail.cur…

JS加密/解密之HOOK实战2

上一篇文章介绍了HOOK常规的应用场景&#xff0c;这篇我们讲一下HOOK其他原生函数。又是一个新的其他思路 很多时候&#xff0c;当我们想要某些网站的请求参数的时候&#xff0c;因为某些加密导致了获取起来很复杂。 这时候hook就十分方便了 源代码 var _JSON_Parse JSON.…

在Pytorch中使用Tensorboard可视化训练过程

这篇是我对哔哩哔哩up主 霹雳吧啦Wz 的视频的文字版学习笔记 感谢他对知识的分享 本节课我们来讲一下如何在pytouch当中去使用我们的tensorboard 对我们的训练过程进行一个可视化 左边有一个visualizing models data and training with tensorboard 主要是这么一个教程 那么这里…

Java多线程并发(二)

四种线程池 Java 里面线程池的顶级接口是 Executor&#xff0c;但是严格意义上讲 Executor 并不是一个线程池&#xff0c;而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。 newCachedThreadPool 创建一个可根据需要创建新线程的线程池&#xff0c;但是在以前…

麒麟系统进入救援模式或者是crtl D界面排查方法

如出现以下图片的情况可能需要修复磁盘&#xff1a; V10GFB-desktop&#xff1a; 开机后发现一致卡在此界面&#xff1a; 按esc键后有以下报错信息说明在/etc/fstab里面编写的外挂磁盘的命令有问题 解决方法如下&#xff1a;进入单用户模式对/etc/fstab进行修改&#xff1a; …

Opencv制作电子签名(涉及知识点:像素过滤,图片通用resize函数,像素大于某个阈值则赋值为其它的像素值)

import cv2def resize_by_ratio(image, widthNone, heightNone, intercv2.INTER_AREA):img_new_size None(h, w) image.shape[:2] # 获得高度和宽度if width is None and height is None: # 如果输入的宽度和高度都为空return image # 直接返回原图if width is None:h_ratio …