继承与头文件

类就是c文件,接口就是h文件。
类的extends或implements的过程,就是c文件include的过程。
类实例化一个对象,相当于对c文件做一个实例到内存上下文。
类的私有成员,相当于c文件的静态变量
类是静态方法,就是c文件中固定不变的的函数

面向对象引入类的概念,只是将代码文件变成了一种,可以增殖出实例的原型

静态的类型,其本身就是独一无二的对象,原型与实例的统一。

继承的扁平化与垂直化

Go、Rust、Nim等新兴语言,为什么都抛弃了constructor? - 知乎
为什么go和rust语言都舍弃了继承? - 知乎
为什么go和rust语言都舍弃了继承? - 知乎
先声明:这里说的”组合“,是以Rust为主导的,把继承关系限制在一层的类的组织架构。没有双层继承,就没有菱形继承。
大家对继承与组合孰优孰劣讨论非常热烈。

先看Rust的鲜明特点“组合”吧,这是由“特征”Trait所组合而成,在Java中称为“接口”

pub trait Summary { fn summarize(&self) -> String; }

再看Java等传统办法“继承”,实际上就是没有Rust那么爱干净,可以在“接口”里面掺代码成为“基类”

public class Summary { String summarize() { System.out.println("加点废话"); } }

“组合”的支持者所反对的是“继承”的一层又一层,妥妥的近亲繁殖。

“马”属于“驴”还是“驴”属于“马”?
一只骡子,细分还有“马骡”与“驴骡”,算马还是算驴?
要说马骡属于马,驴骡属于驴,那它们的共性就被淡化了。
要说“骡就是骡”[1],多一个新类,代码复用性低下。

要说多重继承……继承越来越多,类之间的成员可能冲突。比如骡奔跑,是继承马的还是驴的?骡子不可生育,两个类进行合成以后他失去了生育属性,必须强行修改(重载)了。

垂直化:Java的继承,不支持多重继承(后来增加了接口的概念,支持多重接口)

继承网:菱形继承/重复嵌套的应对策略

python入门 -- 钻石继承(菱形继承) - 知乎
Python3中super()的参数传递 - 随风飘-挨刀刀 - 博客园
python的super函数详解 - 知乎
像使用Rust的trait一样使用Python的class - 知乎
C++虚继承和虚基类详解 - 知乎

方案一:拒绝多层继承,学习Rust让继承只有一层

graph TD
Super --> Parent1Real
Parent1 --> Parent1Real
Parent1 --> Son
Super ---> Son
Parent2 --> Son
Parent2 --> Parent2Real
Super --> Parent2Real

扁平化继承层次,杜绝菱形继承,最后的派生类直接继承最初的基类。中间层不再进行部分实现,而是额外定义最后的派生类来实现各种部件。

附录:
与python直接给对象赋予成员不同,C++用using引入成员,需要存在继承关系。
private继承,类可以访问定义在基类的public成员和protected成员,实例对象不能访问定义在基类的任何成员。用using重新开放实例对象对于成员的访问权

class Super {
public:
    void superMethod() { std::cout << "Super方法" << std::endl; }
    void commonMethod() { std::cout << "Super通用方法" << std::endl; }
};

class Parent1 {
public:
    void parent1Method() { std::cout << "Parent1方法" << std::endl; }
    void commonMethod() { std::cout << "Parent1通用方法" << std::endl; }
};

class Son : private Super, private Parent1 {
public:
    // 使用using导入特定方法,避免方法链
    using Super::superMethod; // 可选择性地暴露基类方法
    using Parent1::parent1Method;
    
    // 解决命名冲突:明确选择要导入哪个版本
    using Super::commonMethod;  // 选择Super的版本
    // 或者 using Parent1::commonMethod;  // 选择Parent1的版本
    
    Son() {
        // 现在可以直接调用,不需要方法链
        superMethod();      // 直接调用
        parent1Method();    // 直接调用
        commonMethod();     // 直接调用(使用选择的版本)
    }
};

方案二:自动重载继承,按照优先级或者某种规则搜索

向上搜索,把多层继承转换成单层继承关系,并按某种规则排除重复项

MRO顺序

官方的做法是,引出了python中super()函数及MRO顺序

1. super()函数是用来调用父类的一个方法,是为了解决多重继承的问题的
2. 使用super()函数调用的是在mro顺序中的直接父类
3. super()的主要作用是不需要使用父类名来调用父类的方法,单子类改为继承其他父类的时候,不需要对子类
内部的调用父类的函数做任何修改就可以调用新父类的方法。增强了代码的可维护性。不需要在所有调用的地方进
行修改。
4. super()函数返回一个代理对象作为代表来调用父类的方法。对于访问已经在类中重写的继承方法是十分有用的
class Base:
    def __init__(self):
        print('Base.__init__')


class A(Base):
    def __init__(self):
        super().__init__()
        print('A.__init__')


class B(Base):
    def __init__(self):
        super().__init__()
        print('B.__init__')


class C(Base):
    def __init__(self):
        super().__init__()
        print('C.__init__')


class D(A, B, C):
    def __init__(self):
        super(B, D).__init__(self)  # D是B的子类,并且需要传递一个参数
        print('D.__init__')


D()

print(D.mro())

从结果可以看出,是按照D的MRO顺序,从B开始调用,因此跳过了A

Base.__init__
C.__init__
D.__init__
[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.Base'>, <class 'object'>]

MRO顺序本质上执行的是广度优先搜索,从左到右,搜索完同一层级的时候,向上爬升。
保证了每个类中的方法只会被执行一次。避免了同一个类被调用多次的情况。

查看MRO顺序:

类名.__mro__

(<class '__main__.Son'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class '__main__.Super'>, <class 'object'>)

虚继承

C++(23)——理解多重继承(菱形继承、半圆形继承)、虚基类和虚继承_c++23-CSDN博客

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。

改成虚继承,A就变成了虚基类

class A
{
public:
	A(int data) :ma(data) { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
protected:
	int ma;
};
class B :virtual public A
{
public:
	B(int data) :A(data), mb(data) { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
protected:
	int mb;
};
class C :virtual public A
{
public:
	C(int data) :A(data), mc(data) { cout << "C()" << endl; }
	~C() { cout << "~C()" << endl; }
protected:
	int mc;
};
class D :public B, public C
{
public:
	D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
	~D() { cout << "~D()" << endl; }
protected:
	int md;
};

我们做出修改:
将B和C的继承方式都改为虚继承;接下来继续运行代码:
此时会报错:

请添加图片描述

提示说:“A::A” : 没有合适的默认构造函数可用;
为什么会这样呢?

是因为:

刚开始B和C单继承A的时候,实例化对象时,会首先调用基类A的构造函数,,到了D,由于多继承了B和C,所以在实例化D的对象时,会首先调用B和C的构造函数,然后调用自己(D)的。
但是这样会出现A重复构造的问题,所以,采用虚继承,把有关重复的基类A改为虚基类,这样的话,对于A构造的任务就落到了最终派生类D的头上,但是我们的代码中,对于D的构造函数:
D(int data) : B(data), C(data), md(data) { cout << “D()” << endl;}
并没有对A进行构造。
所以会报错。
那么我们就给D的构造函数,调用A的构造函数:

D(int data) :A(data), B(data), C(data), md(data) { cout << “D()” << endl; }

方案三:选择性继承,如自身的构造工厂不被继承

C++构造函数详解及显式调用构造函数(explicit)_c++ 显式构造函数-CSDN博客
Python的构造函数都是手动调用,简洁性和灵活性这一点我要点赞

class Parent1(Super):
	def __init__(self):
		Super.__init__(self)
		self.init()
	def init(self):
		print("Parent1特有的初始化")

class Son(Parent1, Parent2):
	def __init__(self):
		Super.__init__(self)
		Parent1.init(self)
		Parent2.init(self)
		self.init()
	def init(self)
		print("Son特有的初始化")

很可惜的一点,C++不像Python一样,C++继承时会强制调用基类的构造函数,除非遇到菱形继承问题。所以我用函数重载,把继承用和实例化用的构造函数分离。

Success

这就是参数多态和组合多态碰撞的火花了。我用构造函数的参数多态,本质上是为了实现构造函数protected和public职责的区分,如果把函数和结构体都看作行为特殊的类,类是函数与结构体的概念融合,那么两种多态其实都是一种东西。参见Data + Logic = Program

Note

在C++中,要实现函数重写(override),基类函数必须声明为 virtual
纯虚函数 virtual func() = 0; 相当于Python的 @abstractmethod

class ParentA : virtual public Super {
public:
    ParentA() : Super() {  // 用于继承中的构造函数
        std::cout << "ParentA __init__ (for inheritance)" << std::endl;
    }
    ParentA(bool a) : Super() { // 单独实例化中,提供自动模式的构造函数
        std::cout << "ParentA __init__ (standalone)" << std::endl;
        ParentA::init();  // ✅ 单独实例化时自动调用,注意指定类型名,否则子类初始化的时候容易重定向到子类的同名函数
    }
    
    // 将init改为虚函数,允许子类重写行为
    virtual void init() {
        std::cout << "ParentA init" << std::endl;
    }
    
    void commonMethod() {
        std::cout << "ParentA common method" << std::endl;
    }
};

class Child : virtual public ParentA, virtual public ParentB {
public:
    Child() : Super(), ParentA(), ParentB() {  // C++强制要求:所有基类都必须被正确构造
        std::cout << "Child __init__ (for inheritance)" << std::endl;
    }
    Child(bool a) : Super(), ParentA(), ParentB() {  // 这里【必须】显式调用Super()
        std::cout << "Child __init__ (standalone)" << std::endl;
        Child::init();  // 自动调用重写的init
    }
    
    // ✅ 重写init方法,提供自定义初始化逻辑
    virtual void init() override {  // ✅ 语法正确,但virtual是多余的
        std::cout << "Child init" << std::endl;
        
        // 明确调用特定父类的方法,完全手动控制初始化顺序和内容
        ParentA::init();  // 调用父类的默认init
        ParentB::init();  // 调用父类的默认init
        
        // C++多了一种类型转换的多态,但在这不建议,可能会再次调用Child::init(),以后再研究
        // static_cast<ParentA*>(this)->init();
        // dynamic_cast<ParentA*>(this)->init();
        
        std::cout << "3. Child特有初始化..." << std::endl;
        // 添加你的代码
    }
};

当然,也可以再写工厂方法,(实现一个创造出自己的函数,应对多种构造方式时候很有用)

class ParentA : virtual public Super {
protected:
    // 只有派生类可以调用的构造函数
    ParentA() : Super() {
        std::cout << "ParentA __init__ (for inheritance)" << std::endl;
    }
    
private:
    // 只有工厂方法可以调用的构造函数
    ParentA(bool) : Super() {
        std::cout << "ParentA __init__ (standalone)" << std::endl;
        ParentA::init();
    }
    
public:
    // 工厂方法用于单独实例化
    static ParentA* createStandalone() {
        return new ParentA(true);
    }
    
    virtual void init() {
        std::cout << "ParentA init" << std::endl;
    }
};

继承与原型

继承与组合

面向对象基础设计原则:5.组合/聚合复用原则 - 知乎
何时用继承,何时用组合 - 沧海一滴 - 博客园
我们表示一个范畴是另一个范畴的外延,如图

graph TD
人 --> 雇员
人 --> 经理
人 --> 学生
雇员 --> 田所浩二
学生 --> 田所浩二

我们既可以创建一个对象并赋予参数——”人“可以拥有多种”角色“
我们还可以定义一个新类并更改原型——”人“可以继承多种”角色“

图中对于继承,是菱形继承的困境——二层基类会多次实例化
图中对于组合,是空间浪费的困境——成员中有二层成员重复

在JavaScript中,有着”原型编程“的思想,类和对象并作不区分
在Python中,类之上还有元类
在RT-Thread中,用一个指针表示结构体的”基类“

本来都只是范畴的外延罢了。写成继承,只是比写成嵌套少费点口舌,如我.头.皮.头发,被编译器的继承机制自动包装成员,变成我.头发。嵌套则需要手动包装。

动态链接,就相当于在实例化的时候绑定指针互相沟通。当然解释型语言的继承关系也可以动态更改

继承的动态化

不懂就问,为啥设计模式教程大多是Java的。? - 知乎
工厂模式 | 菜鸟教程
装饰器模式 | 菜鸟教程
Python 中的鸭子类型和猴子补丁 - AlwaysBeta - SegmentFault 思否

动态的办法,可以解决一些“继承与组合问题”(数据之间、函数之间),和“表达式问题”(数据与函数、函数与数据)

“你大可以把一只兔子耳朵和毛全部去掉加上羽毛,然后能给它插上翅膀,加个尖嘴,变成老鹰。”

但是这个功能不能没有,没有代码一开始就天衣无缝,动态修改可以作为小补丁。
毕竟数据noun和操作verb本质上是正交的两个维度,谁说我不能动态地绑定一组坐标,强行定义“阿珍和阿强今天举行婚礼”呢……至于依赖混乱的问题,严格保证临时办法临时使用,不要让承重墙建在脚手架上就好。

Note

元编程

模板元编程的“自动传参/继承”,是把类型指派的工作交给了元语言和编译器。或者说编译器收到参数后,已经把函数本身变成一个新函数,类变成一个新类,暴露出来了。
"反射算法"lisp程序版本0.3已编写完毕【人工智能吧】百度贴吧
(47 封私信 / 48 条消息) Python黑魔法:元类和元编程 - 知乎
第九章:元编程 — python3-cookbook 3.0.0 文档
(47 封私信 / 48 条消息) C++ 模板元编程:一种屠龙之技 - 知乎
对动态语言,支持元编程的语言,尤其是支持反射(一门语言同时也是自身的元语言的能力称之为反射。)的语言,具有天然优势,因为这种语言可以把自己本身的概念拿过来进行操作(用元类来操作类,或者用装饰器操纵函数),我可以写代码操纵我自己的代码。

固定的继承关系,无论是重重继承还是单层组合,都是类似一个固定的逻辑(跳转表、网表、ROM……),其实就是解释环节的固定。

Question

一个语言,最好能让用户用它语法本身来规定自身对于数据类型、类等概念的反应。有没有语言可以让我对语义重新解释或篡改(“修正主义”(误)),在开发中自定义解释器行为(数据类型是什么、启动时像.bashrc一样自动import哪些包……)?


  1. 连裤袜是裤子还是袜子——“最高法”谣言考 – 初风 ↩︎