首页 > 其他分享 >20.C语言多文件编译与管理技巧

20.C语言多文件编译与管理技巧

时间:2025-01-16 22:01:24浏览次数:3  
标签:文件 20 int make C语言 编译 bar foo

目录

1.前言

本篇原文为:20.C语言多文件编译与管理技巧

更多C++进阶、rust、python、逆向等等教程,可点击此链接查看:酷程网

一个软件项目往往包含多个源码文件,编译时需要将这些文件一起编译,生成一个可执行文件。

假定一个项目有两个源码文件foo.cbar.c,其中foo.c是主文件,bar.c是库文件。

所谓“主文件”,就是包含了main()函数的项目入口文件,里面会引用库文件定义的各种函数:

// File foo.c
#include <stdio.h>

int main(void) {
  printf("%d\n", add(2, 3));  // 5!
}

上面代码中,主文件foo.c调用了函数add(),这个函数是在库文件bar.c里面定义的:

// File bar.c

int add(int x, int y) {
  return x + y;
}

现在,将这两个文件一起编译:

gcc -o foo foo.c bar.c

# 更省事的写法
gcc -o foo *.c

上面命令中,gcc 的-o参数指定生成的二进制可执行文件的文件名,本例是foo

这个命令运行后,编译器会发出警告,原因是在编译foo.c的过程中,编译器发现一个不认识的函数add()foo.c里面没有这个函数的原型或者定义。

因此,最好修改一下foo.c,在文件头部加入add()的原型:

// File foo.c
#include <stdio.h>

int add(int, int);

int main(void) {
  printf("%d\n", add(2, 3));  // 5!
}

现在再编译就没有警告了。

你可能马上就会想到,如果有多个文件都使用这个函数add(),那么每个文件都需要加入函数原型。

一旦需要修改函数add()(比如改变参数的数量),就会非常麻烦,需要每个文件逐一改动。

所以,通常的做法是新建一个专门的头文件bar.h,放置所有在bar.c里面定义的函数的原型:

// File bar.h

int add(int, int);

然后使用include命令,在用到这个函数的源码文件里面加载这个头文件bar.h

// File foo.c

#include <stdio.h>
#include "bar.h"

int main(void) {
  printf("%d\n", add(2, 3));  // 5!
}

上面代码中,#include "bar.h"表示加入头文件bar.h。这个文件没有放在尖括号里面,表示它是用户提供的;它没有写路径,就表示与当前源码文件在同一个目录。

然后,最好在bar.c里面也加载这个头文件,这样可以让编译器验证,函数原型与函数定义是否一致:

// File bar.c
#include "bar.h"

int add(int a, int b) {
  return a + b;
}

现在重新编译,就可以顺利得到二进制可执行文件:

gcc -o foo foo.c bar.c

2.重复加载

头文件里面还可以加载其他头文件,因此有可能产生重复加载。

比如,a.hb.h都加载了c.h,然后foo.c同时加载了a.hb.h,这意味着foo.c会编译两次c.h

最好避免这种重复加载,虽然多次定义同一个函数原型并不会报错,但是有些语句重复使用会报错,比如多次重复定义同一个 Struct 数据结构。

解决重复加载的常见方法是,在头文件里面设置一个专门的宏,加载时一旦发现这个宏存在,就不再继续加载当前文件了:

// File bar.h
#ifndef BAR_H
  #define BAR_H
  int add(int, int);
#endif

上面示例中,头文件bar.h使用#ifndef#endif设置了一个条件判断。

每当加载这个头文件时,就会执行这个判断,查看有没有设置过宏BAR_H

如果设置过了,表明这个头文件已经加载过了,就不再重复加载了,反之就先设置一下这个宏,然后加载函数原型。

3.extern

当前文件还可以使用其他文件定义的变量,这时要使用extern说明符,在当前文件中声明,这个变量是其他文件定义的:

extern int myVar;

上面示例中,extern说明符告诉编译器,变量myvar是其他脚本文件声明的,不需要在这里为它分配内存空间。

由于不需要分配内存空间,所以extern声明数组时,不需要给出数组长度。

extern int a[];

这种共享变量的声明,可以直接写在源码文件里面,也可以放在头文件中,通过#include指令加载。

4.static

正常情况下,当前文件内部的全局变量,可以被其他文件使用。

有时候,不希望发生这种情况,而是希望某个变量只局限在当前文件内部使用,不要被其他文件引用。

这时可以在声明变量的时候,使用static关键字,使得该变量变成当前文件的私有变量。

static int foo = 3;

上面示例中,变量foo只能在当前文件里面使用,其他文件不能引用。

5.编译策略

多个源码文件的项目,编译时需要所有文件一起编译,哪怕只是修改了一行,也需要从头编译,非常耗费时间。

为了节省时间,通常的做法是将编译拆分成两个步骤。

第一步,使用 GCC 的-c参数,将每个源码文件单独编译为对象文件(object file)。

第二步,将所有对象文件链接在一起,合并生成一个二进制可执行文件。

gcc -c foo.c # 生成 foo.o
gcc -c bar.c # 生成 bar.o

# 更省事的写法
gcc -c *.c

上面命令为源码文件foo.cbar.c,分别生成对象文件foo.obar.o

对象文件不是可执行文件,只是编译过程中的一个阶段性产物,文件名与源码文件相同,但是后缀名变成了.o

得到所有的对象文件以后,再次使用gcc命令,将它们通过链接,合并生成一个可执行文件。

gcc -o foo foo.o bar.o

# 更省事的写法
gcc -o foo *.o

以后,修改了哪一个源文件,就将这个文件重新编译成对象文件,其他文件不用重新编译,可以继续使用原来的对象文件,最后再将所有对象文件重新链接一次就可以了。

由于链接的耗时大大短于编译,这样做就节省了大量时间。

6.make

大型项目的编译,如果全部手动完成,是非常麻烦的,容易出错。

因此在实际工程开发中,一般会使用专门的自动化编译工具,比如 make。

make 是一个命令行工具,使用时会自动在当前目录下搜索配置文件 makefile(也可以写成 Makefile)。

该文件定义了所有的编译规则,每个编译规则对应一个编译产物,为了得到这个编译产物,它需要知道两件事。

  • 依赖项(生成该编译产物,需要用到哪些文件)
  • 生成命令(生成该编译产物的命令)

比如,对象文件foo.o是一个编译产物,它的依赖项是foo.c,生成命令是gcc -c foo.c,对应的编译规则如下:

foo.o: foo.c
  gcc -c foo.c

上面示例中,编译规则由两行组成,第一行首先是编译产物,冒号后面是它的依赖项,第二行则是生成命令。

注意,第二行的缩进必须使用 Tab 键,如果使用空格键会报错。

完整的配置文件 makefile 由多个编译规则组成,可能是下面的样子:

foo: foo.o bar.o
  gcc -o foo foo.o bar.o

foo.o: bar.h foo.c
  gcc -c foo.c

bar.o: bar.h bar.c
  gcc -c bar.c

上面是 makefile 的一个示例文件,它包含三个编译规则,对应三个编译产物(foo.obar.ofoo),每个编译规则之间使用空行分隔。

有了 makefile,编译时,只要在 make 命令后面指定编译目标(编译产物的名字),就会自动调用对应的编译规则。

make foo.o

# or
make bar.o

# or
make foo

上面示例中,make 命令会根据不同的命令,生成不同的编译产物。

如果省略了编译目标,make命令会执行第一条编译规则,构建相应的产物。

make

上面示例中,make后面没有编译目标,所以会执行 makefile 的第一条编译规则,本例是make foo

由于用户期望执行make后得到最终的可执行文件,所以建议总是把最终可执行文件的编译规则,放在 makefile 文件的第一条,makefile 本身对编译规则没有顺序要求。

make 命令的强大之处在于,它不是每次执行命令,都会进行编译,而是会检查是否有必要重新编译。

具体方法是,通过检查每个源码文件的时间戳,确定在上次编译之后,哪些文件发生过变动,然后重新编译那些受到影响的编译产物(即编译产物直接或间接依赖于那些发生变动的源码文件),不受影响的编译产物,就不会重新编译。

举例来说,上次编译之后,修改了foo.c,没有修改bar.cbar.h。于是,重新运行make foo命令时,Make 就会发现bar.cbar.h没有变动过,因此不用重新编译bar.o,只需要重新编译foo.o。有了新的foo.o以后,再跟bar.o一起,重新编译成新的可执行文件foo

Make 这样设计的最大好处,就是自动处理编译过程,只重新编译变动过的文件,因此大大节省了时间。

标签:文件,20,int,make,C语言,编译,bar,foo
From: https://blog.csdn.net/weixin_50964512/article/details/145148320

相关文章

  • C语言break和continue用法详解(跳出循环,新手必看)
    使用while或for循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。C语言break关键字在我原创教程里的《C语言switchcase语句》一文中,我讲了:用 break来跳出switch语句。当break关键字用于while、for循环时,会终止循......
  • C语言typedef用法详解(新手必看)
    C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。起别名的目的不是为了提高程序运行效率,而是为了编码方便。例如有一个结构体的名字是stu,要想定义一个结构体变量就得这样写:structstustu1;struct看起来就是多余的,但不写又会报错。如果为structstu起了......
  • THUWC2025 游记
    Day-C先进入金国大臣面积群,然后发现xyf又在行联考学生群故事。Day-1早上赶飞机进京。飞机上启动钢丝。到达大兴机场之后坐火车前往北京西站,然后坐地铁到海淀黄庄。非常饿,但是决定先排队。排队队伍非常抽象,还有些神秘生物要处理五分钟/人次。。。就导致排了两个小时,三点半......
  • C语言数据结构编程练习-单向不带头链表的操作
    单向链表单向链表是由若干个节点组成的数据结构,每个节点包含两个部分:数据域和指针域。数据域存储节点的数据,指针域存储下一个节点的地址。  #include"03.h"voidfn3(){ intorder=0; elementTypeval; elementTypeelementVal; LinkNode*ListNode=NULL; ......
  • C语言数据结构编程练习-用指针创建顺序表,进行创销和增删改查操作
     使用多文件进行编程main.c文件#include"02.h"intmain(){ fn2(); return0;} 02.h 头文件#pragmaonce#define_CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<stdlib.h>#include<memory.h>#defineMAX_NUMBER100typedefi......
  • 编译原理第一章
    1.翻译、编译、解释的概念翻译程序:将一种语言程序(源)转换成另一种语言程序(目标),两者在逻辑上是等价的。编译程序:如果源语言是高级语言,如Pascal,C,Ada,Java语言等,目标语言是低级语言,如汇编语言或机器语言之类的低级语言,则称为编译程序。先编译,后执行解释程序:边转换边执行,不生......
  • C语言流程控制
    程序的三种流程:顺序、选择(分支)、循环。程序 :一定是这些循环的组合 选择:选择(有排他性)一、关系运算符和关系表达式(一)运算规则运算结果---反映关系是否成立---真假   //在c语言中10表示运算优先级(二)关系表达式二、逻辑运算反映多个表达式之间的......
  • P1047 [NOIP2005 普及组] 校门外的树
    题目:某校大门外长度为 ll 的马路上有一排树,每两棵相邻的树之间的间隔都是 11 米。我们可以把马路看成一个数轴,马路的一端在数轴 00 的位置,另一端在 ll 的位置;数轴上的每个整数点,即 0,1,2,…,l0,1,2,…,l,都种有一棵树。由于马路上有一些区域要用来建地铁。这些区域用......
  • 【前端框架】2025 React 状态管理终极指南!
    全文约10800字,预计阅读需要30分钟。React作为当下最受欢迎的前端框架,在构建复杂且交互丰富的应用时,状态管理无疑是至关重要的一环。从简单的本地状态,到能让多个组件协同工作的全局状态,再到涉及服务器通信、导航切换、表单操作以及持久化存储等不同场景下的状态管理,每一个方面......
  • 2025省选模拟6
    2025省选模拟6T1、圣诞树原cf140E先说60pts做法:首先考虑如何处理两层之间的转移。每两层之间我们只需要用总方案数减去两层重合的方案数即可,对于两层重合的方案数,我们其实并不需要知道具体集合是什么,只需要知道集合的大小,然后乘上一个组合数即可,所以我们需要知道不考虑......