继承与头文件
类就是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],多一个新类,代码复用性低下。
要说多重继承……继承越来越多,类之间的成员可能冲突。比如骡奔跑,是继承马的还是驴的?骡子不可生育,两个类进行合成以后他失去了生育属性,必须强行修改(重载)了。
- 拆分出一个“只会奔跑的抽象马”,让马和驴和骡都继承它的奔跑。
- 拆分出一个“不会生育的抽象马”,让马和驴都继承它的生育。
不如全部拆散七零八落,那还算哪门子继承,基类一步一步细分,就回到了组合的地步。
树状的文件夹和符号链接变成了tag标签的组合。
当然,海量的tag(trait)也是一种折磨。一个具象概念可以有非常多抽象范畴。
垂直化: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()函数返回一个代理对象作为代表来调用父类的方法。对于访问已经在类中重写的继承方法是十分有用的
- 第一个参数决定了从哪个类开始调用,一般也可以是本身类,即从本身这个类的顺序中的下一个类开始调用该方法
- 第二个参数决定了调用父类的顺序,比如self,就是调用super函数的对象本身,它有一个__mro__属性,决定了按什么顺序调用父类的方法
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++继承时会强制调用基类的构造函数,除非遇到菱形继承问题。所以我用函数重载,把继承用和实例化用的构造函数分离。
这就是参数多态和组合多态碰撞的火花了。我用构造函数的参数多态,本质上是为了实现构造函数protected和public职责的区分,如果把函数和结构体都看作行为特殊的类,类是函数与结构体的概念融合,那么两种多态其实都是一种东西。参见Data + Logic = Program
在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;
}
};
继承与原型
- 过程式范式 (C): 数据是“原型”,行为是静态(”原型“和”实例“的统一)
- 你的
struct Person {...};是一个被动的“原型”,必须创建独立的、主动的函数void printPerson(struct Person* p)来操作它的“实例”。 - 你的操作无法被你实例化。运行时短暂创建出函数堆栈并执行后销毁,并不由你决定。
- 你的
- 面向对象范式 (Java/C++): 无论数据和行为即整个代码都可以被打包进“原型”中从而具有主动性
- 你的
class Person { void print() { ... } }是一个主动的“形”,它将数据和对这些数据的操作(方法)打包在了一起。
- 你的
- 函数式范式 (Haskell): 行为是“原型”,数据是静态
- 代数数据类型,ADT定义得非常严谨和强大;柯里化等操作是对函数本身的编排
- 函数式编程中的函数不保留任何状态;不会改变输入的数据,而是生成新的数据集作为输出
继承与组合
面向对象基础设计原则:5.组合/聚合复用原则 - 知乎
何时用继承,何时用组合 - 沧海一滴 - 博客园
我们表示一个范畴是另一个范畴的外延,如图
graph TD 人 --> 雇员 人 --> 经理 人 --> 学生 雇员 --> 田所浩二 学生 --> 田所浩二
我们既可以创建一个对象并赋予参数——”人“可以拥有多种”角色“
我们还可以定义一个新类并更改原型——”人“可以继承多种”角色“
图中对于继承,是菱形继承的困境——二层基类会多次实例化
图中对于组合,是空间浪费的困境——成员中有二层成员重复
在JavaScript中,有着”原型编程“的思想,类和对象并作不区分
在Python中,类之上还有元类
在RT-Thread中,用一个指针表示结构体的”基类“
本来都只是范畴的外延罢了。写成继承,只是比写成嵌套少费点口舌,如我.头.皮.头发,被编译器的继承机制自动包装成员,变成我.头发。嵌套则需要手动包装。
动态链接,就相当于在实例化的时候绑定指针互相沟通。当然解释型语言的继承关系也可以动态更改
继承的动态化
不懂就问,为啥设计模式教程大多是Java的。? - 知乎
工厂模式 | 菜鸟教程
装饰器模式 | 菜鸟教程
Python 中的鸭子类型和猴子补丁 - AlwaysBeta - SegmentFault 思否
动态的办法,可以解决一些“继承与组合问题”(数据之间、函数之间),和“表达式问题”(数据与函数、函数与数据)
-
工厂模式让构造更自由,装饰模式让修改更自由。
工厂函数(构造函数广义化),可高度定制对象
装饰函数(函数本身的函数),可高度定制函数 -
强制转换、鸭子类型,让函数的参数类型可以变来变去(或许可以理解为一种自动传参。一个临时函数定义是原有函数外加类型检测)
-
方法重载、猴子补丁,让对象的方法成员可以变来变去(或许可以理解为一种自动继承。一个临时类继承自原来的类)
“你大可以把一只兔子耳朵和毛全部去掉加上羽毛,然后能给它插上翅膀,加个尖嘴,变成老鹰。”
但是这个功能不能没有,没有代码一开始就天衣无缝,动态修改可以作为小补丁。
毕竟数据
元编程
模板元编程的“自动传参/继承”,是把类型指派的工作交给了元语言和编译器。或者说编译器收到参数后,已经把函数本身变成一个新函数,类变成一个新类,暴露出来了。
"反射算法"lisp程序版本0.3已编写完毕【人工智能吧】百度贴吧
(47 封私信 / 48 条消息) Python黑魔法:元类和元编程 - 知乎
第九章:元编程 — python3-cookbook 3.0.0 文档
(47 封私信 / 48 条消息) C++ 模板元编程:一种屠龙之技 - 知乎
对动态语言,支持元编程的语言,尤其是支持反射(一门语言同时也是自身的元语言的能力称之为反射。)的语言,具有天然优势,因为这种语言可以把自己本身的概念拿过来进行操作(用元类来操作类,或者用装饰器操纵函数),我可以写代码操纵我自己的代码。
固定的继承关系,无论是重重继承还是单层组合,都是类似一个固定的逻辑(跳转表、网表、ROM……),其实就是解释环节的固定。
一个语言,最好能让用户用它语法本身来规定自身对于数据类型、类等概念的反应。有没有语言可以让我对语义重新解释或篡改(“修正主义”(误)),在开发中自定义解释器行为(数据类型是什么、启动时像.bashrc一样自动import哪些包……)?