彻底搞定面试题--继承篇(C++继承讲解),万字解析,弄透继承!

面向对象程序设计的特性之一–继承(C++讲解)


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5kt6g9w8-1665377649319)(D\gitee仓库\博客使用的表情包\举个例子.jpg)]

1.继承的概念🐣

​ 顾名思义,就像现实生活中的财产继承一样,继承上一代的财产。那么编程里面的继承呢–是为了实现类级别的复用

举个栗子:

​ 学生类与老师类,他们存在着很多共同点

  1. 有姓名
  2. 有年龄
  3. 有学校

​ 也存在着很多不同点:

  1. 学生-学号
  2. 老师-教师号
  3. 老师有薪资
  4. …………

​ 在编程中,当相同的地方多次进行时,最喜欢设计一个通用的东西,然后实现复用,例如:一个地方有多次进行比大小,那么我们可以设计一个比大小的函数,然后多次调用此函数。类似的,当多个类有一些共同的属性时,我们可以设计一个类,作为其他类的基础,在此基础上完成其他功能的实现


稍微官方一点的概念就是:

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

代码演示:

#include<iostream>
using namespace std;

class Person//基类
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和
//Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
//调用Print可以看到成员函数的复用。
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;
}

​ 上述的studentteacher类就是继承了person类,继承了person类的print成员函数,还继承了其私有成员,name,age


2.继承的方式👻

​ 根据继承方式的不同,子类对父类的访问方式也不同。在上述代码中我们使用的是public公有继承,当然了也有protected–保护继承与private–私有继承

​ 根据继承方式的不同,以及基类中的访问权限的不同,子类对基类的访问会产生9种效果(3*3)。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

​ 最后在派生类中一共有4种情况。

  1. public–派生类可以直接访问
  2. protected–派生类不可以直接访问,但是可以通过派生类成员函数访问
  3. private–派生类不可以直接访问,但是可以通过派生类成员函数访问
  4. 不可见–父类的的成员已经继承过来了,但是语法上限制子类不能访问,只能通过父类的公有成员函数才能访问

事实上,平时用的最多的就是公有继承

​ 当然了,在派生类中的访问方式是有规律的

  1. 遵循小权限,权限大小:public > protected > private
  2. 如果基类中的private成员,那么在派生类中是不可见的

这里我们也知道了,private与protected的区别了。

大家可以通过上述的代码,修改一下继承方式和基类的成员访问方式,通过调式–来看看4种情况下的效果。


3.赋值兼容规则🤖(切片/切割)

派生类与基类是可以进行赋值的,但是并不是相互的,其中派生类可以给父类进行赋值,而父类不能给子类进行赋值

ps:了解这个赋值兼容规则,对于学习后面的C++多态是必要的,也是必须的

#include<iostream>
using namespace std;

class Person
{
public:
	string _name; // 姓名
	string _sex;  // 性别
	int	_age;	 // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{
	Person p;
	Student s;
	s._name = "张三";
	s._sex = "男";
	s._age = 18;

	p = s;    // 父<-子   可以的  切割/切片
	// s = p; // 子x-父   不行的

	Person* ptr = &s;

	Person& ref = s;

	return 0;
}

​ 由于咱们还没有介绍派生类的构造函数,为了方便设置派生类的成员的值,就将成员变量设置为public权限。

图解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1H9bdYAq-1665377649320)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010093832681.png)]


4.隐藏(重定义)🐱‍👤

​ 首先我们回顾以下作用域的概念。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V9Q6aRj8-1665377649321)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010094315710.png)]

​ 全局域里面有个num的值是99,局部域里面有个同名的num的值是100

全局域的作用域:程序源文件的的整个范围

局部域的作用域:局部的代码块里面

当我们在局部域里面访问num,将由于访问局部域里面的num,如果要访问全局域的num,要指明其作用域。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QCKt1K3P-1665377649321)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010094757984.png)]


成员变量的隐藏
#include<iostream>
using namespace std;

class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; 	   // 身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份号:" << Person::_num << endl;
		cout <<  _num  << endl;
	}
protected:
	int _num = 999; // 学号
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

​ 运行上述代码,发现输出的_num显示的是999–学号。

也可以给大家展示一下成员变量隐藏下的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R4Y4Z4Ca-1665377649322)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010095825940.png)]

原理解释:

派生类域基类都有他们对应的作用域:

  1. 基类的作用域,基类的类区域
  2. 派生类的作用域,派生类的类区域

那么我们在派生类不指定作用域的情况下,即使基类的_num被继承下来了,优先访问的肯定是派生类的同名成员。

同理我们要访问基类的同命成员变量–指定作用域。

ps:不建议写出成员变量构成隐藏的代码

#include<iostream>
using namespace std;

class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; 	   // 身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
		cout << " 身份证号:" << Person::_num << endl;
	}
protected:
	int _num = 999; // 学号
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

成员函数的隐藏

​ 成员函数构成隐藏的条件:继承下函数同名即构成隐藏

#include<iostream>
using namespace std;

class A
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void func(int i)
	{
		cout << "func(int i)->" << i << endl;
	}
};
int main()
{
	B b;
	b.fun(10);
	return 0;
};

​ 上述代码种,调用的是B类里面的func函数,我们我们要调用A类里面的成员函数。指定作用域即可

b.A::func();–将这句代码加进去即可。

ps:很多同学容易以为上述的两个func函数构成函数重载,这是错误的说法

函数重载条件:

  1. 同一作用域
  2. 函数名相同,函数参数不同

而隐藏是不同作用域下的(一个作用域是基类,另一个是子类)


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

class Person
{
public:
	Person(const char* name)
		: _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() // -> 因为后面多态的一些原因,任何类析构函数名都会被统一处理成destructor()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	//student的默认成员函数
    //…………
private:
	int _stuid;
};
派生类的构造函数
Student(const char* name = "Peter" , int id = 18)
	:Person(name)//Person部分调用其构造
	,_stuid(id)
	{
		cout << "Student()" << endl;
	}
  1. 先调用基类的构造,完成基类的构造,由于初始化列表是成员变量定义的地方,我们要在初始化列表完成Person的定义。

  2. 再处理派生类自己的成员变量部分


派生类的拷贝构造函数
Student(const Student& s)
		:Person(s)//Person部分调用其拷贝构造
		,_stuid(s._stuid)
	{
		cout << "Student(const Student& s)" << endl;
	}

同理:

  1. 要先完成基类的拷贝构造问题是,如何将基类的部分取出来,交给基类取拷贝构造,赋值兼容规则有用了,直接将Student的对象传进去,基类使用Person类的引用接收,根据切片规则即可取出Student对象的基类部分。
  2. 再处理派生类的拷贝构造部分即可

派生类的赋值运算符重载
Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			operator=(s);//Person部分调用其赋值
			_stuid = s._stuid;
		}
		return *this;
	}

同理:

  1. 要先完成基类的赋值
  2. 再完成子类的赋值

​ 但是这个上述的赋值运算符重载写错了,当然逻辑上没问题,是先调用基类的,再派生类的。但是上述代码调用基类发生了错误,

派生类域基类的赋值运算符重载的函数名相同,就是perator=,那么根据成员函数的隐藏条件(函数名相同即构成隐藏),那么上述我们想调用基类,可是调用的却是派生类的operator=

​ 解决方法:指定作用域!

Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			::operator=(s);//Person部分调用其赋值
			_stuid = s._stuid;
		}
		return *this;
	}

派生类的析构函数

​ 同理:

  1. 先调用基类的析构函数
  2. 再设计派生类的析构函数

ps:C++为了实现多态的一些功能的完整性,将基类域派生类的析构函数名都变成了destructor()

所以如果我们直接调用的话就会构成隐藏,所以调用基类的析构函数时需要指定作用域

~Student()
	{
		Person::~Person();//析构可以显示调用
		cout << "~Student()" << endl;
	}

​ 我们拿一段代码测试一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KmwlJZcM-1665377649323)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010103310648.png)]

​ 我们发现,多调用了一次析构函数,这其实时析构函数做的处理,为了保证,LIFO–后构造的先析构的规则,编译器会在子类的析构函数调用结束后,自动调用父类的析构函数

这样也使得调用顺序更加合理。

由于我们上述的代码没有做指针,内存的一些清理工作,所以没有报错,不然析构两次是会报错的。

所以咱们的析构函数这样写就够了:

~Student()
	{
		cout << "~Student()" << endl;
	}

6.思考题(面试题):请设计一个无法被继承的类🙉

方式一:将基类的构造函数设置成private权限
#include<iostream>
using namespace std;

class A
{
	private:
	A()
	{
		cout << "A()" << endl;
	}

	protected:
	int _a;
};

class B :public A
{
public:
	B()//无法继承了,会报错
	{
		cout << "B()" << endl;
	}
	void func()
	{
		cout << _b << endl;
		cout << _a << endl;
	}


protected:
	int _b;
};


int main()
{
	B b;
	b.func();
	return 0;
}

​ 我们已经知道基类中的private成员在派生类中是不可见(不可访问)

​ 上述代码中,B继承了A,那么在B的构造函数中是会去自动调用A的构造函数的,而A的构造函数时不可见(不可被B访问),所以是会报错的。这是一个很巧的办法。


方式二:加上关键字final

​ 当我们在基类设计时,在后面加上final关键字,那么此类就无法被继承。

代码如下:

#include<iostream>
using namespace std;

class A final
{
public:
	A()
	{
		cout << "A()" << endl;
	}

	protected:
	int _a;
};

class B :public A
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	void func()
	{
		cout << _b << endl;
		cout << _a << endl;
	}


protected:
	int _b;
};


int main()
{
	B b;
	b.func();
	return 0;
}

7.友元是不被继承的🐡

#include<iostream>
using namespace std;

class Student;//声明有Student类,不然父类里面的友元就无效了

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

class Student : public Person
{
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);
}

​ 上述代码会报错,因为,虽然**Display是基类的友元,但友元并不会继承下去,即Display不会通过继承一下就不成派生类的友元**,要想成为派生类的友元,必须也在派生类里面再加一句friend void Display(const Person& p, const Student& s);


8.继承下的静态成员🎲

​ 静态成员是共享的成员,不仅仅基类的多个对象共用这个成员,派生类的对象也和基类的对象一起共用这个成员

注意,静态成员函数中是无this指针的,多态会考,先打预防针

一个经典的统计人数题
#include<iostream>
using namespace std;


class Person
{
public:
	Person() { ++_count; }
	Person(const Person& p) { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;

class Student : public Person//每创建一个学生对象,都会区调用其基类的构造函数
{
protected:
	int _stuNum; // 学号
};

class Graduate : public Student//每创建一个毕业生的对象--基类的构造函数
{
protected:
	string _seminarCourse; // 研究科目
};

void func(Student s)
{}

int main()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;

	func(s1);//传参也会创建临时对象

	cout << Person::_count << endl;
	cout << Student::_count << endl;
	cout << Graduate::_count << endl;//静态成员被基类与派生类所共享

	return 0;
}

​ 上述代码,Person类是基类,Student,Graduate是派生类,让我们统计Person,Student,Graduate这些类一共创建了多少对象。思路:派生类创建对象都会调用基类的构造函数,所以我们在基类中定义一个静态成员变量来统计人数,然后在基类的构造函数中++count,count就是对象的个数了


9.C++的填坑日记–多继承🎠

​ 生活中会出现这样的场景,一个助教,同时他也是一个学生(博士或者硕士),同时他也是一个老师

那么我们就可以这样设计主教的类了(具有两个类的性质):

class Person
{
public:
	string _name; // 姓名
};

class Student :  public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

多继承下的隐患–数据冗余与二义性

就拿上述代码举例:

图解:

Assistant同时继承了Student,Teacher类,而Student,Teacher类又都继承了Person

这样就形成了菱形继承,也就会产生数据冗余–_name有两份,二义性–_name同名,访问不明确

如果硬要去用也可以这样:

#include<iostream>
using namespace std;


class Person
{
public:
	string _name; // 姓名
};

class Student :  public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	Assistant a;
	a.Student::_name = "小张";
	a.Teacher::_name = "张老师";
    return 0;
}

​ 为了修改数据的便捷,我们将Person类的_name设置为public,上述代码要指明作用域才能避免二义性,如果指定会报错访问不确定,而且此方式没有解决数据冗余


解放方法–虚继承

​ 虚继承是可以解决数据冗余与二义性的。

语法,在腰部加上virtual关键字

#include<iostream>
using namespace std;


class A
{
public:
	int _a;
};

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

//class C : public A
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._a = 3;//加了虚继承就不会访问不明确了
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

​ 图解演示:

普通继承时:

在这里插入图片描述

d对象里面先放继承B的成员,再放继承C的成员,再放自己的成员

这个顺序(与构造顺序一致)根继承时的声明关系有关,与初始化列表无关


虚继承下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qdzLB4Mb-1665377649324)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010114617712.png)]

​ 他将虚继承下重复的部分放到了最后面,但是奇怪的是B,C部分的第一个位置放了一个奇怪的值。这个值起始看起来很像一个地址。

虚继承的底层原理

​ 我们将这个地址输入进内存窗口看看里面到底是什么。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdFA0ORB-1665377649325)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221010115533136.png)]

​ 我们看到14(十六进制)–转换成十进制为20,0c(十六进制)–转换成十进制为12

这两个数字正好是,B和C举例A的偏移量。也就是说,编译器是通过这个偏移量找到这个A的。


​ 通常来说,我们将虚继承的类叫做虚基类,上述的虚基类就是A。虚继承下,B和C虚继承了A,那么B和C里面就会放一个指针(叫做虚基表指针),指向的就是虚基表,虚基表里面有虚基类的偏移量。那么为什么虚基表的一个位置放的是0呢,这涉及到后面多态的内容,咱们多态文章再把这个讲明白。


10.继承与组合的区别🚀

​ 未来程序的趋向:高内聚,低耦合

​ 通俗易懂地讲就是:一群程序猿一起设计程序,如果其中一个程序猿的代码出bug了,那么他只需要自己改自己的就可以了,其他程序猿写的代码不受到他的影响。但是每个程序猿之间又是相互联系,复用的。

继承–is-a的关系

​ 其实继承是很不符合高内聚,低耦合的,因为类与类之间的联系太大了,一个改变另一个也要接着改变。

组合–has-a的关系

一个类的成员里面有另一个类的对象。那个类是无法访问另一个类的私有成员的。这样他们之间的联系不会太大,保证了封装性。

#include<iostream>
using namespace std;


class A
{
public:
	void func()
	{}
protected:
	int _a;
};

// B继承了A,可以复用A
class B : public A
{
protected:
	int _b;
};

// C组合A,也可以复用A
class C
{
public:
	void func()
	{
		_a.func();
	}
private:
	int _c;
	A _a;
};

int main()
{
	B b;
	b.func();
	C c;
	c.func();
	return 0;
}

​ 大家可以结合上述代码理解继承与组合区别。

ps:区分方式is-a/has-a/,如果既可以是继承又可以是组合,建议用组合

11.继承面试题🐨

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

答案都在文章里面!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uMizpQ0r-1665377649326)(D:\gitee仓库\博客使用的表情包\给点赞吧.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ibprhFnU-1665377649326)(D:\gitee仓库\博客使用的表情包\要赞.jpg)]

评论 105
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值