C++——继承

作者:几冬雪来

时间:2023年10月22日

内容:C++——继承知识讲解

目录

前言: 

继承:

什么是继承:  

继承的书写:

继承关系和访问限定符:

基类和派生类的赋值转换:

继承中的作用域: 

派生类中的默认成员: 

继承与友元:   

继承与静态成员: 

多继承:

虚基表:

代码:

结尾:


前言: 

在上一篇博客中我们了对模板进阶部分内容的讲解,函数模板板块的结束,意味着我们C++学习的知识正式进入下一个层次,而今天我们将来讲解C++中的有关继承的知识。 

继承:

在现实中继承这个词我们经常听到,比如继承遗产等。

但是在C++中继承并不只是这种行为,要学习继承之前我们就要了解C++的继承是什么东西,它在C++里面发挥着怎么样的作用

什么是继承:  

面向对象的三大特性之间,继承是第二大特性

那么先说一下继承为什么会出现,在我们编写学习所有人的信息的时候。每个人都有他们的公有的信息和独有的信息

接下来可以还要定义一些其他的人,比如保安,图书管理员等。他们都有一些公共的信息也有一些独有的信息

而那些公共信息和数据每个类都得写一遍,和这些信息相关的接口每一个都得写一遍。如果真的要这么做的话,未免也太麻烦了一点

那么基于以上的原因,C++中语法——继承,油然而生。

为了解决上面的那种问题,在这里我们写一个Person。接下来将我们中公有的信息写入到Person中去,然后定义出我们的学生和老师,下一步就是让我们的学生和老师去继承它

那么继承在这里的作用也就显而易见了,继承的本质就是复用,它是一种类设计层次的复用

继承的书写:

既然了解了继承之后,接下来就是要写出它的代码。

像这里一个简单的继承的代码就出来了

在这个地方Student就是要继承的类,在术语界中被叫做派生类或者子类,而且在后面的类Person则是将被继承的类,我们可以称呼它为基类,也可以叫他父类

这样子书写的话,这里我们就可以去调用Person中的数据了。

继承关系和访问限定符:

而在继承这里我们就会牵扯到C++中的访问限定符

在没学习继承之前我们就了解了C++中的三大限定符,一个是公有一个是私有另外一个是保护,那个时候私有和保护所发挥的作用是差不多一样的

但是在继承之后,保护和私有的用法就变得不一样了。

C++它在继承这里使用了三种继承方式,外加三种访问限定符。 

这里规定了父类成员在子类的访问方式是怎么样的。 

而这里继承方式与父类进行组合,最后会组成9种访问方式,也就是三种父类成员与三种继承方式相结合

这里我们要记住一些东西。

1.基类的私有成员在派生类中都不可见。

同时不可见和私有之间并不相同,不可见是语法上限制访问,在类里面或者类外面都不可使用。而private在类外面不能使用,在类里面还是可以使用的

这里还需要注意一个点,那就是父类的私有成员子类无法使用,无论是用什么方式进行继承都不行

2.访问限定符和继承方式选择权限小的那一个。

这里的权限大小为public>protected>private

正如我们上边的那张9中继承方式的图。如果是公有成员公有继承,则是派生类的public 成员,下面权限低的就是对应它们的继承

再如protected成员,如果是public继承的话,它只能是派生类的protected成员不能是public的成员,后面换为protected和private继承才对应它们各自的派生类

3. 继承后父类的Person的成员都会变为子类的一部分。

基类和派生类的赋值转换:

在C++中的正常情况下,两个不同类型的对象去赋值的时候一般是不允许的

唯一一种被允许的情况就是类型转换

而且在C++的继承中也存在一种特殊的类型转换

像这里,我们简单的写了一段继承的代码。接下来在main函数中以Person和Student定义了一个p和一个s

接下来就是二者的赋值了,从代码中来看,将s赋值给p的操作是可行的,但是如果二者的位置调换过来则就不可行了

这是因为从对象的角度来说,父是不能赋值给子的。这是因为子类的一些东西父类并没有,因此语法中禁止这么做,哪怕是强制类型转换都是不行的

这里C++将这两种转换分为向上转换和向下转换。这个地方子给父被称为向上转换,而向上转换是被允许的

都是这种转换和以前的类型转换又有不同之处

在以前学习转换的时候我们有说过,如果要完成int向double之间的转换,在转换的过程中编译器会暂时产生一个临时变量来辅助转换

但是在继承的父子类中却不一样,在语法中进行了特殊处理——它没有发生转换,而是发生了赋值兼容(切割/切片)

这是因为它的转换是天然的,在中间并没有产生临时变量

就如上图一样,我们可以将子类给看出一个特殊的父类

这里赋值的本质就是将与父类相同的子类部分的信息切割出来,接下来将其拷贝给父类

子类就总结出来了一个结论——子类的对象可以给给父类的对象/指针/引用。 

继承中的作用域: 

在以前说过,如果我们定义一个类,那么这个类就有它对应的类域

而在继承中也是如此,在继承中基类和派生类它们都有独立的作用域

在这里父类和子类可以定义同名的成员

就像这里在Person中我们定义了一个num,同时在Student也定义了一个num,结合上面所学习的知识,这个地方我们可以认为在Student中有两个num

而这里地方也遵循查找的就近原则,在这里它输出的结果为999。那么接下来就能写出它的查找的顺序:局部域->当前类域->父类域->全局域。像上面这种情况要访问父类域的num就需要我们去指定父类域

在继承中这种父子类同名问题被叫为隐藏,也就是子类和 父类都有同名成员,子类成员隐藏父类成员,这种行为可以可以被称为重定义

派生类中的默认成员: 

然后接下来就是对派生类中默认成员的讲解

在我们以前的派生类中都是直接初始化自己的成员

但是在继承中是行不通的编译器规定我们不能显示的在初始化列表中初识我们的父类或者基类成员,它只能初始化派生类自己的东西

但是这里又出现了一个问题。

在这里我们没有定义父类对象,但是在这个地方它的去调用父类的构造函数

这是因为在C++中有规定,派生类必须调用父类的构造函数后初始化父类的成员。 

但是这个行为是建立在子类没有调用,编译器自己去初始化列表调用的。它调用的是默认构造,如果在默认构造中我们没有对其进行初始化的话,代码会报错

但是如果父类中没有默认构造的话我们要怎么做?

如果在父类中没有默认构造的话,在子类中我们就需要这样去书写,让name去调用Person的构造函数进行初始化

当然这个也有一个问题,那就是二者谁先进行初始化

这里可以明确的确定name会先被初始化,因为从声明角度来说继承的成员是在自己定义的成员之前。 

同样要是拷贝构造也要去调用父类的拷贝构造

此处我们不能直接初始化基类的成员

这里要解决这个问题还是要去调用父类的拷贝构造,像匿名对象那样进行调用

同样的我们也可以用这个方法去解决子类中operator=的代码。

这里要注意的是,如果子类中用的是保护继承又或者是私有继承的话,在书写代码的时候可能会出错 ,因为有些地方私有和保护继承是不支持的,成员的权限可能会发生变化

因此在平时使用继承的时候只要知道使用公有继承即可

最后在这个地方讲解的就是析构函数

从上图来看,在这里如果像上边那种直接在子类中去调用父类的析构函数是行不通的,在这个地方要写成右边这样才能调用析构函数

这是由于C++中多态(后面学习)的原因,析构函数的函数名被进行了特殊处理,都被统一处理成为了destruct

因为特殊处理的原因,子类和父类之间构成了隐藏,因此在调用析构函数的时候要特别指定。 

但是为什么这里我们又说指定的这种写法也是不对的呢? 

从上面的结果来看,在main函数中我们调用了3次,但是二者的析构次数却是不一样的

有特别指定父类析构的析构函数析构了6次,而什么都没有写的正常的析构了3次。这是因为它会自动调用,为了保证它的析构顺序。 

继承中我们了解到都是父类先构造后析构,子类则是相反的需要先析构

而这个地方的显示调用父类析构,无法保证析构的顺序是先子后父的析构顺序,所以我们就要求子类析构函数称为就自动的调用父类析构,这样就能保证先子后父的顺序。

而这里先析构子再析构父的原因是因为,子类先被析构对父类没有影响,但是父类被析构之后对子类就有很大的影响

继承与友元:   

在继承与友元这里我们只需要记住一句话即可,那就是友元关系不能继承。 

这里我们写一串代码来看看。

从上面的代码来看Display是父类的友元,但是它并不是子类的友元,因此子类中的_stuNum会报错

那么如果要让它不会报错且通过该怎么做?

这里有一种很简单的做法。

这里的解决方法就是在子类中也定义一个友元函数,这样子后面就不会报错了。

继承与静态成员: 

在上面讲解了友元是不能被继承的,那么在这里静态成员又是否可以被继承呢

这里的答案是:静态成员可以理解为它可以被继承也可以理解为它不能被继承

 

在这个地方,我们先写出继承中子类和父类的代码。

从上面的代码可以看出来,在父类中_count是父类的静态成员

接下来就来看看继承和静态成员之间的关系

在C语言中静态成员可以通过对象去访问,也就是上面的写法,但是在编译器中它不喜欢用对象去访问。   

在编译器中它更喜欢写作下面的写法,用类名去访问静态成员

在这里我们可以认为静态成员被继承了,也可以认为静态成员不能被继承。认为它可以被继承是因为在这里能对其进行使用,而不是像友元一样直接不能使用

那么就有一个问题,这里是不是单独给派生类拷贝了一份,它是不是自己定义了一个_count和父类不是一个_count

通过上图我们可以看出来二者地址相同是同一个地址

那么在这里就说明了一件事情:在这个地方我们还是可以认为静态成员可以被继承,但是对比以前的普通成员继承不一样的是——普通成员继承父类,它的派生类对象中有一份父类成员,它和父类对象里面是不一样的

静态成员属于父类和派生类,他不会在派生类中再拷贝一份,它继承的是使用权

多继承:

接下来我们讲解C++中的一个大坑——多继承

在C++中多继承的特征就是一个类满足两个类的需求

接下来我们就来看看什么是单继承

在C++中,一个子类只有一个直接的父类时,这里我们就将这个继承关系称为单继承

迄今为止我们学习到的继承方法都是单继承

然后再来看一看多继承对比单继承又有什么样的变化

这个地方多继承指的是一个子类有两个或者以上直接父类的时候,我们就将这个继承关系称为单继承

这里的写法相较于单继承也有些许不同

在单继承中我们的写法是只有一个继承方式外加一个类名(父类),而多继承的书写方法就是在原继承方式加类名的后面补充一个“,”后,再写一个继承方式加类名。  

这里要继承多个类就用“,”将要被继承的类依次分开

但是多继承在这个地方出现了一些问题

这个地方就出现了一种特殊的多继承的情况,这种特殊的多继承被我们称为菱形继承。 

对比起原多继承,菱形继承稍有不同的是,原先作为Assistant子类的两个基类的Student和Teacher是Person的子类

那么这里的菱形继承会出现什么样的问题呢?

在这里可以看出来在在Assistant有自己的成员也有两个父类Teacher和Student的成员,但是在Teacher和Student里面都有一个_name是从Person那里继承过来的

这个地方就会出现在Assistant中有两个_name,这里就出现了数据冗余和二义性的问题

在这里我们先将菱形继承的代码写出来,按我们上面所分析的话,在Assistant中会出现两个_name和两个_name和两个_age

接下来就来看一下会发生什么问题。

在上面代码中我们先定义了一个as,再借用as去访问它里面的_age,这里我们的代码就会发生报错

因为这里有两个_age,一个是位于Student的_age一个是位于Teacher中的_age,编译器不知道我们要取哪个_age

而缓解这个问题的方法有一个,那就是指定访问。但是在面向对象中,这种行为是一种违背常理的行为

而且为了真真正正的就解决这个问题,编译器在里面引入了一个多继承解决的方案,这个方案就叫做虚继承,也就是添加一个关键字——virtual

那么接下来就来说一下虚继承底层是怎么实现的。

像上面的代码一样,如果是菱形继承的话,这里在B和C二者里面就有一个A的空间,而B和C又在D里面,这样就构造了数据冗余二义性

虚继承的话,则是规定了这里的A只有一份,它不能在B里面也不能在C里面,它会放在最开始或者最末尾的位置,这个具体位置则是由编译器决定,通常编译器将其放在末尾

虚基表:

那么编译器将公共的A放在末尾,A具体放在哪里呢?

我们是怎么知道A被放在末尾的,这里就要牵扯到C++中继承的一个知识——虚基表

就像是这里在编译器中我们去找它的地址

可以看出来在0x00E97BDC和0x00E97BE4中第一行都是全0,但是它们都在下一行中都写有一个有效值

而且这里的有效值就是我们确定A位置的一个重点

按照我们C语言时期计算14的16进制计算是20,0c则是12。 

而且通过前面地址的计算,我们可以算出B到A的距离是20,C到A的距离是12,它们正好对应了第一行代码中的有效值

也就是代码在第一行中类似有一个指针,它指向的位置存的是一个相对距离,也就是距离A的一个偏移量

当然我们在这里将全0换成12或者20,但是编译器没有这样做

这是因为这里可能还有其他的值要存,它为其他的地方进行了预留

在这里如果要直接存两个值的话,它会产生两个地址,对象会变大

 

这里就是我们虚基表的由来,但是在虚基表中我们也不一定只是找基类的偏移量,它也有可能是其他的东西,但是这里要等到多态再去讲解了。

代码:

//#include <iostream>
//
//using namespace std;
//
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	protected:
		string _name = "gzk";
		int _age = 18;
};

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

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


int main()
{
	Person p;
	Student s;

	p = s;
	//s = p;
	return 0;
}
//
class Person
{
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name = "zhanshan", int id = 0)
		:_id(0),
		Person(name)
	{

	}

	Student(const Student& s)
		:Person(s),
		_id(s._id)
	{

	}

protected:
	int _id;
};

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


int main()
{

	return 0;
}
//
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 = "zhanshan", int id = 0)
		:_id(0),
		Person(name)
	{

	}
	Student(const Student& s)
		:Person(s),
		_id(s._id)
	{

	}

	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_id = s._id;
		}
		return *this;
	}

	~Student()
	{
		Person::~Person();
	}

protected:
	int _id;
};

int main()
{
	Student s1;
	Student s2(s1);
	Student s3("lisi", 1);
	return 0;
}
//
class Student;
class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name;
};

class Student :public Person
{
public: 
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum;
};

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

int main()
{
	Person p;
	Student s;
	Display(p, s);
}
//
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:
	int  _seminarCourse;
};

int main()
{
	Person p;
	Student s;
	cout << &p._count << endl;
	cout << &s._count << endl;

	cout << &Person::_count << endl;
	cout << &Student::_count << endl;

	return 0;
}
//
//class Person
//{
//public:
//	string _name; 
//	int _age;
//};
//
//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;
//};
//
//int main()
//{
//	Assistant as;
//	as.Student::_age = 18;
//	as.Teacher::_age = 36;
//
//	return 0;
//}

class A
{
public:
	int _a;
};

class B :virtual public A
{
public:
	int _b;
};

class C :virtual public A
{
public:
	int _c;
};

class D :public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

结尾:

到这里我们C++方面继承的知识就基本上讲解完毕了,继承作为面向对象的第二大特性它经常会被人出为考试题目和面试题目,有的时候更是要多加复习,在下一篇博客中我们将要讲解C++的另一个重要的知识点——多态,这也代表着C++的学习来到了一个全新的高度,最后希望这篇博客能给各位带来些许帮助。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值