C++中老式C风格void*的优雅与简洁
摘要
文章讨论了在C++中使用C风格void*指针与uint8_t*和std::span传递内存块的优缺点,主张void*的简洁性和可读性。
暂无内容
查看缓存全文
缓存时间: 2026/06/09 11:41
# 如何声明一个接受内存块(blob of memory)的 C++ 函数?
来源:https://giodicanio.com/2026/06/05/how-to-declare-a-c-plus-plus-function-that-takes-a-blob-of-memory/
讨论几种选项,从古老的 C 风格 `void*` 指针开始。
在 C++ 中你可能会问一个有趣的问题:“如何声明一个接受内存块(blob of memory)作为输入的函数?”
举个例子,考虑一个对输入数据进行哈希(比如 SHA-256 或其他算法)的函数,或者一个接收二进制数据并将其写入磁盘的函数。
从我 C 语言的背景出发,我首先想到的选项当然是:
```cpp
void DoSomething(const void* p, size_t numBytes)
```
你只需要传递一个指向输入内存块起始位置的 **const void*** 指针,以及该内存块的总大小(以字节为单位)。
然后,可能会有一些 C++ 程序员抱怨:“嘿,你怎么还在用那个 **不安全** 的旧 C 风格 `void*` 指针?用像 `uint8_t` 这样安全的显式类型吧,它明确表示一个 8 位的字节!” 于是他们提议“升级”到以下原型:
```cpp
void DoSomething(const uint8_t* p, size_t numBytes)
```
现在,假设你想向这个函数传递一个自定义结构体,比如:
```cpp
struct MyCustomData { ... };
MyCustomData data;
```
使用原始的 `void*` 版本,你可以 **简单明了地** 调用函数:
```cpp
DoSomething(&data, sizeof(data));
```
代码非常清晰直接:你传递了一个指向自定义数据结构的指针,以及它的字节大小。就是这样,简单明了。
另一方面,对于“安全现代”的 `uint8_t` 原型,函数调用变得更复杂了,因为你需要添加一个类型转换:
```cpp
// void DoSomething(const uint8_t* p, size_t numBytes)
//
// DoSomething(&data, sizeof(data));
//
// 当函数期望 const uint8_t* 而不是 const void* 时,这会导致编译器错误,类似:
//
// 错误:无法将 'MyCustomData*' 转换为 'const uint8_t*'
//
// 这时你需要显式强制转换!
DoSomething( reinterpret_cast<const uint8_t*>(&data), sizeof(data) );
```
为什么人们要用 `uint8_t` 指针(或 `std::byte`)来 **复杂化和丑化** 他们的 C++ 代码呢?`void*` 明明工作得很好!
此外,有些人可能会说:“嘿,在现代 C++20 中,我们有 **std::span**!用它!” 好吧,恭喜你,这进一步增加了代码的复杂度和噪音!实际上,**std::span** 是一个 **类模板**,而有些人会建议让处理通用内存块的函数变成一个 **函数模板**!真的吗?像这样?
```cpp
template <typename T>
void DoSomething(std::span<T> data)
```
或者更复杂的东西,比如这样?
```cpp
template <typename T>
void DoSomething(std::span<const T> data) // 或者这样?
template <typename T>
void DoSomething(std::span<const std::byte> data)
```
哇。用 `std::span`,复杂度指针直接爆表,还更高了!
有人可能建议用 **std::span**?但这也比最初的 `void*` 签名更复杂。你想要一个指向通用内存块的指针吗?C++ 从一开始就从 C 继承了:它叫 `void*`!用它,享受它吧。
我非常不喜欢某些“现代”C++ 程序员的态度,他们做出的选择实际上让代码 **更复杂**、更丑陋、更难以编写和理解。似乎有些人已经失去了对 **良好可读代码** 的品味。来自 C 的一些好习惯仍然可以 **积极地** 用在 C++ 中,比如 `void*` 指针和大小参数。
---
另外,作为一个不错的补充,如果你使用 **SAL 注解**,函数可以稍微装饰一下,以帮助代码分析器检测内存错误:
```cpp
void DoSomething(
_In_reads_bytes_(numBytes) const void * p,
_In_ size_t numBytes
);
```
应用于指针参数的 **_In_reads_bytes_** 注解明确说明:该指针指向输入只读内存(_In_reads_),并且这个输入缓冲区的大小(以字节表示,_bytes_)由 `numBytes` 参数给出。
这样,我们依然保持了函数调用的清晰和简洁:
```cpp
DoSomething(&data, sizeof(data));
```
同时添加了有助于代码分析器和其他工具发现内存错误的信息片段。
如果你想了解更多关于 SAL 注解的内容,可以从这篇 MSDN 文档入手:
[使用 SAL 注解减少 C/C++ 代码缺陷](https://learn.microsoft.com/zh-cn/cpp/code-quality/using-sal-annotations-to-reduce-c-cpp-code-defects?view=msvc-170)
相似文章
关于C数组类型语义的讨论
本文解释了C数组类型的令人困惑的行为,包括它们退化为指针、sizeof和函数参数等例外情况,并将其与函数类型进行比较,提出了一种数组和指针严格分离的心理模型。
用C语言搞怪,第&((int*)-8)[3]部分
一篇幽默的教育性文章,涵盖C语言基础知识,如前向声明、运算符优先级、无条件跳转和基本算术运算,并附带有意搞怪的代码示例。
Bjarne Stroustrup: 如何处理内存泄漏?
Bjarne Stroustrup 回答关于 C++ 内存泄漏的常见问题,并提供现代 C++ 内存管理技术的指导。
C语言中在C++中仍然无法工作的构造——以及一些已发生变化的构造
一篇更新经典调查的博文,关于C语言中在C++中无法工作的构造,涵盖了C++20和C23标准中影响兼容性的变化。
C语言中的一切皆为未定义行为
一位经验丰富的C++开发者认为,所有非平凡的C和C++代码都包含未定义行为,使得内存安全无法实现,并质疑这些语言在现代软件开发中的持续使用。