
别想太多只管去面
21 天冲击大厂第一周 c++高频面试解析第二天开始来了

众所周知,
C 语言不支持
•
函数重载:需要基于参数类型的重载决议+符号改名;C 的链接模型不支持,C++ 标准规定了。
•
模板(静态多态):需要编译期实例化机制;这是 C++ 的核心编译期元编程能力,C 语言规范无此机制。
•
虚函数(动态多态):需要对象模型(vptr/vtable)、RTTI、调用约定支持;C 没有对象模型,C++ 规定了并由编译器生成
C++ 原生支持。
按理说,大二学完就能胜任 C 项目,甚至用 C 语言应对考研也没问题。
结果 学校一开 C++ 课,感觉 c++更高级的 误区中(很多工能不是 c++实现的,依赖编译器等其他库实现)

c++位置
现实是在电信等传统企业,
•
前端设计使用 java,后端设计 c++,不断维护老旧项目,
•
根本用不上特性,基本 if else 判断,跟谈不上架构了,
一入宫门深似海,感觉是 历史的倒退,
然后 跳槽去互联网大厂工作 0-5 年同学 工作面试,十分重视 c++基础知识,
别想太多只管去面: 第二天 函数篇开始 欢迎留言讨论

图1-程序员的自我修养_——链接装载与库
根据 图 1- 你想到
•
c 语言是根据函数名称产生编译符号
•
c++ 通过函数名称,参数,返回值 通过
•
因此 c++支持函数重载
堆栈中发现了一个特别长的函数 _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc,使用 c++filt 命令来还原函数:
$ c++filt _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::compare(char const*) const
符号可以通过 nm 命令获取
在升级一下
本章所指的“二进制兼容性”是在升级(也可能是bugfix)库文件的时候,不 必重新编译使用了这个库的可执行文件或其他库文件,并且程序的功能不被破坏
•
C ABI 使用平坦符号名(无 name mangling)易于被 Python/Rust/Go 等通过 直接调用。
•
万一要跨语言怎么办?很简单,暴露 C 语言的接口。Java 有 JNI 可以调用 C 语言的代码;Python/Perl/Ruby 等等的解释器都是 C 语言编写的,使用 C 函数也不在话下
•
很多 C++/Rust 库对外暴露 C ABI 接口以获得“二进制兼容”和“长期稳定”的好处
什是ABI,注意不是 API,
•
基础功能库的方式提供例如 glibc,使用者需要调用
•
这里不考虑云,微服务,只考虑单机 Linux 系统你发现 so 二进制库
•
更多阅读:陈硕 C++ 工程实践(4):二进制兼容性

对比
•
操作系统的system call可以看成Kernel与User space的interface,kernel在这 个意义下也可以当成sharedlibrary,
•
你可以把内核从2.6.30升级到2.6.35,而不需要 重新编译所有用户态的程序。
内核选择 C ABI,更稳定。
若要长期稳定的二进制兼容(尤其内核/驱动/系统调用层面),优先 C ABI。
为什么 C ABI 更稳
•
符号简单(无 name mangling),跨编译器/版本更可控
•
调用约定/数据布局明确,平台文档化,几十年基本不变
•
无异常/RTTI/模板实例化等跨边界不确定因素
•
Linux 等操作系统对“系统调用 ABI”基本冻结,长期兼容
为什么 C++ ABI 不稳
•
多套 ABI:Itanium(Unix-like)与 MSVC(Windows)差异大
•
标准库/容器/std::string/异常模型随实现与版本变化
•
类布局/vtable/内联/模板实例化对编译器与选项敏感
•
小版本升级也可能破坏二进制兼容
•
以虚函数作为接口在二进制兼容性方面有本质困难:“一旦发布,不能修改”
•
C++ 工程实践(4):二进制兼容性

https://www.chenshuo.com/book/
第 10 章 C++ 编译链接模型精要10.1 C 语言的编译模型及其成因10.1.1 为什么 C 语言需要预处理10.1.2 C 语言的编译模型10.2 C++ 的编译模型10.2.1 单遍编译10.2.2 前向声明10.3 C++ 链接(linking)10.3.1 函数重载10.3.2 inline 函数10.3.3 模板10.3.4 虚函数10.4 工程项目中头文件的使用规则10.4.1 头文件的害处10.4.2 头文件的使用规则10.5 工程项目中库文件的组织原则10.5.1 动态库是有害的10.5.2 静态库也好不到哪儿去10.5.3 源码编译是王道第 11 章 反思 C++ 面向对象与虚函数11.1 朴实的 C++ 设计11.2 程序库的二进制兼容性11.2.1 什么是二进制兼容性11.2.2 有哪些情况会破坏库的 ABI11.2.3 哪些做法多半是安全的11.2.4 反面教材:COM11.2.5 解决办法11.3 避免使用虚函数作为库的接口11.3.1 C++ 程序库的作者的生存环境11.3.2 虚函数作为库的接口的两大用途11.3.3 虚函数作为接口的弊端11.3.4 假如 Linux 系统调用以 COM 接口方式实现11.3.5 Java 是如何应对的11.4 动态库接口的推荐做法11.5 以 boost::function 和 boost::bind 取代虚函数如果你打算新写一个 C++ library,那么通常要做以下几个决策:
•
以什么方式发布?动态库还是静态库?(本文不考虑源代码发布这种情况,这其实和静态库类似。)
•
以什么方式暴露库的接口?可选的做法有:以全局(含 namespace 级别)函数为接口、以 class 的 non-virtual 成员函数为接口、以 virtual 函数为接口(interface)。
•
Java 程序员没有这么多需要考虑的,直接写 class 成员函数就行,最多考虑一下要不要给 method 或 class 标上 final。也不必考虑动态库静态库,都是 .jar 文件。
接口类型 | 客户端维护成本 | 原因简述 |
|---|---|---|
Virtual 函数(interface) | 高 | 新功能需新 interface,多版本管理复杂 |
Non-virtual + Pimpl | 低 | 可原地扩展,客户端无需改动已有代码 |
Virtual 函数作为库接口(interface)
假设库用 virtual 函数提供功能:
class Graphics {
public:
virtual void drawLine(int x0,int y0,int x1,int y1);
virtual void drawRectangle(int x0,int y0,int x1,int y1);
};
•
新增功能:
•
如果要新增 drawLine(double x0,double y0,double x1,double y1):
•
不能直接加到原来的 Graphics 类(会破坏 vtable,导致客户端二进制不可用)
•
•
动态库里的 virtual 函数是通过 vtable offset 调用的。
•
一旦你在原来的类里新增 virtual 函数,vtable 排列就变了,客户端旧的二进制程序仍然调用旧 vtable offset,会调用错误函数
•
必须创建新 interface:
class Graphics2 : public Graphics {
virtual void drawLine(double x0,double y0,double x1,double y1);
};
•
客户端影响:
Non-virtual 成员函数 + Pimpl
假设库用普通成员函数,且内部用 Pimpl 封装实现:
class Graphics {
class Impl;
std::unique_ptr<Impl> impl;
public:
void drawLine(int x0,int y0,int x1,int y1);
void drawRectangle(int x0,int y0,int x1,int y1);
};
•
新增功能:
•
可以在 Graphics 类原地增加新函数:
void drawLine(double x0,double y0,double x1,double y1);
•
内部实现放在 Impl 中,客户端不需要修改现有对象
•
客户端影响:
•
旧功能继续可用,不受影响
C语言不支持函数重载,但是提供 2 个方式解决多态问题
•
库 方式 在链接时候 提供强弱符号 参考 c++ new 实现
•
定义全局数组,里面函数指针方式 存储
回到 c++,c++支持函数重载,带来便利同时 带来复杂性 在继承情况 函数被隐藏情况
C++中
•
函数重载(Overloading)
•
重写(Overriding)
•
覆盖(Hiding)
•
C++11的 override关键字能帮助编译器检查是否成功覆盖
特性 | 函数重载 (Overloading) | 函数覆盖/重写 (Overriding) | 函数隐藏 (Hiding) |
|---|---|---|---|
作用域 | 同一个类或作用域内 | 分别位于基类和派生类中 | 分别位于基类和派生类中 |
函数名 | 相同 | 相同 | 相同 |
参数列表 | 必须不同 | 必须相同 | 可以相同,也可以不同 |
返回类型 | 可以不同 | 在C++11中,派生类函数返回类型必须相同或为协变类型 | 可以不同 |
virtual关键字 | 可有可无 | 基类函数必须有 | 与虚函数无关,两种情况都会触发隐藏 |
•
函数隐藏是由函数名相同触发的名称屏蔽机制,
•
而虚函数和覆盖是为了实现动态多态。
•
虚函数本身不解决隐藏问题
#include <iostream>
using namespace std;
class Base {
public:
void func(int x) { } // 非虚函数,版本1
virtual void func(double d) { } // 虚函数,版本2
};
class Derived : public Base
{
public:
// 情况1:参数不同(const char*),隐藏了Base中所有的func函数
void func(const char* s) { }
};
void test1()
{
Derived d;
d.func("Hello"); // 正确:调用Derived::func(const char*)
// d.func(10); // 错误!Base::func(int) 已被隐藏
// d.func(3.14); // 错误!Base::func(double) 也被隐藏了
// 必须使用作用域解析运算符来显式调用基类被隐藏的函数
d.Base::func(10); // 正确:调用Base::func(int)
d.Base::func(3.14); // 正确:调用Base::func(double)
}

c++对象模型

每个章节看 解决 90% c++ 面试
line a =b --拷贝构造函数
a =b 调用 ---复制operater 操作
类结构 | 对象内含 vptr 数量 | 对应 vtable 数量 | vptr 来源 | vtable 所属 |
|---|---|---|---|---|
单继承(Base→Derived) | 1 | 1 | 继承自 Base | Derived 重新生成 |
多重继承(Base1, Base2→Derived) | 2 | 2 | 分别继承自 Base1、Base2 | Derived 重新生成两张表 |
虚继承(virtual Base) | 通常 1(共享) | 1 | 来自虚基类 | 由最派生类统一生成 |
Derived d; // Derived : Base
内存布局:
+-----------------+
| Base 子对象 |
| +-------------+|
| | vptr ------> Derived::vtable |
| +-------------+|
| Base 成员数据 |
+-----------------+
| Derived 成员数据 |
+-----------------+
说明:
- Base 子对象里有一个 vptr,指向 Derived 的 vtable(因为 Derived 覆盖了 Base 的虚函数)。
- Derived 本身没有额外 vptr。
vtable(Derived):
Derived::vtable:+---------+| f() -> Derived::f |+---------+
struct Base1 { virtual void f(); };
struct Base2 { virtual void g(); };
struct Derived : Base1, Base2 {
void f() override;
void g() override;
};
Derived d; // Derived : Base1, Base2
内存布局:
+-----------------+
| Base1 子对象 |
| +-------------+|
| | vptr ------> Derived::Base1_vtable |
| +-------------+|
| Base1 成员数据 |
+-----------------+
| Base2 子对象 |
| +-------------+|
| | vptr ------> Derived::Base2_vtable |
| +-------------+|
| Base2 成员数据 |
+-----------------+
| Derived 成员数据 |
+-----------------+
说明:
- 每个基类子对象都有独立 vptr。
- 每个 vptr 指向对应派生类生成的新 vtable。
- Derived 自身没有额外 vptr(除非声明新的虚函数)。
Derived vtable(覆盖虚函数):
Derived::Base1_vtable:
+-----------------+
| f() -> Derived::f |
+-----------------+
Derived::Base2_vtable:
+-----------------+
| g() -> Derived::g |
+-----------------+
Base / \ A B \ / DerivedDerived├─ A::Base.x├─ B::Base.x└─ Derived自己的成员struct A : virtual Base {};struct B : virtual Base {};struct Derived : public A, public B {}Derived├─ A (虚基指针 vbptr -> 指向共享的Base)├─ B (虚基指针 vbptr -> 指向共享的Base)├─ 共享的 Base.x(只有一份)└─ Derived自己的成员•
Derived 对象里有 两份 Base.x,访问 x 会二义性
✅ 要点
1
每个基类子对象一张虚表
2
派生类覆盖虚函数 → 生成新 vtable,vptr 指向新表
3
派生类自身不产生额外 vptr(除非自己声明虚函数)
4
客户端通过基类指针调用虚函数时,vptr 定位到对应派生类函数
虚继承是编译器层面的机制, 通过引入 虚基表(vbtable) 和 虚基指针(vbptr) 来确保基类唯一。
•
构造函数是虚函数吗?
•
不是。C++ 禁止构造函数为虚函数(对象尚未构造完成包括**虚基指针(vbptr,无法进行动态分派)
•
构造函数能调用虚函数吗?
•
能写,但不会动态分派。构造期间的虚函数调用会绑定到“当前正在构造的类”的版本,派生类版本不会被调用。
•
析构函数是虚函数吗?
•
可以是,且“用于多态基类时应当为虚”。否则通过基类指针删除派生对象会导致未定义行为(派生析构不被调用)。
•
析构函数能调用虚函数吗?
•
能写,但同样不会动态分派。
•
析构期间调用虚函数会绑定到“当前正在析构的类”的版本;派生部分已被销毁,派生重写不会被调用
要点
•
核心思想:用组合 + 回调(可调用对象)替代继承 + 虚函数。
•
工具演进:优先用 C++11 的 std::function + lambda。
思想 | 关键词 | 优点 |
|---|---|---|
旧:虚函数体系 | 继承 + 重写 | 运行时多态,但强耦合、脆弱 |
新:可调用对象 | 组合 + 回调 | 更灵活、更稳定、更符合现代 C++ |
背景:为什么要用 std::function / lambda 替代虚函数
传统面向对象的做法:
struct Animal {
virtual void speak() = 0;
virtual ~Animal() = default;
};
struct Dog : Animal {
void speak() override { std::cout << "Woof\n"; }
};
调用:
std::unique_ptr<Animal> a = std::make_unique<Dog>();
a->speak();
✅ 优点:运行时多态 ❌ 缺点:
•
需要继承体系
•
引入 vtable、vptr,破坏 ABI 稳定
•
增加编译耦合(修改基类需重新编译所有派生类)
•
不易组合(类层次一旦固定很难变换行为)
取而代之,可以用 组合(has-a) + 函数回调(callable):
#include <functional>
#include <iostream>
struct Animal {
std::function<void()> speak;
};
使用时:
Animal dog{ [] { std::cout << "Woof\n"; } };
Animal cat{ [] { std::cout << "Meow\n"; } };
dog.speak();
cat.speak();
👉 没有继承,也没有虚函数表。
行为(speak)被 注入为可调用对象,即“策略”(Strategy Pattern)。
特性 | 虚函数实现 | std::function 实现 |
|---|---|---|
多态类型 | 编译期定义,运行期通过 vtable 调度 | 运行期通过函数对象类型擦除调度 |
内存结构 | 每个对象含 vptr | 对象中含 std::function 封装(内部存指针 + 调用器) |
扩展方式 | 通过继承和 override | 通过组合和注入新 callable |
二进制兼容性 | 改一个虚函数签名会破坏 ABI | ABI 稳定,不影响接口结构 |
性能 | 一次 vtable 间接调用 | 一次函数指针间接调用(几乎一样) |
应用场景举例
struct Sorter {
std::function<void(std::vector<int>&)> sort;
};
Sorter quicksorter{ [](auto& v){ std::sort(v.begin(), v.end()); } };
Sorter reverseSorter{ [](auto& v){ std::sort(v.rbegin(), v.rend()); } };
std::vector<int> v{3,1,2};
quicksorter.sort(v);
struct Button {
std::function<void()> onClick;
};
Button b;
b.onClick = [] { std::cout << "Clicked!\n"; };
b.onClick();
以前这类场景往往用 virtual void onClick()。
发布动态库时,只暴露:
class Engine {
public:
void setOnEvent(std::function<void(int)> cb);
};
客户端可以自由绑定 lambda,无需继承、无需重新编译。
💡一句话总结:
C 语言通过“函数指针 + 结构体组合”就能实现多态。
这正是 C++ 虚函数表(vtable)的本质来源。
C++ 概念 | C 实现方式 | 说明 |
|---|---|---|
virtual 函数 | 函数指针 | 存在于“函数表”中 |
vtable | 结构体函数表(ShapeVTable) | 每个类型一个表 |
vptr | 指向 vtable 的指针 | 每个对象一个 vptr |
override | 不同子类提供不同函数表 | 替换函数指针 |
Shape* s = new Circle | 向上转型为基类指针 | 通过 vptr 调度行为 |
我们来结合 Linux 内核中的文件系统 VFS 层(Virtual File System)
—— 内核是如何用 C语言 + 函数指针 实现“面向对象 + 多态”的。
Linux 支持 ext4、xfs、btrfs、procfs、tmpfs ……
但所有系统调用(open, read, write 等)
都通过统一的接口访问。
也就是说:
用户调用
read(fd, buf, len)→ 内核根据 fd 找到文件对象 → 文件对象内部指向不同文件系统实现的函数表 → 调用file->f_op->read(...)同样的调用,不同的实现。 这就是“多态”。
C++ 概念 | 内核对应结构 | 说明 |
|---|---|---|
class Shape | struct file_operations | 定义接口(read/write/...) |
virtual 函数 | 函数指针成员 | 每个文件系统定义自己的函数表 |
vtable | file_operations 结构体实例 | 每个文件系统有一份表 |
vptr | struct file → f_op | 每个打开的文件对象持有指针 |
override | 不同文件系统实现不同函数 | 替换函数表内容 |
动态派发 | file->f_op->read(file, buf, len, pos) | 根据 f_op 指针动态选择实现 |
struct file_operations(来源:include/linux/fs.h)
struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// ... 其他很多操作
};
这就像是一个“虚函数表”(vtable)。
fs/ext4/file.c):const struct file_operations ext4_file_operations = {
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.open = ext4_file_open,
.release = ext4_release_file,
};
fs/tmpfs/file.c):const struct file_operations shmem_file_operations = {
.read_iter = shmem_file_read_iter,
.write_iter = shmem_file_write_iter,
.open = shmem_file_open,
};
这就像 C++ 中不同派生类实现了自己的虚函数。
当用户打开一个文件时,内核会创建 struct file 对象:
struct file {
const struct file_operations *f_op; // vptr!!
void *private_data;
...
};
比如:
•
打开 ext4 文件 → f_op = &ext4_file_operations
•
打开 tmpfs 文件 → f_op = &shmem_file_operations
调用过程示意
步骤 | 动作 | C 语言逻辑 | C++ 类比 |
|---|---|---|---|
1 | 用户调用 read(fd) | 内核找到 file | 找到对象 |
2 | 内核执行 file->f_op->read_iter(...) | 根据函数表调用 | 调用虚函数 |
3 | 具体执行 ext4_file_read_iter() 或 shmem_file_read_iter() | 动态分发 | 运行时多态 |
👉 即使是纯 C 语言,read() 的行为也根据文件类型动态变化。
struct file
│
├──> f_op ──► ext4_file_operations
│ ├── read_iter → ext4_file_read_iter()
│ ├── write_iter → ext4_file_write_iter()
│
└──> f_op ──► shmem_file_operations
├── read_iter → shmem_file_read_iter()
├── write_iter → shmem_file_write_iter()
总结对照表
面向对象术语 | 内核中体现 | 说明 |
|---|---|---|
类定义 | struct file_operations | 接口定义 |
子类 | 各文件系统定义自己的 ops 表 | 不同实现 |
对象实例 | struct file | 拥有指针指向 vtable |
虚函数 | 函数指针 | 通过函数表调用 |
继承/多态 | 函数表组合 | 运行时动态绑定 |