C++--11.继承

继承的概念及定义
基类和派生类对象赋值转换
继承中的作用域
派生类的默认成员函数
继承与友元
继承与静态成员
复杂的菱形继承及菱形虚拟继承
继承的总结和反思

继承的概念及定义

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

class Person//定义Person类
{
public:
	void Print()//公有打印函数
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected://保护变量,姓名
	string _name = "peter"; // 姓名
private://私有变量,年龄
	int _age = 18;  // 年龄
};
//以上为父类,下面两个类都继承它
class Student : public Person//Student类继承Person类
{
public://公有f函数
	void f()
	{
		// 类中的私有和保护在当前类没差别
		// 在继承的后的子类中有差别,private的在子类中不可见
		cout << _name << endl;
		//cout << _age << endl;//子类无法访问父类的私有成员
	}
protected://保护变量,学号
	int _stuid; // 学号
};

class Teacher : public Person//老师继承Person
{
protected://保护变量,工号
	int _jobid; // 工号
};

int main()
{
	Student s;//实例化学生s
	Teacher t;//实例化老师t
//我们可以看到,即使Student与Teacher类没有去定义Print()函数,但是它们都继承了Person类,所以就可以使用Person类的共有函数Print
	s.Print();//调s的Print
	t.Print();//调t的Print
	system("pause");
	return 0;
}

 我们可以看到,当我们将Student与Teacher两个类去继承Person时,S与T中就不仅仅有自己的学号与工号,还拥有了父类的名称与年龄,这便是继承的基础概念,子类可以去调用父类的成员

继承定义

定义格式

 说明继承关系就是直接将继承方式+父类放到子类+:的后面,而我们的继承方式不仅仅有一种

继承关系和访问限定符

 我们是通过使用不同的访问限定符去说明不同的继承方式的

继承基类成员访问方式的变化

那么我们不同的访问限定符都有什么区别呢?

 这张图就说明了子父类不同限定符成员间的访问关系

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

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

 在我们C++中,子父类是支持将子类的对象赋给父类的东西的

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
基类对象不能赋值给派生类对象

 其实我们的赋值规则就是将子类中父类的成员,赋给父类,因为子类中成员更多,所以赋给父类就像从中间切开,自己的保留,父类的才赋值,所以也叫作切片/切割,但是反过来就行不通的,因为子类对象多,父类没办法全赋完,一般只能多赋少,不能少赋多

但是还有一种情况下是可以的,就是这个父类的指针本来就指向子类的,不过也得加强转

例如:Student* sptr =(Student*)ptr;

继承中的作用域

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

我们可以看到,当子类和父类都有num时,实例化子类对象,调用的就是子类的num,而不是父类的,这是就近原则,先到自己的类找,再去父类的找,但是当想访问父类对象时,只让其属于让其属于父类就可以了

 还有这种情况,A与B分别都有fun函数,且形参不同,我们乍一看A和B中的fun构成了重载,实际不是的,实际上为重定义,也称隐藏,原因是两个fun函数的作用域不同,重载要求作用域相同,如果要调的话只能去指定调用

派生类的默认成员函数

6 个默认成员函数, 默认 的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。

 我们知道,当一个类中对象进行实例化时,一定会调用其构造函数,结束时也会调用析构函数,那么我们的子类在继承了父类之后,又是怎么完成这一操作的呢?

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 stuid)//子类构造函数,当子类调用构造函数时先调用父类的构造函数,再调用自己的构造函数,先有个人,才能有个学生
		:Person(name)//子类中会自动调用父类的构造函数
		,_stuid(stuid)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)//拷贝构造同样拷贝的是父类的
		, _stuid(s._stuid)
	{
		cout << "Student(const Student& s)" << endl;
	}

	// s1 = s3
	Student& operator=(const Student& s)//
	{
		if (this != &s)
		{
			Person::operator=(s);//赋值也是先调用的父类的=,再将自己的stuid=
			_stuid = s._stuid;
			cout << "Student& operator=(const Student& s)" << endl;

		}

		return *this;
	}

	~Student()  // 子类的析构函数和父类的析构函数构成隐藏,因为他们的名字会被编译器统一处理成destructor(跟多态有关)
	{
		//Person::~Person(); // 在子类的析构函数中不需要去调用父类的析构函数,会自动调用

		cout << "~Student()" << endl;
	}
protected:
	int _stuid;
};



int main()
{
	Student s1("tom", 1);//构造与析构顺序为,Person,Student,~Student,~Person
//	Student s2;

	Student s2(s1);//

	Student s3("rose", 2);
system("pause");
return 0;
}

这边是我们继承中的子父类默认成员函数的调用关系,总的来说,就是父类管父类的,子类管子类的子类构造时先构造父类的,析构时先析构子类的

继承与友元

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

这点其实没什么复杂的,简单可以理解为,父亲的朋友不一定是你的朋友,要想成为朋友还需要重新友元交朋友

继承与静态成员

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

 在这里我们的_const始终只有一个,Person与Student共用一个

其实可以理解为家里的传家宝,不管是你和你父亲谁,传家宝都是一个

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

菱形继承其实就是我们的多继承,我们在之前的学习中接触的都是单继承,比较简单也比较明了,但C++中还支持多继承,也是C++中一个比较深的坑吧,Java将他避免了,我们就来一起了解下什么是多继承

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

 这是我们之前学的正常继承关系

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

 这是我们的多继承,其实也就是一个类分别继承了两个不同的类,其实这么一看倒也没什么问题,只是有了两波继承下来的成员而已,但我们再来看下面这种情况

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

 当我们的Assistant类继承了老师与学生之后,而老师与学生还会继承Person类,这就会出现问题了

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

 当我们使用Assistant类去调用name时,我们的Student与Teacher中都有从Person中继承下来的name,所以Assistant中就会有两份name,但是我们在调用的时候却没办法去区分到底是从哪个继承下来的,因为两个是一样的

 这里是无法编译通过的,因为没办法知道到底访问的是哪一个,我们如果想调用只能显示访问,但是同样的数据依旧会重复的进行生成,会有冗余

那么C++是如何解决这个问题的呢?答案是采用虚继承的方式

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

 我们只需要在继承语句的那里加上一个关键字virtual,虚拟的,即可解决问题,那么它的原理到底是什么呢?

虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助 内存窗口观察对象成员的模型。
class A {
public:
 int _a;
};
// class B : public A
class B :  public A {
public:
 int _b;
};
// class C : public A
class C :  public A {
public:
 int _c;
};
class D : public B, public C {
public:
 int _d;
};

 这是我们使用虚继承前的内存布局,我们可以发现,在内存中它是将B,C中的a分别存储了起来,用了两个内存地址分别存储B与C中a的值

 这是我们加上了virtual之后的内存布局,其实我们红色的6之前先是1,再变为2,最后才变为6,我们可以发现,在内存中a是存储在一份单独的公共空间中的,并没有在B或者C中任意一个,那么他到底是如何解决数据冗余的问题的呢?

 我们来看这张图,信息量很大,我们先根据vs的小端调出我们b与c的内存布局,发现其中除过其他成员的位置之外还用16进制存放了两个指针,实际数值一个为20,一个为12,这个数值是什么意思呢?实际上是偏移量,是当前地址距离那个存放的公共区域的偏移量,不同对象的偏移量就构成了我们的虚基表,这样我们的b与c就可以找到那个公共区域一起使用a了,但是我们回头再来想想,这个方式好像并没有减少数据啊,之前是b与c一共创建了2个数据,现在反而在每个中多加了一个指针,还创建了公共区域,看起来像大小是更多了,但实际上,当我们的b与c从a中继承了大量的数据时,我们采用虚继承的这种方式只需要每个对象添加一个指针的大小,共同指向公共区域,而原来的方式需要把大量数据创建两份,从大量数据来看是节省了很大的数据冗余的

虽然我们通过这样的方式去访问步骤较为麻烦,会有一定的效率损失,但是对于我们硬件的进步来说,还是微不足道的

但是总的来说,我们还是要尽量避免的去使用菱形继承

继承的总结和反思

1. 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承 ,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是 C++ 的缺陷之一,很多后来的 OO 语言都没有多继承,如 Java
3. 继承和组合
public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
优先使用对象组合,而不是类继承
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语 白箱 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱” 的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值