C++之继承(继承关系、继承切片、派生类与基类、重载与隐藏、菱形继承等)


定义

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
以前我们接触的复用都是函数复用,继承是设计层次的复用。

请看代码:

class Person {
public:
	void Print() {
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	int _age = 19;
	string _name = "peter";
};

class Teacher : public Person {
protected:
	int _jobid;
};

class Student : public Person {
protected:
	int _stuid;
};

int main() {
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	system("pause");
	return 0;
}

运行结果

name:peter
age:19
name:peter
age:19

所以:
继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分
这里体现出了StudentTeacher复用了Person的成员。使用监视窗口查看StudentTeacher对象,就可以看到变量的复用。调用Print()可以看到成员函数的复用。


继承关系与访问方式

class Student : public  Person
	   派生类    继承方式   基类

派生类也叫子类,基类也叫父类
在这里插入图片描述

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

小结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式为 min(成员在基类的访问限定符,继承方式),继承权限大小为public > protected > private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
    protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

举例说明:

class Person{
public:
	void Print(){
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age; // 年龄
};

class Teacher : public Person {
protected:
	int _jobid = 12;
};

class Student : public Person {			//正常执行

//class Student : protected Person{		
	//“Person::Print”不可访问,因为“Student”使用“protected”从“Person”继承
	
//class Student : private Person{		
	//“Person::Print”不可访问,因为“Student”使用“private”从“Person”继承
	
protected:
	int _stunum; // 学号
};

int main() {
	Student s;
	Teacher t;
	s.Print();
	t.Print();
	system("pause");
	return 0;
}

基类和派生类对象赋值转换

  • 派生类对象可以赋值给基类的对象 / 指针 / 引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。

在这里插入图片描述

class Person{
  protected :
   string _name;	 // 姓名
   string _sex; 	// 性别
   int _age;		 // 年龄
};

class Student : public Person{
    public :
     int _No ;		 // 学号
};

void func(){
   Student sobj ;
   
   // 1.子类对象可以赋值给父类对象/指针/引用
   Person pobj = sobj ;
   Person* pp = &sobj;
   Person& rp = sobj;

   //2.基类对象不能赋值给派生类对象
   sobj = pobj;

   // 3.基类的指针可以通过强制类型转换赋值给派生类的指针
   pp = &sobj
   Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
   ps1->_No = 10;

   pp = &pobj;
   Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
   ps2->_No = 10;
}

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显式访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

请看代码:

class Person{
  protected :
   string _name = "小李子"; 	// 姓名
   int _num = 111;			 // 身份证号
};

class Student : public Person{
public:
   void Print(){
     cout << " 姓名:" << _name << endl;
     cout << " 身份证号:" << Person::_num << endl;
     cout << " 学号:" << _num << endl;
   }
  protected:
   int _num = 999; // 学号
};
void Test(){
   Student s1;
   s1.Print();
};

代码中Student_numPerson_num构成隐藏关系,可以看出这样代码虽然可以运行,但是非常容易混淆,存在数据二义性。

重载与隐藏

class A{
	public:
	void fun(){
		cout << "func()" << endl;
	}
};

class B : public A{
public:
	void fun(int i){	// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
		A::fun();		//突破类域,显式调用
		cout << "func(int i)->" << i << endl;
	}
};

// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏

void Test(){
	B b;
	b.fun(10);
};

派生类的默认成员函数

6个默认成员函数中“默认”的意思是指“用户不显式给出,编译器会自动生成一个”,那么在派生类中,这几个成员函数是如何生成的?

  1. 派生类的构造函数:必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员(不用用户显式调用基类析构函数)因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化:先调用基类构造,再调派生类构造。
  • 派生类对象析构清理:先调用派生类析构,再调基类的析构。(后进先出)
class Person {
public:
	//构造函数
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	//拷贝构造
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	//赋值操作符重载函数
	Person& operator=(const Person& p){
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p) {
			_name = p._name;
		}
		return *this;
	}

	//析构函数
	~Person(){
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student : public Person{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator = (const Student& s){
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s){
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}

	~Student(){
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};

void Func(){
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}

实现一个不能被继承的类

  1. C++98的方式:构造函数私有化,派生类中调用不到基类的构造函数。则无法继承。
class NonInherit{
public:
	static NonInherit GetInstance(){
		return NonInherit();
	}
private:
	NonInherit(){}
};
  1. C++11的方式:使用新的关键字 final ,禁止继承。
class NonInherit final {};

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Student;	//前置声明,否则先定义的 Person 类无法识别 Student 类

class Person{
public:
	friend void Display(const Person& p, const Student& s);	//友元
protected:
	string _name; // 姓名
};

class Student : public Person{
protected:
	int _stuNum; // 学号
};

void Display(const Person& p, const Student& s){
	cout << p._name << endl;
	cout << s._stuNum << endl;
}

void main(){
	Person p;
	Student s;
	Display(p, s);
}

继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有1个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

class Person{
public:
	Person() {
		++_count;
	}
protected:
	string _name;		// 姓名
public:
	static int _count;	 // 统计人的个数。
};

int Person::_count = 0;	//全局成员需要在类外初始化

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};

void TestPerson() {
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

int main() {
	TestPerson();
	system("pause");
	return 0;
}

输出结果:

人数:4
人数:0

菱形继承及菱形虚拟继承问题

  • 单继承::一个子类只有一个直接父类。
    在这里插入图片描述

  • 多继承:一个子类有两个或以上直接父类。
    在这里插入图片描述

  • 菱形继承:菱形继承是多继承的一种特殊情况
    在这里插入图片描述

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余二义性的问题。在Assistant的对象中Person成员会有两份。

请看代码:

class Person{
public:
	string _name; // 姓名
};

class Student : public Person{
protected:
	int _num; //学号
};

class Teacher : public Person{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher{
protected:
	string _majorCourse; // 主修课程
};
void Test(){
	Assistant a;
	a._name = "peter";		//报错:Assistant::_name 不明确
	// 这样会有二义性,无法明确知道访问的是哪一个

	// 需要显式指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

数据二义性,无法明确知道访问的是哪一个。需要显式指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决

那么怎么处理或者预防?

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
如上面的继承关系,在StudentTeacher的继承Person时使用虚拟继承(关键字virtual),即可解决问题。需要注意的是,虚拟继承不要在其他地方使用。

上面代码修改为:

class Person{
public :
	string _name ; // 姓名
};

class Student : virtual public Person{
protected :
	int _num ; //学号
};

class Teacher : virtual public Person{
protected :
	int _id ; // 职工编号
};

class Assistant : public Student, public Teacher{
protected :
	string _majorCourse ; // 主修课程
};
void Test (){
	Assistant a ;
	a._name = "peter";
}

此时运行程序,通过监视窗口就可以发现操作的是同一份数据。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

giturtle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值