[c++]——继承

1.继承的概念

目录:

  • 继承的概念
  • 基类与派生类的赋值转换
  • 继承中的作用域
  • 派生类的默认成员函数
  • 继承与友元关系
  • 继承和静态成员
  • 继承和组合
  • 面试笔试题

ps:下文中 父类 = 基类 子类 = 派生类

1.1继承的概念

继承是面向对象程序设计中可以使代码复用的重要手段,他是让我们在原有类的基础上进行拓展增加新的功能,这样产生的新的类叫做派生类或者子类。(继承是类设计层次的复用)

1.2继承的定义

继承书写格式:

A称为子类,B称为父类

class A:继承方式 + 类名B

下面代码中,student继承了preson

class person
{
protected:
	int name;
	int age;
};
class student:public person
{
protected:
	int id;
};
int main()
{
	student s;
	return 0;
}

通过调试窗口我们可以看到s对象继承了person的成员变量,事实上,父类的成员变量与成员函数都会被子类继承。
[c++]——继承

1.3继承的关系

我们已经知道类中有3中访问限定符,由于我们需要限定派生类中各成员的访问权限,我们这里引入了继承方式
[c++]——继承
原类中不同的访问权限在子类不同方式的继承方式下子类中成员访问权限又是什么呢?

表格看起来很复杂,但是我们记住他的权限是往小的缩就不那么难记了,public>protectod>private,基类中private成员在派生类中都是不可见(不可访问不等于私有)的。

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

小结

  • 1.基类中private的成员无论以什么继承方式继承给子类都是不可见的,语法上他依然被继承给了子类,但是我们不管是从子类内还是类外都无法对他的成员进行访问。
  • 2.protected的出现就是为了解决某些问题,假设我们基类中的成员不想在类外被访问,但是继承给子类后它可以在子类中被访问,所以出现了protected这个访问限定符。
  • 3.通常只使用public继承,因为其他方式的继承只能在类内访问成员,维护性不高。
  • 4.class默认是私有继承,struct默认是公有继承(了解)

2.基类与派生类的赋值转换

问题:基类对象可以赋值给派生类对象吗?反过来可以么?他们的指针和引用是否也可以相互赋值呢?

2.1对象的赋值

可以看出:   父类---》子类不行   子类---》父类可以
class person
{
protected:
	int name;
	int age;
};
class student:public person
{
protected:
	int id;
};
int main()
{
	person p;//父类对象
	student s;//子类对象
	
	p = s;//可以赋值
	s = p;//报错

	return 0;
}

解释:这里出现了一种行为叫切片或者切割,意思是把子类的一部分(父类也拥有的)赋值给父类,所以子类可以赋值给父类,父类无法赋值给子类
[c++]——继承

2.2指针的赋值

这次,父类的指针可以指向子类,而子类的指针无法指向父类

class person
{
protected:
	int name;
	int age;
};
class student:public person
{
protected:
	int id;
};
int main()
{
	person p;
	student s;

	person* pp = &s;//可以
	student* ss = &p;//报错

	return 0;
}

事实上,指针是一个无脑且bug的东西,他只根据自己的类型来判断访问的空间的大小,如下
[c++]——继承
接着问题来了,事实上我们知道c/c++有一种bug的操作是强制类型转换,下面代码经过强制转换编过了,但是假设我们访问ss指向的id时程序会奔溃,为什么强转可以编过接着往下看。。。

person* pp = &s;
student* ss = (student*)&p;

来看这几句代码你就明白了:

pp实际上可能本来就指向student对象,现在这样赋值就算访问id成员也不会有问题,但是这样确实是不规范的行为

person p;
student s;

person* pp = &s;
student* ss = (student*)pp;

2.3引用的赋值

关于引用这里就不再提太多,但是要明白,引用底层的实现其实也是指针,所以道理与上面的指针的赋值是相同的。

父类可以引用子类
子类不可以引用父类

小结

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。
  • 记住切片这个重要的行为
  • 基类对象不可以赋值给派生类对象
  • 基类的指针可以通过强制类型转换赋值给派生类的指针

3.继承中的作用域

问题:请问下面代码输出2222还是1111???

class person
{
protected:
	int name = 2222;
	int age = 18;
};
class student:public person
{
public:
	void fun()
	{
		cout << name << endl;
	}
protected:
	int name = 1111;
};
int main()
{
	student s;
	s.fun();

	system("pause");
	return 0;
}

答案:1111

3.1隐藏行为

隐藏:隐藏也叫重定义,子类中如果与父类中拥有相同的函数变量名和函数就构成重定义,子类的函数或者变量就屏蔽父类的函数与变量,牢记函数只需要函数名相同就构成隐藏。

ps:如果我们需要调用父类的函数或者变量需要指定作用域

cout << name << endl;
cout << person::name << endl;

小结

  • 基类与派生类具有不同的独立作用域
  • 子类隐藏父类同名变量和函数也叫重定义
  • 之所以父类与子类同名函数不构成重载就是因为他们的作用域不同
  • 尽量不要使用同名成员

4.派生类的默认成员函数

问题:类中的默认成员函数有几个?分别是哪些?

类中有6个默认成员函数(cpp11有8个),如下:

  • 构造函数
  • 拷贝构造函数
  • 析构函数
  • 赋值重载函数
  • 取地址重载
  • const取地址重载

4.1构造函数

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
class Person
{
public:
	Person(const char* name = "lcx")
		: _name(name)
	{}
	
	~Person()
	{}
	
protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{}
	
	~Student()
	{}
	
protected:
	int _num; //学号
};
void Test()
{
	Student s1("lcx", 18);	
}

4.2拷贝构造函数

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
Student(const Student& s)
		: Person(s)
		, _num(s._num)
	{}

4.3赋值重载

  • 派生类的operator=必须要调用基类的operator=完成基类的复制。

一定要指定调用的是父类的赋值重载函数,否则导致程序奔溃。

Student& operator = (const Student& s)
	{
		if (this != &s)
		{
			Person::operator =(s);//指定调用
			_num = s._num;
		}
		return *this;
	}

4.4析构函数

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

有趣的是,父类与子类的析构函数构成隐藏,上面说只有函数名相同的才构成隐藏,这里是一个例外,因为他们都会被处理成相同的名字。

牢记:编译器会自动帮程序员先调用子类析构函数,再调用父类析构函数。

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

关于初始化与析构

  • 派生类对象初始化先调用基类构造再调派生类构造。
  • 派生类对象析构清理先调用派生类析构再调基类的析构。

为什么是这样的顺序?
从原理上来讲,我们构建对象需要压栈,先构建父类对象就意味着先进后出先析构父类对象再析构子类对象,从现实中例子来说:
[c++]——继承

小结

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分,如果基类没有默认的构造函数,那么必须在派生类的初始化列表显式的初始化
  • 派生类拷的贝构造函数必须调用基类的拷贝构造来初始化基类的那一部分
  • 赋值重载函数在调用基类的时一定显示的注明作用域
  • 析构函数不需要程序员自己调用,编译器会帮你调用
  • 一定牢记舒适化和析构的顺序

5.继承与友元关系

友元关系是不可以继承的,B不是fun函数的友元

class A
{
	friend void fun();
};

class B:public A
{

};

void fun()
{}

6.继承和静态成员

在类A B C中a这个静态成员最终只有一个,可以在类B C中直接访问a

class A
{
public:
	static int a;
};

class B :public A
{};

class C :public B
{};

7.菱形继承及菱形虚拟继承

上面讲述了那么多继承概念与特点都比较好理解,那么接下来就是继承关系的硬菜了。

7.1单/多/菱形继承

单继承:一个子类只有一个直接父类(这里写错了,继承应该是public不是class,emmmm。。)
[c++]——继承
多继承:一个子类都多个直接父类
[c++]——继承
菱形继承:
[c++]——继承

7.2菱形继承所带来的问题

B C 继承 A ,D继承B C,创建一个D的dd对象

class A
{
public:
	int a;
};
class B : public A
{
public:
	int b;
};
class C : public A
{
public:
	int c;
};
class D : public B, public C
{
public:
	int d;
};
int main()
{
	D dd;
	system("pause");
	return 0;
}

造成的问题:dd对象有俩个a
[c++]——继承
对a进行赋值:报错访问不明确
[c++]——继承
指定访问:

D dd;
dd.B::a = 10;

上面所说的问题叫数据的二义性,我们可以指定访问那个类中的成员,但是还有一个问题,数据的冗余性。

冗余性:假设A中有性别一项,D代表描述人的类,人可以拥有俩个性别么??所以为了解决这个问题引入了虚拟继承

7.3虚拟菱形继承

没有虚拟继承之前:从观察内存的确可以看出a存在俩个
[c++]——继承

  • virtual关键字在这里是为了解决菱形继承问题时加上的。

现在进行虚拟继承,给B C继承A时加上virtual。

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 dd;	
	dd.B::a = 1;
	dd.C::a = 2;
	dd.b = 3;
	dd.c = 4;
	dd.d = 5;

	system("pause");
	return 0;
}

打开监视窗口:我们此时发现确实只存在一个a(这里其实将A独立出来放到最下面),他的值是最后赋值的结果,且他被从对象组成的最下面,但是和B和C每个都多了一个地址,这是什么?接着往下看。
[c++]——继承
我们把这俩个地址输入到新的监视中:发现一个存了10进制20,一个存了10进制12
[c++]——继承
实际上这是表示a这个变量相对于对于他们本身的偏移量:这里他们多存了一个指针,这个指针指向了一个叫虚基表的东西,这个指针叫做虚基表指针
[c++]——继承
虚基表:存了偏移量,可以找到A
[c++]——继承
有的同学发现本来对象只有20字节,但是引用虚继承后对象成为了24字节,这样造成了对象的变大,但是实际上我们这里a是int变量,如果a是自定义类型相信同学们可以理解使用虚基表可以省掉不少空间。

小结

  • 使用虚基表来解决菱形继承的二义性和数据冗余问题
  • 虚拟继承相当于将重复继承的那部分独立出来通过虚基表指针去寻找
  • 为什么要找到A?因为当D对象给C赋值时,有一部分需要赋值给A

8.继承和组合

8.1什么是继承什么是组合?

组合:一种has-a的关系(拥有被拥有的关系),可以说一个人拥有两只手,如下就是组合

class head//手
{
private:
	int finger;
};

class preson//人
{
private:
	head _h;
};

继承:一种is-a的关系(是与不是的关系),学生就是一个人,如下是继承

class preson//人
{
public:
	int name;
};

class student:public preson//学生
{
private:
	int age;
};

你不可以说人一个手,你也不可以说学生有一个人。。。。

8.2由继承与组合引发的思考

优先使用组合,但是适合继承下还是使用继承,为什么?

我们开发软件讲究高内聚低耦合原则

举个旅游的例子:

  • 内聚度:你一个人出去玩非常独立且自由,可以类似认为内聚度高
  • 耦合度:你有没同伴你需要和你的同伴一同行动,如果同伴少你们不用考虑互相的习惯认为耦合度低,反之耦合度高

高内聚低耦合:一个人出去自由内聚度高,不要与其他同伴有不相同的习惯耦合度低,类与对象也同样,将联系紧密的成员放在一个类中,类与类直接联系不要太紧密,否则出现问题非常麻烦。

9.面试笔试题

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

相信你仔细读了上文后一定对这几个问题有了新的理解。

总结

  • 这里就不再过多叙述知识点,上面已经都涉及到了,以上是笔者对继承的浅解,因为还处于cpp学习阶段,有的问题可能回答的不是很到位,还希望各路大佬小白补充,谢谢阅读哈哈。。。(90度弯腰鞠躬)。