继承-菱形继承

文章目录

    • 继承
    • 继承方式与访问限定符
      • 限定了啥?
      • 继承后的子类是否将成员变量拷贝一份?
    • 切片
      • 向下转换
    • 隐藏
    • 派生类的默认成员函数
      • 构造函数
      • 拷贝构造
      • 赋值运算符重载
      • 析构函数
    • 菱形继承
      • 菱形继承的问题 - 数据冗余 和 二义性
      • 只要有公共的部分就是菱形继承
    • 菱形虚拟继承
      • 菱形虚拟继承原理
        • 菱形继承内存分布
        • 菱形虚拟继承内存分布
      • 收益
    • 继承总结与反思

继承

继承是类设计层次的复用

继承方式与访问限定符

继承-菱形继承_第1张图片

限定了啥?

1.根据表中我们可以看到 基类的私有成员在子类不可见,但还是被继承了下来
2.根据继承方式和成员在基类的访问限定符小的那个来决定了子类访问基类成员的访问方式
例如如果是public继承,那么基类中protected成员继承到子类中访问限定符就是protected(类外不可访问,类内可以访问)
因为private和protected在类和对线阶段他们没什么区别,都是类内可以访问,类外不可访问,但是到了继承这里,private成员的不可见,导致proteced的出现

继承后的子类是否将成员变量拷贝一份?

是的,他们互相独立
但单只成员变量,不是指的成员函数,成员函数在代码段(常量区)

切片

继承-菱形继承_第2张图片

子类是可以赋值给父类的,称为向上转换
并且这种转换是天然支持的,而且不生成临时空间

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; // 工号
};

继承-菱形继承_第3张图片
ps = st;发生了什么?
st把自己父类的部分拷贝给给了ps,并且没有开临时空间

回顾
继承-菱形继承_第4张图片

person& rp = st;
这句没加const即可证明没开临时空间
rp是直接引用student的一部分,这样的话随着rp的改变,子类也会被改变
继承-菱形继承_第5张图片
如果是指针的话,同样只想子类中父类的那一部分
继承-菱形继承_第6张图片
我们可以看到用指针引用修改对象,他们父类部分都会跟着改变
继承-菱形继承_第7张图片

向下转换

对象的转换都是不可以的
但如果是本身就指向子类的父类指针,再把这个父类指针转换回子类是可以的

隐藏

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

成员函数和成员变量都会发生隐藏

这种隐藏遵循了局部域优先,子类里面找到了就不会去父类里找

// 两个fun构成什么关系?
// a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错
// 答案:a (父子类域中,成员函数名相同就构成隐藏)
继承-菱形继承_第8张图片
重载是在同一作用域中利用函数名修饰规则来区分不同的函数,如果没有函数名修饰规则的话就无法区分。
隐藏是在不同的类域中直接就可以区分出来不同函数。

总结:
尽量不要使用隐藏,搞出同名成员

如果隐藏了只能指定类域访问成员

成员变量名字相同,类型不同也是构成隐藏的
继承-菱形继承_第9张图片

派生类的默认成员函数

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++规定了子类的构造必须调用父类的构造函数初始化父类的成员(在初始化列表调用)
他把父类整体当成一个对象成员来处理,父类的成员交给父类的构造,派生类初始化自己的成员
继承-菱形继承_第10张图片
编译器规定你不能在初始化列表显示初始化基类成员,但在函数体内可以,这就是规定
继承-菱形继承_第11张图片

拷贝构造

同样禁止直接初始化父类继承-菱形继承_第12张图片
需要注意的是调用父类拷贝构造需要一个父类对象的引用,这里没有,但是直接传s就可以,因为会发生切片,把s1的父类部分引用给s2,那么拷贝构造就正常走了
注意如果不写Person(s),那么默认构造调用父类的默认构造,与预期拷贝s的父类不符
继承-菱形继承_第13张图片
理念就是父亲干父亲的活,孩子干孩子的活

赋值运算符重载

坑就是重复自我调用,栈溢出
继承-菱形继承_第14张图片

析构函数

先子后父
涉及子类使用了父类成员,就必须先析构子类再析构父类
父类的析构函数编译器自动帮助我们调用了
.
涉及了多态导致析构函数名都被统一处理destructor,子类和父类的析构函数发生了隐藏

继承-菱形继承_第15张图片

菱形继承

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; // 主修课程
};

菱形继承的问题 - 数据冗余 和 二义性

数据冗余的本质是浪费空间
二义性是指的我们不知道要访问哪一个,访问谁不确定
继承-菱形继承_第16张图片

二义性可以用指定类域来解决,但是冗余没有虚拟继承解决不了
继承-菱形继承_第17张图片

继承-菱形继承_第18张图片
我们只需要一份身份证数据,一份地址

只要有公共的部分就是菱形继承

继承-菱形继承_第19张图片
D里面会有2份a,菱形继承主要是有数据冗余二义性

继承-菱形继承_第20张图片

菱形虚拟继承

在腰部位置加上virtual,变成虚继承,让_age只有一份,就可以解决数据冗余二义性
继承-菱形继承_第21张图片

继承-菱形继承_第22张图片

菱形虚拟继承原理

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;
};

继承-菱形继承_第23张图片

菱形继承内存分布

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在包含,监视窗口只是方便看

继承-菱形继承_第24张图片

在内存上d对象的成员是连续的
继承-菱形继承_第25张图片

菱形虚拟继承内存分布

继承-菱形继承_第26张图片
监视窗口看起来有三份,已经不准了
我们还是看内存窗口
图中关于虚基表中预留位置00 00 00 00 是给谁预留的呢?图中说的不对
这个位置应该是给A类的父类预留的,具体的继承结构图是这样
继承-菱形继承_第27张图片

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;
}

继承-菱形继承_第28张图片

它利用了虚基表存储了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;
//}

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;
}

继承-菱形继承_第29张图片

值得注意的是,B对象在虚继承后也发生了变化,在访问_a时也利用虚基表偏移量来访问
B* ptr 作为一个基类指针,它既可以指向B,也可以指向派生类D。
但是当B* ptr指向D时,发生切片,对于B* ptr来讲指向的还是一个B对象,所以它不知道指向B还是D
但是有了虚基表和偏移量,不管ptr指向谁,我都按照偏移量来修改成员变量

收益

菱形继承大小 20 菱形虚拟继承大小 24
我们看到菱形虚拟继承反而大于菱形继承
此时花费了2个4字节指针,使得原来2个_a变成1个_a,收益是4,这肯定亏了,原因在于A对象成员大小不够大
继承-菱形继承_第30张图片
我们将A对象成员改成数组,这样在菱形虚拟继承时,只有一份数组,而不是两份数组,D对象大小减少大概一半
继承-菱形继承_第31张图片

继承总结与反思

菱形继承会有性能损失
搞出菱形继承就需要菱形虚拟继承,在写构造函数时就尤为复杂
多继承谨慎使用,避免搞出菱形继承

继承-菱形继承_第32张图片
继承的耦合度更高,组合相对而言依赖关系更低
继承-菱形继承_第33张图片
使用中更符合继承就用继承,符合组合就用组合

你可能感兴趣的:(C++,c++)