placement new(这是第7章讨论过的重要特性)有多种用途,但其中一个有趣的用途是:能将软件对象映射到内存映射硬件,从而让我们可以像操作软件一样驱动硬件。
要编写一个实际可用的示例会比较棘手,这会涉及“非可移植代码领域” —— 需要使用操作系统特定功能,来获取特定设备的地址,并讨论如何获取通常由软件驱动程序访问的内存位置的读写权限。因此,需要构建一个具有说明性的示例,并请读者们假设这个示例是完整的。
首先,假设正在为一块代号为“super_video_card”的新型显卡开发驱动程序。为了说明目的,用以下类来模拟这个显卡:
#include <cstdint>
class super_video_card {
// ...
public:
// 超级显卡寄存器
volatile std::uint32_t r0{}, r1{}, r2{}, r3{};
static_assert(sizeof(float) == 4); // sanity check
volatile float f0{}, f1{}, f2{}, f3{};
// 初始化显卡状态
super_video_card() = default;
super_video_card(const super_video_card&) = delete;
super_video_card& operator=(const super_video_card&) = delete;
// 可用于重置显卡状态
~super_video_card() = default;
// 各种服务(为了简洁而省略)
};
// ...
这个类最重要的特性如下:
不可复制性: 作为映射到特定内存区域的类型,复制该类型对象会适得其反(通过=delete显式禁止复制操作)。
硬件内存布局映射:类的设计使其状态在概念上能与硬件寄存器对应:内存布局起始处预期是4个32位整型寄存器,紧随其后是4个32位浮点寄存器(使用
编译期验证与volatile修饰:通过static_assert在编译期验证类型尺寸与对齐要求;寄存器对应的成员变量标记为volatile,因为硬件状态可能被外部改变,确保每次访问都实际执行I/O操作(符合C++抽象机模型对volatile访问的语义要求)。
若不熟悉volatile变量的用法,可能会疑惑为何要在内存映射硬件类中使用该限定符。其重要性在于:需要阻止编译器基于假设进行优化 —— 比如“若代码未主动修改这些变量,则其状态不会改变”,或“若代码写入后未立即读取,则可视为无效操作”。通过volatile限定,向编译器说明:“这些对象的状态变化超出你的认知范围,请勿擅自优化。”
为了简化实现,我们使用了一个将数据成员清零的构造函数和一个简单的析构函数。但在实际开发中,可以通过(默认或其他)构造函数将内存映射设备初始化为所需状态,并通过析构函数将设备重置到某个可接受状态。
通常,程序要访问内存映射硬件需要与操作系统交互,调用相关服务函数,并传入标识目标设备的参数,来获取其内存地址。在本例中,将模拟这种情况 —— 假设能直接访问一块具有正确大小和对齐要求、可读写的内存区域。操作系统函数通常会以void*类型返回原始内存地址,这也符合现实中的情况:
// 在内存中的某个位置,我们具有读/写访问权限,该位置对应着实际设备的内存映射硬件
alignas(super_video_card) char
mem_mapped_device[sizeof(super_video_card)];
void* get_super_card_address() {
return mem_mapped_device;
}
// ...
接下来将演示如何使用placement new将对象映射到内存映射硬件位置。需要包含<new>头文件,因为placement new在此定义。实现目标的具体步骤如下:
最终实现如下:
// ...
#include <new>
int main() {
// 将我对象映射到硬件上
void* p = get_super_card_address();
auto the_card =
new(p) super_video_card{ /* 参数 */ };
// 通过指针 the_card,使用实际的内存映射硬件
// ...
the_card->~super_video_card();
}
若显式调用析构函数存在风险(可能抛出异常的代码路径中),可采用带自定义删除器的std::unique_ptr(参见第5章)来安全终结super_video_card对象:
// ...
#include <new>
#include <memory>
int main() {
// 将对象映射到硬件上
void* p = get_super_card_address();
std::unique_ptr<
super_video_card,
decltype([](super_video_card *p) {
p->~super_video_card(); // do not call delete p!
})
> the_card {
new(p) super_video_card{ /* args */ }
};
// 通过指针 the_card,使用实际的内存映射硬件
// ...
// 程序块结束时,unique_ptr 会自动调用其删除器,隐式调用 the_card->~super_video_card()
}
此实现中,std::unique_ptr仅负责终结所指对象(即super_video_card实例)的生命周期,而不会释放其内存存储空间。这种设计能确保即使the_card变量生命周期内发生异常,代码仍能保持健壮性。