C++ ---继承(二)

1、继承和静态成员

基类定义了static静态成员,则整个体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。

cpp 复制代码
#include<iostream>

using namespace std;

class Person
{
public:
	string _name;
	static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum;
	
};
int main()
{
	Person p;
	Student s;
	// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
	// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
	cout << &p._name << endl;
	cout << &s._name << endl;
	// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
	// 说明派⽣类和基类共⽤同⼀份静态成员
	cout << &p._count << endl;
	cout << &s._count << endl;
	// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
	cout << Person::_count << endl;
	cout << Student::_count << endl;
	return 0;
}

运行结果是:

2、多继承和菱形继承问题

继承模型:

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。

单继承;

多继承:

多继承的存在可能会造成菱形继承

数据冗余

A ← 基类

/ \

B C ← B、C 各自继承 A("virtual"在这里加)

\ /

D ← D 同时继承 B 和 C

cpp 复制代码
class A { public: int x; };
class B : public A {};
class C : public A {};
class D : public B, public C {};  // 菱形继承
cpp 复制代码
D d;
// d.x 有歧义:是 B::A::x 还是 C::A::x?
d.x = 1;              // 编译错误:对 x 的引用不明确
d.B::x = 1;           // 可以,但说明存在两份数据
d.C::x = 2;

这个可能会造成数据冗余,D对象中会包含两份A的成员。

二义性

访问基类成员必须显式指定路径,否则编译器无法确定使用哪一份。

解决办法:虚继承

虚继承

让B、C虚继承A,则D中只保留一份A子对象

cpp 复制代码
class A { public: int x; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

D d;
d.x = 1;   // 正确,只有一份 A::x
cpp 复制代码
#include <iostream>
using namespace std;

// M 是成员对象
class M {
public:
    M() { cout << "M" << endl; }
    ~M() { cout << "M" << endl; }
};

// A 是虚基类
class A {
public:
    A(int val) { cout << "A" << endl; }
    ~A() { cout << "A" << endl; }
};

// B 和 C 虚继承 A
class B : virtual public A {
public:
    B() : A(1) { cout << "B" << endl; } // 写 A(1),作为中间类时被忽略
    ~B() { cout << "B" << endl; }
};

class C : virtual public A {
public:
    C() : A(2) { cout << "C" << endl; } // 写 A(2),作为中间类时被忽略
    ~C() { cout << "C" << endl; }
};

// D 是最终派生类,继承 B 和 C,里面有一个成员 M
class D : public B, public C {
public:
    M m;
    D() : A(3), B(), C() { cout << "D" << endl; } // D 写 A(3),真正生效的就是这个
    ~D() { cout << "D" << endl; }
};

int main() {
    cout << "--- 实例化 D 对象 ---" << endl;
    {
        D d; 
        // 预期构造输出:A B C M D
    }
    cout << "--- D 对象销毁结束 ---" << endl;

    cout << endl;

    cout << "--- 单独实例化 B 对象 ---" << endl;
    {
        B b; 
        // 预期构造输出:A B
    }
    cout << "--- B 对象销毁结束 ---" << endl;

    return 0;
}

运行结果是

cpp 复制代码
--- 实例化 D 对象 ---
A
B
C
M
D
D
M
C
B
A
--- D 对象销毁结束 ---

--- 单独实例化 B 对象 ---
A
B
B
A
--- B 对象销毁结束 ---

例子回顾一下:A 是虚基类,B 和 C 虚继承 A,D 继承 B 和 C,D 里还有一个成员对象 M。D 的初始化列表写 A(3),B 的列表写 A(1),C 的列表写 A(2)。

第一条,为什么输出是 A B C M D 。构造 D 时先构造虚基类 A,用的参数是 3,因为 D 是最终派生类,只有它给 A 的参数算数。然后按继承声明顺序构造普通基类 B 和 C,各自打印自己。接着构造 D 的成员 M。最后执行 D 自己的构造函数体。所以顺序就是虚基类、普通基类、成员、自身。

第二条,为什么 B 和 C 里的 A(1) 和 A(2) 被忽略。因为 B 和 C 在这里是中间类,不是最终被实例化的类。C++ 规定虚基类只由最终派生类初始化 ,**中间类在初始化列表里写虚基类会被编译器跳过。**这样保证 A 只构造一次,不会出现两份 A。

第三条,为什么单独实例化 B 时 A(1) 又生效了。写 B b 的时候,B 自己就是最终派生类,没有更下层的类来负责 A,所以轮到 B 初始化 A,A(1) 就用上了,输出变成 A B。这说明中间类还是最终派生类,取决于当前实例化的是谁。

第四条,成员 M 的构造位置。M 排在普通基类之后、自身构造体之前。而且 M 按它在类里声明的顺序构造,不是按初始化列表里写的顺序。如果 D 里有多个成员,谁先声明谁先构造。

第五条,**析构为什么是 D M C B A。**析构严格反着来:先执行 D 自身析构体打印 D,再析构成员 M 打印 M,再析构普通基类,按构造逆序先 C 后 B,最后析构虚基类 A 打印 A。A 只析构一次。

第六条,虚基类 A 的构造和析构都只发生一次,这是虚继承的核心效果。对比非虚继承,如果不写 virtual,D 里会有两份 A,A 会构造两次、析构两次,访问 x 还会有二义性。

第七条,各角色的对应关系。A 是虚基类,负责被共享、只一份。B 和 C 是普通基类,同时在这里是中间类。M 是成员对象。D 的构造函数花括号和析构函数花括号是自身构造体和自身析构体。D 是最终派生类,负责虚基类的初始化和析构。

第八条,这个例子能验证的一句话总结:虚基类由最终派生类管,中间类说了不算;构造从虚基类往里走,析构从自身往外走,虚基类两头都只碰一次。

构造顺序:虚基类 -> 普通基类 -> 成员对象 -> 自身构造体。重点是虚基类由最终派生类负责初始化,中间类构造函数里对虚基类的初始化会被忽略。比如A是虚基类,B和C虚继承A,D继承B和C,构造D时A只构造一次,用的是D的初始化列表里给A的参数,B和C里写的A初始化被丢弃。如果单独实例化B,B就是最终派生类,B里写的A初始化才生效。

析构顺序:自身析构 -> 成员 -> 普通基类 -> 虚基类。和构造严格相反。虚基类的析构只执行一次,也由最终派生类负责,所以虚基类析构函数最好声明为virtual,并且不要依赖中间类的析构逻辑。

实现机制:含虚基类的对象通常多带一个虚基类表指针 -> 指向一张偏移表 -> 运行时查表定位虚基类子对象的位置。代价是空间上每个含虚基类的子对象多一个指针,时间上访问虚基类成员要间接寻址。虚继承对象的布局依赖具体编译器,不能跨编译器假定内存布局。做指针或引用转换时,派生类到虚基类不能靠固定偏移,需要运行时查表调整,static_cast在这种场景受限,从虚基类向下转型一般用dynamic_cast。

3、IO库的菱形虚拟继承

4、继承和组合

public继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。

组合是一种has-a的关系。假设B组合了A,每一个B对象中都有一个A对象。

继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度很高。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或者组合对象来获得。

对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse)

因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每一个类被封装。

优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码的维护性好。不过也不太绝对,类之间的关系就是适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。

类之间的关系既适合用继承(is-a)也适合用组合(has-a),就用组合。

相关推荐
陈飞EXCEL2 小时前
北京杰飞 IT 旗下妙码生花少儿Python编程上线两大学习系统
开发语言·python·学习
码林拾遗WJ3 小时前
告警分级的自动化实测:模型判断 vs 人工拍板
人工智能·算法·自动化
Tisfy3 小时前
LeetCode 0453.最小操作次数使数组元素相等:两次遍历(反向思维)
算法·leetcode·题解·遍历·反向思维
代龙涛3 小时前
WordPress Query 查询机制详解(WP_Query)
开发语言·后端·php·wordpress
君万3 小时前
【LeetCode每日一题】回溯:46. 全排列 78. 子集 39. 组合总和
算法·leetcode·go
znsoft3 小时前
MaClaw(码卡龙)技术拆解:一个 Go + React 的可自进化智能体平台
开发语言·react.js·golang
Keep_Trying_Go4 小时前
Consistent-Point半监督人群计数算法详解
人工智能·深度学习·算法·计算机视觉·人群计数
在所不辞兄4 小时前
【零基础学智能仿真-69】综合项目——材料参数驱动的仿真研究
人工智能·深度学习·神经网络·算法·机器学习·1024程序员节
凉茶钱5 小时前
红黑树:用“颜色”换“平衡”,set 与 map 的底层发动机
开发语言·c++·红黑树