c++中的继承(上)

继承的定义

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

总结一下上面的话语:继承的本质也就是复用。即假设我在这里写了两个类,这两个类中含有公共的函数,那么我们将这个公共的函数抽取出来。形成一个新的类,然后让另外的两个类去调用这个函数。

我们使用下面的例子来详细解释一下什么是继承。

这里我写了两个类一个是student类,一个是teacher类。

c++中的继承(上)_第1张图片

从上图中我们可以看到这两个类中含有相同的属性例如姓名,年龄。但是除了相同的属性之外还有不同的属性,例如学生具有学号,班级,专业。而老师则是工号,学院和课程。并且函数也有可能是相同的。

那么我们这里就将这些公共的部分提取出来,又形成一个类。然后让下面的类去继承上面的类。

画图理解:

c++中的继承(上)_第2张图片

下面我们来演示一下:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18;   // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
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;
}

下面我们就来调式查看一下s中含有的东西。我们只看student类可以看到这个类中只含有一个学号(_stuid)属性,但是我们使用监控调式查看一下。

c++中的继承(上)_第3张图片

可以看到s中除了自己的属性之外,还具有一个父类(Person)的对象。同理对于Teaccher类t也是一样。我没有在s中设定名字,学号的属性,但因为我这个子类从父类之中继承了,所以在s和t中也会存在姓名和年龄的属性。至于为什么这里的姓名和年龄是存在值得,因为我在person父类中设定了缺省值。所以监控得属性中会含有值。

并且上面的代码运行之后。也会打印出值:

c++中的继承(上)_第4张图片

prin函数会打印出值得原因自然也是我使用了全缺省的值。

继承的定义

继承的格式

在c++中继承的格式如下图:

c++中的继承(上)_第5张图片

继承的方式和访问限定符

下面我们来看一下c++继承的方式有三种,并且和访问限定符一样都是具有的三种如下:hhn

c++中的继承(上)_第6张图片

那么我们想象一下,如果父类的访问限定符为根据继承方式和访问限定符的约束一共能够产生9种不同的继承方式,即父类的成员在子类中的访问权限就有9种,那么在这里我们就要介绍使用。下面就有一张图可以解释

c++中的继承(上)_第7张图片

protected限定的访问符号和private限定的访问符号在继承时产生的不同影响了。

我们先不谈这些继承方式造成得到影响,我们先来谈一下为什么要存在这些不同的继承方式,

首先在写继承的时候,一定会存在一种情况,那就是我不希望父类的某些属性能够被子类查询到或者得到。所以就产生了不同的继承方式。

下面我们就来总结一下这上面的那个图:

c++中的继承(上)_第8张图片

首先我们总结第一点就是在父类中的private成员在子类继承(无论是什么方法继承),它在子类中都是不可见的。

即如果我的父类中的一个private属性被继承到子类,成为了不可见那么在子类中无论你使用什么方法都是无法访问父类的成员,除非你调用父类的某一个共有函数,里面能够去访问这个private属性成员。但是这个方法属于是在父类里面去访问父类的属性了,自然是可行的。

那么如果父类的protected成员被使用public继承到子类后呢?那么这个protected成员在子类中任然是protected属性的,那么这个属性就允许子类在类中(派生类)直接去访问这个成员,在子类外不能访问这个成员也就是protected属性。

除此之外还有一个点就是使用class如果不写访问限定符那么默认的就是private,不写继承的方式默认的就是private。如果是使用的struct创建类,那么默认的访问限定符号位public,继承的方式为public。

总结一下:父类的private成员在派生类中都是不可见的。至于父类的其它限定符修饰的成员在派生类中取Min(成员在基类的访问限定符,继承方式),其中三种方式的大小关系为:public>proitected>private。

但是下面的这段很重要

下面是不可见和私有的区别

c++中的继承(上)_第9张图片

这上面的这个类泛指的是派生类。

那么这里还可以区分一下private继承和protected继承的区别。

父类第一次private继承给第一个子类之后,子类中的父类成员就变成了private,虽然在这一个子类中任然可以使用但是,如果我将这个子类作为新的父类往下继承呢?这样的话第二个继承的子类就能不能得到最开始的父类中的成员了(此时的父类成员在这个孙子类中已经是不可见的了)。

如果父类是以protected的方式继承那么在孙子类那里,这个父类的成员任然是protected,在孙子类中任然可以使用,但是在类外无法使用。所以我们在父类中不希望被子类得到的属性一般是private而要让子类能在类中使用则是protect。然后继承方式一般都只是选择public。

赋值兼容规则

首先我们先来看一下下面的规则

c++中的继承(上)_第10张图片

但是如果你将以一个teacher的对象赋值给一个Person对象。中间是否会产生临时对象呢?

int main()
{
	//我们在之前的时候说过这样一个现象
	double b = 2.0;
	int c = b;//那么这里是进行了什么呢?
	//这里其实是使用b创建了一个临时的对象,然后将这个临时对象的值赋值给了a
	//int& c = b;//所以如果我们这样写是会报错的,因为生成的临时对象是具有常属性的
	const int& c = b;//这样写就可以了
	//那么这里如果我先实例化一个Teacher对象
	Teacher T;
	Person P = T;//然后将这个T赋值给这个P,那么按照我们上面的理解在这个赋值的过程中会生成临时对象但是请看下面的情况
	Person& pr = T;//发现对于这里即使是引用也不用加上const修饰
	return 0;
}

如果我们将上面的代码放到编译器中会发现居然不用加const也能让一个person的引用指向T,即这里没有产生临时对象。那么难道是因为自定义类型的临时对象没有常性吗?但是下面又会报错

c++中的继承(上)_第11张图片

加上const之后才可行

c++中的继承(上)_第12张图片

那么这就否定了自定义类型临时对象没有常性的解释。

那么这是怎么回事呢?

c++中的继承(上)_第13张图片

在图中的上面两个情况是会产生临时对象的,而下面的则就是我们现在要理解的赋值兼容规则,我们认为public,继承下父类和子类是一个is-a的关系,就拿上面的三个类为例子,学生(子类)是人(父类),老师(子类)是人(父类)。

我们怎么理解呢

c++中的继承(上)_第14张图片

我们先来理解第一个图第一个图的意思也就是现在我有了一个student对象,然后我使用这个student(子)类对象给person(父)对象赋值。其实在内存的底部也就是将student中person需要的部分复制出来,对于父类对象而言,子类对象中一定存在父类对象需要的属性。所以这里并不会产生临时对象。

在将一个double赋值给int的时候,int和double是截然不同的两个类型,所以需要一个临时对象。而这里的父类对象需要的东西,子类对象中是存在的,所以子类对象可以直接给父类对象赋值,并且不会产生临时对象。所以对于父类对象的引用rp来说他是直接将子类对象中的父类对象需要的属性起了一个别名。这里也可以验证上面我们得出的结论,即将子类对象赋值给父类对象是不会产生临时对象的。

当然我用上面的rp让年龄++,那么student的对象s的_age也会++,因为这里的rp引用的就是子类对象中父类所需要的那一部分(图上),

这也就是赋值兼容规则也叫做切割或者切片,当然这个规则仅限于子对象给与父对象(通常情况下父是不能给与子的)。因为子有的父不一定有。

继承中的作用域

c++中的继承(上)_第15张图片

那么我现在在一个父类对象中定义一个_num属性,那么我在子类对象中能否定义一个一样名字的属性呢?

c++中的继承(上)_第16张图片

那么上面的代码运行之后的结果是什么呢?

c++中的继承(上)_第17张图片

那么这里是为什么呢?当编译器运行到print函数这里要去打印_num的值,然后按照就近原则,此时的编译器在B类的作用域中(存在_num)所以就打印999了。

那么我们能否打印111呢?答案当然是可以的我们只需要指定作用域即可。

c++中的继承(上)_第18张图片

指定了A的作用域。那么打印出来的自然是111了

c++中的继承(上)_第19张图片

总结:

这个结论对于成员函数一样也是成立的。即如果你想要访问父类里面的同名函数,那么你也需要指定作用域。

如下图:

c++中的继承(上)_第20张图片

即要使用父类中和子类同名的一个函数直接指定作用域即可。

派生类对象的默认构造

代码如下:

class A
{
public:
	A(string name = "张三", int num = 111)
	{
		_name = name;
		_num = num;
		cout << "A(string name = 张三, int num = 111)"  << endl;
	}
protected:
	int _num = 111;
	string _name;
};


class B: public A
{
public:
	void print()
	{
		cout << _num << endl;
		cout << A::_num << endl;
	}
protected:
	int _num = 999;
};

int main()
{
	B it;//这里我并没有在B中写默认构造函数
	return 0;
}

这里如果我在子类中没有写默认构造函数那么编译器底层会自动的去调用父类的默认构造函数。

c++中的继承(上)_第21张图片

那么如果我想要给子类也写一个默认构造函数呢?

c++中的继承(上)_第22张图片

可以发现这里就直接报错了。

那么如果你要想写一个派生类的默认构造函数,那么对于派生类你只用管好你自己的属性即可,不用去管父类的属性,直接调用父类的默认构造即可。

c++中的继承(上)_第23张图片

在派生类中直接调用A类的默认构造。

那么上面我们说了构造,下面我们来试一下拷贝构造。

c++中的继承(上)_第24张图片

那么如果我想要自己来写一个子类的拷贝构造呢?

c++中的继承(上)_第25张图片

这里就是使用了切割/赋值兼容规则来写的。

那么赋值拷贝呢?

依旧如此如果我们自己在子类中没有写赋值,那么编译器会自动地去调用父类地赋值。

c++中的继承(上)_第26张图片

c++中的继承(上)_第27张图片

如果我们写了那么,就需要这么写:

c++中的继承(上)_第28张图片

即也需要去显示的调用父类的赋值函数。

这里唯一的不同点就是析构函数

所以在下图中如果你想要访问Person类(父)你需要指定作用域。

c++中的继承(上)_第29张图片

但是上图中这么写析构是会报错的因为在父(person)析构函数被调用之后(此次调用是在子类的析构函数中),在子类的析构完成之后,编译器会自动地再次调用析构。所以上图应该不写person的析构。至于原因如下

c++中的继承(上)_第30张图片

上图中说了一般构造是先父后子,而析构一般是先子后父,因为如果析构也是先父后子的话,我们在子类的析构中可能会访问父类的成员,那么父类的成员如果被销毁了就会出现问题。所以析构一般是先子后父。先父后子存在风险。因为父不能访问子

所以析构函数在子类的析构完成之后,编译器会自动的去调用父类的析构函数。

希望这篇博客能对你有所帮助,如果发现了错误欢迎指正。

你可能感兴趣的:(c++)