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

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

1.  #include 命令的作用

1.1  什么情况不使用 include

  1. //a.c文件
  2. void test_a()
  3. {
  4. return;
  5. }
  6. //b.c文件
  7. void test_a();  // 函数声明
  8. void test_b()
  9. {
  10. test_a();    // 由于上面已经声明了,所以可以使用
  11. }

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

1.2  什么情况使用 include

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

  1. //a.c文件
  2. void test_a()
  3. {
  4. return;
  5. }
  6. //a.h文件
  7. void test_a();
  8. //b.c文件
  9. #include "a.h"    // 包含含有 test_a() 函数声明的头文件
  10. void test_b()
  11. {
  12. test_a();
  13. }

1.3  #include 起到什么效果

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

  1. //b.c 预编译后的临时文件
  2. void test_a();
  3. void test_b()
  4. {
  5. test_a();
  6. }

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

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

2. static 关键词的使用

2.1  什么叫函数重复定义

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

  1. //a.c文件
  2. void test()
  3. {
  4. return;
  5. }
  6. //b.c文件
  7. void test()
  8. {
  9. return;
  10. }

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

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

2.2  在.h里面写函数实现

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

  1. //a.h文件
  2. void test_a()
  3. {
  4. return;
  5. }
  6. //b.c文件
  7. #include "a.h"
  8. void test_b()
  9. {
  10. test_a();
  11. }

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

  1. //b.c 预编译后的临时文件
  2. void test_a()
  3. {
  4. return;
  5. }
  6. void test_b()
  7. {
  8. test_a();
  9. }

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

  1. //c.c文件
  2. #include "a.h"
  3. void test_c()
  4. {
  5. test_a();
  6. }

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

  1. // c.c 预编译后的临时文件
  2. void test_a()
  3. {
  4. return;
  5. }
  6. void test_c()
  7. {
  8. test_a();
  9. }

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

multiple definition of `test_a'

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

2.3  static 关键词

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

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

  1. //a.c文件
  2. static void test()
  3. {
  4. return;
  5. }
  6. void test_a()
  7. {
  8. test();
  9. }
  10. //b.c文件
  11. static void test()
  12. {
  13. return;
  14. }
  15. void test_b()
  16. {
  17. test();
  18. }

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

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

  1. //a.h文件
  2. static void test()
  3. {
  4. return;
  5. }
  6. //b.c文件
  7. #include "a.h"
  8. void test_b()
  9. {
  10. test();
  11. }
  12. //c.c文件
  13. #include "a.h"
  14. void test_c()
  15. {
  16. test();
  17. }

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

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

3.  防止头文件重复包含

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

  1. #ifndef    XXX
  2. #define   XXX
  3. ……
  4. #endif

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

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

  1. //a.h文件
  2. static void test_a()
  3. {
  4. return;
  5. }
  6. //b.c文件
  7. #include "a.h"
  8. void test_b()
  9. {
  10. test_a();
  11. }
  12. //c.c
  13. #include "a.h"
  14. void test_c()
  15. {
  16. test_a();
  17. }

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

  1. //a.h文件
  2. static void test_a()
  3. {
  4. return;
  5. }
  6. //b.h文件
  7. #include "a.h"
  8. //c.h文件
  9. #include "a.h"
  10. //main.c文件
  11. #include "b.h"
  12. #include "c.h"
  13. void main()
  14. {
  15. test_a();
  16. }

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

  1. //main.c 预编译之后的临时文件
  2. static void test_a()
  3. {
  4. return;
  5. }
  6. static void test_a()
  7. {
  8. return;
  9. }
  10. void main()
  11. {
  12. test_a();
  13. }

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

但是,如果在 a.h 里面加上了 #ifndef……#define……#endif 的话,就不会出现这个问题了。

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

  1. //a.h 文件
  2. #ifndef  A_H
  3. #define A_H
  4. static void test_a()
  5. {
  6. return;
  7. }
  8. #endif

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

  1. //main.c 预编译后的临时文件
  2. #ifndef A_H
  3. #define A_H
  4. static void test_a()
  5. {
  6. return;
  7. }
  8. #endif
  9. #ifndef A_H
  10. #define A_H
  11. static void test_a()
  12. {
  13. return;
  14. }
  15. #endif
  16. void main()
  17. {
  18. test_a();
  19. }

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

本文出自 “对影成三人” 博客,请务必保留此出处http://ticktick.blog.51cto.com/823160/596179

http://blog.chinaunix.net/uid-26435987-id-3077444.html

因为对于一个大程序而言,我们可能要定义很多常量( 不管是放在源文件还是头文件 ),那么我们有时考虑定义某个常量时,我们就必须返回检查原来此常量是否定义,但这样做很麻烦.if defined宏正是为这种情况提供了解决方案.举个例子,如下: 
#define .... 
#define .... 
    .... 
    .... 
#define a 100 
    .... 
此时,我们要检查a是否定义(假设我们已经记不着这点了),或者我们要给a一个不同的值,就加入如下句子 
#if defined a 
#undef a 
#define a 200 
#endif 
上述语句检验a是否被定义,如果被定义,则用#undef语句解除定义,并重新定义a为200

同样,检验a是否定义: 
#ifndef a    //如果a没有被定义 
#define a 100 
#endif

以上所用的宏中:#undef为解除定义,#ifndef是if not defined的缩写,即如果没有定义。

这就是#if defined 的唯一作用!

1) 
#if defined XXX_XXX 
#endif 
是条件编译,是根据你是否定义了XXX_XXX这个宏,而使用不同的代码。

一般.h文件里最外层的 
#if !defined XXX_XXX 
#define XXX_XXX 
#endif 
是为了防止这个.h头文件被重复include。

2) 
#error XXXX 
是用来产生编译时错误信息XXXX的,一般用在预处理过程中; 
例子: 
#if !defined(__cplusplus) 
#error C++ compiler required. 
#endif

有一道经典的C语言问题,关于宏定义中#和##符号的使用和宏定义展开问题
 
程序如下:

#include <stdio.h>
#define f(a,b) a##b
#define g(a)  #a
#define h(a) g(a)
 
int main()
{
        printf("%s\n", h(f(1,2)));
        printf("%s\n", g(f(1,2)));
        return 0;
}

答案:第一行:12  第二行:f(1,2)

说明:

1、关于符号#和##

两个符号都只能用于预处理宏扩展。不能在普通的源码中使用它们,只能在宏定义中使用。

简单的说,#是把宏参数变为一个字符串,##是把两个宏参数连接在一起。

关于这两个符号的具体意义和用法可以参见两篇文章:

#和##在宏替换中的作用 http://www.linuxidc.com/Linux/2014-06/102921.htm

C/C++ 宏中"#"和"##"的用法 http://www.linuxidc.com/Linux/2014-06/102924.htm

还有GCC帮助文档上的解释:

3.4 Stringification

3.5 Concatenation

2、关于宏展开

预处理过程的几个步骤:

1)字符集转换(如三联字符)

2)断行链接/

3)注释处理,/* comment */,被替换成空格

4)执行预处理命令,如#inlcude、#define、#pragma、#error等

5)转义字符替换

6)相邻字符串拼接

7)将预处理记号替换为词法记号

第4)步即如何展开宏函数的规则:在展开当前宏函数时,如果形参有#或##则不进行宏参数的展开,否则先展开宏参数,再展开当前宏。

宏替换顺序英文描述如下:

A parameter in the replacement list, unless preceded by a # or ## preprocessing token or followed by a ## preprocessing token, is replaced by the corresponding argument after all macros contained therein have been expanded.

3、总结

综合以上,对于这道题来说,第一行h(f(1,2)),由于h(a)非#或者##所以先展开其参数f(1,2),即12,所以变成h(12),然后再宏替换为g(12),再次替换为12。

第二行g(f(1,2)),宏g(a)带有#,所以里面的f(1,2)不展开,所以变成f(1,2)

类似的这种问题在《你必须知道的495个C语言问题》中出现过,在121页的“预处理功能”的问题11.19,有兴趣的朋友可以看一看。

转载于:https://www.cnblogs.com/virusolf/p/4921922.html

深入理解include预编译原理相关推荐

  1. 【Java核心技术大会 PPT分享】陈阳:深入理解 Java 虚拟机编译原理

    导读:深入理解 Java 虚拟机编译原理 直播分享PPT Java核心技术大会2022 分享主题:深入理解 Java 虚拟机编译原理 分享嘉宾:陈阳,京东科技架构师,曾就职于美团.去哪网,负责自研消息 ...

  2. 理解前端Babel编译原理

    大厂技术  坚持周更  精选好文 背景 我们知道编程语言主要分为「编译型语言」和「解释型语言」,编译型语言是在代码运行前编译器将编程语言转换成机器语言,运行时不需要重新翻译,直接使用编译的结果就行了. ...

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

    阿里妹导读:对于开发者而言,Flutter工程和我们的Android/iOS工程有何差别?Flutter的渲染和事件传递机制如何工作?构建缓慢或出错又如何去定位,修改和生效呢?凡此种种,都需要对Flu ...

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

    问题背景 对于开发者而言,什么是Flutter?它是用什么语言编写的,包含哪几部分,是如何被编译,运行到设备上的呢?Flutter如何做到Debug模式Hot Reload快速生效变更,Release ...

  5. 编译原理词法分析器的c++实现

    一.题目的理解和说明 编译原理这门课是计算机专业的核心课程之一,是一门研究软件是什么,为什么可以运行,以及怎么运行的学科.编译系统的改进将会直接对其上层的应用程序的执行效率,执行原理产生深刻的影响.编 ...

  6. Linux(gcc编译原理、过程以及常用调试命令)

    PS:红色字体表示重点,绿色字体表示重要标题,块引用中表示Linux终端中的命令. 1.gcc / g++的安装 命令:sudo apt install gcc /gcc+ 2.gcc编译连接 //m ...

  7. gcc——预处理(预编译),编译,汇编,链接

    一,预编译 操作步骤:gcc -E hello.c -o hello.i 主要作用: 处理关于 "#" 的指令 [1]删除#define,展开所有宏定义.例#define port ...

  8. 实例讲解js中的预编译

    js作为一本脚本语言,可以不经过编译直接运行,但遇到预编译的问题,尤其是变量或函数同名时,这点知识就尤其必要了.为了更好地了解js语言背后的运行机理.笔者采用实例化的方式讲解我理解的预编译.    理 ...

  9. 龙书啃不动?老司机带你从零入门编译原理,开发编译器

    计算机只认识二进制的,但是我们平常开发中根本不会使用二进制进行开发,我们使用的都是 Java.C.Python 这类的高级语言.每种语言都会经过一系列的转换才能被计算机识别,那么到底是谁做的这项工作呢 ...

最新文章

  1. MySQL LIMIT:限制查询结果的记录条数
  2. Ubuntu下deb与rpm包的安装方法
  3. (80)Vivado综合约束方法
  4. 《Linux内核设计与实现》读书笔记 - 目录 (完结)【转】
  5. swagger python自动化用例_自动生成robot自动化测试用例
  6. go hash 密码:加密、校验
  7. Flask 推理模型,显存一直增长。
  8. 用Matlab把SMAP vegetation optical depth (VOD)数据转换成带投影信息EASE GRID2的tif文件
  9. 使用阿里云,5分钟搭建私有云盘
  10. Oracle snapper ASH监控工具
  11. 银行业务系统(c/s架构、socket网络编程、多线程)
  12. 国庆日快到啦!国庆版头像来了!总有一款适合你!
  13. 接口自动化测试框架(一):框架介绍
  14. Java爬虫初学——爬取BT电影天堂电影的磁力链接并筛选下载
  15. tensorflow.python.framework.errors_impl.InvalidArgumentError exception str() failed解决办法
  16. agv ti 毫米波雷达_一种基于毫米波雷达的AGV小车的制作方法
  17. 5.Python基础之面向对象
  18. Boolean value of Tensor with more than one value is ambiguous
  19. 小程序与h5的相互跳转
  20. MapReduce中文翻译

热门文章

  1. 笔记本电脑如何保养_笔记本电脑过热 五招教你轻松解决
  2. 手机怎么打开f12_碰到生僻字看不懂怎么办?打开手机这个开关,一键即可秒懂...
  3. tt c mysql t4 bll_通过T4模板实现代码自动生成
  4. js删除字符串的最后一个字符几种方法
  5. Protocol ‘https‘ not on whitelist ‘file,crypto‘ ——m3u8下载协议不在白名单,m3u8下载器推荐
  6. 乌班图系统修改服务器时间的命令,ubuntu 修改系统时间无效
  7. java一行输入多个数据类型_Java中的3种输入方式实现解析
  8. wpsppt设置页码和总页数_Word页码,这几个技巧真的很实用!
  9. java 反射创建属性_使用Java反射机制确定基本数据类型属性
  10. [leetcode]5178. 四因数