阅读 C++ SDK 头文件时,最容易卡住的不是 if 和 for,而是 const char*、void**、T&、数组长度和谁负责释放内存。本篇不把指针讲成玄学,而是围绕三个问题展开:对象在哪里、一个表达式指向谁、对象能活多久。
1. 类型决定可执行的操作
C++ 内置了整数、浮点、字符和布尔类型,也允许定义枚举、结构体和类:
| |
std::uint32_t、std::uint64_t 等固定宽度整数在平台提供对应精确宽度类型时可用,适合协议字段和经过设计的 ABI。普通 int 至少为 16 位,但标准不承诺它在所有平台都恰好是 32 位。跨语言边界不能只凭本机经验猜大小。
sizeof(T) 返回类型或对象占用的字节数,但结构体大小不一定等于各字段大小简单相加:编译器可能为对齐插入填充。字段类型一致也不代表不同编译器选项下的布局一定兼容。
2. 值、引用和指针
| |
调用方式如下:
| |
T按值传递,函数获得一个参数对象;复制可能被移动或省略规则优化,但接口语义仍是值;T&是引用,必须绑定到有效对象,常表示非空借用;T*是指针,保存地址,可用nullptr表示没有对象;&object取得地址,*pointer解引用并访问所指对象。
引用不是“更安全的指针”这么简单。引用和指针都可能在目标对象生命周期结束后悬空;区别主要体现在语法和接口表达力,而非自动生命周期管理。
3. const 要看修饰的是谁
| |
函数参数中最常见的是:
| |
它表示函数只读 values 指向的元素,但没有说明指针能否为空、实际有多少元素、内存由谁拥有。count 和指针之间的对应关系只是接口约定,调用者必须同时满足。
在纯 C++ 内部代码中可使用 std::span<const double> 把“指针 + 长度”组合为一个非拥有视图:
| |
不过 std::span 是 C++ 标准库类型,不应直接放进供 P/Invoke 使用的 C ABI。跨边界时仍需展开为基础类型。
4. 数组、容器与字符串
| |
std::vector<T>拥有一段连续元素,可自动管理容量和释放内存;std::string拥有字符序列;std::string_view只是字符视图,不拥有底层数据;- 原生数组
T[N]的长度是类型的一部分,但传给多数函数后常退化为指针,长度信息不会随指针自动传递。
string_view 很轻量,但必须比它观察的数据活得短。下面的函数会返回悬空视图:
| |
动态库也不能把内部临时 std::string 的 c_str() 返回给 C# 后继续假设它永远有效。字符串编码、终止符、缓冲区容量和有效期必须成为接口契约的一部分。
5. 存储位置不等于生命周期模型
初学资料常把内存简单分成“栈”和“堆”,但 C++ 标准层面更重要的是存储期和对象生命周期:
| |
局部对象通常由实现放在调用栈,但优化器可以改变具体存放方式;new 创建的对象具有动态存储期,必须由匹配的所有者释放。现代 C++ 业务代码一般不应裸写上面的 new / delete,而应优先使用局部对象、容器和智能指针。
对象生命周期结束后,之前指向它的指针不会自动变成 nullptr。继续解引用就是未定义行为:程序可能崩溃、貌似正常,或在优化构建中表现完全不同。
6. 未定义行为为什么危险
常见来源包括:
- 数组或缓冲区越界;
- 解引用空指针或悬空指针;
- 读取未初始化的基础类型;
- 有符号整数溢出;
- 数据竞争;
- 用不匹配的方式释放内存。
“未定义”不是“标准规定必须抛异常”。它表示程序已经违反语言要求,标准不再约束后续行为。调试版能运行不能证明代码正确,跨编译器、跨优化级别或升级工具链后尤其容易暴露问题。
MSVC 可使用 AddressSanitizer 帮助发现部分内存错误:
| |
GCC 和 Clang 常用:
| |
Sanitizer 是运行时检测工具,只能覆盖实际执行到的路径,不能替代正确的所有权设计和测试。
7. 怎样读 SDK 函数签名
假设厂商头文件给出:
| |
逐项追问:
device是否允许为空,由谁创建和销毁?buffer是调用者提供的可写内存,还是函数会替换指针?capacity以字节还是元素计数?bytes_written是否必填,失败时是否仍会写入?- 返回
int是状态码还是实际长度?错误码如何解释? - 函数返回后,库是否仍保留任何指针?是否允许并发调用?
头文件只描述类型,未必描述完整契约。P/Invoke 声明必须以供应商头文件和文档为依据,不能只看函数名字推断。
8. 跨 ABI 时需要更保守
在 DLL 边界应谨慎对待这些类型:
long:Windows 上通常为 32 位,许多 64 位 Unix 平台上为 64 位;wchar_t:Windows 常为 16 位,许多 Unix 平台常为 32 位;bool:不要假设与 C# 默认 P/Invoke 布尔封送布局相同;size_t:随进程位数变化,C# 通常用nuint对应;- 指针和句柄:随进程架构变化,C# 通常用
nint或SafeHandle; - C++ 标准库类型:
std::string、std::vector等布局属于具体实现的 ABI,不直接暴露给 P/Invoke。
更稳妥的公共接口使用固定宽度整数、明确的指针 + 长度、显式编码和不透明句柄。C# 侧的具体类型映射可继续阅读 C# 原生互操作与设备 SDK(二):C# 类型与 C ABI。
9. 本篇小结
值表达独立对象,引用和指针通常表达借用或间接访问,但它们不会自动管理目标生命周期。现代 C++ 内部优先让容器和资源管理对象持有内存;进入 ABI 边界后,再把接口收窄为尺寸、编码和所有权都明确的基础类型。