一句话核心结论 :C++ 效率优化的”下半部分”——重载避免隐式转换 、op= 优于 op (少一次拷贝)、不同库的性能差异巨大 、虚函数 + 多重继承 + RTTI 的”真实成本” (vptr + 间接寻址 + 阻止 EBO)。这 4 个条款是性能调优的”最后一块拼图”。
系列导航 前言:效率优化的”4 大工具” 第 5 篇讲了 5 大”软优化”——80-20、lazy、摊销、临时对象、RVO。
本篇讲 4 大”硬优化”——重载、op=、库选择、虚函数成本。
graph TB
A["C++ 效率(下)"] --> B["重载避免隐式转换\n(条款 21)"]
A --> C["op= 优于 op\n(条款 22)"]
A --> D["其他库的成本\n(条款 23)"]
A --> E["虚函数成本\n(条款 24)"]
B -.->|类型精确| B1["const char* 重载"]
C -.->|少一次拷贝| C1["a += b vs a = a + b"]
D -.->|库选择| D1["std::list vs std::vector"]
E -.->|vptr| E1["虚函数 + MI + RTTI"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333一、条款 21:利用重载技术避免隐式类型转换 1.1 经典反例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class String {public : String (const char * s); }; class Database {public : void add (const String& s) ; }; Database db; db.add ("hello" );
问题 :
db.add("hello") 中 "hello" 是 const char*隐式构造临时 String("hello") 拷贝/移动到 add 参数 3 次操作(构造 + 移动 + 析构) 1.2 解决方案:重载 const char* 1 2 3 4 5 6 7 8 9 10 class Database {public : void add (const String& s) ; void add (const char * s) ; }; Database db; db.add ("hello" ); db.add (String ("hello" ));
1.3 实战:string 的常见重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class BetterString { std::string s_; public : BetterString (const char * s) : s_ (s) {} BetterString (const std::string& s) : s_ (s) {} size_t find (const char * s) const ; size_t find (const std::string& s) const ; size_t find (char c) const ; };
1.4 案例:complex 的算术 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Complex {public : Complex (double r, double i); Complex operator +(const Complex& rhs) const ; }; Complex c (1.0 , 2.0 ) ;Complex d = c + 3.0 ; Complex e = 3.0 + c; class Complex {public : Complex operator +(const Complex& rhs) const ; Complex operator +(double r) const ; };
1.5 关键启示 隐式转换 = 临时对象 ——可能很贵重载精确类型 ——避免隐式const char* 重载 是常见模式不要”为了避免重载”放过隐式 ——性能是硬指标二、条款 22:考虑以操作符复合形式(op=)取代其单独形式(op) 2.1 复合 vs 单独的差异 对比 :
维度 a + ba += b临时对象 1 个 0 个 拷贝次数 1~2 0 实现 a + b 返回临时a += b 原地
2.2 性能对比 1 2 3 4 Widget a, b; a = a + b; a += b;
2.3 实战:实现 += 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Matrix { double data_[N][N]; public : Matrix& operator +=(const Matrix& rhs) { for (int i = 0 ; i < N; ++i) { for (int j = 0 ; j < N; ++j) { data_[i][j] += rhs.data_[i][j]; } } return *this ; } Matrix operator +(const Matrix& rhs) const { Matrix tmp (*this ) ; tmp += rhs; return tmp; } };
2.4 案例:string 的 += 1 2 3 4 5 6 7 8 9 10 11 12 13 14 std::string s = "hello" ; std::string result = s + " " + "world" ; std::string result; result.reserve (s.size () + 1 + 5 ); result += s; result += " " ; result += "world" ;
2.5 关键启示 op= 优于 op ——少 1 次临时大型类尤其重要 ——80KB 拷贝 vs 原地操作基于 += 实现 + ——DRY + 性能string 用 += 优于 + ——避免临时三、条款 23:考虑使用其他程序库 3.1 不同库的性能差异 1 2 3 4 5 6 7 8 9 10 11 std::vector<int > v; std::list<int > l; v.insert (v.begin (), 1 ); l.push_front (1 ); v[500 ]; l[500 ];
3.2 实战:不同容器的”性能地图” 操作 vector list deque map unordered_map 头部插入 O(N) O(1) O(1) log N O(1) avg 尾部插入 O(1) amortized O(1) O(1) log N O(1) avg 中间插入 O(N) O(1) O(N) log N O(1) avg 随机访问 O(1) O(N) O(1) O(N) O(1) avg 查找 O(N) O(N) O(N) O(log N) O(1) avg 内存 连续 离散 分段 树 哈希
3.3 案例:std::string 的 SSO 1 2 3 4 5 6 std::string s = "hello" ; std::string big = "a" * 1000 ;
3.4 实战:不同库的选择 1 2 3 4 5 6 7 8 9 10 11 12 13 nlohmann::json j; rapidjson::Document d; simdjson::ondemand::parser p; std::cout << "log\n" ; spdlog::info ("log {}" , x); Quill::LOG_INFO ("log" ); std::valarray<double > v; Eigen::VectorXd v;
3.5 案例:标准库 vs Boost 3.6 关键启示 不同库性能差异巨大 ——可能 1000x选择合适的容器 ——vector vs list选择合适的库 ——标准库不一定最快用 benchmark 测 ——别凭直觉四、条款 24:了解 virtual functions、multiple inheritance、virtual base classes、runtime type identification 的成本 4.1 虚函数的成本 1 2 3 4 5 6 7 8 class Base {public : virtual void foo () ; };
成本 1:vptr 空间 1 2 3 4 5 6 7 8 9 10 11 class Base { int x_; virtual void f () ; }; class NoVirtual { int x_; };
成本 2:vtable 空间 1 2 3 4 5 6 7 class Base { virtual void f1 () ; virtual void f2 () ; virtual void f3 () ; };
成本 3:间接寻址 1 2 3 4 5 6 7 Base* pb = new Derived (); pb->f1 ();
性能影响 :
虚函数调用 ≈ 直接调用的 1.5-2x 慢 但编译器可以”devirtualize”——在某些场景下消除间接寻址 成本 4:阻止 EBO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Empty {};class WithEBO : private Empty { int x_; }; class WithMember { Empty e_; int x_; }; class WithVirtual { virtual void f () ; int x_; };
4.2 多重继承的成本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class A { int a_; virtual void f () ; }; class B { int b_; virtual void g () ; }; class C : public A, public B { int c_; };
成本 :
多个 vptr 指针转换需要调整(A* → B* 偏移) 4.3 虚基类的成本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class File { int data_; virtual void f () ; }; class InputFile : virtual public File { }; class OutputFile : virtual public File { }; class IOFile : public InputFile, public OutputFile { };
成本 :
虚基类指针(vbptr) 访问虚基类成员是间接寻址 构造/析构复杂 4.4 RTTI 的成本 1 2 3 4 5 6 class Base { virtual ~Base () {} }; class Derived : public Base { };Base* pb = new Derived (); auto * pd = dynamic_cast <Derived*>(pb);
成本 :
编译时:每个有虚函数的类存一个 type_info 运行时:dynamic_cast 查 type_info(strcmp) RTTI 关闭 :
1 g++ -fno-rtti myprogram.cpp
4.5 实战:什么时候用虚函数? 1 2 3 4 5 6 7 8 9 10 11 12 class Shape {public : virtual void draw () const = 0 ; virtual ~Shape () = default ; }; class Point { int x_, y_; };
4.6 替代方案:模板 + 编译期多态 1 2 3 4 5 6 template <typename T>void process (const T& x) { x.draw (); }
4.7 关键启示 虚函数 = vptr + vtable + 间接寻址 + 阻止 EBO 多重继承 = 多个 vptr + 指针调整 虚继承 = vbptr + 间接访问 RTTI = type_info + dynamic_cast 查询 性能敏感场景用模板 ——编译期多态五、4 个条款的”效率(下)”全景 graph TB
A["C++ 效率(下)"] --> B["重载避免隐式转换\n(条款 21)"]
A --> C["op= 优于 op\n(条款 22)"]
A --> D["其他库的成本\n(条款 23)"]
A --> E["虚函数成本\n(条款 24)"]
B --> B1["const char* 重载"]
C --> C1["少一次临时"]
D --> D1["容器/库选择"]
E --> E1["vptr + vtable + 间接寻址"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333六、常见误区与陷阱 6.1 误区 1:不重载 const char* 1 2 3 4 5 6 7 8 9 10 11 class Database { void add (const String& s) ; }; db.add ("hello" ); class Database { void add (const String& s) ; void add (const char * s) ; };
6.2 误区 2:用 + 而非 += 1 2 3 4 5 6 7 std::string s = a + b + c; std::string s; s.reserve (a.size () + b.size () + c.size ()); s += a; s += b; s += c;
6.3 误区 3:用 list 做随机访问 1 2 3 4 5 6 7 std::list<int > v; int x = v[500 ]; std::vector<int > v; int x = v[500 ];
6.4 误区 4:性能场景用虚函数 1 2 3 4 5 6 7 8 9 10 11 class MathOp {public : virtual double apply (double a, double b) = 0 ; }; template <typename Op>double compute (Op op, double a, double b) { return op (a, b); }
七、C++11/14/17 的演进 主题 C++98 时代 C++11/14/17/20 时代 重载 隐式转换 显式构造 + 模板重载 op= += 等同 + 移动 += 库选择 标准库 + Boost + 第三方 虚函数 必要 概念(concepts)+ 模板替代 容器 auto_ptrunique_ptr + vector字符串 std::stringstd::string + SSO哈希 无 std::unordered_*RTTI 必要 std::any / std::variant
C++17 的 std::string_view :
1 2 3 void process (std::string_view s) ; process ("hello" );
C++20 的 concepts :
1 2 3 4 5 template <typename T>concept Drawable = requires (T x) { x.draw (); };template <Drawable T>void process (const T& x) { x.draw (); }
八、面试高频考点 8.1 必背题 题目 答案要点 怎么避免隐式转换? 重载精确类型 op= vs op? op= 少一次临时 vector vs list? vector 适合随机访问;list 适合频繁插入 虚函数的成本? vptr + vtable + 间接寻址 + 阻止 EBO 多重继承的成本? 多个 vptr + 指针调整 RTTI 怎么关闭? -fno-rtti怎么减少虚函数成本? 模板 / final / 性能分析 SSO 是什么? 小字符串优化——存在 string 内部
8.2 高频追问 追问 关键点 虚函数调用比直接调用慢多少? ~1.5-2x 怎么消除虚函数? 模板 / CRTP / type erasure 虚继承的代价? vbptr + 间接访问 dynamic_cast 慢在哪? 查 type_info -fno-rtti 能做什么? 减小二进制 + 加速 移动语义 vs 拷贝? 移动更快(无实际复制)
九、配套实验 9.1 实验 1:op= vs op 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <chrono> class BigArray { static constexpr int N = 100000 ; int data_[N]; public : BigArray () : data_{} {} BigArray& operator +=(const BigArray& rhs) { for (int i = 0 ; i < N; ++i) data_[i] += rhs.data_[i]; return *this ; } BigArray operator +(const BigArray& rhs) const { BigArray tmp (*this ) ; tmp += rhs; return tmp; } }; int main () { BigArray a, b; a += b; a = a + b; return 0 ; }
9.2 实验 2:vector vs list 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> #include <vector> #include <list> #include <chrono> int main () { const int N = 100000 ; { auto start = std::chrono::steady_clock::now (); std::vector<int > v; for (int i = 0 ; i < N; ++i) { v.insert (v.begin (), i); } auto end = std::chrono::steady_clock::now (); auto us = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << "vector insert head: " << us << " μs\n" ; } { auto start = std::chrono::steady_clock::now (); std::list<int > l; for (int i = 0 ; i < N; ++i) { l.push_front (i); } auto end = std::chrono::steady_clock::now (); auto us = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << "list push_front: " << us << " μs\n" ; } return 0 ; }
9.3 实验 3:虚函数成本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <chrono> class NoVirtual {public : void foo () { } }; class WithVirtual {public : virtual void foo () { } }; int main () { std::cout << "sizeof(NoVirtual) = " << sizeof (NoVirtual) << "\n" ; std::cout << "sizeof(WithVirtual) = " << sizeof (WithVirtual) << "\n" ; return 0 ; }
9.4 实验 4:重载避免隐式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream> #include <string> class String { std::string s_; public : explicit String (const char * s) : s_(s) { } explicit String (const std::string& s) : s_(s) { } const std::string& data () const { return s_; } }; class Database {public : void add (const String& s) { std::cout << "add String: " << s.data () << "\n" ; } void add (const char * s) { std::cout << "add const char*: " << s << "\n" ; } }; int main () { Database db; db.add ("hello" ); db.add (String ("world" )); return 0 ; }
十、回到 4 条黄金法则 条款 黄金法则 21 重载精确类型——避免隐式转换 22 op= 优于 op——少一次临时23 库选择 = 性能选择——不同库差异巨大 24 虚函数 + MI + RTTI = 真实成本——性能敏感用模板
十一、结尾思考题 思考题 1 :用 perf / gprof 找你的项目虚函数调用——是热点吗?
思考题 2 :把 a = a + b 改为 a += b,实测性能差异。
思考题 3 :比较 std::vector / std::list / std::deque 在你的场景下的性能。
思考题 4 :你的项目能关闭 RTTI 吗?-fno-rtti 有什么收益?
思考题 5 :用 CRTP 改写一个虚函数体系——对比虚函数的开销。
十二、本篇速查表 主题 关键 API / 模式 适用场景 重载避免隐式 const char* 重载大型类 op= vs op a += b性能 库选择 vector / list / unordered_map 不同场景 虚函数成本 vptr + vtable 性能分析 模板替代 编译期多态 性能敏感 关闭 RTTI -fno-rtti嵌入式
十三、系列导航 下一篇 :第 7 篇《技术:虚拟构造、智能指针、引用计数、双重分派》——条款 25-31 一起讲透 C++ 高级技术:虚拟构造、对象计数、heap 限制、智能指针、引用计数、proxy class、双重分派。
行动建议 :
今天 :用 += 替换你项目里的 + 链今天 :重载你的 add / find 等接受 const char*本周 :用 benchmark 测你项目用的库——可能换库能快 10x本周 :识别你项目的虚函数成本——考虑模板替代思考 :你的项目能关闭 RTTI / 异常吗?