在C++里写了几年游戏引擎核心,后来又用Python做AI训练和数据处理,这两种语言来回切换时,最让我觉得“既熟悉又陌生”的就是Class继承。语法上看起来都是“子类 extends 父类”,但真正用起来,完全是两套思维模型。你拿C++那套去写Python,能跑但别扭;拿Python那套去写C++,编译期直接教你做人。
这篇文章我结合自己的实际项目经验,把Python和C++在类继承方面的差异掰开揉碎了讲一遍。不会只停留在“语法不同”这种表面,而是深入到内存布局、方法查找、多继承策略、构造析构机制这些底层逻辑,再配上大量可运行的代码和踩坑记录。无论你是C++转Python、Python转C++,还是平常只用其中一门想补全另一门的知识盲区,这篇都能给你实在的参考。
1. 两种继承的底层哲学:一个管内存,一个管名字
1.1 C++ 继承在内存里真正“复制”了基类
C++的继承本质上是内存布局的复用和扩展。我这么说可能有点抽象,但你看代码就明白了。
cpp复制class Animal {
public:
Animal(const std::string& name) : name_(name) {}
virtual void speak() const {
std::cout << name_ << " makes a sound" << std::endl;
}
protected:
std::string name_;
};
class Dog : public Animal {
public:
Dog(const std::string& name) : Animal(name) {}
void speak() const override {
std::cout << name_ << " barks" << std::endl;
}
};
在C++里,当你声明 Dog dog("旺财") 时,内存中 dog 这个对象从低地址到高地址依次排列着 Animal 的子对象和 Dog 自己的成员。也就是说,Animal 的 name_ 成员变量在内存中是实实在在存在的一份数据副本。
这一点在实际工程中影响非常大。我早期写一个消息系统时,基类里放了一个 std::vector 作为缓冲区,派生类不断地往这个缓冲区里塞数据。因为继承就是内存子对象的嵌套,sizeof(Derived) 直接等于 sizeof(Base) + 各自成员大小,我一开始算错了内存上限,导致缓冲区在栈上越界。后来我深刻记住了:C++的继承不是“逻辑上的属于”关系,而是“物理上的包含”关系。
而且C++的虚函数是靠虚函数表(vtable)实现的。每个有虚函数的类,在编译期会生成一张虚函数表,表格里保存的是函数指针。类对象的内存里会多一个虚函数表指针(vptr)指向这张表。派生类重写基类的虚函数时,实际上是在自己的虚函数表里,用新的函数地址覆盖了基类函数地址对应的槽位。
我之前排查过一个奇怪的bug:基类和派生类声明了同名同参数的同名函数,但基类没加 virtual,派生类加了。调用时表现出的行为完全不像“多态”,我一度以为是编译器优化的问题。实际上就是虚函数表里压根没有这个函数的槽位,普通的成员函数调用在编译期就确定地址了。
1.2 Python 继承只是多了一条“查方法”的路
Python的继承和C++完全不一样。Python的类对象本质上是 type 类型的实例,类之间的关系靠的是 __bases__ 元组和一个方法解析顺序(MRO)。当你调用一个对象的某个方法时,Python不是直接去“找内存里的函数指针”,而是沿着MRO去各个类对象的 __dict__ 里查这个名字。
python复制class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> None:
print(f"{self.name} makes a sound")
class Dog(Animal):
def __init__(self, name: str):
super().__init__(name)
def speak(self) -> None:
print(f"{self.name} barks")
在这个例子里,当你执行 dog.speak() 时,Python先看 Dog.__dict__ 里有没有 speak,有就直接用。如果没有,就顺着MRO往后找,在 Animal 里找到了就用。实例对象的 self.name 则存放在每个实例自己的 __dict__ 里。
这种设计带来的一个关键差别是:内存里并不存在一个物理上的 Animal 子对象。Dog实例就是一个普通的字典(加了一些特殊属性),它“继承”来的 name 属性,只是因为在 Animal.__init__ 里 self.name = name 时才被创建出来。
所以Python的继承更像一个“属性查找的顺序表”,而不是“内存布局的复制”。这个差异直接决定了:
- C++中你无法在运行时给对象增加新的成员变量,Python可以;
- C++中一个空类继承一个基类,
sizeof至少是1,内存布局在编译期就固定了,Python的对象大小则取决于实例字典里放了多少内容。
我见过很多从C++转Python的人容易犯一个毛病:写Python类时,先定义一个空类,再手动 obj.xxx = 10 去补充属性。这在Python里完全合法,因为它压根不限制实例可以有哪些属性。但在C++里,你必须在类体里把所有成员都声明好,运行时没有任何办法给对象“加塞”一个新成员。这就是这两种语言在继承哲学上最本质的分歧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 语法层面:声明、构造、析构的差异化体验
2.1 类声明与对象创建:C++ 的户口本 vs Python 的黑板
C++中类和对象是“分离”的。类定义通常写在头文件里,声明的成员函数实现可以放在 .cpp 文件里。这种分离带来了编译期的强约束:你在写一个类的时候,必须告诉编译器这个类长什么样、有哪些成员、哪些函数是虚函数。这就相当于给对象上了一本户口本,所有信息在编译期就要登记清楚。
cpp复制// animal.h
#ifndef ANIMAL_H
#define ANIMAL_H
#include <string>
class Animal {
public:
Animal(const std::string& name);
virtual ~Animal();
virtual void speak() const;
protected:
std::string name_;
};
#endif
cpp复制// animal.cpp
#include "animal.h"
#include <iostream>
Animal::Animal(const std::string& name) : name_(name) {}
Animal::~Animal() = default;
void Animal::speak() const {
std::cout << name_ << " makes a sound" << std::endl;
}
这种分离式写法在大型项目里是必须的,能显著缩短编译时间,各个模块可以并行编译。但在小项目里写起来确实繁琐。我记得刚用C++写服务器的时候,最烦的就是每次新增一个类,头文件和源文件来回切换,改个成员函数签名还得手动把两边的声明和定义都同步改掉。后来用Clion的自动重构功能才好了一些。
而Python的类定义是一次性写在同一个模块里的,没有头文件和实现文件的区别。你打开一个 .py 文件,就能看到这个类的全部面貌。这相当于直接在黑板上写定义,写完了就能用,不需要提前告诉编译器“我要定义一个类”。
python复制class Animal:
def __init__(self, name: str):
self.name = nam
