C#调用C++结构体需用StructLayout和MarshalAs显式控制内存布局:设Pack=1禁填充,bool映射byte,定长数组用MarshalAs(ByValArray),指针字段须分步处理并注意内存释放。

结构体在C#中用StructLayout和MarshalAs对齐C++内存布局
C++导出的结构体在C#里直接Marshal.PtrToStructure会崩溃或读错字段,根本原因是默认的托管结构体按.NET规则打包(自动重排、字段对齐),而C++结构体是按编译器默认规则(通常是4字节或8字节对齐)布局的。必须显式控制。
关键操作:
- 给C#结构体加
[StructLayout(LayoutKind.Sequential, Pack = 1)]——Pack = 1禁用填充,最安全;若C++用#pragma pack(4),这里就写Pack = 4 - 所有字段类型严格对应:C++的
int→ C#的int(不是Int32别名,但效果一样),bool→byte(C++bool通常占1字节,C#boolmarshal默认是4字节,会错位) - 字符串字段不能直接用
string,要声明为IntPtr,后续用Marshal.PtrToStringAnsi或PtrToStringUTF8转换
示例C++结构体:
struct Config {
int version;
char name[64];
bool enabled;
};
对应C#结构体:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Config
{
public int version;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] name; // 不用 IntPtr,因为是定长数组
public byte enabled; // 用 byte,不是 bool
}
C++导出函数接收char*时,C#端必须用StringBuilder或IntPtr传入可写缓冲区
C++函数如果形参是char* buffer, int size用于输出字符串(比如int get_error_msg(char* out_buf, int len)),C#不能传string——.NET字符串是不可变且带BOM/编码元数据的,直接传指针会导致访问违规或乱码。
正确做法分两种场景:
- 已知最大长度且需要C++写入后读取:用
StringBuilder,并用[MarshalAs(UnmanagedType.LPStr)]标记参数 - 需要手动分配/释放或对接复杂内存管理:用
Marshal.AllocHGlobal申请非托管内存,传IntPtr,调用完再Marshal.PtrToStringAnsi转字符串,最后Marshal.FreeHGlobal
推荐第一种,简洁安全:
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int get_error_msg([MarshalAs(UnmanagedType.LPStr)] StringBuilder buf, int len);
<p>// 调用:
var sb = new StringBuilder(256);
get_error_msg(sb, sb.Capacity);
string msg = sb.ToString();</p>
const char*输入字符串在C#里用string加UnmanagedType.LPStr即可,但要注意编码和生命周期
C++函数若只读取字符串(如void log_message(const char* msg)),C#传string是可行的,但必须指定[MarshalAs(UnmanagedType.LPStr)],否则默认按Unicode传宽字符,C++收到的是乱码地址。
注意两个坑:
- 编码:
LPStr走ANSI编码(系统本地代码页),若C++期望UTF-8,就得改用UnmanagedType.LPUTF8Str(.NET 5+支持;旧版本需手动Marshal.StringToHGlobalUTF8) - 生命周期:P/Invoke会自动在调用期间固定字符串内存,但**不能传
null字符串**——C++侧没做空指针检查的话会崩;稳妥做法是传string.Empty或显式判断
声明示例:
[DllImport("mylib.dll")]
private static extern void log_message([MarshalAs(UnmanagedType.LPStr)] string msg);
结构体含指针字段(如char* data)时,C#不能直接marshal,必须拆解为两步操作
这是最容易翻车的地方:C++结构体里有char* data成员,C#用Marshal.PtrToStructure读出来只是个地址值,不代表能直接当字符串用——那个指针指向的内存可能在C++堆上,生命周期不由C#控制,下次GC或函数返回后就失效了。
必须放弃“一步marshal”,改用:
- 先用
Marshal.PtrToStructure读出结构体基本字段(不含指针) - 再用
Marshal.ReadIntPtr取出指针字段值 - 根据C++文档确认该指针指向内容是否可读、是否需释放、编码格式
- 用
Marshal.PtrToStringAnsi等转换,并立即复制内容(不要缓存IntPtr)
例如C++结构体:
struct Packet {
int len;
char* payload;
};
C#处理逻辑:
var ptr = Marshal.AllocHGlobal(sizeof(Packet)); // ... 调用C++填入ptr ... var pkt = Marshal.PtrToStructure<packet>(ptr); IntPtr payloadPtr = Marshal.ReadIntPtr(ptr, offsetof<packet>(x => x.payload)); string payload = Marshal.PtrToStringAnsi(payloadPtr); // 立即转字符串 Marshal.FreeHGlobal(ptr); // 结构体本身内存可释放</packet></packet>
真正麻烦的永远不是怎么传,而是谁负责释放那块payload内存——如果C++用malloc分配,C#就得调用对应的C++释放函数,不能用FreeHGlobal。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











