文章目录
继承
继承是类设计层次的复用
继承方式与访问限定符
限定了啥?
1.根据表中我们可以看到 基类的私有成员在子类不可见,但还是被继承了下来
2.根据继承方式和成员在基类的访问限定符小的那个来决定了子类访问基类成员的访问方式
例如如果是public继承,那么基类中protected成员继承到子类中访问限定符就是protected(类外不可访问,类内可以访问)
因为private和protected在类和对象阶段他们没什么区别,都是类内可以访问,类外不可访问,但是到了继承这里,private成员对子类的不可见(类内类外不可访问),导致proteced的出现让子类能继承到父类的成员并且只在类内访问
继承后的子类是否将成员变量拷贝一份?
是的,他们互相独立
但单只成员变量,不是指的成员函数,成员函数在代码段(常量区)
切片
子类是可以赋值给父类的,称为向上转换
并且这种转换是天然支持的,而且不生成临时空间
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; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
ps = st;发生了什么?
st把自己父类的部分拷贝给给了ps,并且没有开临时空间
回顾
person& rp = st;
这句没加const即可证明没开临时空间
rp是直接引用student的一部分,这样的话随着rp的改变,子类也会被改变
如果是指针的话,同样只想子类中父类的那一部分
我们可以看到用指针引用修改对象,他们父类部分都会跟着改变
向下转换
对象的转换都是不可以的
但如果是本身就指向子类的父类指针,再把这个父类指针转换回子类是可以的
隐藏/重定义
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
成员函数和成员变量都会发生隐藏
这种隐藏遵循了局部域优先,子类里面找到了就不会去父类里找
// 两个fun构成什么关系?
// a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错
// 答案:a (父子类域中,成员函数名相同就构成隐藏)
重载是在同一作用域中利用函数名修饰规则来区分不同的函数,如果没有函数名修饰规则的话就无法区分。
隐藏是在不同的类域中直接就可以区分出来不同函数。
总结:
尽量不要使用隐藏,搞出同名成员
如果隐藏了只能指定类域访问成员
成员变量名字相同,类型不同也是构成隐藏的
派生类的默认成员函数
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
{
构造函数
先父后子
C++规定了子类的构造必须调用父类的构造函数初始化父类的成员(在初始化列表调用)
他把父类整体当成一个对象成员来处理,父类的成员交给父类的构造,派生类初始化自己的成员
初始化列表 初始化顺序如图下所示,它会先初始化父类的成员再初始化子类自己的成员!
编译器规定你不能在初始化列表显示初始化基类成员
拷贝构造
同样禁止直接初始化父类
需要注意的是调用父类拷贝构造需要一个父类对象的引用,这里没有,但是直接传s就可以,因为会发生切片,把s1的父类部分引用给s2,那么拷贝构造就正常走了
注意如果不写Person(s),那么默认构造调用父类的默认构造,与预期拷贝s的父类不符
理念就是父亲干父亲的活,孩子干孩子的活
赋值运算符重载
如果直接是operator=,这和父类的operator=发生了隐藏,这里只能调用到子类自己的operator=,导致重复自我调用,栈溢出
所以需要用Person::operator=来解决隐藏,调用父类的operator=,再赋值子类自己的成员
析构函数
为什么要先子后父?
1、构造是先构造父再构造子,根据栈的顺序,要先析构子再析构父
2、涉及子类使用了父类成员,就必须先析构子类再析构父类
父类的析构函数编译器自动帮助我们调用了,那子类的析构函数中就析构子类的动态申请的空间就可以了
涉及了多态导致析构函数名都被统一处理destructor,子类和父类的析构函数发生了隐藏
菱形继承
class Person
{
public:
string _name; // 姓名
int _age;
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
菱形继承的问题 - 数据冗余 和 二义性
数据冗余的本质是浪费空间(一个助手会有两个名字,两个年龄)
二义性是指的我们不知道要访问哪一个,访问谁不确定,如果你想去访问助手的年龄而又不加上老师或者学生的域名,编译器都不知道你要访问哪一个,如下图所示
二义性可以用指定类域来解决,虽然解决了但是看起来还是不合理,为什么我要有两个年龄呢?而且冗余的问题没有虚拟继承解决不了
我们只需要一份身份证数据,一份地址
只要有公共的部分就是菱形继承
D里面会有2份a,菱形继承主要是有数据冗余二义性
菱形虚拟继承
在腰部位置加上virtual,变成虚继承,让_age只有一份,就可以解决数据冗余二义性
先看看普通的菱形继承
这是普通的菱形继承
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
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;
}
不要认为d里面有个B在包含,监视窗口只是方便看
在内存上d对象的成员是连续的
如上的继承例子我还要说一下,就是D继承了B,C,那么D里面会有几个A的_a呢?
D只继承了B,C的话,B和C里面各有一份_a,D并没有继承A,那么D里面就只有2个_a。
有些时候会想不明白感觉好像有好几个_a一样,是不对的
菱形虚拟继承内存分布
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;
d._a = 0;
return 0;
}
监视窗口看起来有三份,已经不准了
我们还是看内存窗口
它利用了虚基表存储了A的偏移量,让A的成员变量始终只有一份
这时候问题就来了,为什么不直接把偏移量保存到D的内存模型里面呢?非要搞个间接的虚基表呢?
1、保存这个偏移量需要空间,图上是4字节,A里面可能不止一个_a,可能会继承很多个成员变量,都保存导致D的对象大小变大
2、这个D对象可能有很多个,如果都在D里面保存偏移量或者是指向同一个_a的指针,都是需要空间的,我们有了虚基表保存它的偏移量,就可以让每个D的对象都根据这一张表找到_a的偏移!只需要保存指针就够了!
那问题又来了
那存储这个偏移量有什么意义呢?你想我不就是为了找到同一个_a的地址吗,还能有啥其他意义呢?
有!对于D对象 d1来说,它访问_a直接就能找到_a去修改(类内的成员),但是对于B类型的指针,也就是
发生切片时,用B对象的指针去修改_a的时候这个偏移量就有用了!
有一些新的场景就又有问题了!
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._a = 1;
B b;
b._a = 2;
b._b = 3;
B* ptr = &b;
ptr->_a++;
ptr = &d;
ptr->_a++;
return 0;
}
值得注意的是,B对象在虚继承后也发生了变化,正常我们以为B里面只有_a和_b,但是实际上B对象也变了,B和D要保持一样的对象模型,这样的话处理起来才方便。b里面同样是保存了_a的偏移量!
B* ptr = &b;
ptr->_a++;
ptr = &d;
ptr->_a++;
B* ptr 作为一个基类指针,它既可以指向B,也可以指向派生类D。
B* ptr = &b 时,B根据变化后新增的_a的偏移量(这个_a是B b对象里面的_a)去找到_a
当B* ptr指向D时,发生切片,对于B* ptr来讲指向的还是一个B对象,所以它不知道指向B还是D,假如单看 ptr -> _a++这一句代码,它到底指向B还是D是不确定的,你到底能从是直接从B里面去取_a的地址呢还是发生切片后需要根据偏移量来找,把B也变成和D相同的模型后就不需要进行区分了。
所以有了虚基表和偏移量,不管ptr指向谁,我都按照偏移量来修改成员变量,D里面也有_a的偏移量,那么就去找就可以了
图中关于虚基表中预留位置00 00 00 00 是给谁预留的呢?
1、A类中不止_a一个成员变量,可能有很多个 但是我看内存并没有在虚基表里添加上。
2、是给A类的父类AA预留的,具体的继承结构图是这样,这是在虚表里真的加上了
class AA
{
public:
int _aa=7;
};
class A : virtual public AA
{
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;
d._a = 0;
return 0;
}
收益
菱形继承大小 20 菱形虚拟继承大小 24
我们看到菱形虚拟继承反而大于菱形继承
此时花费了2个4字节指针,使得原来2个_a变成1个_a,收益是4,这肯定亏了,原因在于A对象成员大小不够大
我们将A对象成员改成数组,这样在菱形虚拟继承时,只有一份数组,而不是两份数组,D对象大小减少大概一半
继承总结与反思
菱形继承会有性能损失
搞出菱形继承就需要菱形虚拟继承,在写构造函数时就尤为复杂
多继承谨慎使用,避免搞出菱形继承
继承的耦合度更高,组合相对而言依赖关系更低,就像全包旅游团和半包旅游团一样。
使用中更符合继承就用继承,符合组合就用组合
理解派生类对象构成图
D先继承谁,谁就在上面,这里先继承的B,则B在C的上面。
来看看这题吧,一开始我居然以为是p1 = p2 = p3 ,既然错了那就把它把他改了就完了
这里p2发生切片只看到Base2的那一部分,所以说 p1 = p3 != p2
再来看看这题吧,A到底初始化几次?菱形虚拟继承保证了A对象只有一份,那你凭什么初始化多次呢?A不再B和C中,则B和C不走A的构造,只在D里面走A的构造。
还有一个问题,如果B和C里面的A不走构造,那么把B和C中A(s1)不写行不行?
不行,如果单独创建B对象,那么你的A不初始化吗?
如果给了A的默认构造,那么就可以不写,但是还是走了初始化列表初始化A
所以这里牵扯出来菱形虚拟继承的一个问题,那就是构造函数很复杂。
比如说D继承了B和C,那走初始化列表时,你写不写B和C和A的初始化构造函数,因为规定了子类要调用父类的构造进行初始化,那你不给默认构造你就得写。