C++之继承详解

一.继承基础知识

继承定义:

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

例如:

cpp 复制代码
class Person
{
public:
	//
private:
	int _age;
	string cellphone;
	string name;
};
class Student :public Person
{
public:
	//
private:
	int _student_id;
};
class Teacher :public Person
{
public:
	//
private:
	int _teacher_id;
};

你仔细观察上面代码,会发现再学生和老师类上我们后面都跟了:public Person说明这两个类是继承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继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

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

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

如下:

cpp 复制代码
class Person
{
public:
private:
	int age;
	string name;
};
class Student :public Person
{
public:
private:
	string _id;
};
class Teacher :public Person
{
public:
private:
	string _id;
};
int main()
{
	Student s;
	//子类可以切片给父类对象
	//派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用
	//派生类对象 可以赋值给 基类的对象
	Person p1 = s;
	//派生类对象 可以赋值给 基类的指针
	Person* p2 = &s;
	//派生类对象 可以赋值给 基类的引用
	Person& p3 = s;
	//但注意;基类不能赋值给派生类
	Person p4;
	//Teacher t1 = p4;
	return 0;
}

但基类一定不能传给派生类吗???

我们发现此时是可以赋值给子类了,这是特殊情况的处理,而且转换时可能会出现越界的情况

知识扩展:

我们知道在不同类型赋值时会存在类型转换,那么你知道下面这行代码出错的原因吗???

看到报错原因,你以为是int不能传给double,还是要强制类型转换

实际上这是因为引用赋值时,会产生一个临时变量,该零时变量具备常性,所以我们必须用const修饰,防止权限扩大

这样就可以通过了

总结:如果要通过引用来复制拷贝,我们一定要观察是否会出现零时变量,从而确定是否需要用const修饰

三.继承中的作用域

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

如果你仔细观察过上一个代码,你会发现在Student和Teacher中都出现了_id这个成员变量,这就是隐藏

下面在看这道题:

cpp 复制代码
class A
{
public:
	void func()
	{
		//
	}
private:
	int _a;
};
class B :public A
{
public:
	void func(int a=10)
	{
		//
	}
private:
	int _a;
};

请问B中的func函数和A中的func函数是什么关系???

a. 无关系 b. 重定义(隐藏) c.重载

答案是:b

c重载是指在相同作用域中两个同名函数由于参数的不同所构成的,而且只有返回类型不同是不构成重载的

而且这两个类属于不同作用域,看定义:
成员函数的隐藏,只需要函数名相同就构成隐藏

那么我们如何访问父类成员变量呢???

如下:

四.派生类的默认成员函数(重点)

cpp 复制代码
class Person
{
public:
	//构造函数;
	Person(const string& s)
		:_name(s)
	{}
	//拷贝构造
	Person(const Person& p)
		:_name(p._name)
	{}
	//复值重载
	Person& operator=(const Person& p)
	{
		if (&p != this)
		{
			this->_name = p._name;
			return *this;
		}
		else
		{
			exit(-1);
		}
	}
	//析构函数
	~Person()
	{
		//析构
	}
protected:
	string _name;
};
class Student :public Person
{
public:
	//构造函数;
	//派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
	//的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
	Student(const Student& s)
		:_age(s._age),Person(s)
	{}
	Student(const int& age,const string& name)
		:_age(age),Person(name)
	{}
	//注意的是:构造函数实际给成员变量赋值时,是先给父类赋值的,即使我们初始化列表中子类在前!!!
	//拷贝构造
	//派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
	Student(const Student& s)
		:_age(s._age),Person(s)//注意这里是切割
	{}
	//赋值重载
	//派生类的operator = 必须要调用基类的operator = 完成基类的复制
	Student& operator=(const Student& s)
	{
		if(this!=&s)
		{
			_age = s._age;
			Person::operator=(s);//注意这里operator函数和父类是隐藏的关系
		}
		else
		{
			exit(-1);
		}
	}
	//析构函数:
	//派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
	//保证派生类对象先清理派生类成员再清理基类成员的顺序
	//析构先析构子类,构造先构造父类
	~Student()
	{
		//析构子类
		//父类会自动调用的,不用写
	}
protected:
	int _age;
};

五.友元和静态成员

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

此时我们写成友元就可以访问了

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例
即我们可以通过父类和子类访问同一个静态成员

六.复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

此时就会出现一种特殊的继承;菱形继承:菱形继承是多继承的一种特殊情况

菱形继承的问题:可以看出菱形继承有数据冗余和二义性的问题。在d的对象中A成员会有两份

此时我们通过限定符去访问A中的成员对象就会出现B和C都可以访问,而且访问的是不同的两个成员,此时就会出现二义性和冗杂

此时,C++引入了虚继承的概念。

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在B和C的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

cpp 复制代码
class Person
{
public:
protected:
	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; // 主修课程
};

如图,虚继承只需要在:后面写上一个virtual即可,还要注意的是,不同的父类要通过,分隔开

接下来我们深入底层来看看虚继承是如何解决问题的。

菱形继承例子:

这就是一个典型的菱形继承关系

最后,感谢大家的支持!!!

相关推荐
敲代码的奥豆20 分钟前
C++:日期类的实现
开发语言·c++
技术无疆30 分钟前
ButterKnife:Android视图绑定的简化专家
android·java·android studio·android-studio·androidx·butterknife·视图绑定
看山还是山,看水还是。33 分钟前
c#进度条实现方法
c语言·开发语言·笔记·c#
孑么37 分钟前
C# 委托与事件 观察者模式
开发语言·unity·c#·游戏引擎·游戏程序
ZachOn1y39 分钟前
Java 入门指南:JVM(Java虚拟机)垃圾回收机制 —— 垃圾收集器
java·jvm·后端·java-ee·团队开发·个人开发
敲代码不忘补水1 小时前
Python Pickle 与 JSON 序列化详解:存储、反序列化与对比
开发语言·python·json
攸攸太上1 小时前
Java通配符的作用
java·学习·通配符
蜡笔小新星1 小时前
机器学习和深度学习的区别
开发语言·人工智能·经验分享·深度学习·学习·机器学习
齐 飞1 小时前
使用jackson将xml和对象、List相互转换
xml·java·spring boot·后端·list
liwulin05061 小时前
java-在ANTLR中BaseListner的方法和词法规则的关系0.5.0
java·开发语言