![](https://i-blog.csdnimg.cn/blog_migrate/f516dd38a6aa6ed18d80478acfc32f86.jpeg)
"你在酒杯还未干的时间里,收藏这份情谊"
一、回顾继承
什么是继承?
继承是面向对象编程语言的三大特征之一。通过继承机制,面向对象的程序设计可以很大限度地对代码进行复用。
它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
继承 呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继承是类设计层次的复用。
C++中继承定义格式/继承关系与访问限定符
![](https://i-blog.csdnimg.cn/blog_migrate/cdff4343af4089b99d9dbabccdae1316.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8a4346c2af3f9807481b89b8eb90321e.png)
二、切片对象vs临时对象
我们先来看看下面的代码。
int main()
{
double i = 2.3;
int j = i;
cout << j << endl;
return 0;
}
初始化一个int类型的变量j,但是我们用的不是同一类型的其他变量i。事实上,不同类型的变量是不能进行这样的操作的,因此,为了保证可行性,这里会发生“隐式类型转换”。不是将i的值赋值给j,而是在这个过程中产生一个临时变量 ,先将i的值给临时变量,再由临时变量赋值给j。
口说无凭,我们来看看下面的现象。临时变量具有常量性,因此不是不能用int&,而是需要将"权限"缩小 。
![](https://i-blog.csdnimg.cn/blog_migrate/034a8ddb3e6300ae31103dbdd8e0e07d.png)
那么上面提到了,当不同类型进行赋值的时候,是会发生隐式类型转换的。那么我回到继承上来。这时我们想用父类对象引用子类对象。
int main()
{
Student s;
Person p = s;
Person& rp = s;
return 0;
}
当父类对象引用子类时,并不需要+const修饰。也就意味着,编译器认为这种情况不同于上述发生隐式类型转换的条件。
"父类对象引用子类,不会发生隐式类型转换,也就不会生成临时变量。"
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
三、基类析构函数
我们先来回顾类里的6个重要的默认成员函数。
![](https://i-blog.csdnimg.cn/blog_migrate/f854d8b0fc37bd7dbc399477f60204b7.png)
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
![](https://i-blog.csdnimg.cn/blog_migrate/eef1375d4db0372b5c5152fc4b427e52.png)
我们定义Student s,那么它不仅仅会去调用它自身的构造函数,还会去调用继承的父类对象部分的构造函数进行初始化。其次,当析构的时候,先调用自己的析构函数,完成清理本类的资源。
我们没有调用析构,为什么会自动调用父类的析构函数呢?我们能否手动调用父类的析构?
![](https://i-blog.csdnimg.cn/blog_migrate/9fcff5441c47409f56c282e3745cceea.png)
这两个函数析构在我们看来不是 不同吗?为什么需要指定类域?难道构成隐藏了吗?
这个知识牵涉到后面多态在析构函数的处理。
不管是Student的析构函数、还是Person中的析构函数。在编译器看来,最后都会被特殊处理成destructor函数名。
什么时候调用父类的析构函数?
在本段的开头,我们就发现,当定义一个派生类时,即便我们在该类的析构函数中没有显式调用其基类的析构函数,但是最后打印出来却是调用了基类的析构函数。
当子类调用析构后,会自动调用父类的析构函数。
![](https://i-blog.csdnimg.cn/blog_migrate/8bfe1c67162854192d67e9f908756cf8.png)
四、友元关系不能继承
也就是说基类友元不能访问子类私有和保护成员。
![](https://i-blog.csdnimg.cn/blog_migrate/93ba02128be22b02ba077c4fb2138cde.png)
五、解引用/静态成员变量
在开讲本段之前,我们先来回顾类成员你的存储方式。
(1)类对象的存储方式
类体中的成员分为两类,类变量与类方法(函数)。
![](https://i-blog.csdnimg.cn/blog_migrate/d5c4507c6dd88277116a2f0ee960bf99.png)
上述结果清晰地告知我们,一个类的大小,取决于成员变量的大小。
而类里的方法(函数),是被放在一个公共的代码段。
(3)静态成员变量
我们来看看下面的代码段;
class Person
{
public:
Person() { }
void Print()
{
cout << this << endl;
}
public:
static int _count; // 统计人的个数。
string _name;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
此时基类中有一个静态变量count,那么student继承下去后,会生成一份新的count吗?
![](https://i-blog.csdnimg.cn/blog_migrate/95992996cbdc99020bda32965556315e.png)
答案是不会!静态成员变量在基类,在派生类中也是同一个。"静态变量属于整个类"。
(3)nullptr解引用
Person* ptr = nullptr;
ptr->Print();
cout << ptr->_name << endl;
看完上面的一份代码,是否觉得都会奔溃?
(*ptr).Print();
cout << (*ptr)._name << endl;
![](https://i-blog.csdnimg.cn/blog_migrate/4d35ab7e71d53bfb42f3f67e53e191bd.png)
要理解这个点和类成员的存储方式十分密切。解引用的本质是,访问地址处的类型大小的字节。当我们用ptr->Print()是在解引用吗?当然不是!因为并非是在访问类成员变量,而是直接访问的是类成员方法,这些方法早不在类的大小里!
这也就是为什么ptr->name \ (*ptr).name 才会访问出错,因为name是类成员的变量!此时解引用是对空指针的解引用。
![](https://i-blog.csdnimg.cn/blog_migrate/38ba2e255725db3fc8c69411a5b210de.png)
当我们再在print中打印_name时,此时this就是空指针(ptr->name),所以也就 成了对空指针的解引用。我们用ptr->Print()调用时,只传了一个值给Print()函数,那就是this(nullptr)。
六、菱形继承
(1)多继承
像一个子类只有一个直接父类时称这个继承关系为:单继承。
![](https://i-blog.csdnimg.cn/blog_migrate/be508bb3221e7dd17553c5e0486b1c25.png)
像一个子类有两个或以上直接父类时称这个继承关系为:多继承。
![](https://i-blog.csdnimg.cn/blog_migrate/a1f3707129f6d23bf32acc78614d62fb.png)
(2)二义性与数据冗余
C++设计继承有一个很大的坑,就是支持了菱形继承。那么什么是菱形继承呢?我们来看看这模型。
![](https://i-blog.csdnimg.cn/blog_migrate/f4453c6eefa4593492ff46dd175e0d28.png)
这是一种特殊的多继承情况。
也许仅凭图中的模型,不会让你对这菱形继承望而生畏或者攥紧拳头,我们简单地设计一套继承体系。
class Person
{
public:
string _name; // 姓名
// id 家庭住址 身份证号码
};
class Student:public Person
{
protected:
int _num; //学号
};
class Teacher:public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
这是一个人,但是他有"学生+老师"的双重身份。
![](https://i-blog.csdnimg.cn/blog_migrate/75fbf4f22b9bfb63b1a89d3e486cb1d4.png)
我们定义对象Assistant,此时我们想要给这个对象的内部成员name初始化。但是无法直接进行初始化!因为继承的缘故,_name不仅仅在Student有一份,Teacher中也有一份,从而导致了这样的"二义性"问题!因此,我们不得不指名类域,初始化我们想要的值。
但是,一个人是否仅仅需要一个名字就可以了(在当前这个条件下)?当他是学生的时候有一个名字,当他是老师的名字又有另外一个名字?显然这是很不符合常理的。也许你会说,哎呀一个名字就几个字节大小罢了,多了的那份变量似乎显得不过尔尔。但是如果你发现的这份变量是一个100字节、1000字节甚至更多呢?本来我们仅仅需要一份代码不过100byte,却因为菱形继承,足足增加到了200byte大小却是一模一样的类型。 这种 情况,也叫做"数据冗余"。
(3)虚继承
![](https://i-blog.csdnimg.cn/blog_migrate/daa88d96ab97b3f48666e23f507dcce8.png)
当我们翻看C++的发展史,不难发现,当支持多继承后,肯定会出现菱形继承这样的不好的场景。为此,后一个版本C++又为解决多继承产生的二义性问题 增添了一个关键字virtual。
我们先来看看没有virtual时的菱形继承;
![](https://i-blog.csdnimg.cn/blog_migrate/e0eda51c9ff7c44398019ae3470f142b.png)
从内存角度来看,很清楚地看到d继承的各个类变量的分布情况。此时,有两个地址指向同一份int a基类。
virual继承
class Base
{}
//virtual加在腰部类上
class Derived1:virtual public Base
{}
class Derived2:virtual public Base
{}
class Example:public Derived1,public Derived2
{}
![](https://i-blog.csdnimg.cn/blog_migrate/5898341b6bec21b01e7f37b89c277312.png)
当加上virtual虚继承后,本来两份的a变成了一份。最先被初始化为4的a,后来被覆盖成了5。但是,我们却在d对象里发现两份像地址一样的数字。
![](https://i-blog.csdnimg.cn/blog_migrate/1b3412169b1a9c7f2253405c5f754f9c.png)
我们找到地址处,得到它们的内容,其实记录的是从该位置到变量a的偏移量。
同样,当变为虚继承时,存储的方式也会发生变化,我们来看看子类B。
![](https://i-blog.csdnimg.cn/blog_migrate/3b567b73439a9d0b938275bddbb8db7c.png)
这里是通过了B和C的两个指针,指向的一张表。 这两个指针叫虚基表指针 ,这两个表叫虚基表。虚 基表中存的偏移量 。通过偏移量可以找到下面的A。
为什么存储偏移量;
通过vs的内存查询可以看到,对于虚继承的共有成员,都是放在末尾的,那么往后面遍历即可,为什么非要存储偏移量?因为存储 这个偏移量就花费4字节,是否造成浪费了呢?
![](https://i-blog.csdnimg.cn/blog_migrate/34c7c7de59f84eee71306230ce8126ce.png)
答案肯定不是!一旦继承是虚继承后,B对象、D对象的存储结构都发生了转变。都存储与base成员
变量的偏移量以便访问。
七、继承vs组合
因为C++支持多继承,难免会遇到设计挫的代码中,会遇到菱形继承。有了菱形继承,就有了菱形虚拟继承,不管是学习上还是使用都变得尤为复杂恼人。因此,对于有些场景,不适用继承的类设计,而是适合"组合"这一类设计的方式。
继承是一种 is a 关系 ; 而组合是一种has a关系。
譬如吧,
狗是一种动物 is-a关系更加地清晰贴合。
狗有两双腿 是一个has-a的关系。
class Car 宝马奔驰 是车
{
protected:
string _color;
string _type;
}
class BMW:public Car
{
public:
void Drive(){}
}
class Benz:public Car
{
public:
void Drive(){}
}
class Tire //此时这里车有轮胎 是一个has-a关系
{
public:
string brand; //品牌
size_t size; //大小
}
class Car
{
public:
Tire _tire;
string color;
string _type;
}
组合和继承都是针对不同场景,有不同适应的类设计方法。在继承方式中,基于protected继承下来的成员变量可供子类访问,但是在组合方式中,protected限定的成员变量,却对组合对象而言不可见。
继承一定程度上破坏了基类的封装,基类的改变很大程度上会影响派生类的实现。从而使得继承关系是一种依赖关系很强的设计。耦合度高。
相反,组合类中没有很强的依赖性,因为对象的内部细节是不可见的,不管你怎样改,我不能访问到你的东西始终这些东西不会影响到我。从而使得组合关系是一种依赖性不高的设计,耦合度低。
如果你对类的封装性很严谨,不妨试试组合吧!
总结:
①父类对象引用子类 不产生临时变量。不是发送隐式类型转换。
②当子类析构调用完成后,会自动调用父类的析构函数。
③友元关系不能继承
④类的静态成员属于整个类,类对象指针解引用并不全都是"解引用"。
⑤菱形继承是不好的,如果实在遇到菱形继承,为避免代码冗余和二义性,应当使用virtual虚继承。
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~
![](https://i-blog.csdnimg.cn/blog_migrate/d8ca7a1bf88e932aee81b5b20e99c2ce.jpeg)