C++ 为什么我不应该包含 cpp 文件而是使用标题?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1686204/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Why should I not include cpp files and instead use a header?
提问by ialm
So I finished my first C++ programming assignment and received my grade. But according to the grading, I lost marks for including cpp files instead of compiling and linking them
. I'm not too clear on what that means.
所以我完成了我的第一个 C++ 编程作业并获得了成绩。但根据评分,我丢了分数including cpp files instead of compiling and linking them
。我不太清楚这意味着什么。
Taking a look back at my code, I chose not to create header files for my classes, but did everything in the cpp files (it seemed to work fine without header files...). I'm guessing that the grader meant that I wrote '#include "mycppfile.cpp";' in some of my files.
回顾我的代码,我选择不为我的类创建头文件,而是在 cpp 文件中执行所有操作(它似乎没有头文件也能正常工作......)。我猜评分者的意思是我写了'#include "mycppfile.cpp";' 在我的一些文件中。
My reasoning for #include
'ing the cpp files was:
- Everything that was supposed to go into the header file was in my cpp file, so I pretended it was like a header file
- In monkey-see-monkey do fashion, I saw that other header files were #include
'd in the files, so I did the same for my cpp file.
我#include
使用 cpp 文件的理由是: - 应该进入头文件的所有内容都在我的 cpp 文件中,所以我假装它就像一个头文件 - 在monkey-see-monkey do fashion 中,我看到了其他头文件#include
在文件中,所以我对我的 cpp 文件做了同样的事情。
So what exactly did I do wrong, and why is it bad?
那么我到底做错了什么,为什么不好?
回答by goldPseudo
To the best of my knowledge, the C++ standard knows no difference between header files and source files. As far as the language is concerned, any text file with legal code is the same as any other. However, although not illegal, including source files into your program will pretty much eliminate any advantages you would've got from separating your source files in the first place.
据我所知,C++ 标准知道头文件和源文件之间没有区别。就语言而言,任何带有合法代码的文本文件都与其他文本文件相同。然而,虽然不违法,但将源文件包含在您的程序中几乎会消除您从一开始就分离源文件所获得的任何优势。
Essentially, what #include
does is tell the preprocessorto take the entire file you've specified, and copy it into your active file before the compilergets its hands on it. So when you include all the source files in your project together, there is fundamentally no difference between what you've done, and just making one huge source file without any separation at all.
本质上,什么#include
是告诉预处理器获取您指定的整个文件,并在编译器获得它之前将其复制到您的活动文件中。因此,当您将项目中的所有源文件包含在一起时,您所做的与仅制作一个巨大的源文件而完全没有任何分离的工作之间从根本上没有区别。
"Oh, that's no big deal. If it runs, it's fine,"I hear you cry. And in a sense, you'd be correct. But right now you're dealing with a tiny tiny little program, and a nice and relatively unencumbered CPU to compile it for you. You won't always be so lucky.
“哦,那没什么大不了的。如果它可以运行,那就没问题了,”我听到你哭了。从某种意义上说,你是对的。但是现在您正在处理一个很小的小程序,以及一个不错且相对不受阻碍的 CPU 来为您编译它。你不会总是那么幸运。
If you ever delve into the realms of serious computer programming, you'll be seeing projects with line counts that can reach millions, rather than dozens. That's a lot of lines. And if you try to compile one of these on a modern desktop computer, it can take a matter of hours instead of seconds.
如果您曾深入研究严肃的计算机编程领域,您会看到行数可能达到数百万而不是数十的项目。这是很多行。如果您尝试在现代台式计算机上编译其中之一,则可能需要几小时而不是几秒钟。
"Oh no! That sounds horrible! However can I prevent this dire fate?!"Unfortunately, there's not much you can do about that. If it takes hours to compile, it takes hours to compile. But that only really matters the first time -- once you've compiled it once, there's no reason to compile it again.
“哦不!这听起来很可怕!但我能阻止这种可怕的命运吗?!” 不幸的是,您对此无能为力。如果编译需要几个小时,那么编译也需要几个小时。但这只有第一次才真正重要——一旦你编译了一次,就没有理由再次编译它。
Unless you change something.
除非你改变什么。
Now, if you had two million lines of code merged together into one giant behemoth, and need to do a simple bug fix such as, say, x = y + 1
, that means you have to compile all two million lines again in order to test this. And if you find out that you meant to do a x = y - 1
instead, then again, two million lines of compile are waiting for you. That's many hours of time wasted that could be better spent doing anything else.
现在,如果您将两百万行代码合并成一个巨大的庞然大物,并且需要做一个简单的错误修复,例如x = y + 1
,,这意味着您必须再次编译所有两百万行以进行测试。如果你发现你打算做一个x = y - 1
,那么还有两百万行编译等着你。这浪费了很多时间,本来可以花在做其他事情上。
"But I hate being unproductive! If only there was some way to compiledistinct parts of my codebase individually, and somehow linkthem together afterwards!"An excellent idea, in theory. But what if your program needs to know what's going on in a different file? It's impossible to completely separate your codebase unless you want to run a bunch of tiny tiny .exe files instead.
“但我讨厌效率低下!如果有某种方法可以单独编译我的代码库的不同部分,然后以某种方式将它们链接在一起!” 一个绝妙的主意,理论上。但是,如果您的程序需要知道不同文件中发生的事情怎么办?除非您想运行一堆微小的 .exe 文件,否则不可能完全分离您的代码库。
"But surely it must be possible! Programming sounds like pure torture otherwise! What if I found some way to separate interface from implementation? Say by taking just enough information from these distinct code segments to identify them to the rest of the program, and putting them in some sort of headerfile instead? And that way, I can use the #include
preprocessor directiveto bring in only the information necessary to compile!"
“但它肯定是可能的!否则编程听起来就像是纯粹的折磨!如果我找到某种方法将接口与实现分开怎么办?比如说从这些不同的代码段中获取足够的信息来将它们识别到程序的其余部分,然后放入”而不是在某种头文件中?这样,我可以使用#include
预处理器指令只引入编译所需的信息!“
Hmm. You might be on to something there. Let me know how that works out for you.
唔。你可能正在那里做一些事情。让我知道这对你有什么作用。
回答by ialm
This is probably a more detailed answer than you wanted, but I think a decent explanation is justified.
这可能是一个比您想要的更详细的答案,但我认为一个体面的解释是合理的。
In C and C++, one source file is defined as one translation unit. By convention, header files hold function declarations, type definitions and class definitions. The actual function implementations reside in translation units, i.e .cpp files.
在 C 和 C++ 中,一个源文件被定义为一个翻译单元。按照惯例,头文件包含函数声明、类型定义和类定义。实际的函数实现驻留在翻译单元中,即 .cpp 文件。
The idea behind this is that functions and class/struct member functions are compiled and assembled once, then other functions can call that code from one place without making duplicates. Your functions are declared as "extern" implicitly.
这背后的想法是函数和类/结构成员函数被编译和组装一次,然后其他函数可以从一个地方调用该代码而不会产生重复。您的函数被隐式声明为“extern”。
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
If you want a function to be local for a translation unit, you define it as 'static'. What does this mean? It means that if you include source files with extern functions, you will get redefinition errors, because the compiler comes across the same implementation more than once. So, you want all your translation units to see the function declarationbut not the function body.
如果您希望一个函数对于翻译单元是本地的,您可以将其定义为“静态”。这是什么意思?这意味着如果包含带有 extern 函数的源文件,则会出现重新定义错误,因为编译器不止一次遇到相同的实现。因此,您希望所有翻译单元都能看到函数声明而不是函数体。
So how does it all get mashed together at the end? That is the linker's job. A linker reads all the object files which is generated by the assembler stage and resolves symbols. As I said earlier, a symbol is just a name. For example, the name of a variable or a function. When translation units which call functions or declare types do not know the implementation for those functions or types, those symbols are said to be unresolved. The linker resolves the unresolved symbol by connecting the translation unit which holds the undefined symbol together with the one which contains the implementation. Phew. This is true for all externally visible symbols, whether they are implemented in your code, or provided by an additional library. A library is really just an archive with reusable code.
那么这一切最终是如何融合在一起的呢?那是链接器的工作。链接器读取由汇编器阶段生成的所有目标文件并解析符号。正如我之前所说,符号只是一个名称。例如,变量或函数的名称。当调用函数或声明类型的翻译单元不知道这些函数或类型的实现时,这些符号被称为未解析。链接器通过将保存未定义符号的翻译单元与包含实现的翻译单元连接在一起来解析未解析的符号。呼。对于所有外部可见符号都是如此,无论它们是在您的代码中实现的,还是由附加库提供的。库实际上只是具有可重用代码的存档。
There are two notable exceptions. First, if you have a small function, you can make it inline. This means that the generated machine code does not generate an extern function call, but is literally concatenated in-place. Since they usually are small, the size overhead does not matter. You can imagine them to be static in the way they work. So it is safe to implement inline functions in headers. Function implementations inside a class or struct definition are also often inlined automatically by the compiler.
有两个明显的例外。首先,如果你有一个小函数,你可以让它内联。这意味着生成的机器代码不会生成外部函数调用,而是直接就地连接。由于它们通常很小,因此大小开销无关紧要。您可以想象它们的工作方式是静态的。所以在头文件中实现内联函数是安全的。类或结构定义中的函数实现通常也由编译器自动内联。
The other exception is templates. Since the compiler needs to see the whole template type definition when instantiating them, it is not possible to decouple the implementation from the definition as with standalone functions or normal classes. Well, perhaps this is possible now, but getting widespread compiler support for the "export" keyword took a long, long time. So without support for 'export', translation units get their own local copies of instantiated templated types and functions, similar to how inline functions work. With support for 'export', this is not the case.
另一个例外是模板。由于编译器在实例化它们时需要查看整个模板类型定义,因此不可能像独立函数或普通类那样将实现与定义分离。好吧,也许现在这是可能的,但是获得对“export”关键字的广泛编译器支持需要很长时间。因此,在不支持“导出”的情况下,翻译单元会获得自己的实例化模板类型和函数的本地副本,类似于内联函数的工作方式。在支持“导出”的情况下,情况并非如此。
For the two exceptions, some people find it "nicer" to put the implementations of inline functions, templated functions and templated types in .cpp files, and then #include the .cpp file. Whether this is a header or a source file doesn't really matter; the preprocessor does not care and is just a convention.
对于这两个例外,有些人发现将内联函数、模板化函数和模板化类型的实现放在 .cpp 文件中,然后 #include .cpp 文件中的实现“更好”。这是头文件还是源文件并不重要;预处理器不在乎,只是一个约定。
A quick summary of the whole process from C++ code (several files) and to a final executable:
从 C++ 代码(几个文件)到最终可执行文件的整个过程的快速摘要:
- The preprocessoris run, which parses all the directives which starts with a '#'. The #include directive concatenates the included file with inferior, for example. It also does macro-replacement and token-pasting.
- The actual compilerruns on the intermediate text file after the preprocessor stage, and emits assembler code.
- The assemblerruns on the assembly file and emits machine code, this is usually called an object fileand follows the binary executable format of the operative system in question. For example, Windows uses the PE (portable executable format), while Linux uses the Unix System V ELF format, with GNU extensions. At this stage, symbols are still marked as undefined.
- Finally, the linkeris run. All the previous stages were run on each translation unit in order. However, the linker stage works on all the generated object files which were generated by the assembler. The linker resolves symbols and does a lot of magic like creating sections and segments, which is dependent on the target platform and binary format. Programmers aren't required to know this in general, but it surely helps in some cases.
- 该预处理器运行时,它解析所有以“#”开始的指令。例如,#include 指令将包含的文件与下级连接起来。它还进行宏替换和令牌粘贴。
- 实际编译器在预处理器阶段之后在中间文本文件上运行,并发出汇编代码。
- 在汇编器在汇编文件并发射机代码运行,这通常被称为一个目标文件和如下所讨论的手术系统的二进制可执行的格式。例如,Windows 使用 PE(可移植可执行格式),而 Linux 使用带有 GNU 扩展的 Unix System V ELF 格式。在此阶段,符号仍标记为未定义。
- 最后,运行链接器。所有前面的阶段都按顺序在每个翻译单元上运行。但是,链接器阶段适用于由汇编器生成的所有生成的目标文件。链接器解析符号并执行很多魔术,例如创建部分和段,这取决于目标平台和二进制格式。程序员一般不需要知道这一点,但在某些情况下肯定会有所帮助。
Again, this was definetely more than you asked for, but I hope the nitty-gritty details helps you to see the bigger picture.
同样,这绝对比你要求的要多,但我希望细节能帮助你看到更大的图景。
回答by sharptooth
The typical solution is to use .h
files for declarations only and .cpp
files for implementation. If you need to reuse the implementation you include the corresponding .h
file into the .cpp
file where the necessary class/function/whatever is used and link against an already compiled .cpp
file (either an .obj
file - usually used within one project - or .lib file - usually used for reusing from multiple projects). This way you don't need to recompile everything if only the implementation changes.
典型的解决方案是将.h
文件仅用于声明,将.cpp
文件用于实现。如果您需要重用该实现,请将相应的.h
文件包含到使用.cpp
必要类/函数/任何内容的文件中,并链接到已编译的.cpp
文件(.obj
文件 - 通常在一个项目中使用 - 或 .lib 文件 - 通常使用用于从多个项目中重用)。这样,如果仅实现更改,您就不需要重新编译所有内容。
回答by Dan McGrath
Think of cpp files as a black box and the .h files as the guides on how to use those black boxes.
将 cpp 文件视为黑匣子,将 .h 文件视为如何使用这些黑匣子的指南。
The cpp files can be compiled ahead of time. This doesn't work in you #include them, as it needs to actual "include" the code into your program each time it compiles it. If you just include the header, it can just use the header file to determine how to use the precompiled cpp file.
cpp 文件可以提前编译。这在您#include 中不起作用,因为它需要在每次编译时将代码实际“包含”到您的程序中。如果只包含头文件,它可以只使用头文件来确定如何使用预编译的cpp文件。
Although this won't make much of a difference for your first project, if you start writing large cpp programs, people are going to hate you because compile times are going to explode.
尽管这对您的第一个项目没有太大影响,但如果您开始编写大型 cpp 程序,人们会讨厌您,因为编译时间会爆炸式增长。
Also have a read of this: Header File Include Patterns
回答by int3
Header files usually contain declarations of functions / classes, while .cpp files contain the actual implementations. At compile time, each .cpp file gets compiled into an object file (usually extension .o), and the linker combines the various object files into the final executable. The linking process is generally much faster than the compilation.
头文件通常包含函数/类的声明,而 .cpp 文件包含实际的实现。在编译时,每个 .cpp 文件都被编译成一个目标文件(通常是扩展名 .o),然后链接器将各种目标文件合并到最终的可执行文件中。链接过程通常比编译快得多。
Benefits of this separation: If you are recompiling one of the .cpp files in your project, you don't have to recompile all the others. You just create the new object file for that particular .cpp file. The compiler doesn't have to look at the other .cpp files. However, if you want to call functions in your current .cpp file that were implemented in the other .cpp files, you have to tell the compiler what arguments they take; that is the purpose of including the header files.
这种分离的好处:如果您正在重新编译项目中的 .cpp 文件之一,则不必重新编译所有其他文件。您只需为该特定 .cpp 文件创建新的目标文件。编译器不必查看其他 .cpp 文件。但是,如果要调用在其他 .cpp 文件中实现的当前 .cpp 文件中的函数,则必须告诉编译器它们采用什么参数;这就是包含头文件的目的。
Disadvantages: When compiling a given .cpp file, the compiler cannot 'see' what is inside the other .cpp files. So it doesn't know how the functions there are implemented, and as a result cannot optimize as aggressively. But I think you don't need to concern yourself with that just yet (:
缺点:在编译给定的 .cpp 文件时,编译器无法“看到”其他 .cpp 文件中的内容。所以它不知道那里的功能是如何实现的,因此无法进行积极的优化。但我认为你现在不需要担心这个(:
回答by Luká? Lalinsky
The basic idea that headers are only included and cpp files are only compiled. This will become more useful once you have many cpp files, and recompiling the whole application when you modify only one of them will be too slow. Or when the functions in the files will start depending on each other. So, you should separate class declarations into your header files, leave implementation in cpp files and write a Makefile (or something else, depending on what tools are you using) to compile the cpp files and link the resulting object files into a program.
仅包含头文件且仅编译 cpp 文件的基本思想。一旦您有许多 cpp 文件,这将变得更加有用,并且当您仅修改其中一个文件时重新编译整个应用程序将太慢。或者文件中的功能何时开始取决于彼此。因此,您应该将类声明分离到您的头文件中,将实现保留在 cpp 文件中并编写一个 Makefile(或其他内容,取决于您使用的工具)来编译 cpp 文件并将生成的目标文件链接到程序中。
回答by NeilDurant
If you #include a cpp file in several other files in your program, the compiler will try to compile the cpp file multiple times, and will generate an error as there will be multiple implementations of the same methods.
如果您在程序中的其他几个文件中#include 一个cpp 文件,编译器将尝试多次编译该cpp 文件,并且会产生错误,因为相同方法将有多个实现。
Compilation will take longer (which becomes a problem on large projects), if you make edits in #included cpp files, which then force recompilation of any files #including them.
如果在#included cpp 文件中进行编辑,则编译将花费更长的时间(这在大型项目中会成为问题),然后强制重新编译#include 文件。
Just put your declarations into header files and include those (as they don't actually generate code per se), and the linker will hook up the declarations with the corresponding cpp code (which then only gets compiled once).
只需将您的声明放入头文件并包含它们(因为它们本身实际上并不生成代码),链接器会将声明与相应的 cpp 代码挂钩(然后只编译一次)。
回答by Avi
While it is certainly possible to do as you did, the standard practice is to put shared declarations into header files (.h), and definitions of functions and variables - implementation - into source files (.cpp).
虽然当然可以像您那样做,但标准做法是将共享声明放入头文件 (.h),并将函数和变量的定义 - 实现 - 放入源文件 (.cpp)。
As a convention, this helps make it clear where everything is, and makes a clear distinction between interface and implementation of your modules. It also means that you never have to check to see if a .cpp file is included in another, before adding something to it that could break if it was defined in several different units.
作为惯例,这有助于明确所有内容的位置,并明确区分接口和模块的实现。这也意味着您永远不必检查一个 .cpp 文件是否包含在另一个文件中,然后再向它添加一些可能会破坏的内容,如果它是在几个不同的单元中定义的。
回答by AndersK
re-usability, architecture and data encapsulation
可重用性、架构和数据封装
here's an example:
这是一个例子:
say you create a cpp file which contains a simple form of string routines all in a class mystring, you place the class decl for this in a mystring.h compiling mystring.cpp to a .obj file
假设您创建了一个 cpp 文件,其中包含一个简单形式的字符串例程,所有这些都在 mystring 类中,您将类 decl 放在 mystring.h 中,将 mystring.cpp 编译为 .obj 文件
now in your main program (e.g. main.cpp) you include header and link with the mystring.obj. to use mystring in your program you don't care about the details howmystring is implemented since the header says whatit can do
现在在你的主程序(例如 main.cpp)中,你包含了头文件和与 mystring.obj 的链接。在你的程序中使用 mystring 你不关心mystring 是如何实现的细节,因为标题说明了它可以做什么
now if a buddy wants to use your mystring class you give him mystring.h and the mystring.obj, he also doesn't necessarily need to know how it works as long as it works.
现在,如果一个伙伴想要使用你的 mystring 类,你给他 mystring.h 和 mystring.obj,他也不一定需要知道它是如何工作的,只要它有效。
later if you have more such .obj files you can combine them into a .lib file and link to that instead.
稍后如果您有更多这样的 .obj 文件,您可以将它们组合成一个 .lib 文件并链接到该文件。
you can also decide to change the mystring.cpp file and implement it more effectively, this will not affect your main.cpp or your buddies program.
您也可以决定更改 mystring.cpp 文件并更有效地实现它,这不会影响您的 main.cpp 或您的好友程序。
回答by nobar
If it works for you then there is nothing wrong with it -- except that it will ruffle the feathers of people who think that there is only one way to do things.
如果它对你有用,那么它没有任何问题——除了它会激怒那些认为只有一种做事方式的人。
Many of the answers given here address optimizations for large-scale software projects. These are good things to know about, but there is no point in optimizing a small project as if it were a large project -- that is what is known as "premature optimization". Depending on your development environment, there may be significant extra complexity involved in setting up a build configuration to support multiple source files per program.
这里给出的许多答案都针对大型软件项目的优化。这些都是值得了解的好东西,但是没有必要像优化大项目一样优化小项目——这就是所谓的“过早优化”。根据您的开发环境,设置构建配置以支持每个程序的多个源文件可能会涉及显着的额外复杂性。
If, over time, your project evolves and you find that the build process is taking too long, thenyou can refactoryour code to use multiple source files for faster incremental builds.
如果随着时间的推移,您的项目不断发展,并且您发现构建过程花费的时间太长,那么您可以重构代码以使用多个源文件来加快增量构建。
Several of the answers discuss separating interface from implementation. However, this is not an inherent feature of include files, and it is quite common to #include "header" files that directly incorporate their implementation (even the C++ Standard Library does this to a significant degree).
一些答案讨论了将接口与实现分开。然而,这不是包含文件的固有特性,而且 #include “头”文件直接合并它们的实现是很常见的(即使 C++ 标准库也在很大程度上做到了这一点)。
The only thing truly "unconventional" about what you have done was naming your included files ".cpp" instead of ".h" or ".hpp".
关于您所做的唯一真正“非常规”的事情是将包含的文件命名为“.cpp”而不是“.h”或“.hpp”。