专栏文章
联合编译原理简述
科技·工程参与者 15已保存评论 15
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 15 条
- 当前快照
- 1 份
- 快照标识符
- @mmdpplj2
- 此快照首次捕获于
- 2026/03/06 01:01 5 天前
- 此快照最后确认于
- 2026/03/10 01:01 23 小时前
做 IOI 交互题时发现自己似乎不会联合编译?遂仔细学习一下。
用例子说明(IOI2013 Art Class):
artclass.h
CPP#ifndef __ARTCLASS_H__
#define __ARTCLASS_H__
#ifdef __cplusplus
extern "C" {
#endif
int style(int H, int W, int R[500][500], int G[500][500], int B[500][500]);
#ifdef __cplusplus
}
#endif
#endif /* __ARTCLASS_H__ */
artclass.cpp
CPP#include "artclass.h"
int style(int H, int W, int R[500][500], int G[500][500], int B[500][500]) {
return 2;
}
grader.c
CPP#include <stdio.h>
#include "artclass.h"
static int DIM[2];
static int R[500][500];
static int G[500][500];
static int B[500][500];
#ifdef __cplusplus
extern "C" {
#endif
void __grader_init(int dim[2],
int r[500][500], int g[500][500], int b[500][500]);
#ifdef __cplusplus
}
#endif
int main() {
__grader_init(DIM, R, G, B);
printf("%d\n", style(DIM[0], DIM[1], R, G, B));
return 0;
}
graderlib.c
CPP#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include "jpeglib.h"
#include "artclass.h"
#define fail(s, x...) do { \
fprintf(stderr, s "\n", ## x); \
exit(1); \
} while(0)
#ifdef __cplusplus
extern "C" {
#endif
void __grader_init(int dim[2],
int r[500][500], int g[500][500], int b[500][500]) {
...
}
#ifdef __cplusplus
}
#endif
编译指令为:
SH#!/bin/bash
NAME=artclass
/usr/bin/g++ -DEVAL -static -O2 -o $NAME grader.c graderlib.c $NAME.cpp -ljpeg
命令实际上将 grader.c、graderlib.c 和 artclass.cpp 进行联合编译。在这里我们忽略 graderlib.c 的内容,认为它可以和 grader.c 写在一起。
函数的处理过程
联合编译有三个过程,依次为预处理、编译和链接。前两个过程在单文件编译中也存在。
预处理,所作的工作就是将 include 语句引入的头文件全部写入源文件中,并将源文件中的宏全部展开。则我们可以理解为,经过预处理过程后的 artclass.cpp 和 grader.cpp 变成了如下样子:
artclass.cpp
CPPextern "C" {
int style(int H, int W, int R[500][500], int G[500][500], int B[500][500]);
}
int style(int H, int W, int R[500][500], int G[500][500], int B[500][500]) {
return 2;
}
grader.c
CPP// #include <stdio.h>
// 这一部分是 stdio 头文件引入的所有内容,不包含 #include <stdio.h>
extern "C" {
int style(int H, int W, int R[500][500], int G[500][500], int B[500][500]);
}
static int DIM[2];
static int R[500][500];
static int G[500][500];
static int B[500][500];
extern "C" {
void __grader_init(int dim[2],
int r[500][500], int g[500][500], int b[500][500]);
}
int main() {
__grader_init(DIM, R, G, B);
printf("%d\n", style(DIM[0], DIM[1], R, G, B));
return 0;
}
Tip
宏 __cplusplus 定义为 C++ 版本,若是 C++ 语言则有定义,否则无定义。这是 C/C++ 通用 grader 的特性。extern "C" 的作用是告诉 C++ 编译器,这部分函数应采用 C 语言的链接规范(C Linkage),即不进行名称修饰,以便能与 C 语言编写的目标文件或库进行链接。分析代码时一般可以忽略。
这之后 artclass.h 等所有头文件的使命已经结束了。
接下来的步骤是编译。反直觉的,联合编译的编译步骤其实是多文件分开编译。每一个文件产生一个 .o 文件,是编译之后的机器码(实际上,编译器编译时会分步将源文件先编译为汇编语言,再编译为机器码,不过这不是文章的重点),以及生成一个记录“定义和使用函数”的符号表,供接下来的链接步骤使用。
在 C++ 单文件中,使用一个没有声明的函数当然会报语法错,但是使用一个没有定义的函数在语法上是没有问题的,只要这个函数在使用之前声明过。这是 C/C++ 文件可以进行联合编译的原因之一。
Note
C++ 函数的声明和定义
声明指的是明确一个函数的形式。如下
CPPint solve(int);
声明了一个
solve() 函数,参数是一个整形,返回值是一个整形。然后你就可以在这个源代码之后的编写中使用这个函数。只要参数和返回值符合语法要求该源代码就不存在语法错误。定义则是赋予一个函数声明具体的意义。如下
CPPint solve(int x){
return x+1;
}
定义了
solve() 函数具体的操作。在编译过程中,编译器并不会检查每一个函数具体的意义,只要在一个文件中声明了函数,若没有定义,则编译器会在上文提到的符号表中,留下一个**“Unresolved Symbol”信号**供链接器处理。编译过程中,每个源文件是独立进行编译的,多个源文件之间互不干扰。
参考编译的过程,我们知道了 artclass.cpp 干的事情是:声明并定义了一个
int style(int,int,int[][],int[][],int[][]) 函数。而 grader.c 则声明并引用了 style() 函数(我们假设 grader.c 和 graderlib.c 已经合并写入 grader.c,即 __grader_init() 函数已经在声明之后定义)。两者符号表的关系应该是:grader.c 有一个针对函数 style() 的 Unresolved Symbol 信号,代表这个源文件中声明了一个 style() 函数但是缺少定义。artclass.cpp 有一个符号 style() 记录了这个函数的具体地址。以上步骤之后针对两个源文件的编译就结束了。接下来是链接步骤,就是将两个或多个源文件中的函数(或变量)以机器码地址的形式链接起来,形成完整的可执行机器码。
具体而言,链接器在碰到 Unresolved Symbol 信号之后,会去所有源文件的符号表中寻找对应函数的机器码地址信息,并将其填入该源文件编译结果中引用该函数的地方。解释完所有的 Unresolved Symbol 信号后,链接过程就结束了。
Caution
链接过程中会出现两点常见的错误。
- 链接器如果没有在其它源文件编译结果中找到某 Unresolved Symbol 对应的函数,则会报错
undefined reference to 'solve()'。 - 链接器如果在其它源文件编译结果中找到多个对应函数的定义,则会报错
multiple definition of 'solve()'。
这是函数仅能定义一次的性质。
变量的处理过程
联合编译中的变量(主要是全局变量,因为局部变量的归属和生命周期很明确,但是 static 变量另说)归属的核心在于声明、定义和链接属性。
全局变量最核心的原则是能且仅能定义一次,但是可以声明多次。这一点和函数的声明定义原则是类似的。全局变量一定归属于某一个特定的源文件,而不会同时归属于多个源文件,也不能将其理解为“参与编译的所有源文件共有”。全局变量定义在哪个源文件,就归属于这个源文件,除开这个特定的源文件之外,其它源文件的声明只是一个引用(extern)。
接下来解释变量的链接属性。
全局共享变量
如果全局变量在 A.cpp 中定义,希望在 B.cpp 中共享使用,则可以使用 extern 标记声明 B.cpp 中的变量,代表 B.cpp 中此变量是一个引用。
例子:
A.cpp
CPPint globalVar = 114514;
B.cpp
CPP#include <iostream>
extern int globalVar;
int main(){ std::cout << globalVar << std::endl; }
联合编译 A.cpp 和 B.cpp 输出结果 114514。
文件私有全局变量
如果希望 A.cpp 定义的全局变量仅在 A.o 中使用而不在其它地方起作用,可以使用 static 链接属性修饰变量。
例子:
A.cpp
CPPstatic int globalVar = 1;
B.cpp
CPP#include <iostream>
static int globalVar = 2;
int main(){ std::cout << globalVar << std::endl; }
这份代码不会报错且输出为 2。因为标记 static 的变量不会参与链接,因此不存在多次定义(两次定义的域不交)。
头文件在联合编译中的作用
在单文件编译中头文件一般用于引入库函数的代码。但在联合编译中,头文件还被视为一个“桥”,用于联合统一各源文件之间变量或者函数的形式,避免重复且繁杂的定义。
举例,我希望声明若干个共享变量和若干可能需要在不同源文件中用到的函数,如果没有头文件,我需要在每一个源文件中都重复声明需要使用的变量和函数,使得代码可读性与可维护性降低。如果使用头文件,我就可以将需要声明的函数和变量全部引入头文件中,再在源代码中 include 头文件,简洁且在需要增加维护的变量和函数时,避免多次修改。
head.h
CPPextern int n;
void init();
void solve();
init.cpp
CPP#include "head.h"
#include <iostream>
void init(){
std::cin >> n;
}
solve.cpp
CPP#include "head.h"
#include <iostream>
void solve(){
init();
std::cout << n << std::endl;
}
main.cpp
CPP#include "head.h"
int n; // 必须有定义
int main(){
solve();
}
以上是联合编译的一个 demo,注意编译的时候一定不能将头文件一起联合编译。
然后有一些特别注意。头文件的本质是在 include 的时候全部展开进源代码中,所以操作不当会出现变量和函数的多次定义导致链接错误。
Caution
一定不能放普通函数的函数实现(inline 函数除外),多文件编译时会导致重复定义函数。
一定不能放全局变量的定义,会重复定义变量。
Tip
C++ 库文件避免重复引用的方法
C++ 的库文件由于其本身的作用,一定会包含函数的定义,所以在预处理层,库文件中头文件定义了头文件卫哨(Include Guards),即判断这个头文件是否已经被定义:
CPP#ifndef _GLIBCXX_VECTOR
#define _GLIBCXX_VECTOR
...
#endif
通过头文件卫哨可以在预处理阶段避免重复定义。这使得在一个文件中重复引用头文件不会出现问题。
但是对于多文件联合编译,宏定义在每个源文件之内展开一次,联合编译时不会出现重复定义吗?
答案是不会。原因是库函数使用 inline 内联或者使用模板 template 定义为模板函数。对于内联函数,如果函数完全相同,则自动保留一份抛弃其它的。对于模板函数,链接器在识别到使用模板函数的时候会自动合并去重,这是模板函数的弱链接特性。
这里不讨论非 header-only 库,即有外挂 .lib 的静态库和 .dll 的动态链接库。
以上我们搞清楚了 C/C++ 源文件联合编译的一些基本流程、一些文件的作用和特别需要注意的地方。希望大家以后做函数交互题的时候能够一眼看清楚 grader.cpp、problem.h 和 problem.cpp 的区别和作用,避免在联合编译上花费太多时间。
人工智能辅助创作声明:本文使用人工智能查错。
相关推荐
评论
共 15 条评论,欢迎与作者交流。
正在加载评论...