C++ 相比 C 多了什么:语言能力、并发内存序与 ABI 对比 “C++ 比 C 多了什么”表面是语法清单,实质是设计哲学 差异:C 强调可移植、贴近硬件的过程式 表达;C++ 在兼容 C 子集的前提下,提供多范式抽象 与工程化标准库 ,并逐步形成更完整的并发与类型系统 。
本文用对照表、示意代码、Mermaid 与 ASCII 分层说明:语言与类型 、标准库与资源管理 、并发与内存序 、ABI 与工程边界 。具体行为以当前C(C11/C17/C23) 与C++(C++11/17/20/23) 标准及所用编译器为准。
目录 一句话总览 关系鸟瞰:C 作为 C++ 子集(示意) 类型系统与语法对照 函数与模块化 用户自定义类型:struct / class 与 OOP 泛型:宏、void* 与模板 内存与资源:malloc、new 与 RAII 错误处理:返回值、errno 与异常 I/O 与字符串 标准库与“自带工程设施” 现代 C++ 自 C++11 起的主要特性 并发与内存序:C11 与 C++11+ ABI:为什么 C 稳、C++ 难 选型建议 附录:同功能左右对照(节选) 免责声明 一句话总览 维度 C C++ 设计取向 过程式、贴近硬件 多范式(过程 / OOP / 泛型 / 函数式风格) 抽象与类型安全 相对弱,常靠约定与审查 更强类型与编译期检查手段更多 标准库 小而基础( libc ) 大而全( STL + 线程 + 时间 + …) 资源管理习惯 手动为主 RAII + 智能指针等惯用法并发 C11 起有原子与线程标准接口;工程上 pthread 仍常见 <thread>/<mutex>/<atomic> 等与语言惯用法结合紧ABI 对外接口常以C ABI 为“ lingua franca ” 跨编译器/版本/标准库实现时ABI 风险更高
关系鸟瞰:C 作为 C++ 子集(示意) ┌─────────────────────────────────────┐ │ C++ 完整语言 │ │ ┌─────────────────────────────┐ │ │ │ 与 C 高度重叠的“C 子集” │ │ │ │ (语法/语义仍可能有细微差异)│ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘类型系统与语法对照 主题 C C++ void*与其它指针历史上常可隐式与T*互转(C++ 更严格) 一般需static_cast<T*>(p)等显式转换 boolC99 起有_Bool/<stdbool.h>的bool 原生bool、true、false 枚举 enum枚举符常泄漏到外层作用域enum class :强类型、作用域隔离const有,但语义与用法相对朴素 更细 :constexpr、成员const、重载对const敏感等nullptr用NULL(常为0)表示空指针 nullptr 类型安全复合字面量 C99 有 C++ 规则与版本相关,使用上需注意与 C 的差异
enum class示意:
// C++:Color::RED 不会污染外层名字空间 enum class Color { RED, GREEN} ; Color c= Color:: RED; 函数与模块化 能力 C C++ 函数重载 无(靠不同函数名) 同名不同参列表 默认参数 无 void f(int x = 0);引用 无(用指针) T&,部分场景语义更清晰内联 static inline等inline与 ODR、模板结合更系统命名空间 无,靠前缀libname_ namespace组织大型代码隐式函数声明 历史遗留问题(老代码) C++ 要求先声明后使用
用户自定义类型:struct / class 与 OOP 点 C 的struct C++ 的struct/class 成员函数 无(用函数指针或外部函数模拟) 可有成员函数、this 访问控制 无 public/protected/private(class默认private)构造 / 析构 无 构造函数、析构函数 继承与多态 手写 vtable 模拟 virtual、运行时多态
class Animal { public : virtual void speak ( ) { /* ... */ } virtual ~ Animal ( ) = default ; } ; class Dog : public Animal { public : void speak ( ) override { /* ... */ } } ; 泛型:宏、void*与模板 手段 优点 典型问题 C 宏 无运行时开销 无类型、难调试、易污染名字 void* + sizeof + 回调灵活 易错、靠人工保证类型一致 C++ 模板 类型安全、可编译期求值(constexpr) 编译慢、错误信息冗长
template < class T > Tmax_of ( T a, T b) { return a> b? a: b; } 内存与资源:malloc、new与 RAII 操作 C C++ 分配未初始化内存 malloc::operator new(不一定等价于new T)构造对象 手动在malloc内存上“放置”并初始化(若模拟 C++) new T :分配并调用构造函数释放 free(不调用析构)delete :析构 + 释放异常路径下释放 易遗漏 RAII :析构自动释放
// C++:离开作用域即关闭(示意) struct File { FILE* fp{ } ; explicit File ( const char * path) : fp ( std:: fopen ( path, "rb" ) ) { } ~ File ( ) { if ( fp) std:: fclose ( fp) ; } } ; 错误处理:返回值、errno与异常 风格 C C++ 返回错误码 非常常见 仍常见(尤其 API 边界) errnolibc 错误习惯 仍可用 异常try/throw/catch 无 有;与 RAII 组合做栈展开清理
工程上 C++ 也常采用Expected / outcome / 错误码 风格;异常并非“必须用”,但是一种语言级 机制。
I/O 与字符串 主题 C C++ 格式化 I/O printf/scanfstd::cout/std::format(C++20)等类型安全 printf格式串与实参易不一致iostream /format相对更安全 字符串 char*、strcpy等std::string 动态增长与接口
标准库与“自带工程设施” C 常见:libc + 手写数据结构 C++ 常见:libc + libstdc++/libc++/MS STL + 算法/容器/线程能力 C 常见路径 C++ 常见路径 动态数组 malloc/realloc+ 长度字段std::vector关联表 手写哈希 / 第三方库 std::unordered_map/std::map排序查找 qsort/bsearch或手写std::sort/std::binary_search线程与锁 pthread std::thread/std::mutex时间 time.hstd::chrono
现代 C++ 自 C++11 起的主要特性 特性 作用直觉 auto减少冗长类型名 范围for 遍历容器更简洁 Lambda 就地定义回调 右值引用 /std::move 资源转移、减少拷贝 智能指针 显式所有权、减少泄漏 constexpr编译期计算与约束 Concepts(C++20) 模板约束更可读 Modules(C++20) 改善编译边界(≠ ABI 银弹)
并发与内存序:C11 与 C++11+ 结论先说清 C11 起:<stdatomic.h>提供_Atomic、atomic_int等类型,以及memory_order_* ;可用atomic_store_explicit/atomic_load_explicit 等带内存序的原子操作。C++11 起:std::atomic与std::memory_order 在工程资料与标准库封装上更成体系,并与线程、锁、条件变量 同一套“现代 C++”叙事结合。因此差异不仅是“有没有内存序”,而是:语言 + 标准库 + 惯用法 的整体体验。
C++ 发布-订阅示例(acquire/release) # include <atomic> std:: atomic< int > flag{ 0 } ; int data= 0 ; // 线程 A data= 42 ; flag. store ( 1 , std:: memory_order_release) ; // 线程 B if ( flag. load ( std:: memory_order_acquire) == 1 ) { // 典型模式下可依赖与 data 的同步关系(具体证明依赖标准内存模型) } C11 对应思路(explicit 内存序) # include <stdatomic.h> atomic_int flag; int data; /* 线程 A */ data= 42 ; atomic_store_explicit ( & flag, 1 , memory_order_release) ; /* 线程 B */ if ( atomic_load_explicit ( & flag, memory_order_acquire) == 1 ) { /* ... */ } 常见memory_order对照(C++ 命名;C11 枚举同名思想) 内存序 直觉 relaxed只关心原子性,几乎不管顺序 acquire/release成对建立“先发生于”关系 acq_rel读改写一条指令上的组合 seq_cst全局顺序一致,最保守
线程 B 共享内存 线程 A 线程 B 共享内存 线程 A 写 data release 写 flag acquire 读 flag 读 data(与同步配对时可见)
ABI:为什么 C 稳、C++ 难 ABI 决定二进制能否对接 :调用约定、符号修饰、对象布局、异常传播、标准库内部结构等。
C:为什么常成为“跨边界标准” 原因 说明 对象模型简单 无类布局、vtable 符号名简单 一般接近源函数名 生态惯性 OS、动态库、FFI 长期以C ABI 暴露
C++:高风险点 风险 后果 Name mangling 同名重载经修饰后符号不同;编译器间规则不同 类布局 / vtable 继承、虚函数、对齐填充导致布局差异 异常与 RTTI 运行时实现相关 标准库实现 libstdc++ / libc++ / MS STL 内部对象布局不同
C 调用: add ----符号表----> add C++ 调用: foo(int) / foo(double) ----mangling----> _Z3fooi / _Z3food (示意,非唯一规则)工程实践:对外 C ABI,对内 C++ extern "C" int api_add ( int a, int b) ; // 以 C 链接导出,便于跨语言/跨编译器边界 选型建议 场景 倾向 操作系统内核、极小编译单元、ABI 极敏感边界 C 或C ABI 大型应用、复杂领域模型、强依赖 STL 与抽象 C++ 跨语言 SDK C ABI + C++ 实现 最常见
附录:同功能左右对照(节选) 以下片段用于并排体会 同一意图在 C 与 C++ 中的写法差异:C 侧重显式状态与资源路径 ,C++ 侧重封装与标准库 。为可读性省略部分头文件与极端错误分支;C 侧线程示例以 POSIXpthread示意 (Windows 原生开发常改用 Win32 线程或其它库,思路相同)。
1. 动态数组末尾追加元素 侧 要点 C 自管data / size / capacity,realloc失败要回滚或报错 C++ vector内聚策略与异常/强保证(依实现与操作而定)
C(示意):
# include <stdlib.h> static int push_int ( int * * data, size_t * len, size_t * cap, int x) { if ( * len>= * cap) { size_t ncap= * cap? * cap* 2u : 4u ; int * nd= ( int * ) realloc ( * data, ncap* sizeof ( int ) ) ; if ( ! nd) return - 1 ; * data= nd; * cap= ncap; } ( * data) [ ( * len) ++ ] = x; return 0 ; } /* 使用:int *a=NULL; size_t n=0,cap=0; push_int(&a,&n,&cap,42); … free(a); */ C++(示意):
# include <vector> std:: vector< int > a; a. push_back ( 42 ) ; // 容量增长策略由实现提供 2. 读文件并确保关闭 侧 要点 C 每条错误路径都要记得fclose,常用goto cleanup或层层if C++ 流对象析构关闭句柄;失败用bool/ 异常策略由项目约定
C(示意):
# include <stdio.h> int read_byte_count ( const char * path, long * out_bytes) { FILE* fp= fopen ( path, "rb" ) ; if ( ! fp) return - 1 ; if ( fseek ( fp, 0 , SEEK_END ) != 0 ) { fclose ( fp) ; return - 2 ; } long sz= ftell ( fp) ; fclose ( fp) ; if ( sz< 0 ) return - 3 ; * out_bytes= sz; return 0 ; } C++(示意):
# include <fstream> bool read_byte_count ( const char * path, long & out_bytes) { std:: ifstreamin ( path, std:: ios:: binary| std:: ios:: ate) ; if ( ! in) return false ; out_bytes= static_cast < long > ( in. tellg ( ) ) ; return true ; } // 析构关闭文件,无需手写 fclose 3. 互斥保护共享计数器(两线程各加若干次) 侧 要点 C pthread_mutex_*;锁配对靠人工;勿漏unlockC++ std::mutex+lock_guard/unique_lock;作用域结束自动解锁
C + pthread(示意):
# include <pthread.h> static pthread_mutex_t g_mu= PTHREAD_MUTEX_INITIALIZER; static long g_count= 0 ; static void * worker ( void * arg) { int n= * ( int * ) arg; for ( int i= 0 ; i< n; ++ i) { pthread_mutex_lock ( & g_mu) ; g_count++ ; pthread_mutex_unlock ( & g_mu) ; } return NULL ; } /* main:创建两个线程 join;此处略 */ C++(示意):
# include <mutex> # include <thread> std:: mutex g_mu; long g_count= 0 ; void worker ( int n) { for ( int i= 0 ; i< n; ++ i) { std:: lock_guard< std:: mutex> lock ( g_mu) ; ++ g_count; } } /* main:std::thread t1(worker, n), t2(worker, n); t1.join(); t2.join(); */ 附录小结表 任务 C 常见负担 C++ 常见收益 动态数组 realloc、容量策略、错误码vector统一接口文件句柄 多出口路径下配对关闭 RAII 自动释放 锁 显式 lock/unlock 作用域守卫类
免责声明 本文为技术对比与学习整理。C 与 C++ 兼容细节 (例如const、链接、inline、VLA 等)随标准版本变化;ABI 与优化级别 强相关。生产结论请以目标工具链文档与实测为准。
主题:C、C++、类型系统、模板、RAII、并发、内存序、ABI。