Skip to content

Commit fdedec7

Browse files
committed
Updated to Chapter 7, Section 1
1 parent 765993c commit fdedec7

File tree

9 files changed

+415
-56
lines changed

9 files changed

+415
-56
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,173 @@
11
\section{跨文件编译}
2+
我们曾谈过,一个源代码的核心是主函数 \lstinline@main@。为了突出重点,我们更倾向于把主函数的定义放在其它函数的定义之前。这样就会带来``找不到函数''的问题,因为编译器在遇到一个函数名的时候,它会先向前寻找。\par
3+
为了解决这个问题,我们需要在函数的定义之前先声明。如果有默认值的话,也尽量放在函数声明的时候写出来,否则编译器也找不到它。\par
4+
我们后来又学习了结构体、共用体和类。它们也遵循这样的规则,我们必须在使用它之前就给出声明。
5+
\begin{lstlisting}
6+
class Data; //Data类的声明
7+
void func(Data); //func(Data)的声明
8+
//...
9+
class Data {
10+
//...
11+
}; //Data的定义
12+
void func(Data d) {
13+
//...
14+
} //func(Data)的定义
15+
\end{lstlisting}
16+
其中 \lstinline@Data@ 的声明和 \lstinline@func(Data)@ 的声明是不能颠倒的,否则编译器就找不到 \lstinline@Data@ 了。\par
17+
麻烦在于,一旦我们的程序稍微复杂点——试想,我们为了实现某个功能,需要三个类和十余个函数——我们仍然需要写一大串的定义。这样还是会把 \lstinline@main@ 的定义挤到后面去。整个文件也会极度臃肿,既不方便写,也不方便看。\par
18+
C++允许,也提倡我们把同一段长代码放在不同文件中,然后再通过编译器、链接器\footnote{实际上,很多集成开发环境可以一步到位,把编译、链接等操作一键包揽,这样就能省却我们不少烦心事。}等,生成一个可执行文件。读者可以回顾图1.1,作为参考。\par
19+
想想我们在每编译一个完整的代码时,是不是都要写一句这个呢:
20+
\begin{lstlisting}
21+
#include <iostream>
22+
\end{lstlisting}
23+
这里的 \lstinline@iostream@ 就对应着一个头文件\footnote{在Windows中,一般是\texttt{.h}文件。}。\lstinline@#include@ 是一个预处理指令,它会在编译之前把 \lstinline@iostream@ 库中的内容复制到这个源代码中,所以我们才可以使用这个文件中声明的 \lstinline@cin@, \lstinline@cout@ 等对象。\par
24+
我们也可以自己写头文件。这样,无论我们需要多少声明(及定义),只需要把它们都放在头文件中,然后 \lstinline@#include@ 就足够了。\par
25+
在大规模项目中,仅仅用一个头文件可能还是会把代码写得十分冗长,所以我们可以有选择地把某一类功能放在某一个头文件中,另一些功能放在另外的头文件中。C++就是这么做的。它给我们内置了许多头文件。当我们需要用到数学函数时,就会使用 \lstinline@cmath@ 库;当我们需要使用某个STL容器,比如 \lstinline@vector@ 时,就会使用 \lstinline@vector@ 库。诸如此类。\par
26+
把定义全都写到头文件中也未必合适。定义的代码可能会非常长,如果我们有单独的代码文件来存储它的定义,而仅仅把声明放在头文件中,这样就会让头文件看上去更整洁——头文件中定义了什么函数,一目了然。所以我们通常用另外的源代码文件\footnote{在Windows中,一般是\texttt{.cpp}文件。}来实现头文件中函数的定义。\par
27+
就以 \lstinline@clear_input@ 函数为例,我们可以写一个头文件,用来存储 \lstinline@clear_input@ 的声明;写一个源代码文件(要包含头文件),用来存储 \lstinline@clear_input@ 的定义;再写一个源代码文件(要包含头文件),其中有 \lstinline@main@ 函数,用来调用 \lstinline@clear_input@ 函数。\par
28+
\begin{lstlisting}[caption=\texttt{Header.h}]
29+
#pragma once
30+
#include <iostream>
31+
using namespace std; //使用命名空间std
32+
void input_clear(istream& = {cin}); //在声明中就给出默认参数
33+
\end{lstlisting}
34+
其中的 \lstinline@#pragma once@ 是另一种预处理指令,它保证这个头文件只被包含一次,而不致违反单一定义规则\footnote{单一定义规则(One definition rule, ODR),即任何变量、函数和自定义类型(包括枚举)在每个翻译单元中都可以有多个声明,但只能有一个定义。}。如果你的编译器不支持这种 \lstinline@#pragma once@ 语法,你也可以这么写:
35+
\begin{lstlisting}
36+
#ifndef _Header_ //名字随便起
37+
#define _Header_
38+
//...这里是头文件的代码部分
39+
#endif //与#ifndef配对
40+
\end{lstlisting}\par
41+
一个头文件也可以包含别的头文件,比如这里的\texttt{Header.h}包含了\texttt{iostream}。
42+
接下来我们用一个源代码文件来存储 \lstinline@input_clear@ 的定义部分。
43+
\begin{lstlisting}[caption=\texttt{Definition.cpp}]
44+
#include "Header.h" //它需要包含头文件Header.h
45+
void input_clear(istream& in) { //声明部分已经有默认参数cin,这里不应再写
46+
in.clear();
47+
while (in.get() != '\n')
48+
continue;
49+
}
50+
\end{lstlisting}
51+
我们在这里使用了 \lstinline@#include"Header.h"@。其实用尖括号 \lstinline@<>@ 也是可以的,不过对于我们自定义的头文件来说,用双引号要更好一些\footnote{如果我们用尖括号,编译器会先在标准库中寻找,如果找不到对应的头文件,再寻找自定义头文件;而如果用双引号,编译器会直接从自定义头文件开始寻找,这样就节省了编译的时间。}。\par
52+
\texttt{Definition.cpp}文件中,\lstinline@#include@指令会将对应头文件的代码拷贝到此源代码文件中。所以相当于我们在源代码文件中先声明了 \lstinline@input_clear(istream& ={cin})@,然后给了它定义。所以在定义的时候我们就不需要再设默认值了\footnote{其实是不允许重复设默认值,哪怕是相同的默认值。}。\par
53+
接下来我们就可以在其它的源代码文件中使用它了。
54+
\begin{lstlisting}[caption=\texttt{Test.cpp}]
55+
#include "Header.h"
56+
int main() {
57+
int n {1};
58+
while (n != 0) {
59+
cin >> n;
60+
if(!cin) //检测cin的状态
61+
input_clear(); //清理输入
62+
//...
63+
}
64+
return 0;
65+
}
66+
\end{lstlisting}
67+
我们注意到,当包含了\texttt{Header.h}之后,在\texttt{Test.cpp}中就不需要包含 \lstinline@iostream@ 和 \lstinline@using namespace std@了。这同样是因为 \lstinline@#include@ 指令会把\texttt{Header.h}中的代码拷贝到这里,包括\texttt{Header.h}中的各种文件包含与命名空间使用。\par
68+
在这里我们没有给出 \lstinline@input_clear@ 的定义,但程序仍然知道这个函数要做什么——这正是链接器的功劳。在编译之后,\texttt{Test.cpp}和\texttt{Definition.cpp}分别会生成目标文件,链接器将它们链接起来之后,这个程序就完成了。\par
69+
我们在上面所说的结构是一种最简单的跨文件编译例子。实际的结构可能更加复杂(如图7.1所示),但读者只要把握这几条就足够了:
70+
\begin{itemize}
71+
\item 一个头文件可以被其它的头文件或源文件包含。但是,在任何时候,我们都不应当在别的代码中包含其它源文件\footnote{虽然这样是可以的,但是这样做很没意义,也容易引起误解。}。
72+
\item 当使用 \lstinline@#include@ 进行文件包含的时候,编译器会把被包含的文件的内容拷贝到此处。效果等同于我们自己写了一遍同样的代码。
73+
\item 任何具有外部链接\footnote{我们稍后就会讲到链接方式的问题。}的东西,都必须提供一个且只能提供一个定义(除非它是内联函数,我们会在后文中提及)。
74+
\item 这些代码在编译之后会生成目标文件,编译器会将它们链接起来。一般的IDE都会在编译之后帮助我们完成这些工作,所以无需过分操心。
75+
\end{itemize}
76+
\begin{figure}[htbp]
77+
\centering
78+
\includegraphics[width=0.6\textwidth]{../images/generalized_parts/07_separate_compilation_300.png}
79+
\caption{一个跨文件编译的示意图}
80+
\footnotesize{箭头指向的方向有 \lstinline@#include@ 指令}
81+
\end{figure}
82+
在实际的编程中,我们提倡把下面的内容放在头文件中:
83+
\begin{itemize}
84+
\item 常量表达式及常量的定义
85+
\item 结构体和类的定义
86+
\item 全局函数和成员函数的声明
87+
\item 函数模版和类模版的定义\footnote{类模版比较特殊,类模版的成员最好定义在头文件中。如果你觉得内容太多,那就把它们组织到不同的头文件中。}
88+
\item 一些很短的函数(我们可以以内联形式直接定义在头文件中)
89+
\end{itemize}
90+
至于函数的定义,我们可以,而且最好把它们放在其它的源文件中。而 \lstinline@main@ 函数存放在一个单独的源文件中,这样我们就只需要在写好其它函数之后,关注主函数的功能即可。以6.3实操部分的代码为例,我们可以把它拆成这样三个文件:
91+
\begin{lstlisting}[caption=\texttt{Header.h}]
92+
//本文件用于存放结构体的定义与函数的声明
93+
#pragma once
94+
struct Data { //Data仍需放在前面
95+
int num;
96+
Data* next;
97+
};
98+
void clear_list(Data*); //函数声明的时候可以不注参数名
99+
void insert_after(Data*, int);
100+
bool delete_after(Data*); //重载
101+
bool delete_after(Data*, Data*); //重载
102+
void transfer(Data*, Data*, Data*); //这个函数需要用到utility库,但不是在这
103+
\end{lstlisting}
104+
\begin{lstlisting}[caption=\texttt{Definition.cpp}]
105+
//本文件用于存放Header.h中各函数的定义
106+
#include "Header.h"
107+
#include <utility> //transfer函数用到了utility库中的swap函数
108+
void clear_list(Data *head) { //递归回收*head之后的所有动态内存
109+
if (head->next == nullptr)
110+
return;
111+
clear_list(head->next);
112+
delete head->next;
113+
head->next = nullptr;
114+
}
115+
void insert_after(Data *head, int n) { //在*head下一位置插入n
116+
head->next = new Data{ n,head->next }; //三步并作一步
117+
}
118+
bool delete_after(Data *head) {
119+
//删除*head下一位置的数据,并返回true;若*head是末尾元素,删除失败,返回false
120+
if (head->next == nullptr)
121+
return false;
122+
Data *p = head->next;
123+
head->next = p->next;
124+
delete p;
125+
}
126+
bool delete_after(Data *head, Data* tail) {
127+
//删除*head之后(不包含)直到*tail为止(包含)的单元。若删除失败,返回false
128+
if (head == tail)
129+
return false;
130+
if (tail == nullptr) {
131+
clear_list(head);
132+
return true;
133+
}
134+
Data tmp_head {*head};
135+
head->next = tail->next;
136+
tail->next = nullptr;
137+
clear_list(&tmp_head);
138+
return true;
139+
}
140+
void transfer(Data *head, Data *tail, Data *dest) {
141+
//片段转移,把*head之后(不包含)直到*tail为止(包含)的单元移到*dest之后
142+
if (head == tail || dest == nullptr)
143+
return;
144+
if (tail == nullptr) {
145+
for (tail = head; tail->next != nullptr; tail = tail->next) {
146+
;
147+
}
148+
if (head == tail)
149+
return;
150+
}
151+
std::swap(tail->next, dest->next); //注意要用std::swap
152+
std::swap(head->next, dest->next); //除非using namespace std
153+
}
154+
\end{lstlisting}
155+
\begin{lstlisting}[caption=\texttt{Main.cpp}]
156+
//主函数定义在本文件中
157+
#include <iostream>
158+
#include "Header.h"
159+
using namespace std;
160+
int main() { //仅作为示例
161+
Data head {0,nullptr}, *tail {&head};
162+
while (cin) {
163+
int n;
164+
cin >> n; //输入n的值,直到出现异常输入(如q)
165+
insert_after(tail, n);
166+
tail = tail->next; //更新tail的指向
167+
}
168+
clear_list(&head);
169+
return 0;
170+
}
171+
\end{lstlisting}
172+
请读者注意各文件之间使用 \lstinline@#include@ 的关系。因为在\texttt{Definition.cpp}中没有使用 \lstinline@using namespace std@,所以我们在定义 \lstinline@transfer@ 函数时,调用 \lstinline@swap@ 函数必须要用 \lstinline@std::swap@ 这样的语法。\par
173+
读者可能会好奇,我们不是在\texttt{Main.cpp}中用过了 \lstinline@using namespace std@ 了吗?不然,因为\texttt{Definition.cpp}和\texttt{Main.cpp}之间没有包含关系,所以\texttt{Main.cpp}中写下的代码不能用在\texttt{Definition.cpp}中,反之亦然。\par
42.3 KB
Loading
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<mxfile host="Electron" modified="2024-01-08T06:57:59.628Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.16 Chrome/120.0.6099.109 Electron/28.1.0 Safari/537.36" etag="RgHGLXgmHzMhsmb5DRVk" version="22.1.16" type="device">
2+
<diagram name="Page-1" id="Umb1S6DrYRTSFSJQdrjl">
3+
<mxGraphModel dx="674" dy="463" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
4+
<root>
5+
<mxCell id="0" />
6+
<mxCell id="1" parent="0" />
7+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-20" target="uPSDg4g8-I4lnqD0Hp03-23">
8+
<mxGeometry relative="1" as="geometry" />
9+
</mxCell>
10+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-20" target="uPSDg4g8-I4lnqD0Hp03-21">
11+
<mxGeometry relative="1" as="geometry" />
12+
</mxCell>
13+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-20" value="&lt;pre&gt;.h&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fad7ac;strokeColor=#b46504;" vertex="1" parent="1">
14+
<mxGeometry x="200" y="120" width="80" height="40" as="geometry" />
15+
</mxCell>
16+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-21" target="uPSDg4g8-I4lnqD0Hp03-24">
17+
<mxGeometry relative="1" as="geometry" />
18+
</mxCell>
19+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-30" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-21" target="uPSDg4g8-I4lnqD0Hp03-22">
20+
<mxGeometry relative="1" as="geometry" />
21+
</mxCell>
22+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-21" value="&lt;pre&gt;.h&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fad7ac;strokeColor=#b46504;" vertex="1" parent="1">
23+
<mxGeometry x="200" y="180" width="80" height="40" as="geometry" />
24+
</mxCell>
25+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-22" target="uPSDg4g8-I4lnqD0Hp03-25">
26+
<mxGeometry relative="1" as="geometry" />
27+
</mxCell>
28+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="uPSDg4g8-I4lnqD0Hp03-22" target="uPSDg4g8-I4lnqD0Hp03-26">
29+
<mxGeometry relative="1" as="geometry" />
30+
</mxCell>
31+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-22" value="&lt;pre&gt;.h&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fad7ac;strokeColor=#b46504;" vertex="1" parent="1">
32+
<mxGeometry x="200" y="240" width="80" height="40" as="geometry" />
33+
</mxCell>
34+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-23" value="&lt;pre&gt;.cpp&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b1ddf0;strokeColor=#10739e;" vertex="1" parent="1">
35+
<mxGeometry x="100" y="120" width="80" height="40" as="geometry" />
36+
</mxCell>
37+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-24" value="&lt;pre&gt;.cpp&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b1ddf0;strokeColor=#10739e;" vertex="1" parent="1">
38+
<mxGeometry x="100" y="180" width="80" height="40" as="geometry" />
39+
</mxCell>
40+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-25" value="&lt;pre&gt;.cpp&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b1ddf0;strokeColor=#10739e;" vertex="1" parent="1">
41+
<mxGeometry x="100" y="240" width="80" height="40" as="geometry" />
42+
</mxCell>
43+
<mxCell id="uPSDg4g8-I4lnqD0Hp03-26" value="&lt;pre&gt;.cpp&lt;/pre&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b1ddf0;strokeColor=#10739e;" vertex="1" parent="1">
44+
<mxGeometry x="300" y="240" width="80" height="40" as="geometry" />
45+
</mxCell>
46+
</root>
47+
</mxGraphModel>
48+
</diagram>
49+
</mxfile>

0 commit comments

Comments
 (0)