C++分文件操作时,在主函数文件中include的是.h文件,而不是.cpp文件,即包含的是声明部分而不是定义部分

在学习c++分文件操作的时候,我们学习到了在编写项目的时候可以将其分为三个部分(xxx.h + xxx.cpp + main.cpp):

  1. 头文件中写类的声明、函数的声明、#define常数等,一般不写类的实现。
  2. 源文件中包含一个cpp写类和函数的具体实现,通常建议与头文件相对应。
  3. 源文件中另一个为包含主函数main的文件,在其中可以调用定义好的函数。

头文件:

//头文件 f.h
#pragma once	//防止多次定义
#include<iostream>
using namespace std;

void demo();	//函数声明

源文件:

// f.cpp
#include "f.h"

void demo()		//函数实现
{
    
    
	cout << "This is a demo" << endl;
}
//main.cpp
#include<iostream>
using namespace std;
#include "f.h"

int main() {
    
    
	demo();		//函数调用
	return 0;
}

看到这里,我就有了一个疑问,为什么在main.cpp中,我们只include了头文件f.h,却没有include源文件f.cpp。 这不是就表示我们只包含了函数的声明,没有包含函数的实现吗?包含了函数的声明,我们也就可以在main.cpp中调用它,通过编译,但是我们并没有包含函数的实现,那么它在运行时,是如何找到函数的实现的呢?

下面是我在阅读其他大佬的博文和问答中,总结出的一些结论。笔者自己也在学习中,有些地方可能解答的不准确,如有错误,欢迎大家指正。

对于上面这个问题,其实牵扯到C++语言从编写到执行的整个过程,一般来讲,开发一个C++程序需要经过以下几步

  1. 编写代码,
  2. 编译器进行编译,
  3. 编译器进行链接。
  4. 执行。

我在一个相关问答中看到了一个非常好的回答,把c++的程序开发比作建造飞机的过程,详细的回答可以参考https://zhidao.baidu.com/question/935481360407990892.html,以下是原答主的解答内容:

打个比方,你要建造一架飞机,需要发动机,机翼,机身,尾翼,起落架。你可以把这几个部分交给专门的厂商去制作,这就是编译过程。这么多零件交给你,你只需要组装起来就可以了,这就是连接过程。
编译器(比如VC)就是加工零件的工厂,通过编译器的源代码会变成目标文件,也就是零件,VC生成的是.obj文件。
连接器(比如VC下的link)就是组装工厂,它能把所有的零件组装成你需要的东西。
好了,搞懂了编译器和连接器,我们再来看头文件.h和实现文件.cpp的作用。
还是拿飞机举例子。机身和机翼是必须连接起来的,但是他们之间怎么连接呢?制作机翼的只会做机翼,制作机身的也只会制作机身。那么作为组装工厂的你就会提供给他们一份飞机的接口设计图,图纸里面详细描述了机翼和机身怎么连接,但并不描述机翼和机身应该怎么去制作。那些零件工厂拿到结构图纸以后,就知道了,原来机翼是被安放在机身的这个地方,嗯,而且规定了用铆钉(打个比方)连接。好了,我知道了。可以做了,作为机翼制造商,我不用关心机身是怎么做的,我只关心机翼的制作和与机身的接口。换到C++这边来,这个用来描述接口的设计图就是.h文件,也就是头文件。具体机翼的实现也就相当于.cpp文件了。
所以,在程序中只需要应用头文件,也就是只需要知道接口的设计图。等你根据接口设计图设计好了零件,交给组装工厂,组装工厂(也就是连接器)会把所有的零件(编译器编译.cpp生成的.obj)连接起来,这样飞机就可以翱翔天空了。

了解了一个C++程序从编写到编译、链接和运行的整个过程之后,在看这个问题,就会简单多了。我们先来实践一下,将main.cpp中的include"f.h"改成include"f.cpp",运行一下,看看会出什么问题。

#include<iostream>
using namespace std;
#include "f.cpp"

int main() {
    
    
	demo();
	return 0;
}

程序报错,函数demo()重定义。
在这里插入图片描述
那么问题来了,我们只包含了一次f.cpp文件,为什么会显示重复定义呢?

仔细观察报错信息就可以发现,报错发生在链接时期,看到这里应该就有很多人看出问题所在了吧。在编译时,main.cpp文件和f.cpp文件都通过编译生成了可重定向目标文件—— main.obj和f.obj,而在链接时,这两个文件自动被链接器链接起来。这时,问题就出来了,我们在main.cpp文件中包含了f.cpp文件,在编译时被替换成了f.cpp文件中的内容,而我们在链接时,再一次将其中的内容添加进来,这也就导致了f.cpp文件中的demo函数出现了两次定义,所以就造成了链接报错,而编译没有报错。

这其实是VS2019这个强大的IDE造成的,它已经为我们定义好了整个编译、链接和运行过程。我们只需要点击调试按钮,VS2019就自动会自动完成这些操作。不过,这也就造成了我们的这一问题,因为VS2019在编译时,会将整个项目的源文件同时编译,并链接起来。至此,我们之前提出的那个问题解决了,在main.cpp中,不需要包含函数的实现文件f.cpp,链接器会在链接阶段,自动将两个文件内容链接起来,从而主函数能够找到demo()函数的实现。 其实就是编译器同时编译链接了主函数源文件和函数实现源文件,从而将函数实现加入到了最终的可执行文件.exe中。

也就是说,即使没有f.h文件,也不包含f.cpp文件,我们只是手动在main.cpp中添加函数的声明,使其能通过编译,我们同样能够得到相同的结果。

//f.cpp
#include<iostream>
using namespace std;
void demo()
{
    
    
	cout << "This is a demo" << endl;
}
//main.cpp
#include<iostream>
using namespace std;
void demo();

int main() {
    
    
	demo();
	return 0;
}

成功运行:
在这里插入图片描述
那如果我们就是想包含.cpp文件可不可以呢?这其实是可以的,我们右击项目源文件中函数的实现文件,选择从项目中排出即可实现直接包含.cpp文件。

另一种方法,我们可以通过g++指定编译链接的源文件,只编译链接我们的main.cpp文件,也可以得到运行结果。
在这里插入图片描述
通过g++就可以根据直观的看出之前的问题了,只编译运行main.cpp,可以得到结果,但是同时编译链接main.cpp和f.cpp两个文件,就会报错,demo()重定义。(与VS2019中的报错一致)

这些方法只是告诉大家,包含.cpp文件也是可行的,但是在实际的操作过程中,并不推荐这样做。在项目文件很多时,各种文件的包含操作就很容易产生重定义。而函数的声明是可以重复的,将其单独放在头文件中,就能实现为其他源文件提供接口,同时避免重定义问题。(如果头文件包含结构体等一些声明,也可以使用#pragma once或者#ifndef…#define…#endif来避免重定义)

猜你喜欢

转载自blog.csdn.net/lyb06/article/details/127501959