一些容易被忽略的 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. Python OOP面向对象编程

    OOP 思想: 以模块思想解决工程问题 面向过程 VS 面向对象 由面向过程转向面向对象 例子,我要开一个学校,叫XXX 讲师 学生 班主任 教室 学校 常用名词 OO:面向对象 OOA: 分析 OO ...

  2. Spring源码:bean的生命周期(一)

    前言 本节将正式介绍Spring源码细节,将讲解Bean生命周期.请注意,虽然我们不希望过于繁琐地理解Spring源码,但也不要认为Spring源码很简单.在本节中,我们将主要讲解Spring 5.3 ...

  3. 【解决方法】windows连接域时报错:An Active Directory Domain Controller(AD DC) for the domain“chinaskills.com“....

    目录-快速跳转 问题描述 原因分析: 解决方案: 附言: 问题描述 操作环境与场景: 在 VM 内 windos 2019 在连接到域时,提示报错: An Active Directory Domai ...

  4. elSelect点击空白处无法收起下拉框(失去焦点并隐藏)

    学习记录,为了以后有同样的问题,省得再百度了,方便自己也方便你们element 中多选的select 有个问题,就是点击空白或者关闭弹窗,下拉还会一直展示出来百度了好一会,觉得下面两位大佬说的最合理, ...

  5. vue【解决方案】页面/路由跳转后,滚动条消失,页面无法滚动

    原因解析: vue项目中,页面/路由跳转后,body 的内联样式变成 overflow:hidden 解决方案: 使用路由守卫,在页面/路由跳转后,将body 的overflow设置为auto src ...

  6. golang在编程语言排行榜上排名第10,请不要说golang已死。

    四月头条:编程语言 Zig 进入 TIOBE 指数前 50 名 最近,我们讨论了高性能编程语言的出现.由于需要处理的数据量越来越大,这些编程语言正在蓬勃发展.因此,C 和 C++ 在前十名中表现良好, ...

  7. 2022-09-14:以下go语言代码输出什么?A:0 0;B:0 1;C:1 1;D:1 0。 package main func main() { println(f(1)) } func

    2022-09-14:以下go语言代码输出什么?A:0 0:B:0 1:C:1 1:D:1 0. package main func main() { println(f(1)) } func f(x ...

  8. 2021-08-06:天际线问题。城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。每个建筑物的几何信息由数组 build

    2021-08-06:天际线问题.城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓.给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 .每个建筑物的几何信息由数组 build ...

  9. npm install报错node-sass@7.0.1 postinstall: `node scripts/build.js`

    在控制台执行 即可 npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass

  10. TypeError: Cannot read property ‘make‘ of undefined

    这搞个html-webpack-plugin插件进来运行就一大篇报错尴尬 看了一圈又是版本兼容的问题,做下修改.... OK 运行成功