【C++】对 C++ 祛魅——设计中的那些缺陷

106 阅读4分钟

C++ 是一门伟大的语言。它为高性能系统编程、游戏引擎、数据库内核等领域提供了强大的工具,同时又保留了 C 的底层能力。
然而,伟大不等于完美。C++ 作为一门历经 40 多年演化的语言,在一开始的时候,其实并没有那么好用~就算到现在,它依旧还是有诟病之处。

今天就来总结一下~


1. 向后兼容

我们都知道,C++是基于C语言而进行设计的。

C++ 在设计之初就决定与 C 语言完全向后兼容,这虽然让 C 程序员能够平滑迁移,但是有很多本身C语言就存在的问题现在依然还是有。

典型问题:

  • 预处理器仍然存在
    #define#include#ifdef 等机制直接在编译前替换文本,没有类型安全,也容易造成宏污染。
  • 指针的双面性
    既有类型安全的引用,也有 C 风格的裸指针,还能混用,导致内存管理难度大大提高。
  • 数组退化
    这个局限于C的数组,如果你不想使用,可以使用C++的。

这些设计在现代语言(Rust、Go、Java)里早就被规避掉,但在 C++ 里仍然保留,只是通过标准库来进行这个查漏补缺了。


2. 语法!复杂!不直观!

C++ 试图在一门语言里同时容纳面向过程、面向对象、泛型编程、元编程等多种范式,是的,这是一门自由度很高的语言,但是随之而来的就是复杂性与不直观性。

举个例子:

template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
func(T t) { return t; }

这段代码只是为了限制模板参数必须是整数类型,但用了三行代码来写。

这种复杂性在模板元编程中尤为突出。那么会导致什么呢?

  • 错误信息冗长且难以理解;
  • 学习成本陡增;
  • 编译时间显著增加。

3. 多继承与菱形继承

多继承本意是提供更灵活的代码复用方式,但它引入了菱形继承问题,必须用“虚继承”来解决。

其实现在就是因为菱形继承的问题,多继承都没啥人用了。

菱形继承问题:

class A { int x; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; // D 中有两份 A

我们一般使用接口+组合实现一些需要使用多继承实现的问题,有效规避菱形继承。


4. 异常机制

C++ 提供了运行时异常(throw/catch),但在实际项目中,异常往往被禁用(特别是在游戏开发、嵌入式、实时系统中)。

原因:

  • 异常机制在底层实现上依赖栈展开(stack unwinding),会带来额外性能开销;
  • 编译器生成的异常处理表增加二进制体积;
  • 异常会破坏函数调用的“局部性”,调试难度增加。

因为我们的项目一般来说是要以高效率位目的的,那么异常如果会带来额外开销,其实是得不偿失的。


5. 手动内存管理的隐性成本

虽然现代 C++ 引入了智能指针(std::unique_ptrstd::shared_ptr),但裸指针仍然广泛存在。

问题是:

  • 手动 new/delete 极易导致内存泄漏和悬垂指针;
  • shared_ptr 虽然解决了生命周期问题,但增加了引用计数的开销;
  • 循环引用问题仍然存在(需要 weak_ptr 解决)。

相比之下,像 Rust 这样的语言在语言层面就引入了所有权机制,根本避免了悬垂指针和双重释放的问题。


6. 编译速度与构建系统的老大难

C++ 的编译速度慢也算是人尽皆知了。

  • 头文件机制导致巨量冗余编译(同一个头文件可能被不同源文件重复编译成百上千次);
  • 模板代码在实例化时会复制到不同的编译单元,增加编译量;
  • 编译依赖关系错综复杂,稍微改个公共头文件,整个项目都要重编。

虽然 C++20 引入了 Modules 试图解决头文件问题,但是你想让使用了这么多年C++的程序员一下子转换过来,还需要点时间。


C++是世界上最优秀的语言之一,甚至可以说它的缺点在它的优点之下,显得很不起眼。

但是它缺陷造成的问题可能往往会阻碍一个项目的良好发展,所以我们适当了解一下,然后知道大致的解决方案还是有必要的~