🍑个人主页:Jupiter.
🚀 所属专栏:C++学习笔记
欢迎大家点赞收藏评论😊

在这里插入图片描述

在这里插入图片描述

继承

继承的概念及定义

继承的概念
  • 继承 是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。简单地说就是类的复用;
  • 继承一般除了吸收基类成员以为,一还需扩张自己的数据成员,跟基类有所不一样,但是没有也可以,所以,基类与派生类的大小关系为:基类>=派生类
继承的定义
定义的格式

举个栗子(下附代码):
在这里插入图片描述
代码:

class Person
{
protected:  
	string _name;
	int _age;
	string _sex;
	int _tel;
};
class student:public Person     //student继承了Person这个类
{
protected:
	int _id;
};
继承关系和访问限定符

访问权限在类内部,继承权限在类外;
在这里插入图片描述

继承后基类(父类)成员访问方式的变化
类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

总结:

  • 基类的private成员在派生类中无论以什么方式继承都是不可见的,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它;

  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的;

  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected > private;

  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式;

  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强;

举个栗子:
在这里插入图片描述
从上面例子可以看出,private继承后,public,protected成员在派生类可以访问,private成员不可访问;

基类和派生类对象赋值转换

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。注意:但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,会有特殊处理(后面的文章会提到);

在这里插入图片描述

class Person
{
protected:  
	string _name;
	int _age;
	string _sex;
	int _tel;
};
class student:public Person
{
protected:
	int _id;
};

int main()
{
	student s1;
	Person p1;

	p1 = s1;
	//s1 = p1;   //这里不能基类给派生类

	Person& pp1 = s1;     //是对截断后的内容引用
	Person* pp2 = &s1;     //指向的是截断后的内容

	student* ss;
	ss = (student*)&p1;         //基类的指针可以通过强制类型转换赋值给派生类的指针
	return 0;
}

在这里插入图片描述

继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域
    (就凭这一点,因此两个域中的函数不能构成函数重载
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员。

派生类的默认成员函数

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(先父后子)
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的复制。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(先子后父)
  • 编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。如果要显示调用,需要指定作用域;

继承与友元

友元关系不能被继承,也就是说基类友元不能访问子类私有和保护成员

举个例子:
在这里插入图片描述

继承与静态成员

  • 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。因为静态成员函数不是放在类里面的,而是放在静态区的,是这个类的对象所公有的,无论派生出多少个子类,都只有一个static成员实例 ;利用这个特性,可以统计这个类被创建了多少个对象;
  • 如果被继承了,无论派生类实例化出对象了还是基=基类实例化出对象都会调用基类的构造函数,就可以统计出个数;

例子:


class Person
{
public:
	Person()
	{
		_count++;
	}
protected:  
	int _age;
public:
	static int _count;
};
int Person::_count = 0;

class student:public Person
{
public:
	student()
	{}
protected:
	int _id;
};

int main()
{
	student S1;
	student S2;
	Person P1;
	Person P2;

	cout << Person::_count << endl;    //打印结果为4
	return 0;
}

多继承与菱形继承和菱形虚拟继承

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
    在这里插入图片描述
  • 多继承:一个子类只有多个直接父类时称这个继承关系为多继承

在这里插入图片描述

菱形继承:
在这里插入图片描述

  • 菱形继承的问题:菱形继承有数据冗余二义性的问题。在D的对象中A成员会有两份。因为B,C中都继承了A,里面各有一份A,然后D继承了B和C,所以会有两份A;

  • 因为有两份,所以数据冗余了,又因为D里面有两份A的成员,如果想通过D实例化的对象去访问B中继承的A的成员时,不知道是去访问B的还是C的;结合下面例子看看:
    在这里插入图片描述

结合下面代码:

class A
{
public:
	int _a;
};
class B:public A
{
protected:
	int _b;
};
class C:public A
{
protected:
	int _c;
};
class D:public B, public C
{
protected:
	int _d;
};
int main()
{
	D dd;
	//dd._a = 10;      //会报错(访问不明确)
	dd.B::_a = 1;
	dd.C::_a = 2;     //这样指定类域访问也可以,但是代码稍许冗余
	dd._b = 3;
	dd._c = 4;
	dd._d = 5;
	
	return 0;
}

解决方法:

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在B和C的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。

解决后的代码:

class A
{
public:
	int _a;
};
class B:virtual public A    /这里+
{
protected:
	int _b;
};
class C:virtual public A   //这里+
{
protected:
	int _c;
};
class D:public B, public C
{
protected:
	int _d;
};

在哪里+virtual的问题

在造成数据冗余的那两个类的继承方式前面加virtual

比如:
在这里插入图片描述

菱形继承虚拟菱形继承在内存上的差异
我们讨论的是VS上

D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B,C对象里面的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
在这里插入图片描述

其实,不仅是D对象的存储模型发生了改变,B与C对象的存储模型也发生了改变,以下图的B对象为例,B对象的内存模型中,将继承的A对象的成员放到了最下面,用虚基表指针指向虚基表,找到偏移量,找到a。
在这里插入图片描述


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐