欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C语言笔记之头文件与链接(一)

程序员文章站 2022-05-23 08:17:40
  虽然一直在用#include命令包含头文件,但其实一致不太明白头文件的原理。今天就研究了一下。     首先,在大型项目中,仅仅一个源...

 

虽然一直在用#include命令包含头文件,但其实一致不太明白头文件的原理。今天就研究了一下。

 

 

首先,在大型项目中,仅仅一个源文件是不够的,巨大的代码量需要分别放在几个文件中,当然分开存放的更主要的目的是便于模块化。我们把代码按照不同的功能或作用分隔存放在不同的文件中,那么当其中一个功能有改动时,只需要重新编译相关的文件,而不必编译整个项目的所有源文件。

但是,这样就带来了一个问题:在一个文件中定义的变量或函数,能不能在另一个文件中使用呢?或者两个文件中同名的变量会不会引起冲突呢?

为了回答这个问题,首先要明白C语言的源代码如何一步步生成可执行代码的。我们先看只有一个源文件的情况:

首先经过预处理器,替换掉文件中的宏命令;

然后经过编译器,生成汇编代码;

接着是汇编器,生成二进制的目标代码,然而此时的目标代码仍然不能执行,它还缺少启动代码(程序和操作系统之间的接口)和库代码(比如printf函数的实体代码);

最后经过链接器,链接相关的代码,生成最终的可执行代码。

 

既然提到了编译,就不得不介绍一下C语言的编译器gcc,假设我们写好了一个源文件first.c,那么对应上面的步骤,gcc的命令参数如下:

预编译: gcc -E first.c -o first_1.c (注:-o 选项用来指定生成结果的文件名)

汇编: gcc -S first.c -o first.s

编译: gcc -c first.s -o first.o (也可以直接编译源码:gcc -c first.c -o first.o)

可执行: gcc first.o -o first (当然,这里也可以一步到位:gcc first.c -o first)

 

现在我们把目光集中到链接过程上。从上面的分析可以知道,所谓链接,就是把目标代码、启动代码和库代码结合到一起形成可执行代码。上面是只有一个源文件的情况,如果有多个文件,则把多个目标代码与启动代码和库代码粘合在一起。那么问题来了:多个目标代码真的就能随随便便粘合在一起吗?

 

要回答这个问题,还是得回到对源代码的分析上,毕竟目标代码只是源代码的编译版本。虽然源代码被分隔成几个部分并存放到不同的文件中,但是在逻辑或者上下文中,还是必须要保持一致的。也就是说,把几个文件中的代码重新放回到一个文件中,它们还是要保持“兼容”的,比如变量啊、函数啊之类的,不能重复;再比如只能有一个main函数。

 

然而,我们知道,变量和函数的作用域,最大的也就是文件作用域了。???比如,如何保证一个文件中的变量也被其他的文件直接使用并且不会引起冲突呢?答案就是头文件。头文件,就是把各个被分割的文件在逻辑上串起来的关键。

现在给出一个例子,在这个例子中,我用C代码模仿游戏“石头剪子布”,0、1、2分别代表石头、剪子、布。游戏过程中,程序随机产生一个数,同时提示用户输入一个数,然后根据规则做出输赢判断。完整的代码如下:

 

#include 
#include 
#include 

int gen_rnd(void);
void judge(int, int);

int main(void)
{
    int user, computer;

    printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: ");

    while(scanf("%d", &user) && user != 'q') {
        if(user > 2 || user < 0) {
            printf("Please input a number between 0 and 2: ");
            continue;
        }

        computer = gen_rnd();
        judge(user, computer);
        printf("number: ");
    }

    return 0;
}

int gen_rnd(void)
{
    int ret;
    time_t t;

    srand((unsigned)time(&t));
    ret = rand() % 3;

    return ret;
}

void judge(int user, int computer)
{
    char *name[] = {"stone", "scissors", "cloth"};
    int res = abs(user - computer);

    if(res == 0)
        printf("The computer is %s and you are %s, even\n", name[computer], name[user]);
    else if(res == 1) {
        if(user < computer)
            printf("The computer is %s and you are %s, you win\n", name[computer], name[user]);
        else
            printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]);
    }
    else {
        if(user < computer)
            printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]);
        else
            printf("The computer is %s and you are %s, you win\n", name[computer], name[user]);
    }
}

 

file.c

 

源码中有三个函数,分别代表不同的功能:main是主函数;gen_rnd()产生随机数用来模拟电脑;judge()用来判断输赢。每个函数就是一个功能模块,现在我们把这个文件分割成三个,分别是main.c gen_rnd.c judge.c,每个文件只存放一个函数。如下:

 

#include 
#include 

int gen_rnd(void);
void judge(int, int);

int main(void)
{
    int user, computer;

    printf("Please input a number, 0 for stone, 1 for scissors, 2 for cloth and q for quit: ");

    while(scanf("%d", &user) && user != 'q') {
        if(user > 2 || user < 0) {
            printf("Please input a number between 0 and 2: ");
            continue;
        }

        computer = gen_rnd();
        judge(user, computer);
        printf("number: ");
    }

    return 0;
}
main.c

 

 

 

#include 

int gen_rnd(void)
{
    int ret;
    time_t t;

    srand((unsigned)time(&t));
    ret = rand() % 3;

    return ret;
}
gen_rnd.c

 

 

#include 

void judge(int user, int computer) { char *name[] = {"stone", "scissors", "cloth"}; int res = abs(user - computer); if(res == 0) printf("The computer is %s and you are %s, even\n", name[computer], name[user]); else if(res == 1) { if(user < computer) printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); } else { if(user < computer) printf("The computer is %s and you are %s, you lose\n", name[computer], name[user]); else printf("The computer is %s and you are %s, you win\n", name[computer], name[user]); } }

 


judge.c

 

可以看到,由于成为了单独的文件,judge.c必须要自己包含,否则编译目标文件时会报错:

 

m@sys:~/program/C_codes$ gcc -c judge.c 
judge.c: In function ‘judge’:
judge.c:8:9: warning: incompatible implicit declaration of built-in function ‘printf’ [enabled by default]
         printf("The computer is %s and you are %s, even\n", name[computer], name[user]);
         ^
同样的道理,gen_rnd.c则要自己包含,而main.c则不需要这个头文件了。
现在,我们分别为其生成目标文件:

 

gcc -c judge.c main.c gen_rnd.c

这会在当前目录下自动生成gen_rnd.o judge.o main.o

接着就可以生成可执行文件了:gcc gen_rnd.o judge.o main.o -o exe

这三个目标文件之所以还能被正确的粘合在一起,是因为它们仍然存在着逻辑上的联系:首先,只有main.c文件有一个main函数,这就提供了正确的入口;其次,各个文件都能包含需要的头文件,从而正确的生成各自的目标代码;再次,因为main.c要调用另外两个函数,所以声明了另外两个函数的原型,虽然该文件中没有它们的代码,但是在链接阶段两个函数的代码却会一起组合到可执行文件中,同样的道理,printf()等函数的代码也会在链接阶段被组合到可执行文件中,即所谓的链接库文件。