【C++】继承

【C++】继承_第1张图片

樊梓慕:个人主页

 个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

每一个不曾起舞的日子,都是对生命的辜负


目录

前言

1.继承的概念

1.1定义

1.2格式

2.父类和子类对象的赋值转换

3.继承的作用域 

4.子类的默认成员函数

4.1构造函数

4.2析构函数

4.3拷贝构造

4.4赋值重载

5.继承与友元

6.继承与静态成员

7.复杂的菱形继承与虚拟继承

虚拟继承原理(剖析底层)

(1)采用普通继承下的内存分布

(2)采用虚拟继承下的内存分布 

8.继承的总结和反思


前言

本篇文章主要讲解『 C++继承』的相关内容。


欢迎大家收藏以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:fanfei_c的仓库

=========================================================================


1.继承的概念

1.1定义

为了提高代码的复用性,C++设计出了继承这一概念。

继承(inheritance)机制是面向对象程序设计中使代码可以复用的最重要的手段,它允许程序员在保持『 原有类特性的基础』上进行『 扩展』,『 增加』功能。这样产生的新类,称『 派生类』(或『 子类』),被继承的类称『 基类』(或『 父类』)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

之前接触的复用都是函数复用,继承是类设计层次的复用


1.2格式

【C++】继承_第2张图片

例如:

父类Person:

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "peter"; // 姓名
	int _age = 18; // 年龄
};

 子类Student:

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

子类Teacher:

class Teacher : public Person
{
protected:
	int _jobid; // 工号
};

按照继承的格式我们得知,Student与Teacher『 public继承』 了Person。

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

【C++】继承_第3张图片

我们可以从打印结果了解到,子类继承父类的大概意思就是子类可以继承得到父类的成员,可以理解为『 继承家产』。

那么子类可以继承得到父类的全部成员么?

答案是否定的!

这就要我们剖析一下继承方式了:

继承方式:

  • public继承
  • protected继承
  • private继承
类成员/继承方式 public继承 protected继承 private继承
父类的public成员 子类的public成员 子类的protected成员 子类的private成员
父类的protected成员 子类的protected成员 子类的protected成员 子类的private成员
父类的private成员 在子类中不可见 在子类中不可见 在子类中不可见

上面这张表不需要死记硬背,我们来『 理解着记忆』:

父类的private成员,子类会继承但『 不可见』(不可见不是不存在,而是存在,但不能使用)。

②若父类想要子类能访问到他的成员,父类中的成员『 至少』要定义为protected。

父类的『 public成员』和『 protected成员』在子类中的访问方式为Min(父类中的访问限定符,继承方式)。

public > protected > private

④若不指定继承方式,class默认为private,struct默认为public继承。但最好显示的写出。 

⑤一般都使用public继承。


2.父类和子类对象的赋值转换

(1)『 子类对象』可以赋值给 『 父类的对象』/ 『 父类的指针』/ 『 父类的引用』。这里有个形象的说法叫『 切片』或者『 切割』。寓意把子类中父类那部分切来赋值过去。

(2)『 父类对象』不能赋值给子类对象。

(3)父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)

【C++】继承_第4张图片

例如:

class Person
{
protected:
	string _name;//姓名string_sex;//性别
	int _age;//年龄
};
class Student : public Person
{
public:
	int _No;//学号
};
void Test()
{
	Student sobj;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj; 
	Person* pp = &sobj; 
	Person& rp = sobj;
	//2.基类对象不能赋值给派生类对象
	sobj = pobj;//err
	// (略)3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj;
	Student * ps1 = (Student*)pp;//这种情况转换时可以的。
	ps1->_No = 10;
	pp = &pobj;
	Student* ps2 = (Student*)pp;//这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;
}

3.继承的作用域 

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

这里有非常容易混淆的知识点:『 重载』和『 隐藏』

例如:

class A
{ 
public:
	void fun()
	{
		cout << "func()" << endl;
	} 
};
class B : public A
{ 
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};
  • B中的fun和A中的fun『 不是构成重载』,因为不是在同一作用域!
  • B中的fun和A中的fun『 构成隐藏』,成员函数满足函数名相同就构成隐藏。

4.子类的默认成员函数

4.1构造函数

先父后子:先调用父类的构造函数,再调用子类的构造函数。

即构造子类,会先构造父类,再构造子类

例如:

class Person {
public:
	Person(string name = "小樊")
		:_name(name)
	{
		cout << name << endl;
	}
protected:
	string _name;
};
class student :public Person
{
public:
	student(string name, int age)
		:_age(age)
	{
		cout << name << endl << age << endl;
	}
protected:
	int _age;
};

int main()
{
	student st("小飞", 18);构造子类,会先调用父类然后再调用子类
	return 0;
}

【C++】继承_第5张图片

该样例中,父类构造给了缺省参数,即父类有默认构造,所以子类并没有显示调用父类的构造函数,而是编译器自动调用,如果这里没给缺省值,那么就需要你在子类中给定父类构造的参数。

如:

human(string name)//不给缺省参数,相当于父类无默认构造
		:_name(name)
{
    cout << name << endl;
}
//所以这种情况必须在子类中给父类构造赋值,否则就会报错
student(string name,int age)
		:_age(age)
		, human(name)//显示调用父类构造方式『 记忆』
//新增,子类以自己的name给父类构造中的name赋值
//age和name的顺序随意变动,初始化顺序仅与声明顺序有关

虽然上述代码子类构造的初始化列表中, _age仿佛先于human初始化,但是别忘了:初始化顺序仅与声明顺序有关,与初始化列表顺序无关!!! 

所以这里一定是『 先父后子』。


4.2析构函数

先子后父:先调用子类的析构函数,再调用父类的析构函数。

例如:

class Person {
public:
	Person(string name = "小樊")
		:_name(name)
	{}
	~Person()
	{
		cout << "调用父类析构" << endl;
	}
protected:
	string _name;
};
class student :public Person
{
public:
	student(string name, int age)
		:_age(age)
	{}
	~student()
	{
		cout << "调用子类析构" << endl;
	}
protected:
	int _age;
};

int main()
{
	student st("小飞", 18);
	return 0;
}

【C++】继承_第6张图片

也就是说,子类析构时会自动调用父类析构

所以我们就『 不需要自己再去调用父类析构』了,即『 不要在子类中调用父类析构』,否则会出现『 析构两次』的情况。 


4.3拷贝构造

先父后子:先调用父类的拷贝构造,再调用子类的拷贝构造。

class Person {
public:
	Person(string name = "小樊")
		:_name(name)
	{}
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "调用父类拷贝构造" << endl;
	}
protected:
	string _name;
};
class student :public Person
{
public:
	student(string name, int age)
		:_age(age)
	{}
	student(const student& s)
		:Person(s)//切片,将子类中父类的部分切割赋值给父类
		, _age(s._age)
	{
		cout << "调用子类拷贝构造" << endl;
	}

protected:
	int _age;
};

int main()
{
	student st("小飞", 18);
	student st2(st);
	return 0;
}

注意:代码中注释部分,当需要独立实现子类的拷贝构造『 不满足默认生成的』时,需要在初始化列表中调用父类的拷贝构造,这里直接将子类对象的引用传递给父类即可,上面讲过『 父类和子类对象的赋值转换(切片)』。 


4.4赋值重载

子类的operator=必须要调用父类的operator=完成父类的赋值。

class Person {
public:
	Person(string name = "小樊")
		:_name(name)
	{}
	Person& operator=(const Person& p)
	{
		cout << "调用父类赋值重载" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
protected:
	string _name;
};
class student :public Person
{
public:
	student(string name, int age)
		:_age(age)
	{}
	student& operator=(const student& s)
	{
		cout << "调用子类赋值重载" << endl;
		
		if (this != &s)
		{
			Person::operator=(s);//必须指定,因为父子类中的operator=构成隐藏关系
			_age = s._age;
		}
		
		return *this;
	}

protected:
	int _age;
};

int main()
{
	student st("小飞", 18);
	student st2("小凡",20);
	st = st2;
	return 0;
}

【C++】继承_第7张图片

注意:代码中注释部分,必须要指定父子类的operator=,因为父子类中都有operator=,如果不指定,会构成隐藏关系,子类一直调用子类的operator=构成死循环。


总结:谁干谁的活,子类不能替父类完成默认成员函数干的活,必须调用父类的默认成员函数。


5.继承与友元

友元关系不能继承,即父类友元不能访问子类私有和保护成员。

即父亲的朋友不是你的朋友。

class 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);
}

当然如果你想让父亲的朋友成为你的朋友,你得声明。

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

6.继承与静态成员

父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。

无论派生出多少个子类,都只有一个static成员实例。

  • 『 静态成员不在对象里,在静态区,所以不会继承』

7.复杂的菱形继承与虚拟继承

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

如:

【C++】继承_第8张图片

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

如:

【C++】继承_第9张图片

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

如:

【C++】继承_第10张图片

菱形继承会导致二义性、冗余等问题。

原因:很明显,如图所示的Assistant类会继承两个父类Student和Teacher,又因为Student和Teacher继承了他们的父类Person,所以会导致他们的子类Assistant继承了两份Person,进而导致二义性和冗余。

如:

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; // 主修课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
}

【C++】继承_第11张图片

但是数据冗余问题无法解决。

所以引入『 虚拟继承』的概念。

刚才的代码我们只需要将菱形继承关系中腰线位置加上virtual即可。

【C++】继承_第12张图片

如:

class Person
{
public:
	string _name; // 姓名
};
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; // 主修课程
};
void Test()
{
	Assistant a;
	a._name = "peter";
}

虚拟继承原理(剖析底层)

为了方便研究,建立以下菱形继承体系:

(1)采用普通继承下的内存分布

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 d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

通过内存窗口监视到D类对象中各个成员的内存分布:

【C++】继承_第13张图片

 即:

【C++】继承_第14张图片

通过该模型,我们可以发现有两份A,所以当然会造成二义性和数据冗余。

(2)采用虚拟继承下的内存分布 

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;
}

通过内存窗口监视到D类对象中各个成员的内存分布: 

【C++】继承_第15张图片

 其中D类对象当中的_a成员被放到了最后的『 公共区域』。

而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表。

虚基表中包含两个数据:

  • 第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心)
  • 第二个数据就是当前类对象位置距离公共虚基类的偏移量。

也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a。如下图:

【C++】继承_第16张图片

抽象如下图:

【C++】继承_第17张图片


8.继承的总结和反思

(1)多继承会导致出现菱形继承,为了解决这一问题,C++引入了虚拟继承,虚拟继承设计的初衷就是为了解决菱形继承的,倘若不存在多继承,也就不会有后面这些复杂的设计。很多后来的语言避免了C++多继承的缺陷,如java。

(2)继承和组合

  • 继承是一种is-a的关系,也就是说每个子类对象都是一个父类对象;
  • 组合是一种has-a的关系,若是B组合了A,那么每个B对象中都有一个A对象。

例如,车类和宝马类就是is-a的关系,它们之间适合使用继承。

class Car
{
protected:
	string _colour; //颜色
	string _num; //车牌号
};
class BMW : public Car
{
public:
	void Drive()
	{
		cout << "this is BMW" << endl;
	}
};

而车和轮胎之间就是has-a的关系,它们之间则适合使用组合。

class Tire
{
protected:
	string _brand; //品牌
	size_t _size; //尺寸
};
class Car
{
protected:
	string _colour; //颜色
	string _num; //车牌号
	Tire _t; //轮胎
};

若是两个类之间既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合

  • 继承:一定程度破坏了父类的封装,父类的改变,对子类有很大的影响。子类和父类间的依赖关系很强,耦合度高。白箱复用:父类的内部细节对子类可见。
  • 组合:对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是『 不可见』的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。

类之间的关系可以用继承,可以用组合,就用组合。


注意:有关接口继承的相关内容放在下一节的多态部分讲解。 

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

博主很需要大家的支持,你的支持是我创作的不竭动力

~ 点赞收藏+关注 ~

========================================================================

你可能感兴趣的:(C++,c++,开发语言)