C++ - 继承

继承的概念

 继承机制是面向对象程序设计当中,非常重要且好用的手段,这种手段可以允许程序员原有的类的特性基础之上,进行拓展从而产生新的类,这些新产生的类,我们成为派生类。在以前,我们实现的复用是函数的复用,现在衍生到类的设计层次上的复用,就是继承,就是一种对类设计上的复用。

C++ 在此处设计的理念是,父亲干父亲的活,孩子干孩子的活,具体的可以在文章当中去感受。

比如一个在一个学校当中有,很多的个学生和老师,在一个学校当中学生和老师的信息就有相同的,和自己类独特的。比如学生和老师有名字,电话,家庭住址,邮箱等等这些信息是每一个老师和学生都有的信息;

而对于学生,宿舍号,学号,专业等等就是属于学生这个类的独有信息;工号,所属学院,职称等等这些是属于老师的独有信息;所谓独有信息就是两个类之间不共有的信息。

当然,在一个学校当中不只有学生和老师两个“类”,还有食堂工作人员,保安等等多个职位,这些职位都是有共同之处的,比如:都有名字,电话,住址等等这些信息,如果我们把这些“类”都重新设计成各自的类,是不是就是非常的冗余了。

基于上述的问题,继承的作用就非常明显了,基于上述例子,我们可以搞一个 person类,这个类当中存储的是学校的当中所有人员的所共有的信息,比如:名字,年龄,电话号码,住址等等。

然后,所创建的 Student类 , teacher类 等等在类当中只需要写出自己类所独有的信息,再去继承 person 这个类就行了。

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
//Student和Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可
//以看到变量的复用。调用Print可以看到成员函数的复用。class Student : public Person
{
protected:int _stuid; // 学号
};

如上述,Student类 继承了 person类。

 而派生类当中(成员函数肯定不存储在类和对象当中)也不止 _stuid 这个成员,除了自己的成员之外,派生类的当中还有从父类当中继承过来的 成员

也就是说,派生类当中分为两个部分,一个是 派生类 本身的成员部分;另一个是 派生类 从父类当中继承过来的 成员部分

继承的语法

 如下图所示:Person是父类,也称作基类。Student是子类,也称作派生类。

 继承关系和访问限定符

 在上述的语法介绍当中我们发现,有继承方式这一个语法,在C++当中的继承规则这一块其实设计还是有点复杂的,如下图所示,根据父类当中的访问限定符有三种,继承方式同样有三种,如下图所示:

在C++当中是以 父类当中访问限定符 和 继承方式两两组合,形成一种新的访问限定,那么总结下来,在继承当中就有 9 种访问方式,如下图所示:

 对于上述 9 种结果,其实是有规则可循的,总结如下

  • 基类的private成员,在派生类当中都是不可见的。而 不可见  是  语法上限制访问(类里和类外都不能使用),而private是类外不能使用,类当中说可以使用的。(父类的私有成员,子类无论以什么方式继承不能用)
  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private取两者小的那一个访问方式,作为最后的访问方式
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected可以看出保护成员限定符是因继承才出现的
  •  C++当中支持默认继承,如果不写继承方式,使用关键字 class默认是私有(private)继承;使用关键字 struct 默认是公有(public)继承。(但是建议写上继承方式)(之所以 class 是私有继承,是防止多次继承对基类的影响。如果继承方式是 protected的,那么子类再被继承,产生的派生类还是可以访问到最开始的基类当中的成员,但是如果基类当中的成员是被private的,那么其子类再被继承而产生的派生类,就不能在访问到基类当中的peivate成员了)
  •  在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

 实际当中只用如下图所示的访问方式:

基类和派生类的赋值转换

 如果是不同类型的内置类型相互赋值,是会发生类型转换的!!

比如:

int a = 1;
double b = 0;b = a;

而类型转换是会产生临时变量来帮助转换的,同样的,基类基类派生出的派生类之间相互赋值也是会发生类型的转换的。

 需要注意的是,基类基类派生出的派生类之间相互赋值所发生的类型转换是有条件的

 只能是 派生类赋值给 基类,不能是 基类赋值给 派生类。

 因为如果是基类赋值给 派生类,那么对于派生类当中 独有信息 是基类当中没有的,基类无法给派生类当中的 独有信息 进行赋值。

 而且对于基类赋值给派生类这一操作,就算给强制类型转换,都是无法使用的语法,这一操作在语法上直接禁掉了

 报错:

 而且,对于基类和派生类的类型转换还和我们之前了解的 隐式类型转换和强制类型转换不一样;后两者在转换的时候,会产生临时变量

 而且 基类 和 派生类的类型类型转换不会产生临时变量,他们之间的类型转换 是 赋值兼容(切割,切片)

 我们认为,一个子类对象一定是一个特殊的父类对象,至少子类对象的当中一定有父类的成员。子类赋值给父类的话,会把子类当中父类的成员切割出来,然后再把这些成员拷贝给父类。

 如果是父类转化为子类的话,单纯的对象之间是不能转换的,但是如果是引用和指针的话还是可以的,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

 验证 赋值兼容 没有产生临时对象

 如下例子:

student s;      ·········1
person& p = s;int a = 1;      ·········2
double& b = a; 

如上所示,我们知道,对于p这个引用,如果是发生在内置类型或者是没有父子关系的对象之间,会发生隐式类型转换,从而产生临时对象,所以如果不是 赋值兼容的 话(如上述例子2),引用应该指向的是 类型转换 所产生的临时变量

临时变量具有常性,上述例子2 的引用 b 没有用const 修饰,就会报错。

而反观例子1 ,就不会报错,说明 例子1 当中没有产生临时变量。

 此时例子1 当中的 引用 是 s 对象的一个别名。

 继承当中的作用域

 对于基类 和 基类派生出的派生类,都是有独自的作用域。所以如果你在基类 和 派生类当中定义出相同名字的成员,这个语法是通过的。如下代码所示:

// 父类
class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};// 子类
class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;}
protected:int _num = 999; // 学号
};

如上代码所示,父类和子类当中都有一个 _num 成员。但是没有报错

 如果我们此时调用 Print()函数,那么打印的 _num 的值是多少呢?

 答案是 999

  •  这是因为,如果在父类和子类当中有同名的成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。在子类成员函数中,可以使用 基类::基类成员 显示访问

显示访问父类成员

	void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << Person::_num << endl;}

 同样的,这样的同名,不仅仅允许在成员,也允许在成员函数,而且同名的成员函数也是和同名的成员一样的,子类会对父类的进行隐藏,默认是调用子类的同名成员函数。当然也可以显示的调用父类的同名成员函数

class Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << Person::_num << endl;}
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份证号:" << Person::_num << endl;cout << " 学号:" << Person::_num << endl;}
protected:int _num = 999; // 学号
};void Test()
{Student s1;s1.Person::Print();
};

 此时的子类当中Print()和父类当中的Print()因为不是在同一作用域,不是构成重载,此时是子类和父类的关系的成员函数满足函数名相同就构成隐藏。

域是查找规则的概念,他规定编译器应该去哪一个域当中去寻找变量或者函数(而且此时的 查找是在编译时期的查找,编译器首先要查找有没有这个变量/函数,没有就要报错。还有一个查找是生成代码之后,更具地址去查找变量/ 函数),所以编译器对于一个域当中没有查找到的变量/函数,他会在下一个域去寻找,而下一个域也是按照顺序来的。比如:子类当中没有的成员,就会在父类当中去寻找

但是即使是允许子类和父类之间使用同名成员,我们也不建议这样 在继承体系当中 去 定义子类和父类当中的成员和成员函数。

 派生类的默认成员函数

 一个类当中有 6 个默认成员函数,其中“默认”的意思就是,我们不写这6个成员函数,编译器会自动生成一个。6 个 当中常用的就是 4 个,如下所示:

 构造函数 和 拷贝构造函数

 派生类 中的 构造函数 当中的初始化列表只能初始化本派生类的成员不能初始化父类当中的成员;但是,在构造函数的定义的当中对 父类当中的成员进行修改是可以

class Person
{
protected:string _name = "小李子"; // 姓名
};class Student : public Person
{Student(const char& name, int num):_name(name),_num(num){_name = "nnn";}
protected:int _num = 999; // 学号
};

如上父类当中的 _name 成员不能再 子类 的 构造函数 的初始化列表当中用,但是可以在构造函数的定义的当中被修改:

再如下例子,我们把父类(Person) 的构造函数 和  析构函数写出来,并打印一下名字,方便给出提示,提示我们哪一个函数被调用了。其次,我们在主函数当中只定义子类对象 (Student)

class Person
{
public:Person(){cout << Person() << endl;}~Person(){cout << ~Person() << endl;}
protected:string _name = "小李子"; // 姓名
};class Student : public Person
{Student(int num):_num(num){}
protected:int _num = 999; // 学号
};int main()
{Student s;
}

打印结果:

 但是打印结果 却 打印的父类的 构造函数 和 析构函数名

 这是因为,C++规定,派生类必须调用 父类的构造函数 去初始化父类的成员,不管在主函数当中有没有定义 父类对象。(这种自动调用,都是调用的默认函数,此时就是默认构造函数,如果我们没有提供默认构造就会报错)

 所以,如果父类没有默认构造函数,我们就要在子类的初始化列表当中,对父类进行初始化(就像定义一个匿名对象一样)如下代码所示:

class Person
{
public:Person(const char& name):_name(name){cout << Person() << endl;}~Person(){cout << ~Person() << endl;}
protected:string _name = "小李子"; // 姓名
};class Student : public Person
{
public:Student(const char& name , int num):_num(num),Person(name){}protected:int _num = 999; // 学号
};

 注意

  • 而且此时是 Person(name)先初始化,_num 后初始化。因为初始化列表 初始化的顺序 不是跟初始化列表的顺序相同的,是和 各个成员声明的顺序相同的
  •  而此时是继承的关系,子类会优先调用 父类 构造函数去初始化父类的 成员,所以会优先 初始化 Person。

 对于拷贝构造函数也是和构造函数是一样的,不能在子类的拷贝构造函数的 初始化列表当中去初始化 父类的 成员;同样会优先调用父类的构造函数,去初始化父类的 成员。

 对于拷贝构造函数的 对父类的初始化,我们也是使用 和之前构造函数一样的 类似于匿名对象的用法来初始化。但是此时就会出现一个问题,父类当中的 拷贝构造函数 的参数是 父类的 类型,而在拷贝构造函数当中,我们只有子类的 类型可以传参此时,不用管这么多,可以直接传入子类。(如下代码所示)

// 父类
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}// 拷贝构造函数,此处传入参数是父类 类型的Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};// 子类
class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s)  // 此时直接传入 子类对象即可, _num(s._num){cout << "Student(const Student& s)" << endl;}~Student(){cout << "~Student()" << endl;}
protected:int _num; //学号
};

因为此处,父类的拷贝构造函数的参数是 引用,如果传入的是 子类的类型对象,会发生 赋值兼容转换,也就是切割,把子类当中父类的成员切割出来,进行赋值。

 赋值操作符重载函数

 对于 赋值操作符重载函数 ,也是一样的,我们把 子类当中 父类的成员部分,和 子类成员部分 都赋值就行:

	Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator =(s);  // 父类当中已经直接 operator= 函数,此处直接调用_num = s._num;}return *this;}

需要注意的是,我们上述调用了父类当中的 operator= 函数,如果我们不显示去调用,像如下方式写的话就会 出现 “隐藏”:

operator =(s);

上述的代码默认调用 子类 当中的 operator= 函数,就会出现递归死循环,栈溢出代码奔溃。

 所以我们要显示调用父类当中的 operator= 函数。

 析构函数

 析构,同样要析构 派生类自己的,还要析构 派生类父类的;析构父类可以直接调用 父类当中的析构函数,但是不能像父类当中其他函数一样直接调用,需要显示指定类域:

~Student()
{Person::~Person();
}

具体原因是因为C++ 当中多态的原因。析构的函数名被进行了特殊处理。由于要构成多态,被统一处理成了 destructor

 因为都被处理成为了 destructor,所以此处就构成了 隐藏 ,所以要显示调用 父类的析构函数

 但是,上述在程序在执行完之后,结果如下:

 在 3 个子类 Student 当中有 3 个 Person,按道理,在最后应该只调用 3 次 Person 析构函数,但是此处调用了 6 次

 其实此处是Person 对象自动调用了 自己的析构函数,因为 整个程序 无论你怎么先,都是先构造父类对象,在构造子类对象(也就是说先构造 Person 在构造 Student)·····    这样总共构造了 3 个Student 对象 和 3 个Person对象;

单看一组,根据 先构造的 后 析构,后构造的先析构,那么 Student 对象就会先析构,然后再析构 Person 对象;但是在 Student 的析构函数 已经调用了 Person对象的 析构函数,就在此处发生了重复析构的问题。

如果我们不在 Student 的 析构函数当中去调用 Person对象的析构函数,就是析构3次,是正确的。

因为在构造的时候,是先父后子的顺序进行构造的,所以在析构的时候,应该满足先子后父的析构顺序,但是在子类析构函数当中显示调用 父类析构函数,是无法保证先子后父的析构顺序,所以编译器在子类析构函数完成之后就,自动调用父类析构函数

其实这里可以不用自动析构,完全可以自己在 子类 Student 的析构函数当中实现上述顺序的 析构顺序。但是C++语法就是有自动调用析构函数,所以我们在 继承体系当中写析构函数的时候,一定要注意这一点,因为多次重复的析构可能会出现问题,比如:重复析构的对象当中有我们自己手动开辟的空间的时候,重复析构就会报错

 为什么在析构的时候要先子后父

 之所以说不能先显示调用父类的析构函数,是因为,在子类的析构函数当中,可能还会用到 父类当中的成员或者成员函数,如果先就析构父类的话,无法调用了,而且在父类当中是不会用到子类当中的成员的,所以我们决定先析构子类,在析构父类。

 继承与有元

有元不能继承!!!

 也就是,爸爸的朋友不是我的朋友,我爸爸的朋友不能去访问我的成员。

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;
}
void main()
{Person p;Student s;Display(p, s);
}

上述有元函数 Display 可以访问父类 Person的 _name ;但是不能访问 子类 Student 的 _stuNum成员。

如果想要 友元函数 Display 也能访问到 子类的 _stuNum 成员,需要在子类当中也加上有元声明。

 继承与静态成员

 如果在父类当中定义了静态成员变量,这个静态成员能不能继承,其实认为可以,和认为不可以都是对的。

在继承当中,子类对象当中都有一个父类对象的存储,相当于是在子类当中构造了一个父类的对象,每一次构造一次子类,都要去调用其父类的构造函数去构造父类出来;但是对于 父类当中的静态成员,究竟是不是 从父类当中拷贝一份到子类当中,我们先看如下例子:

class Person
{
public:Person() { ++_count; }public:static int _count; // 统计人的个数。string _name; // 姓名
};
// 父类当中的 静态成员变量
int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};int main()
{Person p;Student s;Graduate g;// 父类当中的 非静态成员cout << &p._name << endl;cout << &s._name << endl;cout << &g._name << endl;cout << "-----这是一个分界线-----" << endl;// 父类当中的 静态成员cout << &p._count << endl;cout << &s._count << endl;cout << &g._count << endl;return 0;
}

我们把 父类当中 非静态成员 _name 和 静态成员 _count ,在 父类 Person 和 两个子类 Student 和 Graduate 当中打印出地址。

结果输出:

000000EEADF6F508
000000EEADF6F548
000000EEADF6F5A0
-----这是一个分界线-----
00007FF6871B2440
00007FF6871B2440
00007FF6871B2440

我们发现,非静态成员的地址是不一样的,说明在子类当中每一次构造对象时候,都对父类当中的非静态成员进行了拷贝构造;

而 下面的 静态成员的地址是一样的说明后序子类的对父类的构造,并没有对静态成员进行构造,上述的父类和两个子类都是共用一个静态成员变量

 由上述例子,我们得出结论,在父类当中定义的 static 静态成员,则在其整个继承体系当中只有这一个静态成员。而且无论派生出多少个子类,都只会实例化这一个静态成员。

 所以我们才会说,对于父类当中的静态成员,其子类其实是继承了,因为可以在子类当中使用这个静态变量;但是这种继承和其他的继承不一样,他不会去在子类当中拷贝构造一个新的静态成员出来,而是整个体系使用一个几台成员。(静态成员继承的是使用权,静态成员同时属于父类和其派生类)

我们上述的例子,还记录了 Person 这个父类,总计创建了多少个派生类(包括Person自己)。实现也非常简单,就是在构造函数当中 ++_count 这个静态成员变量。利用子类在创建时候会先调用父类的构造函数这一特性,来记录派生类创建的个数

 

复杂的菱形继承及菱形虚拟继承(多继承)

 在现实世界当中,会有一种类别,同时具有两种(多种)类别的特征,也就是同时继承多个父亲。所以在C++当中开发人员也考虑到这种情况的发生,就实现了多继承

 像我们之前的举的例子都是单继承,如下图所示:

 而多继承是一个子类有多个父亲:

 我们发现,这这种多继承在一般情况下,还是非常符合现实世界的,用起来也是没有问题:

class Student
{
protected:int _num; //学号
};class Teacher
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};

上述这个人,既是一个老师,又是一个学生,这种是没有问题的。

但是如果 Assistant 这个人继承的 Student , Teacher 两个父类都继承了一个 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; // 主修课程
};

 上述这种情况,我们称之为菱形继承。他是多继承的一种特殊情况。

这种情况出现的问题不是对person的多次构造和析构,因为两个构造的person对象是存在于Student 和 Teacher 当中的,不会出问题。

真正的问题是对于 Assistant 对象,拥有了多个 Person 类当中的成员属性。如下图所示:

 在一个 Assistant 类当中有了 两个 Person 当中的 _name 这个属性,但是不是每一个人都有多个名字,一般一个人经常使用的只有一个名字;此时这种情况就是数据冗余二义性

 就算你认为一个人可以在不同场景下有两个名字,比如当学生的时候,老师叫你小郑,当老师的时候学生叫你郑老师;其实只是这里的例子太偶然,如果 Person 当中不止一个数据呢?

比如还有 _id 这个成员来表示你的身份证号码,一个人不会有两个身份证号码。

 对于上述的 数据冗余 和 二义性所带来的坑是:

  • 数据冗余:对于存储空间有一定会的浪费。
  • 二义性:在访问的时候不知道要访问哪一个成员。

 上述例子,如果直接使用 Assistant 的对象来访问 _name ,就会报错,编译器不知道要访问那一个 _name 。如下代码所示:

void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个Assistant a ;a._name = "peter";
}

 手动解决二义性(可以显示制定域来访问某一个 _name ,但是 数据同于问题无法解决)

// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";

 只要使用多继承,就有可能会发生菱形继承,因为只要继承了上一个父类,上一个父类的继承关系就会衍生到这一个子类当中。

 而对于单继承就不会出现多次继承某一个父亲的情况:

 当B对象被构造的时候,会先去构造 B 的父类 A,在构造 A 的时候,又会先构造  Person。所以在B当中是没有两个 Person对象的,Person对象只是在 构造A的时候被构造了一次。

 虚继承

 C++为了解决 上述菱形继承所带来啊的二义性的问题,增加了一个功能——虚继承

 语法:
在出现菱形继承的“腰部”,使用 virtual 来修饰继承的父类,修饰位置如下所示:
 

 代码:

class Person
{
public :string _name ; // 姓名
};// virtual 修饰
class Student : virtual public Person
{
protected :int _num ; //学号
};// virtual 修饰
class Teacher : virtual public Person
{
protected :int _id ; // 职工编号
};class Assistant : public Student, public Teacher
{
protected :string _majorCourse ; // 主修课程
};

虚继承的原理

 菱形继承的存储结构

 理解原理之前,我们先来搞清楚菱形继承当中的对象模型,所谓对象模型就是这个对象在内存当中存储的结构。

有下面这个例子:

class A
{
public:int _a;
};
// class B : public A
class B : public A
{
public:int _b;
};
// class C : public A
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;
}

这个例子中当中 A B C D 四个类的继承关系如下图所示:

 上述例子创建了 一个 D类的对象d,然后把d当中的 B类对象和C类对象当中各自的A类对象中的 _a 成员变量进行了修改,然后再把d当中存储 B类对象当中 _b 成员 和 C 类对象中的 _c 成员修改。最后在对d本身的对象进行修改。

我们在在调试当中查看内存,如下所示:

 看似菱形继承关系很复杂,其实在内存当中的存储结构就是依次顺序存储。

 

 菱形虚拟继承的存储结构 

 

class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
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;d._a = 0; // 不显示指定域,直接就修改return 0;
}

还是上述的例子,但是我们在菱形继承的腰部位置加上了 virtual 修饰(虚拟继承)。

内存中存储如下图所示:

 我们发现,菱形继承和菱形虚拟继承不同点在于:对于 A 类(共同继承的父类)的存储空间不在是 B 和 C 当中都有一个了,而是 B C D 三者共用一个 A对象当中的成员。

 菱形继承的存储结构 和 菱形虚拟继承的存储结构 两者的不同

  在使用 virtual 修饰之后(虚拟继承),共同继承的父类不再是分开存储,而是只存储一个,而派生出的子类(或者是派生类派生出的子类)都共用一个 父类对象。从而,不管是在上述哪一个类当中,修改父类对象当中成员,修改的都是一个对象当中的成员。

 而且,不仅仅是这样,我们发现,虚拟继承 比 继承 还多存储了一些东西

 

 我们调用内存窗口,查看这两个地址位置存储了什么(如下两图所示):

 

 在两个地址位置处,存储的都是0,但是在两地址的下一地址处,存储了一些值;第一个是 14,转换为二进制就是 20;第二个是 0c 转化为二进制就是 12;

这里的 20 和 12 代表的就是 在上图 左侧内存块当中,两个多出来的存储地址,距离 A 对象存储地址位置的相对距离(偏移量);这里可以理解为指向 那 两个多出来的存储地址 的两个指针。

 这里之所以使用偏移量这种方式,来代替指针是因为,对于上述例子 D 类的对象可能不会只有一个,虽然可能 不同的 D类对象 当中存储的值不会相同,但是 其中 B类对象 和 C类对象当中对于上述多出的两个地址位置,和D类对象其中的 A类对象 地址位置的偏移量是相同的,所以不需要每创建一次 D类对象,就要把 其中的A类对象的地址保存;只需要使用偏移量来计算出 A类对象的存储位置即可。也就是说所谓的 一表多用。

 

 如上图所示,创建了 d1 对象 和 d2 对象都可以使用 右边已经计算好的偏移量表(内存块)。

 这里使用 虚基表(存找基类偏移量的表) 的 方式来解决 每创建一个对象都需要 存储 一次 A类对象 地址的问题。

 偏移量存储的意义

  还是上述的 菱形继承的结构,对于 d1._a 是不需要用偏移量去寻找 A类对象当中的 _a 成员的,因为 在 D类对象当中,所有的 D类成员 和 继承的所以 A B C 三个类的成员,都是有自己的声明定义顺序的(之前也描述过),编译器找 基类 (A类)的 _a 尘缘不需要利用偏移量,编译器知道 虚拟继承后,A类对象就存储在 D类对象空间的最后

 那么既然用不到,那为什么要定义偏移量呢?

其实上述只是一个普通场景,一些特殊场景下会用到,比如下面这个例子:

D d1;
B* pb = &d1;
pb->_a = 0;

这里的 pb 是一个B类型的指针,所以,就算pb直线的是 d1 对象的开头,他去数据也只能取到 一个 B类对象 大小的数据,而 存储 _a 的A类对象 是在 d1 对象的末尾,直接取肯定取不到 _a 的。这时候,就可以用偏移量来找到 A类对象的位置。

 向上述的 pb 其实发生了 切片,本来 B类对象 数在 d1 这个对象里面的,但是要我指针类型是 B* ,用 pb 访问数据的话,不能把 d1 对象的所以数据都访问完,只能访问到从 d1对象起始位置开头的 B类对象大小的数据,那么其余下面的数据就被切片了。

 也正是有了偏移量的存在,在上述这种情况(切片)发生的时候,就不用因为找不到 A类,来进行特殊处理(如果存储的A类地址的话),使用偏移量,在 pb->_a 访问 _a 的时候,只需要按照地址找到 虚基表,计算偏移量就行。

再如下例子感受:

D d;
d->_a = 1;B b;
b->_a = 2;B* ptr = &b;
ptr->_a++;ptr = &d;
ptr->_a++;

因为 B类 也是被 virtual 修饰的,所以在 B类 当中的对象模型,和之前的 d1 对象模型是一样的,都是共用一个 A类。

但是,我们知道 b 对象 和 d 对象 两个对象当中存储的 A类对象都是在其对象的 末尾处,但是对于 ptr = &d 上述说过是要发生切片的,此时的 ptr 指针还是只能访问 B类对象大小的数据,而A类数据不在其中。ptr指针 不能分辨 它此时 指向的对象是 b 还是 d,也就是说上述的两种情况 ptr 是分辨的,所以都是使用的 偏移量计算 来得出 A类对象的存储地址,而不是直接从指向对象的末尾处找(如下图所示):

 上两处的 ptr->_a++;使用的编译语法都是一样的,都是取 偏移量,计算出 A类对象的地址,然后再对 _a 进行操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/63828.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开发JavaWeb入门项目--十天掌握

原文网址&#xff1a;从零开发JavaWeb入门项目--十天掌握_IT利刃出鞘的博客-CSDN博客 简介 这是一个靠谱的JavaWeb入门项目实战&#xff0c;名字叫蚂蚁爱购。从零开发项目&#xff0c;视频加文档&#xff0c;十天就能学会开发JavaWeb项目&#xff0c;教程路线是&#xff1a;搭…

Docker私有镜像仓库(Harbor)安装

Docker私有镜像仓库(Harbor)安装 1、什么是Harbor Harbor是类似与DockerHub 一样的镜像仓库。Harbor是由VMware公司开源的企业级的Docker Registry管理项目&#xff0c;它包括权限管理(RBAC)、LDAP、日志审核、管理界面、自我注册、镜像复制和中文支持等功能。Docker容器应用的…

transformer源码

1.传统RNN网络 每一层都需要上一层执行完才能执行 1.1 自注意力 在一句话中找到it_指代的是什么&#xff0c;它的上下文语境是什么&#xff1f; self-attetion计算 1.2 multi-header机制 1.3 堆叠多层self-attention&#xff0c;相当于再一次卷积 1.4 位置信息编码 1.5 残…

K8S容器OOM killed排查

背景 数据服务平台南海容器k8s设置的内存上限2GB&#xff0c;多次容器被OOM killed。 启动命令 java -XX:MaxRAMPercentage70.0 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/apps/logs/ ***.jar排查过程 1 当收到实例内存超过95%告警时&#xff0c;把jvm进程堆dump下…

Data Rescue Professional for Mac:专业的数据恢复工具

在数字化时代&#xff0c;我们的生活和工作离不开电脑和存储设备。但是&#xff0c;意外情况时常发生&#xff0c;例如误删除文件、格式化硬盘、病毒攻击等&#xff0c;这些都可能导致重要的数据丢失。面对数据丢失&#xff0c;我们迫切需要一款可靠的数据恢复工具。今天&#…

YOLOv8目标检测实战:TensorRT加速部署(视频教程)

课程链接&#xff1a;https://edu.csdn.net/course/detail/38956 PyTorch版的YOLOv8是先进的高性能实时目标检测方法。 TensorRT是针对英伟达GPU的加速工具。 本课程讲述如何使用TensorRT对YOLOv8目标检测进行加速和部署。 •采用改进后的tensorrtx/yolov8的代码&#xff0c;…

全民健康生活方式行动日,天猫健康联合三诺生物推出“15天持续测糖计划”

糖尿病是全球高发慢性病中患病人数增长最快的疾病&#xff0c;是导致心血管疾病、失明、肾衰竭以及截肢等重大疾病的主要病因之一。目前中国有近1.4亿成人糖尿病患者&#xff0c;科学的血糖监测和健康管理对于糖尿病患者来说至关重要。 在9月1日全民健康生活方式行动日前夕&am…

Homebrew下载安装及使用教程

Homebrew是什么&#xff1f; 简单来说&#xff0c;就是用命令行的形式去管理mac系统的包或软件。 安装命令 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"国内请使用镜像源进行下载 执行上述命令后会要求输入…

工具分享 | PDF文档解析工具PyMuPDF

1 需求描述 最近工作需要从PDF文档中按照章节解析出对应的文本和图片(后续可能还会有表格)&#xff0c;经过调研&#xff0c;找到了一个功能强大的解析工具MuPDF&#xff0c;对应的Python包是PyMuPDF。本篇博客记录使用它来实现具体功能。 官方文档&#xff1a;https://pymupd…

JavaScript 生成 16: 9 宽高比

这篇文章只是对 for 循环一个简单应用&#xff0c;没有什么知识含量。 可以跳过这篇文章。 只是我用来保存一下我的代码&#xff0c;保存在本地我嫌碍眼&#xff0c;总想把他删了。 正文部分 公式&#xff1a;其中 width 表示宽度&#xff0c;height 表示高度 16 9 w i d t…

sql各种注入案例

目录 1.报错注入七大常用函数 1)ST_LatFromGeoHash (mysql>5.7.x) 2)ST_LongFromGeoHash &#xff08;mysql>5.7.x&#xff09; 3)GTID (MySQL > 5.6.X - 显错<200) 3.1 GTID 3.2 函数详解 3.3 注入过程( payload ) 4)ST_Pointfromgeohash (mysql>5.…

【python使用 Pillow 库】缩小|放大图片

当我们处理图像时&#xff0c;有时候需要调整图像的大小以适应特定的需求。本文将介绍如何使用 Python 的 PIL 库&#xff08;Pillow&#xff09;来调整图像的大小&#xff0c;并保存调整后的图像。 环境准备 在开始之前&#xff0c;我们需要安装 Pillow 库。可以使用以下命令…

面试官问我MySQL和MariaDB的联系和区别,这我能不知道?

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

MQTT,如何在SpringBoot中使用MQTT实现消息的订阅和发布

一、MQTT介绍 1.1 什么是MQTT&#xff1f; MQTT&#xff08;Message Queuing Telemetry Transport&#xff0c;消息队列遥测传输协议&#xff09;&#xff0c;是一种基于发布/订阅&#xff08;publish/subscribe&#xff09;模式的“轻量级”通讯协议&#xff0c;该协议构建于…

广场舞音乐制作软件,FL Studio怎么做广场舞音乐

广场舞一直以来都是许多人日常的消遣方式之一&#xff0c;富有节奏感的音乐能够让人沉浸其中&#xff0c;这也说明了音乐的重要性。那么如果我们想自己制作一个广场舞风格的音乐&#xff0c;需要具备哪些条件呢&#xff1f;今天我们就来说一说广场舞音乐制作软件&#xff0c;FL…

大数据专业毕业能从事什么工作

大数据从业领域很宽广&#xff0c;不管是科技领域还是食品产业&#xff0c;零售业等都是需要大数据人才进行大数据的处理&#xff0c;以提供更好的用户体验&#xff0c;优化库存降低成本预测需求。 大数据开发做什么&#xff1f; 大数据开发分两类&#xff0c;编写Hadoop、Spa…

无涯教程-JavaScript - POISSON函数

POISSON函数取代了Excel 2010中的POISSON.DIST函数。 描述 该函数返回泊松分布。泊松分布的常见应用是预测特定时间的事件数。 语法 POISSON(x,mean,cumulative)争论 Argument描述Required/OptionalXThe number of events.RequiredMeanThe expected numeric value.Require…

K8s:一文认知 CRI,OCI,容器运行时,Pod 之间的关系

写在前面 博文内容整体结构为结合 华为云云原生课程 整理而来,部分内容做了补充课程是免费的&#xff0c;有华为云账户就可以看&#xff0c;适合理论认知&#xff0c;感觉很不错。有需要的小伙伴可以看看&#xff0c;链接在文末理解不足小伙伴帮忙指正 对每个人而言&#xff0c…

Linux之web服务器

目录 www简介 常见Web服务程序介绍 服务器主机 主要数据 浏览器 网址及HTTP简介 URL http请求方法 状态码 MIME&#xff08;Multipurpose Internet Mail Extension&#xff09; www服务器的类型 静态网站 动态网站 Apache服务的搭建 Apache的安装 准备工作 htt…

在支付宝中 下载社会保险参保证明 方法

这里 我们打开支付宝 选择 市明中心 然后选择 社保 这里 在社保查询下 找到 个人社会参保证明查询 这里 选择好自己的省市区 文件就会出现在下面了 我们直接点击这个文件进入 下面就会有下载的选项了