1. 从一道面试题说起:你真的理解继承吗
很多人在简历上写“熟练掌握C++面向对象编程”,但面试官问一句“继承到底继承了什么”就卡住了。这不是我编的场景,我在技术面里问过不下五十次。标准回答通常是“继承可以复用代码,实现is-a关系”,听起来没错,但等于没说。你要是不知道继承在被编译器处理之后变成了什么,那么写出来的派生类代码大概率是在碰运气。
先给一个最反直觉的结论:继承在C++里不是一种“关系”,它首先是内存布局的复用。基类定义了数据成员,派生类对象的内存里就实实在在包含一份基类子对象,这不是比喻,是物理事实。你定义了一个Derived对象,sizeof(Derived)一定大于等于sizeof(Base),因为基类的成员被“拷贝”了一份放进派生类的内存区域里。搞懂这一点,很多莫名其妙的编译错误、内存踩踏问题都能想通。
这篇文章我打算把C++继承的底层机制、三种继承方式的真正区别、构造析构的调用顺序、名字隐藏与重载/覆盖的区别、菱形继承的解法以及继承vs组合的工程选择全部过一遍。适合正在学C++的初学者,也适合准备面试、想补基础的人。篇幅不短,但每一节都会有可运行的代码和踩坑经验,不是那种“概念听完感觉懂了、一写代码就报错”的科普。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 继承在内存层面的本质:一切语法都是表象
2.1 基类子对象到底长什么样
先看一段最简单的代码:
cpp复制#include <iostream>
class Base {
public:
Base() : m_a(1), m_b(2) {}
void show() const {
std::cout << "a=" << m_a << ", b=" << m_b << std::endl;
}
protected:
int m_a;
private:
int m_b;
};
class Derived : public Base {
public:
Derived() : m_c(3) {}
void dump() const {
// std::cout << m_b; // 编译错误:无法访问基类私有成员
std::cout << "a=" << m_a << ", c=" << m_c << std::endl;
}
private:
int m_c;
};
int main() {
Derived d;
d.show();
d.dump();
std::cout << "sizeof(Base) = " << sizeof(Base) << std::endl;
std::cout << "sizeof(Derived) = " << sizeof(Derived) << std::endl;
return 0;
}
在常见的64位平台、默认对齐下,Base是两个int,所以sizeof(Base)是8字节;Derived包含基类子对象(8字节)加自己的int m_c(4字节),对齐后sizeof(Derived)是12或16字节,取决于编译器对齐填充策略。这就直观说明了一个要点:派生类对象的内存,是“基类部分”+“新增部分”拼起来的,基类的私有成员虽然不能直接访问,但它仍然占据内存空间,只是被语言层面的访问控制挡住了而已。
这也解释了为什么会发生经典的“切片”问题:
cpp复制void func(Base b) { /* ... */ }
Derived d;
func(d); // 发生对象切片,只拷贝了d中的Base部分
当你把派生类对象按值传给接收基类对象的函数时,C++会尝试用派生类对象初始化基类对象,这个过程叫切片,多余的派生类成员被“切掉”了。很多人第一次遇到时觉得很玄,其实从内存布局角度看是必然操作——基类对象里根本没有存放派生类成员的空间,不切片才奇怪。
2.2 为什么protected不是“对外公开”
还有一点和内存布局紧密相关:protected成员到底有什么用。网上的说法往往很模糊,许多人记成“protected的成员,外部不能访问,派生类可以访问”。这句话正确,但不够本质。
从访问控制角度来说,protected和public、private一样,属于编译期检查规则,它不影响内存布局,只决定哪些代码能通过名字访问这些成员。Base里的m_a是protected,所以Derived的成员函数里可以写m_a,而main函数里不能直接写d.m_a。这是编译器做静态检查的结果,和程序运行时一点关系都没有。
实际开发中我见过不少把成员全部设成protected的做法,理由是“方便派生类访问”。这个习惯很危险,因为它等于开了一个口子:任何一个新写的派生类都可以直接读写这些成员,类自身的不变式很容易被破坏。比较健康的做法是基类提供受保护的成员函数或接口,把真正需要保护的字段保持私有。比如上面例子里,如果派生类确实需要修改m_a,应该通过基类提供的setA()方法去改,不要在派生类里直接碰成员变量。这不是教条,是血泪经验——被继承的私有成员暴露出去后,后续维护的每一个子类都可能在不经意间破坏基类假设。
3. 三种继承方式:public、protected、private各有各的坑
3.1 访问级别调整:继承方式就是对基类成员的“二次贴标签”
C++规定,类继承时可以用public、protected、private三种继承方式,它们决定的是基类成员在派生类里的访问级别上限。理解这件事最清晰的方式是把它当成一个查表操作:
| 基类成员访问级别 | public继承后在派生类中 | protected继承后在派生类中 | private继承后在派生类中 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
表里的规则可以一句话总结:取“成员自身级别”和“继承方式级别”中更严格的哪个。public继承最宽松,什么都不降;protected继承把基类的public成员降为protected;private继承把所有能访问到的成员都降为private。
这段规则几乎每个C++教材都会讲,但实践中还是有大量人对它理解不深。我经常看到新手这样写:
cpp复制class Base {
public:
void f() {}
};
class Derived : private Base {
public:
void g() {
