C语言包含头的问题

小弟刚学C 有个问题不太明白
以前是做PHP的 所以对于include 来说 如果是单入口的项目 只要include一次 需要的文件就好了 其他地方不需要引入

但是C 好像也是单入口的 从main函数开始 那么为什么要每个文件都包含一次头文件呢
比如说

main.c
#include<stdio.h>
#include"mystock.h"

a.c
#include<stdio.h>
#include"mystock.h"

b.c
#include<stdio.h>
#include"mystock.h"

如果说 三个文件都用了上面两个头文件的函数 那么要包含三次 但是其实入口都是从main开始 可不可以直接main包含了这个头文件 其他文件什么都不包含呢? 对这块真是太搞不明白了

阅读 4.4k
5 个回答

这个地方你需要了解下编译原理了,首先C/C++和PHP的区别
1) 一个是动态语言(PHP),一个是静态语言(C++)
2) C/C++编译的基本单位是文件,其中采用的是声明和实现分离的方式,这里的声明你可以理解为接口的一种抽象,声明是.h,实现在.c里。 对于 "stdio.h"和“mystock.h”,C++编译器做的首先是根据main.c、a.c、b.c里对着2个头文件的使用进行词法分析、语法分析等,然后编译出一个中间文件obj,编译没问题了再链接,这是有别于PHP的,所以在编译的时候这3者文件可能都被扫描了(是否完全扫描取决于编译器的实现),但最后链接的时候,这3者的实现在内存中是只有一份的。

以下是我对 c 语言 include 的理解,我也不知道对题主是否有帮助,之所以写这么多是因为我当初对这一点也比较困惑,现在自认为弄清楚了,所以啰哩啰嗦的整理了一下。当然,我的理解不一定是正确的,请各位想要阅读的朋友带着怀疑的眼光,如有错误希望指出。

C 语言中头文件(一般情况下)是用来写函数声明的,一般不会涉及函数的具体实现等。所以如果一个头文件被包含多次,无疑会导致同一个(或一组)函数的原型被重复声明被多次。但你要知道重复声明同一个函数并不是一个错误,只有重复定义同一个函数才会报错。例如像下面这样重复声明是没有错误的:

/**
 * @file main.c
 **/

#include <stdio.h>

int add( int a, int b );
int add( int a, int b );
int add( int a, int b );

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

int main( void )
{
    int result;
    
    result = add( 1, 2 );
    printf( "add( 1, 2 ) => %d \n", result );
    
    return 0;
}

但如果你像下面一样重复定义同一个函数,就会在编译时报错:

/**
 * @file main.c
 **/

#include <stdio.h>

int add( int a, int b );
int add( int a, int b );
int add( int a, int b );

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

/* 重复定义 */
int add( int a, int b )
{
    return a + b;
}

int main( void )
{
    int result;
    
    result = add( 1, 2 );
    printf( "add( 1, 2 ) => %d \n", result );
    
    return 0;
}

可是为什么要使用头文件来包含函数的原型声明呢? 来看下面这个例子:

假设我们有两个源文件,一个是 add.c 另一个是 main.c,我们需要在 main.c 中使add.c中定义的一个函数(int add( int a, int b)),下面给出两个文件的代码:

/**
 * @file add.c
 **/

int add( int a, int b )
{
    return a + b;
}
/**
 * @file main.c
 **/

#include <stdio.h>

int main( void )
{
    int result;
    
    result = add( 1, 2 );
    printf( "add( 1, 2 ) => %d \n", result );
    
    return 0;
}

保存以上两个文件后,我们就可以使用编译器来编译程序了, 注意这里我们并没有使用头文件 ,但我们照样可以编译出可执行程序,以 gcc 编译器为例(vc6 也是可以的),编译命令如下:

gcc -o main main.c add.c

可以看到,没有头文件我们也能完成编译,头文件似乎没有必要。但现在我们修改一下 main.c 看看会发生什么:

#include <stdio.h>

int main( void )
{
    int result;
    
    /* 修改之前: */
    /* result = add( 1, 2 ); */
    /* 修改之后: */
    result = add( 1.5 , "B", "ABC" );
    printf( "add( 1, 2 ) => %d \n", result );
    
    return 0;
}

在这里我们故意使用 错误的参数类型 以及 错误的参数个数 去调用 add 函数,按理来说,如果这个时候我们进行编译,编译器一定会为我们指出错误。但是,如果你真的这么做了,你会发现编译器根本不会报错。原因是编译器根本不知道 add 函数的原型 ---- 也就是不知道 add 函数的返回值类型、参数个数以及每个参数的类型。对于不知道原型的函数,编译器假定该函数的返回值类型为整型,且不对其参数进行检查。所以即使我们调用 add 时使用的参数不正确,我们也能通过编译,但是这个程序显然在逻辑上是错误的,遗憾的是由于我们书写代码不规范,编译无法为我们发现这个错误。为了规范代码,我们应该在调用函数前给出函数的原型,以使编译器为我们检查参数。修改后的 main.c 如下:

#include <stdio.h>

/* 添加函数原型的声明 */
int add( int a, int b );

int main( void )
{
    int result;
    
    /* 修改之前: */
    /* result = add( 1, 2 ); */
    /* 修改之后: */
    result = add( 1.5 , "B", "ABC" );
    printf( "add( 1, 2 ) => %d \n", result );
    
    return 0;
}

在上面的代码中,我们添加了函数的原型的声明,再次编译时,编译器就会为我们指出错误。

现在假设我们又添加了一个源文件 newfile.c,我们准备在这个新文件中使用 add 函数,为了使编译器为我们检查参数,我们不得不在 newfile.c 的头部书写 add 函数的原型。如果有一天我们把 add 更名为 plus 那我们同时也必须修改 main.cnewfile.c 中的原型和调用。如果有 50 个源文件中都使用了这个函数,我们至少要修改 100 次(原型 + 调用)。如果我们使用头文件的话,我们只需在头文件中书写一次函数原型,并在各个源文件中 include 这个头文件就行了。当修改函数原型的时候,只需修改头文件中的一个原型即可,当然各个源文件的调用还是要改的。于是我只需修改 51 次即可。但是,讲道理的话,其实修改 51 次和 100 次根本就没有本质上区别。可是如果 add.c 中有 100 个函数且其他每个源文件中都会用到这 100 个函数中的某 80 个函数呢?你愿意在每个源文件中书写 80 次函数的原型吗?更明智的做法恐怕是使用一个头文件来保存这 100 个函数的原型,然后在每个源文件中分别 include 一次这个头文件即可。这样做不仅便于维护,也更节省磁盘空间。

还有就是,c 语言中的 #include 是一个预处理指令,也就是说它会在正式编译之前被编译器处理掉,真正编译的是处理后代码。而对 #include 的处理只不过是把 #include 所在行替换为它所 include 的文件的内容而已,比如有以下的头文件和源文件:

/**
 * @file add.h
 **/
int add( int a, int b );
/**
 * @file test.c
 **/
#include "add.h"

int main( void )
{
    add( 1, 2 );
    return 0;
}

经过预处理后的文件如下:

int add( int a, int b );
int main( void )
{
    add( 1, 2 );
    return 0;
}

使用 gcc-E 选项可以查看预处理后的代码。

综上所述,多次包含同一个头文件并不会降低最终程序的性能,也不会导致编译错误(假定头文件中只有声明没有定义,如果有定义的话,多次包含将会导致重定义,从而出错)。多次包含同一个头文件会增加预处理的时间,但是相比之下这点时间又算得了什么呢?

避免重复链接的办法是

#ifndef SOME_MACRO
#define SOME_MACRO
/* codes here */
#endif

或者在头文件开头写

#pragma once

后者不一定通用,前者是所有编译器都通用的。

C语言里面的include是真的把文件加入到当前文件里面了。所以有时会看到include一个图片(数组形式)等等。所以头文件一般都会用#ifdef #endif包起来。

理论上讲,如果编译的顺序是一定的,确实可以只include一次。但往往大型的项目这样做是不可能的。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏