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

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"><span style="font-size:18px;">虽然一直在用#include命令包含头文件,但其实一致不太明白头文件的原理。今天就研究了一下。</span></span>

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

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

为了回答这个问题,首先要明白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 <stdio.h>
#include <math.h>
#include <stdlib.h>

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 <stdio.h>
#include <math.h>

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

<span style="font-size:12px;">#include <stdlib.h>

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

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

    return ret;
}</span>

gen_rnd.c

<span style="font-family: Arial, Helvetica, sans-serif;font-size:12px;">#include <stdio.h></span>
<span style="font-size:12px;"></span><pre name="code" class="plain">
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必须要自己包含<stdio.h>,否则编译目标文件时会报错:

[email protected]:~/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则要自己包含<stdlib.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()等函数的代码也会在链接阶段被组合到可执行文件中,即所谓的链接库文件。

时间: 2024-11-04 12:48:57

C语言笔记之头文件与链接(一)的相关文章

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

从上篇文章中,我们可以看到一点头文件的作用:就是声明各个函数或变量,以供调用:而至于函数或变量的本体,在链接阶段补上.在main.c中.我们手动声明了两个函数,但其实这样比较费力不讨好,因为如果还有很多其他文件也需要调用这两个函数,那么也要在那些文件中一次次的声明:两个函数还好,如果是成千上百个呢?还要一个一个的去声明吗?这时候,头文件就是一个更好的选择:只要把那些需要用到的函数或变量写进头文件,然后include这个头文件就可以了.头文件就是声明的替代,或者说是批量的声明. 我们的头文件fil

C语言怎么写头文件?

C语言中.h和.c文件解析(很精彩)   简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件) 4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息.(生成.exe文件)

为什么C语言会有头文件

前段时间一个刚转到C语言的同事问我,为什么C会多一个头文件,而不是像Java和Python那样所有的代码都在源文件中.我当时回答的是C是静态语言很多东西都是需要事先定义的,所以按照惯例我们是将所有的定义都放在头文件中的.事后我再仔细想想,这个答案并不不能很好的说明这个问题.所以我在这将关于这个问题的相关内容写下来,希望给大家一点提示,也算是一个总结 include语句的本质 要回答这个问题,首先需要知道C语言代码组织问题,也就是我比较喜欢说的多文件,这个不光C语言有,几乎所有的编程语言都有,比如

Golang使用pkg-config自动获取头文件和链接库的方法

为了能够重用已有的C语言库,我们在使用Golang开发项目或系统的时候难免会遇到Go和C语言混合编程,这时很多人都会选择使用cgo. 话说cgo这个东西可算得上是让人又爱又恨,好处在于它可以让你快速重用已有的C语言库,无需再用Golang重造一遍轮子,而坏处就在于它会在一定程度 上削弱你的系统性能.关于cgo的种种劣迹,Dave Cheney大神在他的博客上有一篇专门的文章<cgo is not Go>,感兴趣的同学可以看一看.但话说回来,有时候为了快速开发满足项目需求,使用cgo也实在是不得

C语言中的头文件

1.头文件#include <> :表示引用标准库头文件,编译器会从系统配置的库环境中去寻找 2.头文件#include "":一般表示用户自己定义使用的头文件,编译器默认会从当前文件夹中寻找,如果找不到,则到系统默认库环境中去寻找. ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 在C语言家族程序中,头文件被大量使用.一般而言,每个C++/C程序通常由头文件(header files)和

转:C语言中的头文件可以自己写吗?

转自:http://www.eefocus.com/computer00/blog/08-09/155791_9ebdc.html 一些初学C语言的人,不知道头文件(*.h文件)原来还可以自己写的. 只知道调用系统库函数时,要使用#i nclude语句将某些头文件包含进去. 其实,头文件跟.C文件一样,是可以自己写的. 头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h保存就行了.头文件中一般放一些重复使用的代码,例如函数声明,变量声明,常数定义,宏的定义等等. <>是标准库的

C语言基础篇—头文件

一.简述 在C语言家族程序中,头文件被大量使用.一般而言,每个C++/C程序通常由头文件(header files)和定义文件(definition files)组成.头文件作为一种包含功能函数.数据接口声明的载体文件,主要用于保存程序的声明(declaration),而定义文件用于保存程序的实现 (implementation). 二.格式 头文件名:xxx.h(xxx为自定义的头文件名称). 文件内容格式: #ifndef _xxx_H_ #define _xxx_H_ 头文件内容 #end

C语言之在头文件中定义全局变量

通常情况下,都是在C文件中定义全局变量,在头文件中声明,但是,如果我们定义的全局变量需要被很多的C文件使用的话,那么将全局变量定义在头文件里面会方便很多,那到底是如何实现的? os_var.c文件内容 1 #define OS_GLOBALS 2 #include “os.h” os.h文件内容 1 #ifdef OS_GLOBALS 2 #define OS_EXT 3 #else 4 #define OS_EXT extern 5 #endif os.h中定义很多的全局变量,但是os.h又需

C++笔记--建立头文件与源文件

新建一个头文件 myadd.h #ifndef MYADD_H #define MYADD_H int fun_add(int a,int b); #endif 新建一个源文件 myadd.cpp #include "myadd.h" int fun_add(int a,int b) { return a+b; } 主程序: #include <iostream> #include "myadd.h" using namespace std; int m