继承
1、使用关键字class默认继承方式是private,使用struct默认是public继承,不过最好显示的写出继承方式: class A : public B
2、实际运用中都是public继承,几乎很少用protected/private继承,也不提倡用protected/private继承,因为继承下来的成员只能在子类中的类(父类)里面使用。
基类(父类)和派生类(子类)
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
//protected: // 在子类可见的 只能防外面
private: // 在子类是不可见(不能用) 不仅仅可以防外人还可以防儿子
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
public:
void func()
{
// cout <<_name << endl;//不可见
cout << "void func()" << endl;
}
protected:
int _stuid; // 学号
};
代码中,Person是Student的父类,继承中除了public和private,还多了一个protected,它的作用是为了防止类外的函数调用其中的东西,但子类可以调用protected的成员且改变值。而private子类也不能调用,对子类来说是不可见的。
基类和派生类对象赋值转换
派生类对象可以赋值给基类的对象/指针/引用。形象的说法叫:切片或者切割。
切片父类只能用子类中继承过去的类型,即用不了子类自带的_NO。
切片是天然支持的,赋值过程不像异类型变量赋值,它是没有隐式转换的,是直接赋值。
隐式转换:double d=1.1; int i=d;
把d赋值给i时,会产生中间值去类型转换。
基类对象不能赋值给派生类。父=子 子!=父
隐藏(重定义)
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 被隐藏
};
class Student : public Person
{
public:
void Print()
{
cout << Person::_num << endl;
cout << _num << endl;//输出的是Student中的_num
}
protected:
int _num = 999; // 学号
};
当子类和父类中有同名成员,子类将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。在子类函数中可以使用基类::基类成员显示访问。(最好不要定义同名)
✔️class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
上面两个fun()函数构成隐藏,当调用fun()时使用的是子类中的fun(),不会构成函数重载。
默认成员函数调用(构造、析构)
子类析构后,会自定调用父类析构,保证先析构子类再析构父类。
构造、析构顺序是栈行为:先构后析,基派派基。
多继承和菱形继承
多继承就是一个子类(Assistant)都继承了多个父类(Student,Teacher)
而菱形继承则是两个父类(Student,Teacher)又继承了一个父类(Person)
但是如果单单是这样定义:
class Student: public Person
class Teacher: public Person
则会导致数据的冗余性和二义性,所以又引入了virtual的概念:虚继承,virtual在使用多继承时需要写在继承形式public前面。
冗余性和二义性
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
//a._name = "张三";
// 数据冗余 和 二义性
a.Student::_name = "小张";
a.Teacher::_name = "老张";
}
当我们没有使用虚继承,定义一个Assistant类对象a时,这个a可以使用:
Student继承Person的_name:
Student::_name
Teacher继承Person的_name:Teacher::_name
而它自己还有从父类继承的_name
一轮赋值下来,我们监视发现,_name有两个值:小张和老张,那么a的_name到底是哪个呢,此时数据就有了二义性
再看内存视图,理论上a继承过的_name只需要一个空间存储即可,这里却有两个不同地址存储着相同的值,如果继承10个,100个父类,那就会浪费很多空间,此时数据的冗余性已经体现出来了。
虚继承
为了解决二义性和冗余性,引入了virtual虚继承这个概念,如下定义:
**class** Student: **virtual public** Person
**class** Teacher: **virtual public** Person
具体是怎么解决的呢,我们定义一段程序,再来看内存空间中是怎么分配的:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
//protected:
public:
int _num=10; //学号
int _addres=12;
};
class Teacher : virtual public Person
{
//protected:
public:
int _id=11; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
//string _majorCourse="张三"; // 主修课程
};
int main()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "_name";
// 数据冗余 和 二义性
a.Student::_name = "小张";
a.Teacher::_name = "老张";
}
程序说明:Student和Teacher都可以访问_name,当它们两个中间的public能够被Assistant继承,那么**_num=10 , _address=12 , _id=11** 在对象a中都可以显现,分别对应十六进制 0a 0c 0b
这样定义方便我们观察内存值。
从图中下面的内存视图中可以清楚定位到**_num , _address , _id** ,也就是0a、0c、0b的位置,我们可以发现,0a的地址前面和0b的地址前面还存了一串数据。
我们在上面的内存视图中寻找可以发现,蓝色地址中存放的是0x10,也就是16,这个数字是地址的偏移量,而红色地址中存放的居然是蓝色地址,也指向了偏移量地址。
这个偏移量是从存储0c的空间末尾开始偏移计算地址的,偏移16个地址位,正好到存储**_name的首地址**。这就是虚继承中的虚表。
Clion和VS内存视图会有些不一样,但是原理是一样的,都是存储偏移量。
虚表
Student和Teacher类空间都应该有一个自己的地址指向偏移量(虚表),也就是说红色地址和蓝色地址各指向一个虚表,Student的虚表中是16,Teacher的虚表中是8;当定义多个Assistant对象时,每个对象都是共用这两个虚表的,解决冗余性和二义性只需要两个地址空间即可。