news 2026/10/1 12:41:46

【C++笔记】从 C 到C++:核心过渡 (上)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++笔记】从 C 到C++:核心过渡 (上)

前言

C 和 C++ 的关系经常被两种极端说法描述:一种说"C++ 就是 C 加上了类",另一种说"C++ 是全新的语言,C 的写法都不作数"。两种都不准确。C++ 确实脱胎于 C(早期叫 "C with Classes"),但它一开始就引入了不同的抽象手段:引用、重载、模板、命名空间、构造与析构函数、异常安全(exception safety)。这些东西不是语法糖,它们改变了"写代码"的基本思路。

本文是上篇,只讲从 C 迁移到 C++ 时最先需要建立的四个核心观念:头文件与标准库的名字体系、引用(reference)与指针的分工、函数重载与名字修饰(name mangling)、类型系统上的几处补强(bool、nullptr、const)。类与对象、构造析构、资源管理留到后面。

一个贯穿全文的原则:C++ 的重点不是"C 语法加 class",而是"用类型和生命周期去管理资源"。想明白这一点,后面很多"为什么非得这么写"的规则就不用死记了。

另外提醒一句:本文所有 C 代码示例都以gcc编译为准,C++ 代码以 C++17 为准,GCC 13 / Clang 17 / MSVC 19.3x 都应能通过。

一、头文件与标准库:stdio.h 与 cstdio 的差别

C 的标准库头文件是stdio.h、stdlib.h、string.h这种带.h的名字。C++ 把标准库的名字收进了std命名空间,同时提供了两套头文件名字:

C 名字C++ 名字内容所在命名空间说明
stdio.hcstdiostd推荐用cstdio,函数在std里
stdlib.hcstdlibstd含malloc、free、atoi等
string.hcstringstd含strlen、memcpy等
math.hcmathstd含sqrt、fabs等
stdbool.h无对应——C++ 自带bool,不需要它

这里有个常被忽略的细节,标准里写得很清楚:<cstdio>是否同时把名字放进全局命名空间,是实现定义的。也就是说,#include <cstdio>之后直接写printf(...),在 GCC 的 libstdc++ 上通常能编过(它同时也注入到全局),但标准并不保证这一点。想要可移植,就写std::printf(...),并且包含<cstdio>。

// 推荐写法:C++17 #include <cstdio> #include <cstring> #include <cmath> int main() { // 显式加 std::,可移植性最好 std::printf("%d\n", static_cast<int>(std::strlen("hello"))); std::printf("%.3f\n", std::sqrt(2.0)); return 0; }

而<stdio.h>这套旧名字在 C++ 里也是合法的(标准保留了它),它保证把名字放进全局命名空间,可能也放进std。所以"C 头文件在 C++ 里能不能用"的答案是能,但新代码没有理由再选它。

C 里还有一件 C++ 完全不需要的东西:<stdbool.h>。C99 之前没有布尔类型,C99 才用宏凑出一个bool;C++ 从第一版起就有内建的bool,并且true、false是关键字而不是宏。

二、引用(reference):不是指针的语法糖

C 只有指针。想在函数里改调用方的变量,就得传地址、在函数里解引用。C++ 加了引用:

// C++17 #include <cstdio> void swap_c(int* a, int* b) { // C 风格:调用方要写 & int t = *a; *a = *b; *b = t; } void swap_cpp(int& a, int& b) { // C++ 风格:调用方写起来像传值 int t = a; a = b; b = t; } int main() { int x = 1, y = 2; swap_c(&x, &y); swap_cpp(x, y); // 编译器自动取地址 std::printf("%d %d\n", x, y); return 0; }

引用和指针的关键差别,不只是"少写一个*":

特性指针(pointer)引用(reference)
能否为空可以是空指针标准要求必须绑定到对象,不存在空引用
能否重新绑定可以改指向一旦绑定,终身不变
是否占存储占(通常一个机器字)语义上是别名;是否占存储由编译器决定
能否取地址&p得到指针的地址&r得到的是被引用对象的地址
算术运算支持p+1不支持
典型用途可选的、可为空的关系函数参数、函数返回值(必须保证有效)

有一条容易记错的规则:引用本身不是对象,所以你不能有"引用的引用"(int&& r是右值引用,不是引用的引用),也不能有"引用的数组"。

关于"引用是否占存储":在语言层面它只是别名,但为了能作为函数参数传递,编译器通常会用地址实现它。所以sizeof(T&) == sizeof(T)并不代表它一定不占空间——这属于实现细节,取决于编译器如何降级(lower)。

返回引用的写法在 C++ 里很常见,因为它避免了拷贝:

// C++17 #include <string> class Config { public: // 返回引用,调用方拿到的是内部对象本身 std::string& name() { return name_; } // const 引用版本,供 const 对象调用 const std::string& name() const { return name_; } private: std::string name_ = "default"; };

代价是调用方必须清楚:拿到的是"借来的东西",一旦被引用的对象生命周期结束,它就是悬垂引用(dangling reference),继续使用属于未定义行为(undefined behavior, UB),标准不保证任何行为。

三、函数重载与名字修饰(name mangling)

C 不支持同名函数,同一个翻译单元里出现两个print就是重定义错误。C++ 支持重载(overload),只要参数列表不同:

// C++17 #include <cstdio> void print(int v) { std::printf("int: %d\n", v); } void print(double v) { std::printf("double: %f\n", v); } void print(const char* v) { std::printf("str: %s\n", v); }

编译器区分它们靠的是名字修饰:把函数名、参数类型(在某些 ABI 下还包括命名空间和返回类型参与编码的方式)编成一个唯一的符号名。GCC/Clang 在 Linux 上用的是 Itanium C++ ABI,上面三个函数编出来大致是_Z5printi、_Z5printd、_Z5printPKc。你可以自己验证:

g++ -c print.cpp -o print.o nm -C print.o # -C 会还原成可读形式 nm print.o # 不加 -C 就能看到修饰后的符号名

而 C 编译器不做名字修饰,print的符号名就是print。这就是 C/C++ 混编问题的根源:C++ 编译器生成的符号名带了参数信息,链接器按名字找符号时对不上。

解决办法是链接规范(linkage specification)extern "C",它告诉 C++ 编译器:这个名字按 C 的规则修饰(也就是不修饰)。

// c_side.h —— 可以被 C 和 C++ 同时包含 #ifdef __cplusplus extern "C" { #endif void c_function(int x); #ifdef __cplusplus } #endif

__cplusplus宏只在 C++ 编译器下定义,这是让同一个头文件被两种语言共享的标准做法。注意extern "C"只影响名字修饰和调用约定,不影响参数类型检查:它禁止的是重载,不是类型安全。

场景符号名(Itanium ABI 示例)链接器能否找到
C 函数void f(int)f是
C++ 函数void f(int)_Z1fi只有 C++ 侧同意用 C 规范时才能
C++ 重载void f(double)_Z1fd与上面不冲突,这正是重载的实现基础

四、类型系统的补强:bool、nullptr、const

bool:C++ 内建类型,取值为true/false。它参与整型提升(integral promotion),bool转int得 0 或 1,但反过来int转bool是"非零即真",不会帮你检查语义错误。C++ 里bool的sizeof由实现决定,通常为 1。

// C++17 bool b = 3; // 合法:值为 true(隐式转换) int i = true; // 合法:值为 1 // bool c = "abc"; // 非法,但历史上 C 里指针能隐式转 bool,C++ 里字符串字面量转 bool 是合法且为 true 的 —— 一个经典陷阱 bool d = "abc"; // 合法!值为 true,因为指针非空

最后一行是真实存在的坑:bool d = "abc";能编译,但和你想表达的"字符串内容是否为真"完全无关,它只是"指针非空"。写出来编译器通常也不会警告。

nullptr(C++11 起):类型是std::nullptr_t,可以隐式转成任意指针类型和bool,但不会被转成整数。这解决了 C 里NULL的老问题——NULL通常就是0,于是在重载场景下会选错函数:

// C++17 #include <cstddef> // NULL #include <cstdio> void f(int) { std::printf("f(int)\n"); } void f(char*) { std::printf("f(char*)\n"); } int main() { f(0); // 调 f(int),因为 0 是 int f(NULL); // 同样是 f(int)(NULL 通常是 0)—— 大多数编译器会给出警告,但代码仍能编过 f(nullptr); // 调 f(char*),这才是"传空指针"的本意 return 0; }

所以新代码一律用nullptr,把NULL只留给必须兼容 C 的头文件。

const:这个关键字在 C 和 C++ 里的能力并不一样。C 里const主要表达"我不想改",但const对象不能用作数组长度(C99 的变长数组是另一回事),也不能替代编译期常量。C++ 里const整数如果由常量表达式初始化,就是常量表达式(constant expression),可以用在数组维度、模板实参等场合;constexpr(C++11)把这件事说得更明确。

// C++17 const int n = 4; int arr[n]; // 合法,n 是常量表达式 const int m = some_func(); // m 是运行期常量 // int arr2[m]; // 非法:m 不是常量表达式 constexpr int k = 4; static_assert(k == 4, ""); // 合法,k 可在编译期求值

顺便说一下:C++ 里关于const最容易被忽略的是const 正确性(const-correctness)会传染。一个成员函数如果没标const,const对象就调用不了它,于是调用方不得不去掉const,整条链子就散掉了。这是设计问题,不是语法问题。

实战:一个 C 与 C++ 混编的最小可编译例子

下面这个例子包含一个 C 源文件、一个 C++ 源文件和一个共享头文件,演示extern "C"的必要性。三个文件都能直接用gcc/g++编译。

/* c_lib.h:C 和 C++ 都能包含 */ #ifndef C_LIB_H #define C_LIB_H #ifdef __cplusplus extern "C" { #endif int c_add(int a, int b); #ifdef __cplusplus } #endif #endif /* C_LIB_H */
/* c_lib.c:用 C 编译器编译 */ #include "c_lib.h" int c_add(int a, int b) { return a + b; }
// main.cpp:用 C++ 编译器编译,C++17 #include <cstdio> #include <string> #include "c_lib.h" namespace demo { // 重载:C 里做不到 std::string describe(int v) { return "int " + std::to_string(v); } std::string describe(double v) { return "double " + std::to_string(v); } std::string describe(const std::string& v) { return "string " + v; } } int main() { int sum = c_add(2, 3); // 调 C 函数,靠 extern "C" 链接成功 std::printf("%d\n", sum); std::printf("%s\n", demo::describe(7).c_str()); std::printf("%s\n", demo::describe(1.5).c_str()); std::printf("%s\n", demo::describe(std::string("hi")).c_str()); return 0; }

编译命令:

gcc -std=c11 -c c_lib.c -o c_lib.o g++ -std=c++17 -c main.cpp -o main.o g++ main.o c_lib.o -o demo

如果把c_lib.h里的extern "C"去掉,main.o里会去找_Z5c_addii这个符号,而c_lib.o里只有c_add,链接阶段会报undefined reference to '_Z5c_addii'(GCC)或unresolved external symbol(MSVC)。这就是名字修饰造成的、最典型的 C/C++ 混编报错。

常见坑点

坑 1:以为extern "C"只写在一侧就够。

❌ 只在main.cpp里对函数指针写extern "C",头文件里不写。

✅ 在头文件里用#ifdef __cplusplus包起来写extern "C",保证声明和定义的链接规范一致。

坑 2:把NULL当空指针用。

❌void* p = NULL;或f(NULL);(重载场景下可能选到int版本)。

✅ 一律用nullptr(C++11 起)。C 代码里继续用NULL。

坑 3:const char*与char*混用。

❌char* p = "hello";——C++11 起字符串字面量是const char[],这会报错(GCC 下是 error,历史上是废弃警告)。

✅const char* p = "hello";。确实需要可写缓冲,就用char buf[] = "hello";(复制一份)或std::string。

坑 4:返回局部变量的引用。

❌

int& bad() { int x = 1; return x; } // 返回悬垂引用

✅ 按值返回(int bad()),或返回静态/堆上对象的引用并明确所有权约定。使用悬垂引用是 UB,标准不保证任何行为。

坑 5:函数重载只改返回类型。

❌int f();与double f();同时出现——重定义错误,因为重载只看参数列表。

✅ 改参数列表,或者改函数名。返回类型不参与重载决议。

坑 6:以为sizeof(bool)一定是 1。

❌ 用sizeof(bool)当字符串序列化格式的假设常量。

✅ 标准说sizeof(bool)由实现定义,通常 1,但不要写进线协议。需要固定宽度用<cstdint>的std::uint8_t。

坑 7:在 C++ 里用malloc/free管理带构造函数的对象。

❌

std::string* s = (std::string*)malloc(sizeof(std::string)); // 没有构造,后续使用是 UB free(s);

✅ 用new/delete,或者更好——用std::string本身、容器、智能指针,让析构自动发生。

坑 8:把#include <cstdio>后直接调printf当成可移植写法。

❌ 依赖"名字被注入全局命名空间"这个实现行为。

✅ 写std::printf。反过来,如果包含的是<stdio.h>,标准保证全局名字存在,此时能不能写std::printf反而是实现定义。

总结

主题C 的做法C++ 的做法关键点
头文件stdio.hcstdio,名字在std全局名字是否注入是实现定义
传参修改指针引用引用不能为空、不能重绑定
同名函数不允许重载靠名字修饰区分
空指针NULL(常为 0)nullptrnullptr不会误转成int
布尔<stdbool.h>的宏内建bool指针隐式转bool是陷阱
编译期常量宏 /enumconst/constexprconstexpr语义更明确
动态内存malloc/freenew/delete、RAII混用是 UB


从 C 到 C++ 的过渡,本质上是从"手动管理一切"转向"让类型系统替你记住规则":引用让"必须存在"成为类型的一部分,重载让"同一抽象的不同实现"有了统一的调用形式,constexpr和bool让编译器能更早发现问题。上篇讲到这里,剩下的类、构造析构、RAII 会在后续展开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:41:24

基于Django和Vue3的Web入侵检测扫描工具构建实践

前一阵子我接到一个Web入侵检测扫描工具的研发任务&#xff0c;需求文档技术栈那栏写得很壮观——PHP、ASP.NET、Java、Springboot、SSM、Vue3排了一整行&#xff0c;末尾还补了一句“技术栈可以再议&#xff0c;优先保证功能落地”。看到这个备注我就明白了&#xff0c;需求方…

作者头像 李华
网站建设 2026/10/1 12:40:39

单招模块试卷出题设计方案V2(架构师版)

1. 项目背景与总体思路 1.1 单招出题场景的痛点 先说清楚这事儿的真实场景。单招&#xff08;单独招生&#xff09;是高职院校面向中职生、普通高中生组织的选拔性考试&#xff0c;和高考统考不太一样。单招的出题往往由院校自己组织&#xff0c;或者委托第三方题库平台来做&a…

作者头像 李华
网站建设 2026/10/1 12:40:10

玻璃拟态+AI技术:个人创客空间控制台搭建实践

造一个个人创客空间&#xff0c;最难的不是买设备&#xff0c;而是让一堆设备听你的话。半年前&#xff0c;我把自己那间塞满3D打印机、激光切割机、焊台和各种传感器的屋子做了一次彻底升级&#xff1a;所有控制入口统一放到一块墙上的触控屏里&#xff0c;视觉风格采用玻璃拟…

作者头像 李华
网站建设 2026/10/1 12:40:07

Python分形绘图实战:曼德勃罗集、L系统与IFS算法全解析

不用装什么重型软件&#xff0c;也不用非得懂图形学&#xff0c;一台装了Python的电脑就够了。分形与算法绘图这个方向&#xff0c;我玩了三四年&#xff0c;从最开始照着教程抄代码&#xff0c;到后来自己调迭代次数、改颜色映射、做L系统参数化&#xff0c;最大的感受是&…

作者头像 李华
网站建设 2026/10/1 12:40:07

每日记录清单:从字段设计到复盘,构建个人效率系统

1. 为什么“每日记录清单”值得你花时间折腾很多人第一次听到“每日记录清单”这五个字&#xff0c;脑子里蹦出来的画面大概是&#xff1a;又一个待办事项App&#xff0c;又一个打卡工具&#xff0c;又一个坚持三天就吃灰的自我感动式项目。我一开始也这么想。直到连续三个月被…

作者头像 李华
网站建设 2026/10/1 12:39:22

粉尘检测数据集实战:格式转换、YOLOv8训练与常见坑排查

简介&#xff1a;粉尘检测数据集包含922张真实环境监测场景图片及其标注文件&#xff0c;采用YOLO目标检测格式&#xff0c;面向需要训练粉尘颗粒物检测模型的AI开发者、环保技术研究人员与工业安全工程人员。数据集划分明确&#xff1a;训练集797张、验证集83张、测试集42张&a…

作者头像 李华