当 C# 项目需要调用厂商 DLL 时,文档里常会同时出现 .h、.lib、.dll、extern "C" 和 __declspec(dllexport)。如果只从 P/Invoke 声明入手,很容易把“函数签名写对”当成全部问题,却不知道这个 DLL 是怎样生成的、为什么符号找不到,以及头文件究竟是不是程序的一部分。
本系列面向已有 C# 等语言经验、需要阅读或维护原生 SDK 的开发者。目标不是穷举 C++ 语法,而是建立一套够用的模型:看懂常见代码,知道怎样编译和链接,并能为跨语言调用设计合理边界。示例使用 C++20 中成熟、常见的能力。
1. C++ 到底是什么
C++ 是一门由 ISO 标准定义的通用编程语言。标准描述语法、类型系统、抽象机制和标准库接口,但不会给你一个名为“C++”的唯一运行程序。真正把源码变成目标平台程序的是具体工具链,例如:
- Microsoft Visual C++(MSVC):Windows 和 Visual Studio 中常见;
- GCC:Linux 发行版中常见;
- Clang/LLVM:Windows、Linux、macOS 均可使用。
“C++”“编译器”“标准库”“IDE”不是同一个东西。Visual Studio 是开发环境,cl.exe 才是 MSVC 编译器驱动程序;GCC 的 C++ 驱动通常叫 g++,Clang 的通常叫 clang++。
C++ 常被称为编译型语言,因为典型实现会在运行前把源代码编译、链接为目标平台机器码。不过,语言标准主要规定程序的可观察行为,并不强制实现必须采用某一条内部流水线。工程上把它理解为“通常提前编译成特定平台的原生程序”就够用了。
2. 第一个程序
| |
这几行已经包含了不少 C++ 概念:
#include由预处理阶段处理,把所需声明引入当前翻译单元;std::string和std::cout来自标准库的std命名空间;const表示初始化后不能通过这个名称修改对象;main是常规宿主环境中 C++ 可执行程序的入口,返回0通常表示成功;- 语句通常以分号结束,代码块由花括号界定。
不要为了少写几个字符就在头文件或大型项目中使用 using namespace std;。显式写出 std:: 能说明名称来自哪里,也可降低大型工程中的名称冲突风险。
2.1 在 Windows 上编译
在 “Developer PowerShell for Visual Studio” 中执行:
| |
/std:c++20 选择语言模式,/EHsc 启用常规 C++ 异常语义,/W4 打开较严格的警告。普通 PowerShell 若没有初始化 Visual Studio 开发环境,可能找不到 cl,这不等于编译器没有安装。
2.2 在 Linux 上编译
| |
编译器警告应当进入日常反馈闭环,但“零警告”不等于程序不存在未定义行为、生命周期错误或业务缺陷。
3. 从源码到程序发生了什么
可以先用四个阶段理解常规构建:
| |
实际工具链可能把若干阶段合并执行,但排错时区分它们很重要:
- 语法错误、类型不匹配通常是编译错误;
- “无法解析的外部符号”或
undefined reference通常是链接错误; - 程序启动后找不到 DLL /
.so是加载阶段问题; - 成功加载后入口点不存在,往往是导出名、架构或声明不匹配。
4. .cpp 与 .h 各自负责什么
头文件通常放供其他翻译单元使用的声明,源文件放定义:
| |
| |
| |
声明告诉编译器“这个名称是什么”,定义提供实体或实现。头文件本身通常不是独立编译单元;它会被 #include 到一个或多个源文件中。#pragma once 用于避免同一头文件在一个翻译单元中被重复包含,主流编译器均支持;跨工具链公共头也常使用传统 include guard。
分别编译再链接时,命令大致如下:
| |
如果头文件只写了声明,却没有把包含定义的目标文件或库交给链接器,源码可以通过编译,最后仍会链接失败。
5. 先掌握这些基础语法
下面的函数足以串起变量、分支、循环、容器和函数:
| |
读函数签名时从外向内看:它名为 average_positive,返回 double,接收一个 std::vector<double> 的常量引用。const ...& 表示函数借用现有容器、不会复制整个 vector,并承诺不通过该引用修改它。
C# 开发者还应注意:
- C++ 局部变量不会自动获得 C# 字段那样的默认值;未初始化的基础类型可能具有不确定值;
- 整数除法仍会截断小数,
5 / 2得到2; ==比较对象时调用何种语义由类型和运算符定义决定;- 编译通过不代表访问越界、悬空指针等错误会被运行时统一拦截。
6. C++、C 和 C++/CLI 不要混为一谈
- C 是另一门语言,和 C++ 有大量共同语法,但拥有自己的标准和类型规则;
- 原生 C++ 通常生成目标平台的原生代码,可构建 DLL、
.so和可执行文件; - C++/CLI 是 Microsoft 面向 .NET 互操作的语言扩展,可同时接触托管与原生世界,但不是跨平台 C++ 的同义词;
- P/Invoke 描述 .NET 如何调用非托管库的函数入口,不会直接理解任意 C++ 类、模板或重载。
因此,看到“C++ DLL”时还不能断定它能直接 P/Invoke。你首先需要知道它导出了哪些符号,以及是否提供了稳定的 C 兼容入口。第四、第五篇会把这条边界完整接起来。
7. 本篇小结
C++ 是语言,MSVC、GCC、Clang 是常见工具链实现。源文件经过预处理、编译和链接,最终成为可执行文件或库;头文件主要提供声明,不是 DLL 本身。遇到原生 SDK 时,先区分编译、链接、加载和调用阶段,很多看似神秘的 P/Invoke 故障就有了明确位置。