面向对象程序设计的三大特性(二):C++中的继承

前言

众所周知,面向对象程序设计有三大特性:封装、继承、多态。在之前的学习中我们对C++中的封装有了一定层次的认知,那么C++中的继承是怎样实现的呢?今天就带大家来简单梳理一下C++中的继承。

1. 继承的概念

继承机制是面向对象程序设计使代码复用的重要手段,它允许程序员在保持原有类(基类、父类) 特性的基础上进行扩展,增加新的功能,这样产生的新类叫做 派生类(子类),继承展现了面向对象程序设计的层次结构,继承是类设计层次的复用

  • 举个栗子:
#include<iostream>
#include<string>
using namespace std;

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

// 派生类
class Student :public Person{
protected:
	int _stuid;
};

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

int main(){
	Student s;
	Teacher t;
	s.Print();
	t.Print();

	return 0;
}
运行结果:
	name:peter
	age:18
	name:peter
	age:18

上述例子我们看出Person类为基类,Student类和Teacher类继承了Person类,基类的成员函数和成员变量都会变为派生类的一部分,实现成员变量和成员函数的复用,并且Student类和Teacher类也有自己特有的成员。

2. 继承的方式

继承方式有三类:public(公有)继承、protected(保护)继承、private(私有)继承

那么不同的继承方式之间有什么差别呢?

  • 三种继承方式对派生类成员的影响

1.基类的private成员在派生类中不可见,但是还是被继承到派生类中。
2.权限大小:public > protected > private,继承方式和访问限定符谁的权限小,派生类中继承而来的成员访问权限就与谁相同。
3.class默认的继承方式是private,struct默认的继承方式是public,不过最好显示的写出继承方式。

3. 基类和派生类之间的赋值兼容规则
  • 规则一:

派生类对象可以赋值给基类的对象、指针、引用。形象的也叫做切片或者切割,意思就是将派生类中父类的内一部分切出来赋值过去。

  • 规则二:

基类对象不能赋值给派生类的对象。基类并不具有派生类的所有成员,所以基类对象赋值给派生类对象时会出错。

  • 规则三:

基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。

4. 继承中的作用域

在继承体系中基类和派生类都有独立的作用域,基类和派生类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏,也叫重定义

在派生类成员函数中,可以使用 <基类 :: 基类成员> 指定作用域,显示访问基类中的同名成员。

  • 举个栗子:
#include<iostream>
#include<string>
using namespace std;

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

int main(){
	Test();
	return 0;
}

如果是成员函数的隐藏,只需要函数名相同就构成隐藏

注意:函数重载要求函数必须在同一作用域中,函数重定义(隐藏)是两个函数在不同的作用域中

5. 派生类的默认成员函数

派生类也有六个默认成员函数,但是这些默认成员函数既要兼顾派生类也要兼顾基类,因此在写法上和常规默认成员函数略有不同。

  • 1. 构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

派生类对象初始化先调用基类构造再调派生类构造,构造函数私有化可设计出一个不能被继承的类。

  • 2. 拷贝构造函数

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

  • 3. 赋值构造函数

派生类的operator=必须要调用基类的operator=完成基类的赋值

  • 4. 析构函数

派生类的析构函数和基类的析构函数构成隐藏。(函数名会被编译器统一处理成destructor)

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

派生类对象析构清理先调用派生类析构再调基类的析构

  • 举个栗子:
#include<iostream>
#include<string>
using namespace std;

// 基类
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 = "jack", int num = 1)
		: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;// 学号
};

int main(){
	Student s1;
	Student s2(s1);
	s2 = s1;
}
6. 继承与友元和继承与静态成员

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

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

7. 复杂的菱形继承及菱形虚拟继承
  • 1. 单继承

一个派生类只有一个直接基类时,称这个继承关系为单继承。

class Person

class Student:public Person

class PostGraduate:public Student
  • 2. 多继承

一个派生类有两个或以上直接基类时,称这个继承关系为多继承。

class Student     class Teacher

class Assistant:public Student,public Teacher
  • 3. 菱形继承

菱形继承是多继承的一种特殊情况。

class Person

class Student:public Person     class Teacher:public Person

class Assistant:public Student,public Teacher

菱形继承的问题:菱形继承有数据冗余和二义性的问题。在Assistantde的对象中Person成员会有两份。

解决办法:显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。

Assistant a ;
// a._name = "peter";  二义性
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
  • 4. 菱形虚拟继承(解决菱形继承中数据冗余和二义性的问题)

例如上面的菱形继承关系,在Student类和Teacher类继承Person类时,加上virtual关键字使用虚拟继承即可解决问题。

代码示例:

class Person

class Student:virtual public Person     class Teacher:virtual public Person

class Assistant:public Student,public Teacher

原理剖析:

1.菱形继承的对象成员模型:清晰的看到数据冗余
在这里插入图片描述
2.菱形虚拟继承的对象成员模型:解决了数据冗余
在这里插入图片描述
但是菱形虚拟继承会有一定的效率损失,需要通过虚基表指针找到虚基表中的偏移量,进行计算。

8. 继承和组合

继承和组合都完成了类层次的复用。

  • 继承

继承:是一种 “是” 的关系(车和宝马的关系),也就是说每个派生类对象都是一个基类对象。继承是一种白箱复用,基类的内部细节对派生类可见,继承一定程度上破坏了基类的封装,基类的改变对派生类有很大的影响,基类和派生类之间的依赖关系很强,耦合度高。但要实现多态,就必须用继承

  • 组合

组合:是一种 “有” 的关系(车和轮胎的关系),对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。假设B组合了A,那么B对象中都有一个A对象。组合是一种黑箱复用,A对B是不透明的,A保持了自己的封装,对象的内部细节是不可见的, 组合类之间没有很强的依赖关系,耦合度低。类之间的关系,优先使用对象组合有助于你保持每个类被封装

小结

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值