• [C]编译和预处理详解


    作者 华丞臧.
    专栏【C语言】
    各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞+收藏+关注)。如果有错误的地方,欢迎在评论区指出。
    在这里插入图片描述

    一、程序的编译环境和执行环境

    ANSI C的任何一种实现中,存在两个不同的环境。

    第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
    第2种是执行环境,它用于实际执行代码。

    二、详解编译环境

    2.1 翻译环境

    程序编译过程如下:
    在这里插入图片描述

    • 组成一个程序的每个源文件(通常是.c文件)通过编译过程分别转换成目标代码(object code)
    • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序(.exe文件)
    • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

    2.2 编译和链接

    在这里插入图片描述

    编译
    编译划分为三个小步骤:预编译(预处理)、编译、汇编。
    1.预编译
    预编译也可以说是预处理,在这个阶段程序会完成以下操作:

    • 头文件包含(#include
    • define定义符号的替换和删除定义的符号
    • 注释的删除

    在预编译过程中,进行的都是文本操作。
    2.编译
    编译阶段会把C语言代码转换成汇编代码
    程序在编译过程中会完成以下操作:

    • 语法分析
    • 词法分析
    • 符号汇总
    • 语义分析

    符号汇总:只会把全局的符号进行汇总,比如:main、全局变量名、函数名等。

    3.汇编
    在汇编阶段会把汇编代码转换成二进制指令,同时会形成符号表
    程序在汇编过程中会完成以下操作:

    • 形成符号表

    符号表:给编译阶段汇总的符号分配一个地址
    注意:如果在当前文件仅仅是声明(如函数声明),那么该符号会分配一个无效的地址。

    链接
    链接会进行以下操作:

    • 合并段表
    • 符号表的合并和重定位

    2.3 运行环境

    程序执行的过程:

    1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
    2. 程序的执行便开始。接着便调用main函数。
    3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
    4. 终止程序。正常终止main函数;也有可能是意外终止。

    三、预处理指令详解

    3.1 预定义符号

    __FILE__     //进行编译的源文件
    __LINE__     //文件当前的行号
    __DATE__     //文件被编译的日期
    __TIME__     //文件被编译的时间
    __STDC__     //如果编译器遵循ANSI C,其值为1,否则未定义
    
    • 1
    • 2
    • 3
    • 4
    • 5
    //这是一个例子
    printf("file:%s line:%d DATE:%s TIME:%s\n", __FILE__, __LINE__, __DATE__,__TIME__);
    
    • 1
    • 2

    在这里插入图片描述

    3.2 #define

    #define定义标识符

    //语法:
     #define MAX 100
    
    • 1
    • 2
    //举个例子
    #define MAX 1000
    #define reg register          //为 register这个关键字,创建一个简短的名字
    #define do_forever for(;;)     //用更形象的符号来替换一种实现
    #define CASE break;case        //在写case语句的时候自动把 break写上。
    // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
    #define DEBUG_PRINT printf("file:%s\tline:%d\t \
                              date:%s\ttime:%s\n" ,\
    __FILE__,__LINE__ ,\
    __DATE__,__TIME__ ) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意

    • define定义标识符时,最好不要在语句后面加上分好“”,容易造成语法错误。

    #define定义宏

    #define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

    //这是一个例子
    #define MAX(x,y) x>y?x:y
    //使用
    //假设有这么一段代码
    int max = MAX(2,3);
    //那么代码经过预处理过后就会被替换成下面的代码
    int max = 2>3?2:3;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:

    参数列表的左括号必须与MAX紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为【x>y?x:y】的一部分。

    define定义宏是完成替换的,那么不难发现以上代码存在一些问题:

    //如果是下面的的代码会出现什么结果?
    #define MUL(x,y) x*y
    //那么当我们这样使用时
    int a = MUL(1+2,1+4); //结果是什么呢?会是15吗?
    //完成替换后如下
    int a = 1+2*1+4;
    //结果是7,并没达成我们的目的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所以在用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

    //这是一个例子
    #define MUL(x,y) ((x)*(y))
    
    • 1
    • 2

    #define替换规则

    在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

    1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
    2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
    3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

    注意:

    1. 宏参数#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
    2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

    3.3 #和##

    #的作用

    使用 # ,可以把一个宏参数变成对应的字符串。

    例如:

    int a = 10;
    #define PRINT(FORMAT, N)\
     printf("the value of " #N "is "FORMAT "\n", N);
    PRINT("%d", a);//产生了什么效果?
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    可以看到#N的作用就是把宏参数变成对应的字符串。

    ##的作用

    ##可以把位于它两边的符号合成一个符号。
    它允许宏定义从分离的文本片段创建标识符。

    //这是一个例子
    #include 
    #define ADD_TO_SUM(num, value) sum##num += value;
                            
    int main()
    {
    	int sum5 = 0;
    	ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
    	//num = 5,所以sum##num == sum5
    	//替换后:sum5 += 10;
    	printf("%d\n",sum5);//sum5等于0吗?
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

    3.4 带副作用的宏参数

    当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
    例如:

    x+1;//不带副作用
    x++;//带有副作用
    
    • 1
    • 2

    MAX宏可以证明具有副作用的参数所引起的问题。

    #define MAX(a, b) ( (a) > (b) ? (a) : (b) )
    x = 5;
    y = 8;
    z = MAX(x++, y++);
    printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    可以看到输出的结果是:

    x=6 y=10 z=9
    
    • 1

    3.5 宏和函数的优劣

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

    命名约定
    一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
    那我们平时的一个习惯是:

    • 把宏名全部大写
    • 函数名不要全部大写

    3.6 #undef

    这条指令用于移除一个宏定义。

    #undef NAME
    //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
    //例子
    #define MAX 100   //定义MAX
    #undef MAX        //移除MAX的定义
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.7 条件编译

    在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
    常见的条件编译指令:

    1.
    #if 常量表达式
     //...
    #endif
    //常量表达式由预处理器求值。
    如:
    #define __DEBUG__ 1
    #if __DEBUG__
     //..
    #endif
    
    2.多个分支的条件编译
    #if 常量表达式
     //...
    #elif 常量表达式
     //...
    #else
     //...
    #endif
    
    3.判断是否被定义
    #if defined(symbol)
    #ifdef symbol
    #if !defined(symbol)
    #ifndef symbol
    
    4.嵌套指令
    #if defined(OS_UNIX)
     #ifdef OPTION1
     unix_version_option1();
     #endif
     #ifdef OPTION2
     unix_version_option2();
     #endif
    #elif defined(OS_MSDOS)
     #ifdef OPTION2
     msdos_version_option2();
     #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3.8 头文件被包含的方式

    • 本地文件包含
     #include "filename"
    
    • 1

    查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。

    通常“”被用来包含自己的创建的.h文件

    • 库文件包含
    #include 
    
    • 1

    查找策略查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
    如果用 “” 的形式包含库文件,查找的效率就低些,因为查找了两次。

    3.9 防止头文件被重复包含

    //方式一
    #pragma once 
    //在第一行头文件加上这一句
    
    //方式二
    #ifndef __TEST_H__
    #define __TEST_H__
    //头文件内容
    #endif   //__TEST_H__
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    小学生python游戏编程arcade----灯光示例
    c#使用ExifLib库提取图像的相机型号、光圈、快门、iso、曝光时间、焦距信息等EXIF信息
    【HMS Core】【SDK集成】如何解决集成华为分析SDK带来的隐私政策合规检测异常的问题
    戴尔PowerEdge R650服务器荣获国家级实验室5项证书
    24.绳子切割
    Python+班级管理系统 毕业设计-附源码171809
    Oracle 体系结构概述
    WMS仓库管理系统库位功能
    Cis-[Pt-1,3-Propanediamine]-2-Me-Tetrazine/IC-MethylTetrazine四嗪的性质
    渗透测试学习day4
  • 原文地址:https://blog.csdn.net/qq_59456417/article/details/127420476