分文件编程思想与条件编译详解

前言

我们在刚学习C语言的时候,通常只创建一个 .c 文件,将头文件、函数的定义和调用写在同一个文件之中。对于初学者而言,由于代码量比较少,逻辑比较简单,这样做是没有问题的。但随着需要实现的功能越来越复杂,工程越来越大,这时我们就不能只用一个 .c 文件来实现了。我们需要创建一个包含头文件和源文件的工程。

首先,我们通过一个简单的例子来了解分文件编程思想:
main.c calcu.c calcu.h input.c input.h 文件如下:

/* main.c文件如下: */
#include <stdio.h>
#include "input.h"
#include "calcu.h"

int main(int argc, char *argv[])
{
    
    
	int a,b,num;
	input_int(&a,&b);
	num = calcu(a,b);
	printf("%d + %d = %d\r\n",a,b,num);
} 
/* calcu.c文件如下: */
#include "calcu.h"

int calcu(int a,int b)
{
    
    
    return (a+b);
} 
/* calcu.h文件如下:*/
#ifndef _CALCU_H
#define _CALCU_H

int calcu(int a,int b); 
#endif
/* input.c文件如下: */
#include <stdio.h>
#include "input.h"

void input_int(int *a,int *b) 
{
    
    
        printf("input two num:");
        scanf("%d %d",a,b);
} 
/* input.h文件如下:*/
#ifndef _INPUT_H
#define _INPUT_H                                                                                                                                                                                             

void input_int(int *a,int *b);
#endif

然后编写 Makefile 文件,Makefile 文件代码如下:

main: main.o input.o calcu.o
	gcc -o main main.o input.o calcu.o
main.o: main.c
	gcc -c main.c
input.o: input.c
	gcc -c input.c
calcu.o: calcu.c
	gcc -c calcu.c

clean:
	rm *.o
	rm main

关于 Makefile 的相关介绍后面会专门写一篇博客,这里只是简单写写。
Makefile 编写好以后我们就可以使用 make 命令来编译我们的工程了:
在这里插入图片描述
执行结果如下:
在这里插入图片描述

1.头文件和源文件的区别

头文件和源文件在本质上没有区别。理论上来说,头文件和源文件中的内容只要是C语言所支持的都可以写,比如在头文件中写函数体,源文件中进行函数声明、宏声明等都是可以的。头文件和源文件是按照功能不同进行区分的。
头文件后缀为 .h,内含函数声明、宏定义、结构体定义等内容。
源文件后缀为 .c ,内含函数实现,变量定义等内容。

而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。这样分开写成两个文件是一个良好的编程风格。

2.头文件和源文件为什么要分开写

既然头文件和源文件本质上没有区别,那么为什么要分开写呢?为什么一般都在头件中进行函数声明、宏声明、结构体声明,而在源文件中去进行变量定义、函数实现呢?

原因如下:

  1. 如果在 .h 文件中实现一个函数,那么如果在多个C文件中引用它,而且又同时编译多个C文件,将其生成的目标文件链接成一个可执行文件,在每个引用此头文件的C文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定义成局部函数,那么在链接时,就会发现多个相同的函数,就会报错。
  2. 如果在 .h 文件中定义全局变量,根据前面所学的知识(C语言—变量的存储方式、作用域和生存周期),当多个 .c 文件引用此头文件时,会在链接时出现变量重定义的错误,这其实和1是类似的。
  3. 如果在 .c 文件中声明宏,结构体,函数等,那么我要在另一个C文件中引用相应的宏,结构体,就必须再做一次重复的工作,如果我改了一个C文件中的一个声明,那么又忘了改其它C文件中的声明,这不就出了大问题了,如果把这些公共的变量放在一个头文件中,想用它的C文件就只需要引用一个就OK了!!!这样岂不方便,要改某个声明的时候,只需要修改头文件就行了 。
  4. 当你需要将你的代码封装成一个库,让别人来用你的代码,你又不想公布源码,那么人家如何利用你的库中的各个函数呢?一种方法是公布源码,别人想怎么用就怎么用,另一种是提供头文件,别人从头文件中看你的函数原型,这样人家才知道如何调用你写的函数,就如同你调用 printf 函数一样,所以在头文件中进行函数等的声明。

从宏观角度讲,或者简单来说,分文件编程思想有很多优点 :

  • 代码具有更好的物理模块性,程序复杂度降低
  • 函数功能划分清楚,代码可扩展性强
  • 方便调试,节约时间

参考文章:
C++头文件和源文件,编译过程
头文件与cpp文件为什么要分开写

3.头文件和源文件编译过程

要想深刻理解头文件与源文件的区别与联系,必须熟悉C程序编译过程,掌握头文件和源文件是如何参与编译的。
(关于C程序编译过程详解,可参考上篇文章:C/C++编译过程详解

一般来说编译器会进行以下几个过程:预处理、编译、汇编和链接。每个过程所做的工作在上篇文章里有详细讲解,这里就不再赘述。

下面,还是通过前言里面的代码来进行理解:

已知头文件"input.h" “calcu.h"是函数声明,源文件"input.c” "calcu.c"中实现了这些函数,"main.c"中#include “input.h” #include “calcu”,调用这些函数。 (以上文件须在同一目录下)

编译器预处理时,要对#include指令进行处理:将头文"input.h" "calcu.h"中的全部内容复制到#include "xxx.h"代码处。

程序编译的时候,并不会去找 input.c 和 calcu.c 文件中的函数实现,只有在链接的时候才进行这个工作。我们在 .c 文件中 #include “xxx.h” 实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生成目标文件(.o文件),目标文件中,这些函数和变量就视作一个个符号。在链接的时候,需要在 Makefile 文件中说明需要链接哪个 .o 文件,此时,链接器会去.o文件中找在 input.c calcu.c 中实现的函数,再把他们build到Makefile文件中指定的可执行文件中。

通常,编译器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示"redefined"。

总结:
#include “xxx.h” 的过程完全可以看成是一个文件拼接的过程。 #include “xxx.h” 这个宏实际工作就是把当前这一行删掉,把 xxx.h 中的内容原封不动的插入在当前行的位置。由于需要写这些函数声明的地方可能非常多(每一个调用 xxx.c 中函数的地方,都要在使用前声明一下),所以用 #include “xxx.h” 这个宏就简化了很多行代码。

接下来,我们来看下经常会存在疑问的几个问题:

  1. input.c 文件中的 #include “input.h” 和 calcu.c 文件中 #include “calcu.h” 是不是多余?
    答:在本例当中,确实是多余的,因为只在 main.c 函数中调用了 input.c 和calcu.c,而main.c中已经 #include “input.h” #include “calcu.h”。但是,当 .c 文件中需要调用同个 .c 文件中的函数时,如果没有这句话,会很麻烦,因为你要在调用之前进行声明。
    若我们在 .c 文件中include同名的 .h 文件,就不需要为声明和调用的顺序而发愁了。
  2. 为什么通常 “xxx.c” 文件中 include 对应的 “xxx.h”?
    答:如1中所述,这已经成为一种代码规范。
  3. 如果 .c 文件中不写 #include “xxx.h”,编译器会自动把 .h 文件中的内容加到 .c 文件中吗?
    答:不会,.c 文件必须在代码开头#include “xxx.h”。

参考文章:
C++头文件和源文件,编译过程
头文件与cpp文件为什么要分开写

4.条件编译详解

条件编译指令是预处理指令的一种。—般情况下,C语言源程序中的每一行代码都要参与编译,但有时候出于对程序代码优化的考虑希望只对其中部分代码进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

条件编译最常见的有三种形式:
第一种:

#if (常量表达式)     
	程序段1           //常量表达式可以不加括号;
#else
	程序段2
#endif

当指定的表达式值为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。
例如:输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输出。

#include <stdio.h>
#define LETTER 1

int main()
{
    
    
        char str[20] = "0";
        char c;
        int i = 0;
        gets(str);
        while((c=str[i]) != '\0')
        {
    
    
                i++;
#if LETTER
                if(c>='a' && c<='z')
                        c = c-32;
#else
                if(c>='A' && c<='Z')
                        c = c+32;
#endif
                printf("%c",c);
        }
        return 0;
}

小结:为什么明明用if语句可以完成的功能却非要引入条件编译命令完成呢?
使用if语句处理一样可以达到相同的效果,但是相比于条件编译

  1. if语句程序主体更加冗长,可读性不高。
  2. if语句程序主体内所有语句全部执行,对效率有一定影响。
  3. 当条件编译段比较多时,条件编译能够减少被编译的语句,从而减少目标的长度。

第二种:

#ifdef (标识符)
    程序段1 
#else 
    程序段2 )    
#endif 

当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2,其中#else部分也可以没有。
例如:我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译:

#ifdef WINDOWS 
#define MYTYPE long 
#else 
#define MYTYPE float 
#endif 

如果在Windows上编译程序,则程序的开始写为 :
#define WINDOWS
如果在其他平台上编译程序,则程序的开始写为:
#define WINDOWS 0
第三种:

#ifndef (标识符)
    程序段1     
#define 
    程序段2    
#endif 

第三种是最为重要的,也是最常用的。
头文件中的 #ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件,而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突;或者说一个头文件在一个C文件中被重复引用时(include嵌套造成),这时就需要借助条件编译指令来解决。

如前言中的代码,就使用了条件编译指令来避免头文件重复引用。

注意问题:变量一般不要定义在.h文件中

这时候有些人会很奇怪,我不是写了 #ifndef #define #endif 这样的命令了吗?
注意:#ifndef只是预处理指令,还没有开始真正的编译,他只能防止同一个编译单元下,重复#include同一个头文件而导致的重复定义。

所以,为了避免变量重定义的问题,我们不在头文件中定义变量。如果你想一个变量被多个C文件使用,那么就在C文件中定义好,并在这个C文件的头文件中,使用extern关键字声明一下。

这个问题在文章:C语言—变量的存储方式、作用域和生存周期中已经提到,具体例子参考该文章,这里不再赘述。

参考文章:
关于为什么不能在头文件中包含变量定义的解释
条件编译#ifdef的妙用详解_透彻
[C] 条件编译

猜你喜欢

转载自blog.csdn.net/little_rookie__/article/details/112712636
今日推荐