以下内容为本人的烂笔头,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/tm2OKiLBQ...
C/C++ 到了寿终正寝的时候否?
最近被热议的「美国白宫提倡软件开发者放弃使用 C/C++ 这种语言再进行新的软件开发」。作为一名热衷于高性能架构开发的码农,笔者我真的很难舍弃如此经典的高性能开发语言。何况直至本文的写作时,在某些开发领域里,C/C++ 几乎是唯一选择。
之所以,C/C++ 被诟病为现代软件安全漏洞的最大隐患,就源自于 C/C++ 编写的软件允许开发人员直接操作内存,而且旧版本的语言标准一直没有提供很好的指针使用规范,导致发布的软件安全漏洞时有发生。其中涉及内存漏洞的重灾区之一就是,数组的使用。
本文就讲讲数组的毛病,和转向现代化的替代品--容器。
数组源自 C 语言标准,在「远古时期」的 C++ 标准里被大规模地被保留应用。但是,在 C++ 进入现代版本后,数组就有了完美的替代品--容器。因此数组也就变成彻底的包袱,食之无味,何不爽快弃之?
说起来也怪,现在居然还能在新代码里看到这种化石级的数组应用,可见 C++ 的现代化教育还没有真正的普及。难怪在浏览网上的 C++ 岗位招聘信息时,噼里啪啦列出一大堆要求,结尾处还不忘加上一行「需要熟悉现代版 C++」。
数组的陷阱
在使用数组时,常见的 bug 总结起来就两点:
- 访问越界
为了遍历或者写入数组,由于对数组长度或者元素偏移个数的计算错误,导致访问了不属于数组范围内的内存地址空间,这种错误往往导致程序崩溃或者异常关闭。
举个例子,假设为了创建九宫格的数字面板,需要存储 9 个用于展示内容的小部件,如下:
csharp
#define NUM 9
class Widget
{
public:
Widget() {}
~Widget() {}
};
void create_widgets() {
Widget *widgets[NUM];
for (int i = 0; i <= NUM; i++)
widgets[i] = new Widget();
}
int main() {
create_widgets();
return 0;
}
上面的代码中,当 i==NUMPAD_DIGITS
(创建了 10 个部件,超出 9 个部件的存储空间限制) 时,widgets[i] = new Widget()
就会尝试往数组 widgets 范围外的地址空间写入数据,这是系统不允许的。所以编译上面的代码并运行,系统立马报错如下
arduino
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)
这种错误是因为开发人员常常需要依赖人力计算清楚数组的存储空间大小,可见数组长度是一个安全隐患。
有的小伙伴可能会说,当数组存储字符串时,可以用 *char 类型指针代表数组地址,再通过 strlen() 就可以自动计算数组长度嘛。但是,这里引用的函数 strlen() 有个限制就是只能计算以空结束字符结尾的数组内容,应用范围明显受限。
- 占用过多空间
为了简单随意分配空间,很多时候直接定义数组,并指明元素个数。但是,又不清楚最终确切需要使用的空间大小,所以就随意指定一个比较大的元素个数。
比如,定义一个读取输入缓冲的接口,由于输入内容的数据长度未能固定,所以接口内部就随意分配一个比较大的缓冲空间:
csharp
void read_data() {
char buf[1024];
// read ...
}
上面这段代码,在调试运行过程中会发现很多空间被浪费没有使用到。历史遗留代码中比较耗费空间的代码往往来自上面这种接口实现,优化时应该重点关注这种代码。
- 指针退化
上面说到使用数组时,由于人为错误导致的访问越界问题,归根到底是依赖人力计算数组长度。虽然一般情况下,可以使用操作符 sizeof() 自动计算数组占用空间的大小(非数组内部实际存储有效数据的大小)。
但是,在数组被转存为指针之后,sizeof() 返回的就不是数组的长度了,而是指针的大小。这就是数组的指针退化的一种表现。
c
#include <iostream>
int test(int array[])
{
return sizeof(array);
}
int main() {
int arr[10];
int *p = arr;
std::cout << "arr size=" << sizeof(arr)
<< " fun param size=" << test(arr)
<< " pointer size=" << sizeof(p) << std::endl;
return 0;
}
上面演示了两种将数组转存为指针的情形,分别是将数组地址赋值给同类型指针变量,和将数组作为参数输入到函数
kotlin
arr size=40 fun param size=8 pointer size=8
从上面的输出来看,利用 sizeof() 操作符计算数组大小的结果已经失效,64 位计算机的指针大小就是 8。
另外,数组的指针退化还可以通过对数组比较、整体赋值等操作来观察到。
假设定义两个数据和长度都一样的数组,然后比较它们是否相等
c
#include <iostream>
int main() {
int arr1[2] {1, 2};
int arr2[2] {1, 2};
if (arr1 != arr2) {
std::cout << "Arrays are different!\n";
}
return 0;
}
输出:
sql
Arrays are different!
从结果来看两个数据和长度都一样的数组不相等,那么为什么呢?因为在比较两个数组时,实际比较的是两个数组的地址,也就是把数组转成了指针。
再假设定义两个长度一样的数组,但是分别初始化不同的值,然后再执行赋值操作
ini
#include <iostream>
int main() {
int arr1[2] {1, 2};
int arr2[2] {3, 4};
arr1 = arr2;
return 0;
}
实际上这样对数组赋值是无法编译通过的,也就是数组不支持整体赋值。和对数组调用比较操作符类似,在赋值表达式 arr1 = arr2
中,引用数组名时,把数组转成了指针,而且这个指针是不可修改的。
从上面的数组指针退化来看,数组的使用收到极大限制,也就是常说的功能被阉割了。当然,想要历数某样事物的缺点是可以无穷无尽的,我们的目的不是这样,而是从需求出发,找到突破。
既然数组如此落后,何不换个趁手的工具?
全文未结束,本文只是上篇。如果各位同学朋友有什么疑问可以联系笔者,当然笔者也愿意和你进一步探讨这方面的问题。另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。下拉到文章底部有我的联系方式!
最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!