我最近在课程中学习了C ++,并且我试图更深入地了解标头和类之间的伙伴关系。在每一个示例或教程中,我都查看了头文件,它们都使用带有构造函数的类文件,然后继续使用方法(如果包含)。但是,我想知道仅使用头文件来容纳一组相关函数是否很好,而不必每次使用它们时都需要使该类成为对象。
//main file
#include <iostream>
#include "Example.h"
#include "Example2.h"
int main()
{
//Example 1
Example a; //I have to create an object of the class first
a.square(4); //Then I can call the function
//Example 2
square(4); //I can call the function without the need of a constructor
std::cin.get();
}
在第一个示例中,我创建一个对象,然后调用该函数,我使用两个文件“ Example.h”和“ Example.cpp”
//Example1 cpp
#include <iostream>
#include "Example.h"
void Example::square(int i)
{
i *= i;
std::cout << i << std::endl;
}
//Example1 header
class Example
{
public:
void square(int i);
};
在example2中,我直接从下面的文件“ Example2.h”调用该函数
//Example2 header
void square(int i)
{
i *= i;
std::cout << i;
}
最终我想我要问的是,仅使用头文件来容纳一组相关功能而不创建相关类文件是否可行。如果答案是否定的,其背后的原因是什么。无论哪种方式,我都确定我已经看过了一些东西,但是一如既往,我感谢你们对此提供的任何见识!
答案 0 :(得分:3)
C ++具有一个定义规则(ODR)。该规则指出,函数和对象仅应定义一次。这是问题所在:标头通常不只包含一次。因此,您的square(int)
函数可能会定义两次。
ODR不是绝对规则。如果您将square
声明为
//Example2 header
inline void square(int i)
// ^^^
{
i *= i;
std::cout << i;
}
然后,编译器将通知链接器可能存在多个定义。然后,这是您的工作,以确保所有inline
的定义都相同,因此不要在其他地方重新定义square(int)
。
模板和类定义是免税的;它们可以出现在标题中。
答案 1 :(得分:3)
当然,只有标头就可以了(只要您已经考虑了前面提到的“一个定义规则”即可)。
您也可以编写没有任何头文件的C ++源代码。
严格来说,标头不过是归档的源代码片段,它可以#include
d(即粘贴)到多个C ++源文件(即翻译单元)中。记住这个基本事实有时对我很有帮助。
我做了以下人为设计的反例:
main.cc
:
#include <iostream>
// define float
float aFloat = 123.0;
// make it extern
extern float aFloat;
/* This should be include from a header
* but instead I prevent the pre-processor usage
* and simply do it by myself.
*/
extern void printADouble();
int main()
{
std::cout << "printADouble(): ";
printADouble();
std::cout << "\n"
"Surprised? :-)\n";
return 0;
}
printADouble.cc
:
/* This should be include from a header
* but instead I prevent the pre-processor usage
* and simply do it by myself.
*
* This is intentionally of wrong type
* (to show how it can be done wrong).
*/
// use extern aFloat
extern double aFloat;
// make it extern
extern void printADouble();
void printADouble()
{
std::cout << aFloat;
}
希望您注意到我宣布了
extern float aFloat
中的{li> main.cc
extern double aFloat
中的{li> printADouble.cc
那是一场灾难。
编译main.cc
时出现问题?否。翻译单元在语法和语义上(对于编译器)是一致的。
编译printADouble.cc
时出现问题?否。翻译单元在语法和语义上(对于编译器)是一致的。
将这些混乱链接在一起时出现问题吗?否。链接器可以解析所有需要的符号。
输出:
printADouble(): 5.55042e-315
Surprised? :-)
符合预期(假设您以及我都没有任何理智的认识)。
printADouble()
作为float
变量(8字节)访问已定义的double
变量(4字节)。这是未定义的行为,在多个级别上都出错。
因此,使用标头不支持,但可以启用C ++中的(某种)模块化编程。 (直到我曾经不得不使用还没有预处理器的C编译器时,我才意识到差异。因此,以上概述的问题对我造成了很大的打击,但对我来说也确实很有启发。) / p>
恕我直言,头文件是对模块化编程的基本功能(即接口的明确定义以及接口和实现的分离,作为语言功能)的实用替代。这似乎也惹恼了其他人。看看A Few Words on C++ Modules,了解我的意思。
答案 2 :(得分:2)
C ++是一种多范式编程语言,它可以是(至少):
在this quora answer中查看更多详细信息。
面向对象的范例(类)只是可以利用C ++进行编程的众多范例之一。
您可以将它们混合使用,也可以只使用一种或几种,这取决于解决软件问题的最佳方法。
因此,回答您的问题:
是的,您可以在同一头文件中将一堆(最好是相互关联的)函数分组。这在“旧的” C编程语言或更严格的过程语言中更为常见。
这就是说,就像MSalters的回答一样,只是要意识到C ++一个定义规则(ODR)。如果放置函数(主体)的声明,而不仅仅是函数的定义(免除模板),请使用inline关键字。 有关what "declaration" and "definition" are的说明,请参见此SO答案。
附加说明
要实施答案,并将其扩展到C ++中的其他编程范例,
在最近几年中,有一种将整个库(函数和/或类)放在单个头文件中的趋势。
这可以在开源项目中普遍且公开地看到,只需转到github或gitlab和search for "header-only":
答案 3 :(得分:0)
通常的方式是,并且一直都是将代码放入.cpp文件(或您喜欢的任何扩展名)中,并在标头中声明。 有时将代码放入标头中有一些好处,这可以使编译器进行更巧妙的内联。但是同时,由于每次编译器都必须处理所有代码,因此它可能会破坏您的编译时间。
最后,当所有代码都是标头时,具有循环对象关系(有时是理想的)通常很烦人。
某些例外情况是模板。许多新的“现代”库(例如boost)都大量使用模板,并且常常是“仅标头”。但是,仅应在处理模板时执行此操作,因为这是处理模板时的唯一方法。
编写仅标头代码的一些缺点 如果四处搜寻,您会发现很多人都在尝试寻找一种方法来减少处理boost的编译时间。例如:How to reduce compilation times with Boost Asio,它看到包含增强的单个1K文件的14s编译。 14s似乎并没有“爆炸”,但肯定比典型的要长得多,并且累积起来很快。在处理大型项目时。仅标头的库确实以相当可衡量的方式影响编译时间。我们只是容忍它,因为升压非常有用。
此外,还有很多事情不能仅在标头中完成(即使boost具有某些线程,文件系统等某些部分都需要链接的库)。一个主要的例子是,您不能在仅标头的lib中拥有简单的全局对象(除非您诉诸于单例的可憎),否则会遇到多个定义错误。 注意: C ++ 17的内联变量将使该示例在将来可行。
更具体地讲,Boost是库,而不是用户级代码。因此它不会经常更改。在用户代码中,如果将所有内容放在标头中,则每个小的更改都将导致您不得不重新编译整个项目。这是巨大的时间浪费(对于不从编译到编译不变的库而言,情况并非如此)。当您在标头/源和更好之间进行拆分时,使用前向声明减少包含,可以在一整天的总和中节省重新编译的时间。