一些容易被忽略的 C 与 C++ 的不兼容特性

头文件和命名空间

C 标准库头文件名在 C++ 中通常去除扩展名,并加上 c 前缀,如:

  • stdio.h -> cstdio
  • stdlib.h -> cstdlib

其中一个重要的区别是后者保证与 C 库兼容的各个函数名可以在 std 命名空间中找到,但并不保证它们不存在于根命名空间中,这可能会引发一些难以发现的 bug。

/*
* abs.cpp - 求绝对值的小程序
*
* 该代码有一处较为隐蔽的 bug,请尽力找出它!
*/
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cctype>
#include <cmath> int main(int argc, char *argv[])
{
if(argc == 1) { // 校验输入命令行参数
fprintf(stderr, "usage: %s <numbers>\n", program_invocation_short_name);
exit(EXIT_FAILURE);
} for(int i = 1; i < argc; ++i) { // 对每个参数,输出运算结果
char *endp;
double num = strtod(argv[i], &endp);
while(isspace(*endp)) ++endp; // 跳过尾部空白字符,这一般不会出现
if(*endp) { // 没有到达字符串结尾,说明出现了错误
fprintf(stderr, "ERROR: invalid number: %s\n", argv[i]);
}
num = abs(num); // 计算绝对值,即使是无效输入也要输出
printf("%lf\n", num);
} return 0; // 此处始终退回成功值,不是 bug
}

无论你是用人脑,IDE,反汇编器还是调试器发现了这个 bug,你都会将矛头指向命名空间。接收浮点参数的 abs() 函数在本案例中仅在 std 命名空间可见,故函数重载解析时不会考虑它们。其中一种解决方案是简洁明了的,即在 main() 函数定义前加上一句:

using std::abs;

现在一切正常。

lyazj@HelloWorld:~$ g++ -Wall -Wshadow -Wextra abs.cpp -o abs
lyazj@HelloWorld:~$ ./abs 0 1 -1 1.5 -1.5
0.000000
1.000000
1.000000
1.500000
1.500000

另一种解决方案是不加 using 申明,但将头文件名修改为 C 风格:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <errno.h>

结果仍然一切正常。

lyazj@HelloWorld:~$ g++ -Wall -Wshadow -Wextra abs.cpp -o abs
lyazj@HelloWorld:~$ ./abs 0 1 -1 1.5 -1.5
0.000000
1.000000
1.000000
1.500000
1.500000

在本例测试环境下,/usr/include/c++/11/stdlib.h:54 给出了原因(读者应该也已经从文件路径中发现,C++ 版本的同名头文件与 C 版本应当有较大区别,所以并非位于 /usr/include/ 下):

using std::abs;

现在让我们来做最后的测试,不改变上一步的代码,改用 gcc 编译,结果如下:

lyazj@HelloWorld:~$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. lyazj@HelloWorld:~$ gcc -xc -Wall -Wshadow -Wextra abs.cpp -o abs -D_GNU_SOURCE
abs.cpp: In function ‘main’:
abs.cpp:25:11: warning: using integer absolute value function ‘abs’ when argument is of floating-point type ‘double’ [-Wabsolute-value]
25 | num = abs(num); // 计算绝对值
| ^~~
lyazj@HelloWorld:~$ ./abs 0 1 -1 1.5 -1.5
0.000000
1.000000
1.000000
1.000000
1.000000

gcc 发出了我们所期待的警告,这是在本节开头 g++ 没有做到的事情。

问题补充:

  • 应当避免随意使用 using namespace std;,这并非解决这类冲突的好办法
  • 使用 C 库函数时,遵循 C 风格,使用后缀区别不同类型的 abs() 函数
  • 使用 IDE 或调试器追踪检查函数重载解析结果,确保符合预期

对于 C 和 C++ 风格头文件处理命名空间行为不一致的问题,可以尝试:

  • 不要混用两种风格的头文件
  • 如使用 C++ 风格的头文件,请坚守命名空间规则,确保使用的函数重载在当前作用域可见
  • 如使用 C 风格的头文件,请以兼容 C 的方式编写代码,包括以后缀区分接收不同形参类型的函数

NULL

这个大家应该比较熟悉,只需记住在 C 中它的类型一般是 void *,而在 C++ 中一般是 int,在类型参数匹配时可能有坑,故在 C++ 中应当尽量使用 nullptr

static 变量的初始化和 static 对象的析构

C++ 中 static 变量的初始化可能是有副作用的,特别是当初始化过程可能抛出异常时,编译器会生成复杂的代码来确保线程安全和初始化过程的原子性。同时 C++ 中 static 对象的析构顺序在分离编译时难以确定,当有多个 static 对象析构时可能破坏对象之间的依赖关系,需要避免这样的依赖。

C++ 中的类与 C 中的结构体有着根本区别

GCC 以虚函数表的方式支持 C++ 运行时多态,在 C 中我们可能习惯使用 memset() 来初始化结构体,在 C++ 中,如果 memset() 的地址仍然写对象地址,则可能破坏多态对象的虚表。就算是不需要分配资源,初始化 C++ 类也应当在构造函数中进行。在 C 中我们可以将以相同方式对齐的具有相同成员变量的结构体指针互相转化,来向用户隐藏其内部结构,在 C++ 中这则可能导致严重错误。在 C++ 中,应当使用封装继承或嵌套类等方式实现底层实现的隐藏。

new/delete 不简单等同于带有构造/析构语义的 malloc/free

不同于 malloc/freenew/delete 支持以全局或成员的方式重载,故不能永远假设 new 会调用 malloc(),也不能永远假设 delete 会调用 free(),虽然默认如此。内存的分配和释放应当始终坚持成对原则。

C++ 中的异常处理不仅限于长跳

C 程序员常使用 setjmp()longjmp() 系列函数处理异常,这在 C++ 中通常是不可取的,因为前者不能保证异常调用栈上的对象正确析构。同时,C 和 C++ 代码混合调用时如需支持异常,需要在 C 代码编译时显式打开相应 GCC 编译开关,否则可能缺少足够的上下文信息来捕捉异常。

构造/析构函数与一般的函数不同

在一个构造函数中,使用定位 new 表达式委托另一构造函数是严重的错误行为,因为当后者抛出异常时,所有的成员变量将被析构,若前者未捕捉异常,则造成 double free 问题,若前者捕捉异常,则将构造出一个畸形的对象。无论如何,这不是一个很自然的解决方案。在析构函数中抛出异常,如前一异常尚未处理完毕,程序将异常终止(在本测试环境下,打印出 terminate called after throwing an instance of... 并引发 SIGABRT 终止程序)。

C++ 表达式的类型包含引用

char buf[64];
printf("%zu\n", sizeof(0, buf));

这是一个经典的例子。在 C 中输出 4 或 8(或其它,取决于 sizeof(char *)),在 C++ 中输出 64。

布尔类型

考虑对 C++ 的兼容性,应当避免在 C 代码中使用 _Bool,而是引入头文件 stdbool.h,并将布尔类型写作 bool

关系运算表达式值类型

printf("%zu\n", sizeof(0 == 0));

在 C 中,0 == 0int 类型,其大小一般为 4;在 C++ 中,0 == 0 是布尔类型,其大小一般为 1。

宏定义

在本测试环境下,GCC 为 C++ 编译环境默认定义了宏 _GNU_SOURCE,但在 C 环境下默认没有定义。直接使用 #define _GNU_SOURCE 定义该宏的代码可能在 C++ 编译器下产生宏重复定义的警告。当然,类似这样的差别还有很多。

auto 关键字的语义差别

在 C 中,auto 并不常用;在 C++11 及以后标准中,auto 用于值类型自动推导,实现类型简写和编译器多态。

restrict, _Noreturn, register 等关键字

部分关键字为 C 特有的,如常见于 string.h 中 restrict,用于指导编译器优化代码的 _Noreturnregister 关键字,它们并不在最新的 C++ 标准中。

运行时链接

运行时链接,包括 libdl.so 提供的接口甚至是 Python3 ctypes 提供的接口,对 C++ 的支持都非常差,因为 C++ 中的对象有许多需要构造和析构的过程,且不同编译系统实现的名字重整规则也不相同,再加上运行时的对象生命周期管理非常困难,一个较好的解决方案是在外围套一层 C 外壳,隐藏复杂 C++ 对象的实现细节。

编译和链接

C++ 程序,特别是包含大量模板(比如使用了 STL)的 C++ 程序编译比 C 程序慢得多,且二进制文件大得多,但恰当地使用 STL 和其它模板库无疑对开发效率和程序质量非常有帮助。

常量定义

enumconstexpr 等是 C++ 推荐的应当尽可能使用的定义常量的方式,特别是 constexpr,比起宏定义而言,这种方式可以让名字进入作用域和类型系统,增强程序的严谨性。

const 关键字

众所周知,const 关键字是 C++ 语言非常重要的一部分,const 重载也是经常使用的手段之一。然而,C 对 const 的违例行为只会施加以警告,而不会报出 error。有趣的是,C 并不强制字符串字面值常量必须赋给带有底层 const 的指针变量,而 C++ 强制要求这一点。

非平凡的指定初始化器

int nums[100] = {
[99] = 1,
};

在 GCC 中,这是合法的 C99 代码,但在 C++ 编译环境下 GCC 并不认可这样的代码:

lyazj@HelloWorld:~/develop/work$ g++ -Wall -Wshadow -Wextra test.cpp -o test
test.cpp:3:1: sorry, unimplemented: non-trivial designated initializers not supported
3 | };
| ^

【持续更新】C++ 并不完全是 C 的超集!的更多相关文章

  1. 神技!微信小程序(应用号)抢先入门教程(附最新案例DEMO-豆瓣电影)持续更新

    微信小程序 Demo(豆瓣电影) 由于时间的关系,没有办法写一个完整的说明,后续配合一些视频资料,请持续关注 官方文档:https://mp.weixin.qq.com/debug/wxadoc/de ...

  2. iOS系列教程 目录 (持续更新...)

      前言: 听说搞iOS的都是高富帅,身边妹子无数.咱也来玩玩.哈哈. 本篇所有内容使用的是XCode工具.Swift语言进行开发. 我现在也是学习阶段,每一篇内容都是经过自己实际编写完一遍之后,发现 ...

  3. ASP.NET MVC 5 系列 学习笔记 目录 (持续更新...)

    前言: 记得当初培训的时候,学习的还是ASP.NET,现在回想一下,图片水印.统计人数.过滤器....HttpHandler是多么的经典! 不过后来接触到了MVC,便立马爱上了它.Model-View ...

  4. git常用命令(持续更新中)

    git常用命令(持续更新中) 本地仓库操作git int                                 初始化本地仓库git add .                       ...

  5. iOS开发系列文章(持续更新……)

    iOS开发系列的文章,内容循序渐进,包含C语言.ObjC.iOS开发以及日后要写的游戏开发和Swift编程几部分内容.文章会持续更新,希望大家多多关注,如果文章对你有帮助请点赞支持,多谢! 为了方便大 ...

  6. 基于android studio的快捷开发(将持续更新)

    对于Android studio作为谷歌公司的亲儿子,自然有它的好用的地方,特别是gradle方式和快捷提示方式真的很棒.下面是我在实际开发中一些比较喜欢用的快速开发快捷键,对于基本的那些就不多说了. ...

  7. 总结js常用函数和常用技巧(持续更新)

    学习和工作的过程中总结的干货,包括常用函数.常用js技巧.常用正则表达式.git笔记等.为刚接触前端的童鞋们提供一个简单的查询的途径,也以此来缅怀我的前端学习之路. PS:此文档,我会持续更新. Aj ...

  8. 我的敏捷、需求分析、UML、软件设计电子书 - 下载(持续更新中)

    我将所有我的电子书汇总在一起,方便大家下载!(持续更新) 文档保存在我的网站——软件知识原创基地上(www.umlonline.org),请放心下载. 1)软件设计是怎样炼成的?(2014-4-1 发 ...

  9. React Native之坑总结(持续更新)

    React Native之坑总结(持续更新) Genymotion安装与启动 之前我用的是蓝叠(BlueStack)模拟器,跑RN程序也遇到了一些问题,都通过搜索引擎解决了,不过没有记录. 但是Blu ...

  10. RedHat 和 Mirantis OpenStack 产品的版本和功能汇总和对比(持续更新)

    Mirantis 和 Red Hat 作为 OpenStack 商业化产品领域的两大领军企业,在行业内有重要的地位.因此,研究其产品版本发布周期和所支持的功能,对制定 OpenStack 产品的版本和 ...

随机推荐

  1. 「学习笔记」重修 FHQ-treap

    无旋 treap 的操作方式使得它天生支持维护序列.可持久化等特性. 无旋 treap 又称分裂合并 treap.它仅有两种核心操作,即为 分裂 与 合并.通过这两种操作,在很多情况下可以比旋转 tr ...

  2. 沁恒 CH32V208(一): CH32V208WBU6 评估板上手报告和Win10环境配置

    目录 沁恒 CH32V208(一): CH32V208WBU6 评估板上手报告和Win10环境配置 CH32V208 CH32V208系列是沁恒32位RISC-V中比较新的一个系列, 基于青稞RISC ...

  3. 终端命令行前出现(base)

    原因 (base) 的出现是因为电脑安装了conda后,每次打开终端都会自动启动conda的base环境 解决 取消自动启动base环境:conda config --set auto_activat ...

  4. 消息推送平台的实时数仓?!flink消费kafka消息入到hive

    大家好,3y啊.好些天没更新了,并没有偷懒,只不过一直在安装环境,差点都想放弃了. 上一次比较大的更新是做了austin的预览地址,把企业微信的应用和机器人消息各种的消息类型和功能给完善了.上一篇文章 ...

  5. 2023-02-12:给定正数N,表示用户数量,用户编号从0~N-1, 给定正数M,表示实验数量,实验编号从0~M-1, 给定长度为N的二维数组A, A[i] = { a, b, c }表示,用户i报

    2023-02-12:给定正数N,表示用户数量,用户编号从0~N-1, 给定正数M,表示实验数量,实验编号从0~M-1, 给定长度为N的二维数组A, A[i] = { a, b, c }表示,用户i报 ...

  6. 2022-11-01:给定一个只由小写字母和数字字符组成的字符串str。 要求子串必须只含有一个小写字母,数字字符数量随意。 求这样的子串最大长度是多少?

    2022-11-01:给定一个只由小写字母和数字字符组成的字符串str. 要求子串必须只含有一个小写字母,数字字符数量随意. 求这样的子串最大长度是多少? 答案2022-11-01: 经典的滑动窗口问 ...

  7. 2022-06-16:给定一个数组arr,含有n个数字,都是非负数, 给定一个正数k, 返回所有子序列中,累加和最小的前k个子序列累加和。 假设K不大,怎么算最快? 来自亚马逊。

    2022-06-16:给定一个数组arr,含有n个数字,都是非负数, 给定一个正数k, 返回所有子序列中,累加和最小的前k个子序列累加和. 假设K不大,怎么算最快? 来自亚马逊. 答案2022-06- ...

  8. 沁恒 CH32V208(四): CH32V208 网络DHCP示例代码分析

    目录 沁恒 CH32V208(一): CH32V208WBU6 评估板上手报告和Win10环境配置 沁恒 CH32V208(二): CH32V208的储存结构, 启动模式和时钟 沁恒 CH32V208 ...

  9. windows-重启打印服务

    @echo off color a net stop spooler net start spooler ping -n 4 localhost >nul

  10. AI 绘画 - 如何 0 成本在线体验 AI 绘画的魅力

    要想体验 AI 绘画,比较流行的三种方式是 Midjourney.OpenAI 的 DALL·E 2 以及 Stable Diffusion.而 Midjourney 已经停止免费试用,且使用价格不太 ...