MSVC cl.exe 尝试使用 C++ header 范式(定义与声明)

MSVC cl.exe trying to use C++ header paradigm (definition vs declaration)

我正在尝试实现一个使用相当流行的 C++ header 范例的库(在 .h / .hpp 和 .cpp 文件中分离定义和声明)。 我正在使用 MSVC cl.exe 编译器来针对 Windows 操作系统。

我明确使用 #include 指令,目标是 header 文件 abc.hpp,其中表达了定义。我希望编译器自动查找相应的 abc.cpp 文件并将对 abc.hpp::abc() 的调用重定向到 abc.cpp::abc().

(我知道我可以在示例中直接 #include "abc.cpp" 但请记住,我正在尝试解决一个更大的问题,即从 header 文件中包含整个库。)

(我也尝试使用 /I 编译器选项,但在测试和阅读文档后它并没有多大用处。) https://docs.microsoft.com/en-us/cpp/build/reference/i-additional-include-directories

我在下面描述了我 运行 的测试和我得到的编译器错误:

######## main.cpp ######## 
#include <stdio.h> // printf_s
#include "abc.hpp"
int main(int argc, char *argv[]) {
    abc();
    return(0);
}

######## abc.hpp ######## 
void abc();

######## abc.cpp ######## 
#include "abc.hpp"

void abc() {
    printf("ABC!\n");
}

######## command line ########
> cl main.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.X for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

main.cpp
Microsoft (R) Incremental Linker Version 14.X
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:main.exe
main.obj
main.obj : error LNK2019: unresolved external symbol "void __cdecl abc(void)" (?abc@@YAXXZ) referenced in function main
main.exe : fatal error LNK1120: 1 unresolved externals

我对编译器自动将 abc.hpp::abc() 重新映射到 abc.cpp::abc() 的期望是否偏离了目标? 我是否被迫将 .cpp 代码作为单独的 .lib 进行编译,以便能够 /link /LIBPATH: 编译器?

很高兴听到您对此事的见解。

你有一个...让我们对 C++ 编译和 linking 应该如何工作有更合理的意见。但事实是,C++ include compile and link 模型是古老的,似乎与现代语言中的任何现代模块化构建都格格不入。然而,它是如此深入到 C++ 中,这就是它与 C++ 一起使用的方式,而且从一开始就是这样。随着模块被引入 C++20 的标准,事情即将发生变化(缓慢地)(考虑到 C++ 的遗产和 backwards-compatibility,这确实是一项伟大而艰巨的成就)。


所以经典的方法是这样的:你包含包含你需要的声明的 headers。 #include 是一个预处理器指令,它 more-or-less 只是 header 内容的哑 copy-paste。这使您可以单独编译每个“编译单元”。然后你 link 将所有编译单元放入你的最终二进制文件(无论是库还是可执行文件)。

假设您有一个 foo.cpp,其函数定义为 foo

foo.cpp
int foo(int a)
{
     return a * a;
}

如果您需要从另一个编译单元调用 foo,您首先需要使 foo 的声明在该编译单元中可见。您只需编写声明即可轻松做到这一点:

bar.cpp
int foo(int);

void bar()
{
    int r = foo(24);
}

在使用 foo 之前,编译器需要知道 foo 是什么。也就是说,它需要知道它是一个接受 int 参数并返回 int 的函数。这就是我在上面的例子中写 int foo(int) 时所做的。但是您不应该写每个声明。您可以在 foo.hpp 中写一次 foo 的声明,然后包含 header:

foo.hpp
#pragma once // or standard-compliant guard

int foo(int);
bar.cpp
#include "foo.hpp"

void bar()
{
    int r = foo(24);
}

预处理器将在将 #include "foo.hpp" 提供给编译器之前用文件内容替换 #include "foo.hpp"

现在您可以将 bar.cpp 编译成 object 文件。编译器只是编写一个对符号 foo 的调用,该符号已知用于命名一个接受 int 参数并返回 int 的函数。但是 foo 的定义是不可见的。阿卡。编译器没有 foo.

的“源代码”

所以下一步就是link所有的编译单元。

编译和linking可以分两个阶段进行(我在例子中使用g++是因为我对参数比较熟悉,但是[=的原理是一样的69=]):

# compile each compilation unit
g++ bar.cpp -o bar.o
g++ foo.cpp -o foo.o

# link all into the final executable
g++ bar.o foo.o -o my_executable

或者您可以一次完成所有操作:

g++ foo.cpp bar.cpp -o my_executable

因为 cl.exe(以及 clanggcc)既不是编译器也不是 linker。它是一个代表您调用预处理器、编译器 and/or linker 的前端。


您需要了解这些详细信息以了解幕后发生的事情,以便在问题出现时解决问题,但您不应手动调用编译器。使用一个可以为您处理所有这些的构建系统。让 VS 为你做这个或使用 make / cmake。