(一〇九)单独编译(多个源代码文件和头文件)

简介:

单独编译的简单原理:

C++在内存中储存数据提供了多种选择。

 

可以选择数据保留在内存中的时间长度(存储持续性)以及程序的哪一部分可以访问数据(作用域和链接)等。可以使用new来动态地分配内存,而定位new运算符提供了这种技术的一种变种。C++名称空间是另一种控制访问权的方式。

 

通常,大型程序都由多个源代码文件组成,这些文件可能共享一些数据。这样的程序涉及到文件的单独编译。

 

 

————***一〇九谈的是单独编译***一一〇谈的是存储***——————

 

C语言一样,C++也允许,甚至鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将他们链接成可执行的程序。(通常,C++编译器既要编译程序,也要管理链接器)。

 

如果只是修改一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。

 

另外,大多数C++环境都提供了其他工具来帮助管理。例如,UNIXLinux系统都具有make程序,可以跟着程序依赖的文件以及这些文件的最后修改时间。

 

运行make时,如果它检测到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。大多数集成开发环境(包括Embarcadero C++ Builder、Microsoft Visual C++Apple Xcode和Freescale CodeWarrior)都在Project菜单中提供了类似的工具。

 

 

假如有这样一个程序。他包含一个结构,有main()函数和其他两个函数,这三个函数都使用了这样一个结构。

如果我们要把main函数和其他两个函数分拆到两个文件之中,那个这个结构怎么办?是放在main函数还是放在其他两个函数所在的文件?还有,这两个函数的原型怎么办?

 

事实上,是把这个结构放在头文件之中(也可以顺便将两个函数的原型放在同一个头文件之中,原因见之后,是我自己推测的),把main()函数放在一个源代码文件之中,把其他两个函数放在另一个源代码文件之中。这两个源代码文件都包含这个头文件。

程序分为三部分:

①头文件:包含结构声明、以及两个函数原型;

②源代码文件a:包含main()函数;

③源代码文件b:包含另外两个函数。

 

这是一个非常有用的组织程序的策略。

例如,我们编写另外一个程序时,也需要使用这些函数(指另外两个函数),则只需要包含头文件(里面有结构和另外两个函数的原型),并将函数文件(指上面的源代码文件b)添加到项目列表或make列表之中即可。

 

另外,这种组织程序也与OOP方法一致。

一个文件(头文件)包含了用户定义类型的定义;

另一个文件包含操纵用户定义类型的函数的代码。

这两个文件组成了一个软件包,可以用于各种程序之中。

 

 

不要将函数定义变量声明放在头文件之中。

这样对于简单的文件可能是可行的,但通常会引发问题。

例如,如果在头文件之中包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中(因为据说是程序是将多个文件编译后链接在一起)将包含同一个函数的两个定义,除非函数是内联的(why内联就可以?),否则这将出错。

 

 

 

头文件中常包含的内容:

①函数原型;

②使用#defineconst定义的符号常量;

③结构声明;

④类声明;

⑤模板声明;(因为不生成函数定义,只有在调用的时候才生成)

⑥内联函数。

 

原因:

②被声明为const的数据和内联函数有特殊的链接属性,(不清楚)

③结构声明:因为不创建变量,只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。

⑤模板声明:类似结构声明,注意,模板并不直接创建函数定义,而是调用的时候创建对应的函数定义。

 

 

 

如果要将多个源代码文件和头文件组合在一起,应如下做:

①首先,可以将上面所说的,头文件中常包含的内容,放在一个或者多个头文件中;

 

②其他源代码文件需要引用头文件,且不能用方括号“<>”,而应该用双引号“""”。语法为:#include"头文件名"

例如,头文件名为aaa.h,那么其他源代码文件在需要引用这个头文件时,应加入:

#include"aaa.h"

且每个需要引用头文件内容的源代码文件,都需要有这样一行代码。

 

③头文件中如果要包含其他头文件的内容,例如在结构声明中需要string类,就如同普通源文件那样,添加#include<string>,然后需要记得string之前要加std::

例如:

#include<string>

struct abc

{

std::string c;

};

这样才可以正常使用string类。

 

④头文件中一般要包含函数原型。

这样的话,凡是调用这个头文件的cpp文件(源代码文件)都可以省略函数原型,否则还要像正常函数调用那样在main函数之前加入函数原型。

 

⑤在同一个源代码文件,若要包含一个头文件,那么通常只会包含一次。

例如如果需要使用string类,那么在这个源文件内,我们会加入#include<string>

毫无疑问,我们不会将这行代码在同一个cpp文件里输入两遍。

 

头文件可能被调用多次的问题:

但若我们在一个头文件里引用另外一个头文件,就有可能涉及到将同一个头文件引用多次的问题。例如:

1)我们在头文件m1.h里面创建了一个结构声明,然后头文件m2.h里放了一个函数原型;头文件m3.h放了另外一个函数原型;

2)由于m2.h和m3.h都需要使用这个结构,于是在两个函数都引用了头文件m1.h。即,加入了#include"m1.h"

3)然后我们在源代码文件a.cpp里面,引用了头文件m2.h和m3.h(因为要使用它们提供的原型);即:

#include"nn.h"

#include"nn2.h"

4)假如我们没在m1.h里面添加防多次引用的代码。那么这个时候,m2.h里面和m3.h里面都包含了一个结构声明(因为他们都包含了m1.h),编译器会提示错误(因为它遇见了两次结构声明)。

 

防止头文件被调用多次:

因此,我们需要一个解决办法,方法有两种:

1)在头文件的开始,加入#pragma once,那么这个头文件,在编译时,由编译器保证这个头文件不会被拷贝多次;

2)在头文件的开始,加入

#ifndef 宏名字

#define 宏名字

.....  //一堆声明代码

#endif

例如:

#ifndef aa_bb_

#define aa_bb_

struct abc

{

int a;

};

#endif

第二种方法的原理是,#ifndef(if no define如果没有宏xxx的意思)aa_bb_,在编译器第一次遇见这行命令的时候,编译器发现自己没有遇见过这个宏名字(因为之间没有定义aa_bb_);

于是向下执行,遇见#define aa_bb_ ,这个时候,编译器知道宏定义了aa_bb_(这样的话,下次遇见定义的这个名字,#ifndef aa_bb_将不会执行一直到#endif)之间的代码;

因为是第一次遇见,所以在这里,结构声明被编译器编译了(且记住下次遇见就不会被编译)。

这样的话,第一次遇见,于是编译结构abc,下一次遇见这几行代码(比如说又包含了这个头文件),这里的结构声明,便被编译器自动跳过了。

 

 

⑥多个源代码文件,应如正常那样包含头文件,调用名称空间等。

只不过包含头文件的话,就可以理解为将头文件的内容放在了源代码文件之前。即如果头文件里有一个函数原型,源代码文件包含这个头文件,就可以视为源代码文件中的代码,有这个函数原型。

但若一个源代码文件中包含名称空间(假如使用了using namespace std;),那么并不会让另一个源代码文件视为可以节约了这行代码。

 

 

多个文件代码如下:

//头文件mm.h
#pragma once //由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。

//#ifndef COORDIN_H_
//#define COORDIN_H_
//... ... // 一些声明语句
//#endif
// #ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况

#include<string>
struct abc
{
	int a;
	int b;
	std::string c;
};

//头文件nn.h
#pragma once
#include"mm.h"

void plus1(abc&);

//头文件nn2.h
#pragma once
#include"mm.h"

void set(abc&);
void show(abc);

//源代码文件:源.cpp
#include<iostream>
#include"nn.h"
#include"nn2.h"
using namespace std;
int main()
{

	abc a;
	string c = "abb";
	set(a);
	show(a);
	plus1(a);
	show(a);

	system("pause");
	return 0;
}

//源代码文件:源1.cpp
#include<iostream>
#include"nn.h"
#include"nn2.h"

void set(abc &a)
{
	a.a = 5;
	a.b = 3;
	a.c = "abb";
}

void show(abc a)
{
	using namespace std;	//虽然源.cpp包含了using namespace std,但这个函数使用cout和cin,依然要加入这行代码,或者加入using std::cout和using std::cin
	cout << a.a << endl;
	cout << a.b << endl;
	cout << a.c << endl;
}

void plus1(abc &a)
{
	a.a++;
	a.b++;
	a.c += a.c;
}

输出:

5
3
abb
6
4
abbabb
请按任意键继续. . .


头文件mm.h的代码,也可以修改为:


#ifndef aa_bb_
#define aa_bb_
#include<string>
struct abc
{
	int a;
	int b;
	std::string c;
};
#endif

其效果是相同的。


目录
相关文章
|
6月前
|
C语言 C++
C/C++ 自定义头文件,及头文件结构详解
还是从"stdio.h"说起,这是C语言中内置的标准库,也就是说,头文件很多时候其实就是一个“库”,类似于代码的仓库,也就是说将某些具有特定功能的常量、宏、函数等归为一个大类,然后放进这个“仓库”,就像stdio.h就是一个标准输入/输出的头文件
210 1
|
6月前
|
开发框架 编译器 C语言
外部依赖项、头文件、源文件、资源文件
外部依赖项、头文件、源文件、资源文件
232 0
源文件与模块生成时的文件不同,是否希望调试器使用它?如何解决
源文件与模块生成时的文件不同,是否希望调试器使用它?如何解决
|
IDE 编译器 Linux
linux 编译 c或cpp 文件为动态库 so 文件(最简单直观的模板)
linux 编译 c或cpp 文件为动态库 so 文件(最简单直观的模板)
|
C++
C++分文件编写:拆类(.h和.cpp文件)
C++分文件编写:拆类(.h和.cpp文件)
134 0
|
Linux API C语言
编译参数中如何包含头文件和动态链接库
GCC编译参数:如何包含头文件和动态链接库
70 0
|
C++
VS下源文件中有多个代码时如何指定运行特定的代码(一个源文件下有多个代码时运行指定代码)
VS下源文件中有多个代码时如何指定运行特定的代码(一个源文件下有多个代码时运行指定代码)
290 0
|
Linux Windows
CLion 在头文件和源文件之间切换
该快捷方式在键盘图中称为“相关符号”。
2140 0
|
前端开发 JavaScript 编译器
前端实现多文件编译器
在前端工程中,有时我们需要在浏览器编译并执行一些代码,这种需求常见于低代码场景中。例如我们在搭建时需自定义一部分代码,这些代码需要在渲染时执行。为了方便起见,我们写的代码一定是 ES6 语法,如果要在浏览器执行,那么就必须经过编译。下面是前端编译 JS 代码的一些实践。
前端实现多文件编译器