【持续更新】C++ 并不完全是 C 的超集!
一些容易被忽略的 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/free,new/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 == 0 是 int 类型,其大小一般为 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,用于指导编译器优化代码的 _Noreturn 和 register 关键字,它们并不在最新的 C++ 标准中。
运行时链接
运行时链接,包括 libdl.so 提供的接口甚至是 Python3 ctypes 提供的接口,对 C++ 的支持都非常差,因为 C++ 中的对象有许多需要构造和析构的过程,且不同编译系统实现的名字重整规则也不相同,再加上运行时的对象生命周期管理非常困难,一个较好的解决方案是在外围套一层 C 外壳,隐藏复杂 C++ 对象的实现细节。
编译和链接
C++ 程序,特别是包含大量模板(比如使用了 STL)的 C++ 程序编译比 C 程序慢得多,且二进制文件大得多,但恰当地使用 STL 和其它模板库无疑对开发效率和程序质量非常有帮助。
常量定义
enum,constexpr 等是 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 的超集!的更多相关文章
- 神技!微信小程序(应用号)抢先入门教程(附最新案例DEMO-豆瓣电影)持续更新
微信小程序 Demo(豆瓣电影) 由于时间的关系,没有办法写一个完整的说明,后续配合一些视频资料,请持续关注 官方文档:https://mp.weixin.qq.com/debug/wxadoc/de ...
- iOS系列教程 目录 (持续更新...)
前言: 听说搞iOS的都是高富帅,身边妹子无数.咱也来玩玩.哈哈. 本篇所有内容使用的是XCode工具.Swift语言进行开发. 我现在也是学习阶段,每一篇内容都是经过自己实际编写完一遍之后,发现 ...
- ASP.NET MVC 5 系列 学习笔记 目录 (持续更新...)
前言: 记得当初培训的时候,学习的还是ASP.NET,现在回想一下,图片水印.统计人数.过滤器....HttpHandler是多么的经典! 不过后来接触到了MVC,便立马爱上了它.Model-View ...
- git常用命令(持续更新中)
git常用命令(持续更新中) 本地仓库操作git int 初始化本地仓库git add . ...
- iOS开发系列文章(持续更新……)
iOS开发系列的文章,内容循序渐进,包含C语言.ObjC.iOS开发以及日后要写的游戏开发和Swift编程几部分内容.文章会持续更新,希望大家多多关注,如果文章对你有帮助请点赞支持,多谢! 为了方便大 ...
- 基于android studio的快捷开发(将持续更新)
对于Android studio作为谷歌公司的亲儿子,自然有它的好用的地方,特别是gradle方式和快捷提示方式真的很棒.下面是我在实际开发中一些比较喜欢用的快速开发快捷键,对于基本的那些就不多说了. ...
- 总结js常用函数和常用技巧(持续更新)
学习和工作的过程中总结的干货,包括常用函数.常用js技巧.常用正则表达式.git笔记等.为刚接触前端的童鞋们提供一个简单的查询的途径,也以此来缅怀我的前端学习之路. PS:此文档,我会持续更新. Aj ...
- 我的敏捷、需求分析、UML、软件设计电子书 - 下载(持续更新中)
我将所有我的电子书汇总在一起,方便大家下载!(持续更新) 文档保存在我的网站——软件知识原创基地上(www.umlonline.org),请放心下载. 1)软件设计是怎样炼成的?(2014-4-1 发 ...
- React Native之坑总结(持续更新)
React Native之坑总结(持续更新) Genymotion安装与启动 之前我用的是蓝叠(BlueStack)模拟器,跑RN程序也遇到了一些问题,都通过搜索引擎解决了,不过没有记录. 但是Blu ...
- RedHat 和 Mirantis OpenStack 产品的版本和功能汇总和对比(持续更新)
Mirantis 和 Red Hat 作为 OpenStack 商业化产品领域的两大领军企业,在行业内有重要的地位.因此,研究其产品版本发布周期和所支持的功能,对制定 OpenStack 产品的版本和 ...
随机推荐
- Vue3 element-plus 下拉分页 select分页
由于用 input 实现下拉分页不太理想,转换了一个角度,用 select 实现,以下是具体实现(script-setup TS) script-setup <script lang=" ...
- 【Visual Leak Detector】源码编译 VLD 库
说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记.本篇介绍 VLD 源码的编译.同系列文章目录可见 <内存泄漏检测工具>目录 目录 说明 1. VLD 库的依赖文件 2. 源码 ...
- 在Linux上安装redis7
1.检测虚拟机环境 1.1 bit检测命令:getconf LONG_BIT(建议使用64bit做开发) 1.2 gcc环境检测:gcc -v 如果不具备gcc环境,则使用yum -y install ...
- Linux redhat7.2 制作u盘问题总结
Linux redhat7.2 制作u盘问题总结 其实呢,觉得本来没必要写一篇关于装系统的文章,毕竟我觉得大多数搞it的人都会,比如win10.ubuntu做个启动盘啥的应该都会,但是说实在的今天 ...
- 响应式的 switchboard:让又大又慢的Vue/AIpine 页面爆快
我的提示: AIpine 是一个js 库,官网口号是 "一个新的轻量极javascript框架",其实我之前也没接触过,翻译这篇文章时才注意到 官方地址: [AIpine.js]h ...
- 2021-06-04:给定三个参数:二叉树的头节点head,树上某个节点target,正数K,从target开始,可以向上走或者向下走。返回与target的距离是K的所有节点。
2021-06-04:给定三个参数:二叉树的头节点head,树上某个节点target,正数K,从target开始,可以向上走或者向下走.返回与target的距离是K的所有节点. 福大大 答案2021- ...
- Django-账号用户密码修改
Django账号密码修改命令: python manage.py changepassword python manage.py changepassword 实操分析: 第一次修改失败是因为违反了密 ...
- 使用vite的创建vue项目
首先也是打开项目文件目录 在标签处快速打上cmd即可打开cmd窗口 然后按照顶部图进行操作即可完成 安装完成的样子如下图 紧接着输入 npm run dev 将Local 的IP复制到浏览器打开,出现 ...
- MVC 三层架构案例详细讲解
MVC 三层架构案例详细讲解 @ 目录 MVC 三层架构案例详细讲解 每博一文案 1. MVC 概述 2. MVC设计思想 3. 三层架构 4. MVC 与 三层架构的关系: 5. 案例举例:用户账户 ...
- 7-2 Broken Pad (20 分)
1.题目描述: The party began, the greasy uncle was playing cards, the fat otaku was eating, and the littl ...