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字符串:一个50年的错误
对C语言中空终止字符串的批评,认为基于长度的字符串在现代编程中更优越,可以减少错误并提高性能。
关于C数组类型语义的讨论
本文解释了C数组类型的令人困惑的行为,包括它们退化为指针、sizeof和函数参数等例外情况,并将其与函数类型进行比较,提出了一种数组和指针严格分离的心理模型。
我一直在思考空指针
作者探讨了空指针的概念,讨论了其语义角色,并提出可以像NaN装箱那样,使用其他无效指针值来存储额外信息。
写给 Rust 程序员的 C 语言
一篇由 Rust 程序员撰写的博文,记录了其自学 C 语言过程中遇到的各种奇特行为,包括通过 <stdbool.h> 实现布尔类型、以 null 结尾的字符串与 Rust 中长度前缀的 &str 之间的对比,以及相关的内存权衡。对于从 Rust 转向 C 的开发者来说,它是一份实用的配套指南。
C 语言中的类型安全泛型数据结构
一篇技术博客文章,探讨在 C 语言中编写类型安全泛型数据结构的技巧,从头文件包含宏和 void 指针,逐步深入到联合体与内联存储,并以链表实现为例进行说明。