我正在尝试实现一个使用相当流行的C++标头范式的库(在.h/.hpp和.cpp文件中分隔定义和声明(。 我正在使用 MSVC cl.exe 编译器来定位 Windows 操作系统。
我显式使用 #include
指令,该指令针对表示定义的头文件abc.hpp
。我希望编译器自动查找相应的abc.cpp
文件,并将对abc.hpp::abc()
的调用重定向到abc.cpp::abc()
。
(我知道我可以直接在示例中#include "abc.cpp"
,但请记住,我正在尝试解决一个更大的问题,即从它的头文件中包含整个库。
(我也尝试使用 /I
编译器选项,但在测试和阅读文档后,它没有多大用处。 https://learn.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++编译和链接应该如何工作提出更合理的意见。但事实是,包含编译和链接模型C++是古老的,似乎与现代语言中的任何现代模块化构建都格格不入。然而,它被如此刻在C++上,这就是它与C++相伴的方式,从一开始就是这样。随着模块在 C++20 年被引入标准,情况即将(缓慢地(发生变化(考虑到 C++ 的遗产和向后兼容性,这确实是一项伟大而艰巨的成就(。
所以经典的方法是这样的:你包括包含你需要的声明的标头。 #include
是一个预处理器指令,它或多或少只是标头内容的愚蠢复制粘贴。这使您可以单独编译每个"编译单元"。然后,将所有编译单元链接到最终的二进制文件中(将其作为库或可执行文件(。
假设您有一个关于函数foo
定义foo.cpp
。
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
声明,然后包含该标头:
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"
替换为文件的内容。
现在,您可以将bar.cpp
编译为目标文件。编译器只是编写对一个符号的调用,foo
已知该符号命名一个采用 int 参数并返回 int 的函数。但是,foo
的定义是不可见的。即。编译器没有foo
的"源代码"。
因此,下一步是将所有编译单元链接在一起。
您可以在两个单独的阶段进行编译和链接(我在示例中使用 g++
,因为我更熟悉参数,但 cl.exe 的原理是相同的(:
# 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
(以及clang
和gcc
(既不是编译器也不是链接器。它是一个前端,代表你调用预处理器、编译器和/或链接器。
您需要了解这些详细信息以了解幕后发生的事情,以便在出现问题时解决问题,但不应手动调用编译器。使用将为您处理所有这些问题的构建系统。让VS为您执行此操作或使用make/cmake。