【C++】继承

C++中的继承

  • 继承的简要介绍
    • 继承定义
      • 定义格式
      • 继承关系和访问限定符
  • 基类和派生类对象的赋值转换
  • 继承中的作用域问题
  • 派生类的默认成员函数
  • 继承中的友元和静态成员
  • 菱形继承及虚拟继承
    • 菱形继承的问题
    • 虚拟继承

继承的简要介绍

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

继承定义

定义格式

【C++】继承_第1张图片
  继承后父类的Person成员(成员函数和成员变量)都会变成子类的一部分。这里体现出了Student复用了Person的成员
【C++】继承_第2张图片
  通过监视窗口我们可以看到继承后父类的成员变量的确是被子类对象复用了
【C++】继承_第3张图片
  通过函数调用我们可以看到父类成员函数在子类对象中的复用

继承关系和访问限定符

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

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

总结

  1. 基类中private成员在派生类中不论是以什么方式继承都是不可见的(这里的不可见指的是基类的私有成员虽然还是被继承到了派生类对象中,但是在语法上限制了派生类对象不管在类外面还是类里面都不能去访问基类的private对象)
  2. 基类的private成员在派生类中不可见,如果基类成员不想在类外直接被访问,但是需要在派生类中进行访问,就需要将该基类成员的访问权限改为protected
  3. 根据以上表格我们可以发现一个规律,即;基类的私有成员在子类中是不可见的,基类的其他成员在子类的访问方式==Min(成员在基类的访问限定符,继承方式)。其中,访问限定符的优先级顺序为public>protected>private
  4. 继承方式可以不用显式写出,class的默认继承方式是private继承,struct默认的继承方式是public继承(不过最好还是显式写出)
  5. 在实际运用中一般使用的是public继承,几乎很少使用protected/private继承,因为protected/private继承下来的成员对象只能在派生类中使用,实际中拓展维护性不强

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

  1. 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。
  2. 基类对象不能赋值给派生类对象
class Person
{
protected:
	string _mame = "zhangsan";
	string _sex = "man";
	int _age = 22;
};

class Stuedent :public Person
{
protected:
	int _stuid = 2024;
};

int main()
{
	Stuedent stu;
	Person per = stu;//将派生类对象赋值给子类对象
	return 0;
}

【C++】继承_第5张图片
注意
  将派生类对象赋值给子类对象也可以称为”赋值兼容转换“
  赋值兼容转换与普通赋值转换的区别就是,在一般赋值的时候,若两边对象/操作数 类型不一样的时候,会产生具有常属性的临时变量,这里我在之前博客中也有提到。赋值兼容转换则是当赋值两边对象不同时,中间并不会产生临时变量
【C++】继承_第6张图片
  这里有个形象的说法叫做切片或者切割,寓意是将派生类中父类那部分切来赋值过去

int main()
{
	Stuedent s;
	Person p = s;

	Person& r = s;
	Person* ptr = &s;
	return 0;
}

  在语句Person* ptr = &s;中,prt指向的并不是临时对象也不是单单是对象s,而是子类对象s中从父类Person切出来的那一部分
  同理,在语句Person& r = s;中,r引用的是子类对象s中从父类Person继承下来的那一部分(由于赋值兼容转换,中间没有产生具有常属性的中间变量,所以引用r的前面也不需要加上const修饰)

继承中的作用域问题

关于继承中的作用域,有以下几条性质

  1. 在继承体系中基类派生类都有独立的作用域
  2. 子类和父类在有同名成员的情况下,子类成员将会屏蔽对从父类继承下来的同名成员的直接访问,这种情况叫做隐藏,也叫做重定义(在子类成员函数中,可以使用基类::基类成员 显示访问)
  3. 需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
class Person
{
protected:
	string _name = "zhangsan";
	int _num = 12;
};

class Student :public Person
{
public:
	void print()
	{
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;//访问Student自己的_num成员
		cout << "年龄:" << Person::_num << endl;//访问从Person继承下来的_num成员
	}
private:
	int _num = 99;
};

int main()
{
	Student stu;
	stu.print();
	return 0;
}

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

派生类的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认
    的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。(也就是说基类的构造函数是在派生类构造函数的初始化列表进行调用的)
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
    保证派生类对象先清理派生类成员再清理基类成员的顺序。(防止出现父类的资源已经被清理释放,子类的析构函数有去访问已经释放的资源,造成野指针等风险)
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲
    解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加
    virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

继承中的友元和静态成员

  • 友元关系并不能继承,也就是说基类友元并不能访问子类的private成员和protected成员
  • 基类定义了static静态成员,则整个继承体系中只有一个这样的成员,不论是派生出多少类,有且仅有一个static示例,可以理解为(继承下来的只是访问权和使用权)

菱形继承及虚拟继承

单继承:一个子类只有一个直接父类
【C++】继承_第8张图片
多继承:一个子类有两个或者两个以上的直接父类
【C++】继承_第9张图片

菱形继承:多继承的一种特殊情况
【C++】继承_第10张图片

菱形继承的问题

class Person
{
public:
	string _name;
};

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

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

class Assistant :public Student, public Teacher
{
protected:
	string _majorCource;
};

【C++】继承_第11张图片
  从上面对象成员模型构造,可以看出菱形继承存在数据冗余和二义性的问题,即在Assistant的对象中从Person继承下来的成员会有两份name,但是我们有时根本就不需要两个_name成员变量,存在空间浪费。

虚拟继承

  虚拟继承可以解决菱形继承的二义性和数据冗余问题。就比如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,就可以解决问题

class Person
{
public:
	string _name;
};

//class Student :public Person
class Student :virtual public Person
{
protected:
	int _id;
};

//class Teacher :public Person
class Teacher :virtual public Person
{
protected:
	int _num;
};

class Assistant :public Student, public Teacher
{
protected:
	string _majorCource;
};

  为了更好的解释虚拟继承的原理,这里给出了一个简化的虚拟继承体系

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++】继承_第12张图片
然后我们在通过内存来看
当我们不采用虚拟继承的方式时
【C++】继承_第13张图片

采用虚拟继承
【C++】继承_第14张图片
  以d对象中B部分为例,与普通继承不一样的是,d._b地址前面的位置存的不再是d.A::_a的数据,而是一串地址,根据这串地址我们找过去,发现数据为全0,该地址的下一位地址存的是28(十六进制),其实,这个值就是所谓的偏移量,编译器就是根据这个去找到从B中继承下来的有关A的成员变量
  同理,d._c前面存的也是一串地址。
  因为在虚拟继承中,D类型对象将A类型的成员放到了对象组成的最下面,这个成员同时属于B和C,当B和C要去寻找公共的A时,就会通过B和C两个指针,指向一张表,这两个指针叫做虚基表指针,两个表就叫做虚基表。虚机表中存的偏移量,通过拿到偏移量就可以找到A成员的位置
【C++】继承_第15张图片
所以,此后我们在碰到这样的场景时

D dd;
B*pb=&dd
pb->a++;

   当pb要去访问_a时,就先要利用存的指针找到偏移量,再根据偏移量加上自己的地址就可以找到_a

你可能感兴趣的:(c++,面试)