深入理解include预编译原理

你了解 #include 某个 .h 文件后,编译器做了哪些操作么? 你清楚为什么在 .h文件中定义函数实现的话需要在函数前面加上 static 修饰么?你知道 #ifndef……#define……#endif 这种防止头文件重复包含的精髓所在么?本文就是来探讨这些问题,并给出我的理解和思考,欢迎大家留言交流。

1.  #include 命令的作用

1.1  什么情况不使用 include

//a.c文件 

void test_a()
{
    return;
} 

//b.c文件 

void test_a();  // 函数声明 

void test_b()
{
    test_a();    // 由于上面已经声明了,所以可以使用
}

其实,这样的工程,可以不用使用 include 预编译命令。

1.2  什么情况使用 include

如果工程里面的函数特别多,那么按照上面的做法,则必须在每一个 .c 文件的开头列出所有本文件调用过的函数的声明,这样很不高效,而且一旦某个函数的形式发生变化,又得一个一个改 .c 开头的函数声明。

因此,#include 预编译命令诞生。

//a.c文件 

void test_a()
{
     return;
} 

//a.h文件 

void test_a(); 

//b.c文件 

#include "a.h"    // 包含含有 test_a() 函数声明的头文件 

void test_b()
{
    test_a();
}

1.3  #include 起到什么效果

上述代码在编译器进行预编译的时候,遇到 #include "a.h" ,则会把整个 a.h 文件都copy到 b.c 的开头,因此,在实际编译 b.c 之前,b.c 已经被修改为了如下形式:

//b.c 预编译后的临时文件 

void test_a(); 

void test_b()
{
    test_a();
}

由此可见,得到的效果和手动加 test_a() 函数声明时的效果相同。

#tips# 在Linux下,可以使用 gcc -E b.c 来查看预编译 b.c 后的效果。

2. static 关键词的使用

2.1  什么叫函数重复定义

我们经常会遇到报错,说变量或者函数重复定义。那么,在此,首先我举例说明一下什么叫函数的重复定义。

//a.c文件 

void test()
{
    return;
} 

//b.c文件 

void test()
{
    return;
}

那么,在编译的时候是不会报错的,但是,在链接的时候,会出现报错:

multiple definition of `test‘,因为在同一个工程里面出现了两个test函数的定义。

2.2  在.h里面写函数实现

如果在 .h 里面写了函数实现,会出现什么情况?

//a.h文件 

void test_a()
{
   return;
} 

//b.c文件 

#include "a.h" 

void test_b()
{
    test_a();
}

预编译后,会发现,b.c 被修改为如下形式:

//b.c 预编译后的临时文件 

void test_a()
{
   return;
} 

void test_b()
{
    test_a();
}

当然,这样目前是没有什么问题的,可以正常编译链接成功。但是,如果有一个 c.c 也包含的 a.h 的话,怎么办?

//c.c文件 

#include "a.h" 

void test_c()
{
    test_a();
}

同上,c.c 在预编译后,也形成了如下代码:

// c.c 预编译后的临时文件 

void test_a()
{
    return;
} 

void test_c()
{
    test_a();
}

那么,在链接器进行链接(link)的时候,会报错:

multiple definition of `test_a‘

因此,在 .h 里面写函数实现的弊端就暴露出来了。但是,经常会有这样的需求,将一个函数设置为 内联(inline) 函数,并且放在 .h 文件里面,那么,怎样才能防止出现上述 重复定义的报错呢?

2.3  static 关键词

应对上面的情况,static关键词很好地解决了这个问题。

用static修饰函数,则表明该函数只能在本文件中使用,因此,当不同的文件中有相同的函数名被static修饰时,不会产生重复定义的报错。例如:

//a.c文件 

static void test()
{
    return;
} 

void test_a()
{
    test();
} 

//b.c文件 

static void test()
{
    return;
} 

void test_b()
{
    test();
}

编译工程时不会报错,但是test()函数只能被 a.c 和 b.c 中的函数调用,不能被 c.c 等其他文件中的函数调用。

那么,用static修饰 .h 文件中定义的函数,会有什么效果呢?

//a.h文件 

static void test()
{
    return;
} 

//b.c文件 

#include "a.h" 

void test_b()
{
    test();
} 

//c.c文件 

#include "a.h" 

void test_c()
{
    test();
}

这样的话,在预编译后,b.c 和 c.c 文件中,由于 #include "a.h" ,故在这两个文件开头都会定义 static void test() 函数,因此,test_b() 和 test_c() 均调用的是自己文件中的 static void test() 函数 , 因此不会产生重复定义的报错。

因此,结论,在 .h 文件中定义函数的话,建议一定要加上 static 关键词修饰,这样,在被多个文件包含时,才不会产生重复定义的错误

3.  防止头文件重复包含

经常写程序的人都知道,我们在写 .h 文件的时候,一般都会加上

#ifndef    XXX 
#define   XXX  
…… 
#endif

这样做的目的是为了防止头文件的重复包含,具体是什么意思呢?

它不是为了防止多个文件包含某一个头文件,而是为了防止一个头文件被同一个文件包含多次。具体说明如下:

//a.h文件 

static void test_a()
{
    return;
} 

//b.c文件 

#include "a.h" 

void test_b()
{
    test_a();
} 

//c.c 

#include "a.h" 

void test_c()
{
    test_a();
}

这样是没有问题的,但下面这种情况就会有问题。

//a.h文件 

static void test_a()
{
    return;
} 

//b.h文件 

#include "a.h" 

//c.h文件 

#include "a.h" 

//main.c文件 

#include "b.h"
#include "c.h" 

void main()
{
    test_a();
}

这样就不小心产生问题了,因为 b.h 和 c.h 都包含了 a.h,那么,在预编译main.c 文件的时候,会展开为如下形式:

//main.c 预编译之后的临时文件 

static void test_a()
{
    return;
} 

static void test_a()
{
    return;
} 

void main()
{
    test_a();
}

在同一个 .c 里面,出现了两次 test_a() 的定义,因此,会出现重复定义的报错。

但是,如果在 a.h 里面加上了

#ifndef    XXX 
#define   XXX  
…… 
#endif

的话,就不会出现这个问题了。

例如,上面的 a.h 改为:

//a.h 文件 

#ifndef  A_H
#define A_H 

static void test_a()
{
    return;
} 

#endif

预编译展开main.c则会出现:

//main.c 预编译后的临时文件 

#ifndef A_H
#define A_H 

static void test_a()
{
    return;
} 

#endif 

#ifndef A_H
#define A_H 

static void test_a()
{
    return;
} 

#endif 

void main()
{
    test_a();
}

在编译main.c时,当遇到第二个 #ifndef  A_H ,由于前面已经定义过 A_H,故此段代码被跳过不编译,因此,不会产生重复定义的报错。这就是 #ifndef……#define……#endif 的精髓所在。

转自:http://ticktick.blog.51cto.com/823160/596179/

参考:http://blog.csdn.net/softmanfly/article/details/41699511

时间: 2024-10-02 20:34:11

深入理解include预编译原理的相关文章

JavaScript预编译原理分析

今天用了大量时间复习了作用域.预编译等等知识 看了非常多博文,翻了翻曾经看过的书(好像好多书都没有讲预编译) 发现当初认为自己学的非常明确,事实上还是存在一些思维误区 (非常多博文具有误导性) 今晚就整理了一下凌乱的思路 先整理一下预编译的知识吧,日后有时间再把作用域具体解说一下 大家要明确.这个预编译和传统的编译是不一样的(能够理解js预编译为特殊的编译过程) JavaScript是解释型语言, 既然是解释型语言,就是编译一行.运行一行 传统的编译会经历非常多步骤,分词.解析.代码生成什么的

在EJS脚本内使用“#include”预编译指令

此博客为9925.org的镜像,登录9925.org可以查看到最新博文. 原文出处:http://ily.so/26bMBz 预编译指令是Easton JavaScript脚本解释器对JavaScript语言拓展的重要功能之一,使用预编译指令可以引用外部的JS脚本代码,类似于HTML内的<script>标签引用外部JS脚本. #include语法解释 以“#include”指令开始,一行一个指令,指令后面加不加空格都无所谓,但是为了方便阅读通常情况下都加一个空格. 例如: //引用运行库内的A

浅谈JavaScript预编译原理

这两天又把js的基础重新复习了一下,很多不懂得还是得回归基础,大家都知道js是解释性语言,就是编译一行执行一行,但是在执行的之前,系统会做一些工作: 1,语法分析: 2,预编译: 3,解释执行. 语法分析很简单,就是引擎会简单的检查一下你的代码有没有什么低级的错误,解释执行就是执行代码,执行代码之前会进行预编译,预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数.下面我详细说一下: 预编译可以简单的分成全局预编译和函数体预编译,函数体预编译可以从四个规则入手; 1,创建AO对象;AO{ 

深入理解flutter的编译原理与优化

摘要: 闲鱼技术-正物 问题背景 对于开发者而言,什么是Flutter?它是用什么语言编写的,包含哪几部分,是如何被编译,运行到设备上的呢?Flutter如何做到Debug模式Hot Reload快速生效变更,Release模式原生体验的呢?Flutter工程和我们的Android/iOS工程有何差别,关... 闲鱼技术-正物 问题背景 对于开发者而言,什么是Flutter?它是用什么语言编写的,包含哪几部分,是如何被编译,运行到设备上的呢?Flutter如何做到Debug模式Hot Reloa

深入理解 Flutter 的编译原理与优化

阿里妹导读:对于开发者而言,Flutter工程和我们的Android/iOS工程有何差别?Flutter的渲染和事件传递机制如何工作?构建缓慢或出错又如何去定位,修改和生效呢?凡此种种,都需要对Flutter从设计,开发构建,到最终运行有一个全局视角的观察. 本文由闲鱼技术团队出品,将以一个简单的hello_flutter为例,介绍下Flutter相关原理及定制与优化. Flutter简介 Flutter的架构主要分成三层:Framework,Engine和Embedder. Framework

C++预编译头文件(#include &quot;stdafx.h&quot;)

来源:http://blog.sina.com.cn/s/blog_4ac766c00100qsbd.html http://blog.csdn.net/txh0001/article/details/7031058 作为一个C++菜鸟,在预编译头文件(#include "stdafx.h")上纠结了很久,今天打算彻底弄明白它. 1.预编译头文件的概念 所谓的预编译头文件,其实我们很熟悉的,这里的头文件(Microsoft Visual C++中)一般的说就是我们常见的stdafx.h

atitit.查看预编译sql问号 本质and原理and查看原生sql语句

atitit.查看预编译sql问号 本质and原理and查看原生sql语句 1. 预编译原理. 1 2. preparedStatement 有三大优点: 1 3. How to look  gene  sql 2 1. Hb cfg all debug ,cant see... 2 2. WSExplorer按照进程抓取pack可以看见.. 2 3. Mysql 5.6 开放日志可以看见 2 4. Mysql and msssql的不同实现 2 4. MYSql的实现是jdbc连接完全的sql

GCC编译器原理(三)------编译原理三:编译过程---预处理

Gcc的编译流程分为了四个步骤: 预处理,生成预编译文件(.文件):gcc –E hello.c –o hello.i 编译,生成汇编代码(.s文件):gcc –S hello.i –o hello.s 汇编,生成目标文件(.o文件):gcc –c hello.s –o hello.o 链接,生成可执行文件:gcc hello.o –o hello 一.预处理 预编译程序读出源代码,对其中内嵌的指示字进行响应,产生源代码的修改版本,修改后的版本会被编译程序读入. 在 GNU 术语中,预处理程序叫

关于计算机编译原理

从我个人理解,计算机编译原理,顾名思义,就是关于计算机编程翻译的相关原理,即对计算机编程的,更为深入.更为详细的去解读计算机语言.上网看了许多个人解读编译原理,发现最令我信服的,就是把编译原理类比成人体解剖:只有认真解剖.研究人体各部位,才能在手术中.医治中做到更好的处理.因此,唯有理解编译原理,才能更好地进行计算机的编程等一系列的操作. 博客上有人说到,学习编译原理能够更加容易理解算法之间的关系.能培育自己的观点.提高学习语言的效率.但在我个人看来,学好编译原理,能让我们有一个更强的临时变换的