继承详解——C++深度学习解析

CSDN 2024-06-29 16:35:02 阅读 70

        前言 : 本节内容是讲解面向对象中三大特性之一的继承。 继承是面向对象程序设计中代码复用的一种重要手段。 通过继承, 我们就可以在原本的代码上进行扩展, 而不是重新写一段代码, 让代码变得冗余。 同时, 继承也体现了面向对象程序设计的层次结构,不同于我们以前经常见到的函数的复用, 继承是层次上的复用, 也可以说是类层次的复用

        注:本节讲述的是c++里面的继承, 适合正在学习c++的友友们进行观看。

        

目录

一.什么是继承

1.1继承体系

1.2定义格式

1.3继承方式

二、继承的作用域问题

三、继承的切片问题

四、派生类的默认成员函数

4.1派生类的构造函数:

4.2派生类的析构函数

4.3派生类的拷贝构造函数

 4.4派生类的赋值重载

五、继承与友元

六、继承与静态成员

七、菱形继承和菱形虚拟继承


一.什么是继承

1.1继承体系

        在继承体系中, 被继承的那个类叫做基类或者父类, 继承来的那个类叫做派生类或者子类

        我们通过一个具体的实例来看一下继承是怎么实现类的复用的:

定义一个继承体系:

#include<iostream>

using namespace std;

#include<vector>

#include<string>

//基类

class Person

{

public:

//构造函数

Person(const string name, const string sex)

:_name(name)

,_sex(sex)

{}

//打印接口

void Print()

{

cout << "姓名" << _name << " 性别:" << _sex << endl;

}

private:

string _name; //姓名

string _sex; //性别

};

//学生类, 子类

class Student : public Person //继承的写法就是 :+ 继承方式 + 继承的基类

{

public:

Student(string name, string sex, int num)

:Person(name, sex) //初始化列表调用基类的构造函数方式

,_num(num)

{}

private:

int _num; //学号

};

//教师类, 子类

class Teacher : public Person //继承的写法就是 :+ 继承方式 + 继承的基类

{

public:

Teacher(string name, string sex, int wages)

:Person(name, sex) //初始化列表调用基类的构造函数方式

,_wages(wages)

{}

private:

int _wages; //工资

};

        在这个继承体系里面, 虽然Student和Teacher表面上只定义了_num 或者_wages。但是,Student和Teacher都继承了Person类。

        而Person类里面定义了_name, _sex,以及外部接口Print(),  那么Student和Teacher里面就会隐式的多出来一份Person类的_name, _sex, 以及它的Print()。 

        当我们进行Studen类或者Teacher类的实例化的时候, 实例化对象里面就会有继承来的成员变量或者外部接口。 如图实例化一个Student对象:

        

 所以, 继承就是: 对于子类或者派生类来说。 父类也就是基类有的, 我也有。 

1.2定义格式

 定义一个继承的格式:

1.3继承方式

        派生类使用不同的继承方式从基类继承来的成员,权限是不同的。下面是根据不同方式派生类继承来的成员的权限图:

        上面的图其实总结下来就是可以分为两类:一类是基类里面的private成员、一类是基类里面的public, protected成员。 

        对于基类里面的private成员。 不管派生类使用什么方式继承, 继承后这些成员都在派生类中不可见。        对于基类里面的非private成员, 看继承方式和这个成员在基类之中的权限哪个小。 这个权限就是派生类中的权限。 比如 student私有继承基类的公有成员。 那么继承后的这个成员就是私有类型。 

        实际上, 我们在实际运用中一般只会用到public继承, 因为private和protected继承来的成员只能在类域中使用, 实际的可维护性不强

二、继承的作用域问题

        在继承体系中。 虽然派生类继承了基类。 但是对于派生类的成员变量和基类的成员变量来说, 都有它们自己的作用域。 而且, 假如一个派生类继承了多个基类, 那么每个基类都有一个属于自己的区域。 这就叫类域。

        类域可以帮助我们解决继承体系中的重名问题。并且如果基类的私有成员虽然是不可见的, 但它在自己的类域中同样可以被访问。      

        如果子类和父类中有同名的成员, 那么父类的成员将在自己的类域中隐藏起来。我们直接访问是访问不到的:

class Person

{

public:

//打印接口

void Print()

{

cout << "姓名" << _name << " 性别:" << _sex << endl;

}

private:

string _name = "张三"; //姓名

string _sex = "男"; //性别

};

class Student : public Person

{

public:

void Print()

{

cout << "学号: " << _num << endl;

}

private:

int _num = 111; //学号

};

int main()

{

Student stu;

stu.Print();

}

        这里要注意的是:成员函数只要函数名相同, 就会构成隐藏

想要访问基类中隐藏在类域中的成员, 那么就要利用域操作符显示的调用:

成员变量的隐藏:

        如果是继承多个父类, 并且父类之间有函数名或者成员变量重名, 那么编译器就会显示目标不明确, 无法编译:

//基类1

class Person1

{

public:

//打印接口

void Print()

{

cout << "1:" << "姓名" << _name << " 性别:" << _sex << endl;

}

string _name = "张三"; //姓名

string _sex = "男"; //性别

int _num = 11;

};

//基类2

class Person2

{

public:

//打印接口

void Print()

{

cout << "2:" << "姓名" << _name << " 性别:" << _sex << endl;

}

string _name = "李四"; //姓名

string _sex = "男"; //性别

int _num = 22;

};

//基类3

class Person3

{

public:

//打印接口

void Print()

{

cout << "3:" << "姓名" << _name << " 性别:" << _sex << endl;

}

string _name = "王五"; //姓名

string _sex = "男"; //性别

int _num = 33;

};

class Student : public Person1, public Person2, public Person3

{

public:

};

int main()

{

Student stu;

stu.Print();

}

        想要成功编译就要指定是调用哪个父类接口:

三、继承的切片问题

        派生类和父类的关系就是:父类有的, 派生类也有。 

        并且, 派生类可以给父类进行赋值。 这个赋值操作,有一个形象的叫法叫做 切片

        

        使用派生类给父类赋值、 父类的指针指向派生类, 父类的引用引用派生类。 这些都是切片操作。 (注意, 不能派生类的指针或者引用指向父类, 否则不安全, 容易发生野指针。

        

四、派生类的默认成员函数

对于一个类来说, 有六个默认成员函数:

默认构造函数 : 对于内置类型不做处理, 对自定义类型去调用它的默认构造析构函数 : 对于内置类型不做处理, 对于自定义类型去调用它的析构函数默认拷贝构造 : 对于内置类型进行浅拷贝, 对于自定义类型去调用它的拷贝构造默认赋值重载 : 对于内置类型进行浅拷贝, 对于自定义类型去调用它的赋值重载移动构造函数 :移动构造是c++11新增, 涉及右值引用知识点。本篇不会讲到移动赋值重载 :移动赋值重载是c++11新增, 涉及右值引用知识点, 本篇不会讲到

4.1派生类的构造函数:

        对于一个普通的类, 我们只有内置类型和自定义类型。如果有自定义类型,这个自定义类型是在初始化列表进行初始化,并且我们可以显示的调用该自定义类型的构造函数, 也可以隐式的调用该自定义类型的构造函数(当隐式调用时, 自定义类型必须有默认构造。        如果是在继承体系中的话, 基类部分的初始化同样是在初始化列表部分。 并且我们同样可以选择显示的调用该自定义类型的构造函数, 或者隐式的调用该自定义类型的构造函数(当隐式调用时, 基类中必须有默认构造)。

class Person

{

public:

//构造函数

Person(string name, string sex)

:_name(name)

,_sex(sex)

{

cout << "Person()" << endl;

}

string _name = "张三"; //姓名

string _sex = "男"; //性别

int _num = 11;

};

class Student : public Person

{

public:

Student(string name, string sex, int num)

:_num(num)

,Person(name, sex)

{

cout << "Student()" << endl;

}

int _num = 111; //学号

};

int main()

{

Student stu("李四", "男", 111);

}

 

这里需要注意的是 :初始化列表的初始化顺序是:先初始化基类, 再初始化自定义类型, 最后初始化内置类型。 而对于基类、 自定义类型、 内置类型, 会按照他们的声明顺序进行初始化。

4.2派生类的析构函数

        派生类的析构函数是先析构派生类对象, 再析构基类的对象:

4.3派生类的拷贝构造函数

class Person

{

public:

//构造函数

Person(string name, string sex)

:_name(name)

,_sex(sex)

{

cout << "Person()" << endl;

}

//拷贝构造

Person(const Person& per)

{

cout << "const Person&()" << endl;

}

string _name = "张三"; //姓名

string _sex = "男"; //性别

int _num = 11;

};

class Student : public Person

{

public:

//构造函数

Student(string name, string sex, int num)

:_num(num)

,Person(name, sex)

{

cout << "Student()" << endl;

}

//拷贝构造

Student(const Student& stu)

:Person(stu)

,_num(stu._num)

{

cout << "const Student&()" << endl;

}

int _num = 111; //学号

};

int main()

{

Student stu("李四", "男", 111);

Student st(stu);

}

 

 4.4派生类的赋值重载

        派生类的赋值重载, 必须调用基类的赋值重载完成复制。

class Person

{

public:

//构造函数

Person(string name, string sex)

:_name(name)

,_sex(sex)

{

cout << "Person()" << endl;

}

//赋值重载

void operator=(const Person& per)

{

cout << "operator=()-Person" << endl;

}

string _name = "张三"; //姓名

string _sex = "男"; //性别

int _num = 11;

};

class Student : public Person

{

public:

//构造函数

Student(string name = "张三", string sex = "男", int num = 111)

:_num(num)

,Person(name, sex)

{

cout << "Student()" << endl;

}

//赋值重载

void operator=(const Student& stu)

{

Person::operator=(stu);//调用stu的

cout << "operator=()-Student" << endl;

}

int _num = 111; //学号

};

int main()

{

Student stu("张三", "男", 44);

Student st;

st = stu;

}

五、继承与友元

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

        如下为示例:

class Person

{

public:

friend void Display(const Person& per);

//构造函数

Person(string name, string sex)

:_name(name)

,_sex(sex)

{

cout << "Person()" << endl;

}

private:

string _name = "张三"; //姓名

string _sex = "男"; //性别

int _num = 11;

};

class Student : public Person

{

public:

//构造函数

Student(string name = "张三", string sex = "男", int num = 111)

:_num(num)

,Person(name, sex)

{

cout << "Student()" << endl;

}

private:

int _num = 111; //学号

};

void Display(const Student& stu)

{

cout << "姓名: " << stu._name << " " << "性别: " << stu._sex << "学号: " << endl;

}

int main()

{

Student stu("张三", "男", 44);

Person st = stu;

}

如图, 虽然我们已经在基类中设置了友元。但友元不能继承, 我们在DIsplay函数并不能访问Student中的成员变量。 

六、继承与静态成员

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

class Person

{

public:

//构造函数

Person(string name, string sex)

:_name(name)

,_sex(sex)

{

cout << "Person()" << endl;

}

string _name = "张三"; //姓名

string _sex = "男"; //性别

static int sum; //定义的静态变量

};

int Person::sum = 100; //静态变量初始化

class Student : public Person

{

public:

//构造函数

Student(string name = "张三", string sex = "男", int num = 111)

:_num(num)

,Person(name, sex)

{

cout << "Student()" << endl;

}

int _num = 111; //学号

};

int main()

{

Student stu("张三", "男", 44);

Person st = stu;

cout << Person::sum << endl;

}

七、菱形继承和菱形虚拟继承

        我们先了解一下两个概念之后再了解菱形继承的概念, 这两个概念一个叫单继承, 另一个叫多继承。

        一个派生类只继承了一个基类, 那么这就是一个单继承:

class A

{

public:

A()

{

cout << "A()" << endl;

}

};

//B类继承A

class B : public A

{

public:

B()

{

cout << "B()" << endl;

}

};

//C类继承B

class C : public B

{

public:

C()

{

cout << "C()" << endl;

}

};

 

如果一个派生类继承了多个基类, 那么这就是一个多继承: 

class A

{

public:

A()

{

cout << "A()" << endl;

}

};

//B类继承A

class B

{

public:

B()

{

cout << "B()" << endl;

}

};

//C类继承B

class C

{

public:

C()

{

cout << "C()" << endl;

}

};

class D : public A, public B, public C

{

public:

D()

{

cout << "D()" << endl;

}

};

菱形继承时上面多继承的一种情况, 下面为一个菱形继承的代码:

//基类A

class A

{

public:

A()

{

cout << "A()" << endl;

}

};

//B类继承A

class B : public A

{

public:

B()

{

cout << "B()" << endl;

}

};

//C类继承B

class C : public A

{

public:

C()

{

cout << "C()" << endl;

}

};

class D : public B, public C

{

public:

D()

{

cout << "D()" << endl;

}

};

 

如果我们给A类一个成员变量_a:

那么D类就继承来了两份_a, 如图:

在这里利用代码演示一下:

class A

{

public:

A()

{

cout << "A()" << endl;

}

int _a; //定义一个变量_a

};

//B类继承A

class B : public A //B继承A后, 有一份_a

{

public:

B()

{

cout << "B()" << endl;

}

};

//C类继承B

class C : public A //C继承A后, 有一份_a

{

public:

C()

{

cout << "C()" << endl;

}

};

class D : public B, public C //D继承B, C类, 这样D中就有了两份_a。

{

public:

D()

{

cout << "D()" << endl;

}

};

vs中变量d的监视窗口:

        由上面可以看出菱形继承具有数据冗余和二义性的问题。 

        为了解决这个问题, 祖师爷发明了虚拟继承。虚拟继承可以解决二义性和数据冗余的问题。但是使用虚拟继承的地方必须用对, 虚拟继承一般用在”腰间“比如将B类和C类继承A类的方式改成虚拟继承, 那么就可以解决问题。

---------------------------

接下来就是比较偏向底层的知识。很重要!很重要!很重要!

---------------------------

下面是我定义的一个菱形继承体系:

class A

{

public:

A()

{

cout << "A()" << endl;

}

int _a; //定义一个变量_a

};

//B类继承A

class B : public A //B继承A后, 有一份_a

{

public:

B()

{

cout << "B()" << endl;

}

int _b;

};

//C类继承B

class C : public A //C继承A后, 有一份_a

{

public:

C()

{

cout << "C()" << endl;

}

int _c;

};

class D : public B, public C //D继承B, C类, 这样D中就有了两份_a。

{

public:

D()

{

cout << "D()" << endl;

}

int _d;

};

 我现在定义一个D类型的实例化对象:

从上面的内存图我们就能比较真实的看到d1的底层存储情况。并且能够看到_a的重复存储。(0x11就是_a)

那么看一下菱形虚拟继承在底层是如何解决这个问题的:

class A

{

public:

A()

{

cout << "A()" << endl;

}

int _a = 0x11; //定义一个变量_a

};

//B类继承A

class B : virtual public A //B继承A后, 有一份_a

{

public:

B()

{

cout << "B()" << endl;

}

int _b = 0x22;

};

//C类继承B

class C : virtual public A //C继承A后, 有一份_a

{

public:

C()

{

cout << "C()" << endl;

}

int _c = 0x33;

};

//菱形虚拟继承

class D : public B, public C //D继承B, C类, 这样D中就有了两份_a。

{

public:

D()

{

cout << "D()" << endl;

}

int _d = 0x44;

};

现在我定义d2, 并观察内存图:

         图中我们可以观察到虽然菱形继承的问题被解决了, 但是在B类和C类中出现了一些 ”多余“

的东西。 

        但是, 这些”多余的东西“其实并不多余, 它们是一个叫做虚基表的指针。 通过这两个框框中存的地址, 就可以找到B类或者C类的虚基表

        那么什么是虚基表? 虚基表就是用来寻找虚继承来的数据域的。 对于B类和C类来说, 就是来寻找A类的。 虚基表里面存放的是一个偏移量, 通过这个偏移量, 我们就可以找到A的数据域。 如下图:

        我们先看B类的虚基表, 也就是绿框框的虚基表。 这个虚基表中存的是0x14, 那么转化为10进制就是20。我们可以看到, 从图中画黄色四角星的位置到A的数据域正好是20个字节偏移量。 

        接下来我们再看C类的虚基表, 也就是蓝框框的虚基表, 这个虚基表中村的是0x0c, 也就是12, 我们可以看到, 从图中画绿色四角形的位置到A的类域正好是12个字节的偏移量。 

        通过以上分析我们知道了菱形虚拟继承如何解决的数据冗余和二义性问题。 但是, 为什么要引入虚基表呢? D类中的B, C类数据域为什么要找到A类数据域呢?

        当我们使用切片的时候,B, C类就要去寻找A类的数据域。

int main()

{

D d2;

//切片,b中有_b, _a。 所以要找到d2中的B类数据域和A类数据域。

B b = d2;

//同上。

C c = d2;

}

 

----------------------

以上, 就是本节的全部内容。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。