C语言程序环境中的预处理详解

目录
  • 一、翻译环境
  • 二、执行环境
  • 三、预处理
    • 1.预处理符号
    • 2.#define定义标识符
    • 3.#define定义宏
    • 4.#和##
    • 5.宏和函数的对比
    • 6.条件编译
    • 7.文件包含
  • 总结

一、翻译环境

整个翻译环境大致就可以画成这样一张图。

下列有几点需要说明:

1. 组成一个程序的每一个源文件通过编译过程分别转换成目标文件(在Linux中目标文件的后缀为.o;而在Windows中目标文件后缀为.obj)

2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序

3. 链接器同时也会引入标准C函数库(链接库)中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

接下来介绍每一步在Linux系统下整个翻译环境的实现方法,以及每一个步骤的作用。

编译可分为三个部分:

(1)预处理:输入指令gcc -E test.c -o,就会将test.c文件变为test.i文件。这一步的作用是是对头文件(#include)的包含、删除注释、#define定义符号的替换等文本操作(下文会对预处理这一个步骤展开详细的介绍)

(2)编译:输入指令gcc -S test.i,就会将test.i文件变为test.s文件,这一步主要作用是把C语言代码转换成汇编代码,其中包含4步:1. 语法分析;2. 词法分析;3. 语义分析;4. 符号汇总

(3)汇编:输入指令gcc -c test.s,就会将test.s文件变为test.o文件,这一步是把汇编代码转换成二进制的指令,这一步是会形成符号表,此时的符号表为接下来的链接操作做出了准备

多个.c文件通过编译过程后形成.o目标文件,在要执行链接的时候,输入指令gcc test.o add.o -o test,就会将.o文件变成可执行文件,这其中的操作包括合并段表和符号表的合并和重定位,这一步主要就是将多个目标文件进行连接的时候通过符号表查看来自外部的符号是否真实存在,这样就完成了整个翻译环境的操作。

二、执行环境

对于程序的执行过程可分为以下几个步骤:

1. 程序必须载入内存中。在有操作系统的环境中:一般由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成

2. 程序的执行开始。之后就会调用main函数

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址;程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值

4. 终止程序。正在终止main函数,也有可能是意外终止的情况

三、预处理

1. 预处理符号

在C语言中,有些预处理符号是语言内置的,就比如:

__FILE__   //进行编译的源文件
__LINE__   //文件当前的行号
__DATE__   //文件被编译的日期
__TIME__   //文件被编译的时间
__STDC__   //如果编译器遵循ANSI C,其值为1,否则未定义

2. #define定义标识符

#define定义的标识符可以是常量、简化关键字、一些符号等,例如:

#define M 10   //定义常量
#define reg register   //将关键字简化
#define do_forever for(;;)   //用形象的符号来替换一种实现
#define CASE break;case   //在写case语句的时候会自动地把break写上

对于#define定义标识符来说,如果定义的东西过长,还可以分几行来写,除最后一行外,其他每行都加上'\',例如:

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
							date:%s\ttime:%s\n",\
							__FILE__,__LINE__, \
							__DATE__,__TIME__)

3. #define定义宏

在#define定义标识符外,#define还有一个规定,就是允许把参数替换到文本中,进而就形成了#define定义宏。声明的方式如下:

#define name(parament-list) stuff

这里的parament-list是由一个逗号隔开的符号表,在实际的代码中他们也会存在于stuff中。

其中值得注意的是:

1. 参数列表的左括号必须与name相邻

2. 如果parament-list与stuff两者之间有任何空白存在,参数列表就会被注释为stuff的一部分

了解了#define定义宏是如何写后,接下来就是#define定义宏的替换规则:

1. 在调用宏的时候,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

2. 替换文本随后被插入到程序中原来的文本位置,参数名被它们的值所替换

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

所以,总结以上规则后得出的结论就是:如果是#define定义宏用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或者邻近操作符之间不可预料的相互作用。

当然,对于#define的使用还有几个注意的点:

1. 宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

4. #和##

对于一些想要把参数插入到字符串中的情况,我们会使用#来把一个宏参数变成对应的字符串,下面举个例子:

如果是直接打印出来的话,因为字符串是可以拼接的,所以就如这样:

#include <stdio.h>
int main()
{
	int a = 10;
	printf("the value of ""a"" is %d\n", a);
	return 0;
}

那么,对于定义宏参数来说,就应该这样:

#include <stdio.h>
#define PRINT(n) printf("the value of "#n" is %d\n", n)
int main()
{
	int a = 10;
	PRINT(a);
	return 0;
}

这样字符串中的n才会根据跟着宏参数的值变化而变化。

而##的作用是可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本段创建标识符。但是这样连接必须产生一个合法的标识符,否则会报错说未定义标识符。

5. 宏和函数的对比

宏的优势:1. 在执行一些小型计算工作的时候,定义宏比调用函数和从函数返回的代码执行所需要的时间会更短;2. 函数的参数必须声明为特定的类型,二宏参数不用

宏的劣势:1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度;2. 宏是无法进行调试的,而函数可以;3. 宏由于没有进行类型定义,所以有时候就会不够严谨;4. 宏可能会带来运算符的优先级的问题,导致程序容易出错

属性 #define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中,除了非常小的宏以外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的使用和返回的额外开销,所以相对慢一些
操作符优先级 宏参数的求值是在所有周围表达式上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 函数参数只在函数调用的时候求值一次,它的结果值传给函数。表达式的求值结果更容易预测
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 函数参数只在传参的时候求值一次,结果更容易控制
参数类型 宏的参数与类型无关,只要参数的操作是合法的,它就可以使用于任何参数类型 函数的参数是与类型有关的,如果参数类型不同,就需要不同的参数,即使他们执行的任务的不同的
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

6. 条件编译

下面列举一些编译指令:

1. #undef 该指令用于移除一个宏定义

2. 该指令是判断应该执行哪一个语句块

#if 常量表达式
    执行语块
#elif 常量表达式
    执行语块
#else
    执行语块
#endif

3. 该指令是判断是否被定义

#if define(symbol)
    如果有定义,执行此语句块
or
#ifdef symbol
    如果有定义,执行此语句块
or
#if !define(symbol)
    如果没有定义,执行此语句块
or
#ifndef symbol
    如果没有定义,执行此语句块

4. 对于条件编译指令来说,其实还可以对其进行嵌套,称为嵌套指令

7. 文件包含

我们在一些较大工程进行编译的时候、在多人合作同一块项目工程的时候,可能会出现头文件重复包含的情况,如果真是这样,则会导致整个代码运行时的效率大大降低,所以对头文件避免重复包含就显得十分重要了。那么,如何避免呢?下面就有一段代码可以用来避免这种情况:

#ifndef __TEST_H__
#define __TEST_H__
    写头文件内容
#endif

这段代码就可以很好地解决了头文件重复包含的问题,但是实际上,如果是在VS的环境下进行编译,会自动在最开始的地方写上:#pragma once,这句代码一样也是可以解决重复包含的问题。

那么,解决完头文件重复包含的问题后,就来介绍两种头文件包含的方式:

1. 用引号包含的头文件,例如:#include "test.h"。这种包含方式头文件的查找策略是先在源文件所在的目录下查找,如果该头文件未被找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果还找不到,则会直接报错。

2. 用尖括号包含头文件,例如:#include 。这种包含方式则是未有第一步,直接进行第二步。

但是不能说为了保证万无一失,直接把全部头文件的包含都用引号进行包含,这样的话有些时候其实是用尖括号的情况而错用引号导致程序的执行速度下降、效率下降等。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C语言的程序环境与预处理你真的了解吗

    目录 1.翻译环境 2.运行环境 3.预处理详解 3.1#define定义的符号 3.2#define定义的宏 3.3#define的替换规则 3.4#与## 4.宏与函数对比 5.#undef 6.条件编译 7.文件包含 总结 c语言代码的实现包含两种环境 1.翻译环境,将源代码转化成可执行的机器指令 2.执行环境,执行代码 1.翻译环境 包括两个过程,编译与链接 程序中每一个源文件通过编译器转化成目标文件(obj) 这些目标文件又通过链接器捆绑在一起 链接器同时会链接标准库中的函数以及程序员

  • 详解C语言之预处理(下)

    目录 #define定义宏带副作用的宏参数 #define定义宏的优点 #define定义宏劣势 预处理 预定义符号 预处理指令 条件编译 1.调试性代码 2.防止重复的头文件多次编译 总结 #define定义宏带副作用的宏参数 我们来看如下一段代码 结果分别为12,11,13 当参数替换后,首先判断表达式 (a++)>(b++)?,判断后a的值加1 b的值加1,然后执行表达式(b++)此时执行的值为12,执行完成后b的值加1,则a的值为11,b的值为13.可以看出对于这种情况下的宏是带有副作用

  • C语言的预处理介绍

    目录 前言 一.预定义符号 二.#define 1.宏 2.宏与函数 3.带副作用的宏参数 4. 宏和函数的不同 5.#undef 三.条件编译 四.文件包含 总结 前言 编译一个C语言程序涉及很多步骤.其中第一个步骤被称为预处理.C语言的预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释.插入被#include指令包含的文件内容.定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译. 一.预定义符号 下表为C语言预处理器

  • 详解C语言的预处理效果

    目录 前言 一.预定义符号 二.#define 1.宏 2.宏与函数 3.带副作用的宏参数 4. 宏和函数的不同 5.#undef 三.条件编译 四.文件包含 1.函数库文件包含 2.本地文件包含 总结 前言 编译一个C语言程序涉及很多步骤.其中第一个步骤被称为预处理.C语言的预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释.插入被#include指令包含的文件内容.定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译.

  • c语言的程序环境与预处理详解

    目录 1.翻译环境 2.运行环境 3.预处理详解 3.1#define定义的符号 3.2#define定义的宏 3.3#define的替换规则 3.4#与## 4.宏与函数对比 5.#undef 6.条件编译 7.文件包含 总结 c语言代码的实现包含两种环境 1.翻译环境,将源代码转化成可执行的机器指令 2.执行环境,执行代码 1.翻译环境 包括两个过程,编译与链接·程序中每一个源文件通过编译器转化成目标文件(obj)·这些目标文件又通过链接器捆绑在一起·链接器同时会链接标准库中的函数以及程序员

  • 详解C语言之预处理

    目录 程序的翻译环境 编译 预编译: 编译: 汇编: 链接 合并段表: #define的用法 1.#define定义标识符,例如 2.#define定义宏 3.#define实现将参数插入到字符串中 总结 程序的翻译环境 源文件被转换成可执行的机器指令时所处的环境称为翻译环境. 由源文件(.c)转换成可执行文件(.exe)需要两步 编译通过编译器实现,链接通过链接器实现 每个源文件都会经过编译器处理后生成对应的目标文件,然后链接器将目标文件和链接库链接在一起生成可执行程序 编译和链接的具体操作

  • C语言程序环境中的预处理详解

    目录 一.翻译环境 二.执行环境 三.预处理 1.预处理符号 2.#define定义标识符 3.#define定义宏 4.#和## 5.宏和函数的对比 6.条件编译 7.文件包含 总结 一.翻译环境 整个翻译环境大致就可以画成这样一张图. 下列有几点需要说明: 1. 组成一个程序的每一个源文件通过编译过程分别转换成目标文件(在Linux中目标文件的后缀为.o:而在Windows中目标文件后缀为.obj) 2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序 3.

  • C语言程序的编译与预处理详解

    目录 一.程序的编译 1. 编译阶段 2.链接 二.预处理详解 1.预定义符号 2.#define定义的标识符 3.#define定义的宏 4.#unef 总结 一.程序的编译 我们写的源文件(*.c)是经过怎样的处理生产可执行文件(*.exe)的呢?这种处理有两个步骤-编译和链接.源文件在编译阶段通过编译器将每个源文件转换为目标文件(这些文件是可执行的机器指令),再通过链接器将其捆绑到一起,生成一个完整的可执行程序. 1. 编译阶段 编译阶段可细分为3个阶段:预处理(即预编译).编译.汇编 预

  • VSCode各语言运行环境配置方法示例详解

    系统环境变量的配置 如:将F:\mingw64\bin添加到系统环境变量Path中 VSCode软件语言json配置C语言 创建个.vscode文件夹,文件夹内创建以下两个文件 launch.json 文件配置 { "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg&

  • GO语言开发环境搭建过程图文详解

    一.GO语言开发包 1.什么是GO语言开发包 go 语言开发包其实是对go语言的一种实现,包括相应版本的语法, 编译, 运行, 垃圾回收等, 里面包含着开发 go 语言所需的标准库, 运行时以及其他的一些必要资源 2.GO语言开发包下载地址 Go官方下载地址 : https://golang.org/dl/ Go官方镜像站(上面打不开可使用这个) : https://golang.google.cn/dl/ Go语言中文网下载地址 : https://studygolang.com/dl gop

  • C语言与C++中内存管理详解

    目录 内存分布 动态内存管理方式-堆区 C语言动态内存管理 C++动态内存管理 new和delete的用法 operator new与operator delete函数 new和delete的实现原理 定位new表达式 高频面试题 重点new/delete和malloc/free的区别 内存泄漏 内存分布 主要段及其分布 ​ 每个程序运行起来以后,它将拥有自己独立的虚拟地址空间.这个虚拟地址空间的大小与操作系统的位数有关系.32位硬件平台的虚拟地址空间的地址可以从0~2^32-1,即0x0000

  • Go语言程序查看和诊断工具详解

    想必Java 的开发者没有不知道或者没用过 jps 这个命令的,这个命令是用来在主机上查看有哪些 Java 程序在运行的. 我刚用 Go 语言程序的时候也很苦恼,我部署在公司服务器上的 Go 程序,其他的同事由于不清楚就经常找不到. 那么 Go 语言有没有像 jps 这样的工具呢?当然有,不仅有,而且还是 Google 自己出品的,官方认证(这种问题 Google 不可能自己想不到啊).名称也跟 jps 很像,叫 gops. 安装 gops 并不包含在官方安装包中,不属于标准工具.需要手动获取.

  • C语言程序环境和预处理详解分析

    目录 一.程序的翻译环境和运行环境 程序的翻译环境 链接阶段 执行环境(运行环境) 二.预处理详解 预定义符号 #define定义标识符 #define定义宏 #define 替换规则 #和##两个预处理的工具 带副作用的宏参数 宏和函数对比 #undef移除宏 命令行定义 条件编译 头文件包含 嵌套文件包含 总结 一.程序的翻译环境和运行环境 重点:任何ANSI C(标准C的程序)的一种实现,存在两个不同的环境 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令. 第2种是执行环境,

  • C语言程序的编译与预处理基础定义讲解

    目录 程序的翻译环境和执行环境 1.翻译环境 2.运行环境 预处理详解 预定义符号 #define #define定义宏 #define替换规则 #和## 带副作用的宏参数 宏和函数对比 命名约定 #undef 命令行定义 条件编译 文件包含 程序的翻译环境和执行环境 在ANSIC的任何一种实现中,存在两个不同的环境:翻译环境和执行环境 翻译环境:源代码被转换为可执行的机器指令. 执行环境:实际执行代码. 1.翻译环境 组成一个程序的每个源文件通过编译分别转换成目标文件(object code)

  • C语言程序环境编译+链接理论

    目录 一.程序的翻译环境(编译和链接) 二.程序的运行环境 一.程序的翻译环境(编译和链接) 在ANSI C 的任何一种实现中,存在两个不同的环境: 第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令. 第二种是执行环境,它用于实际执行代码. 如下图:就是我们编译器编译一个源文件到一个可执行文件的大致过程 组成一个程序的每个源文件(test.c)通过编译过程分别转换成目标代码(test.obj) 每个目标文件又由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序. 链接器

随机推荐