C语言预处理预编译命令及宏定义详解

目录

  • 程序翻译环境和执行环境
  • 翻译环境:详解编译+链接
    • 1. 编译 — 预处理/预编译 test.c ---- test.i
    • 2. 编译 — 编译 test.i ---- test.s
    • 3. 编译 — 汇编 test.s ---- test.obj
    • 4. 链接 test.obj ---- test.exe
  • 运行环境
    • 预处理/预编译详解
      • #define 定义标识符
    • #和##
      • #的作用
      • ##的作用
      • 命名约定
      • 命令行定义
      • 条件编译
        • 常见的条件编译指令
      • 文件包含
        • offsetof(宏类型,成员名字)偏移量模拟实现
        .c 源程序 ----- 编译 ----- 链接 ---- exe ----运行 -------->

        程序翻译环境和执行环境 翻译环境:源代码被转换为可执行机器指令(二进制代码)。
        执行环境:用于实际执行代码。

        翻译环境:详解编译+链接 C语言预处理预编译命令及宏定义详解
        文章图片

        1.组成程序的每个源文件通过编译过程分别转换成目标代码。
        2.每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
        3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序员个人的程序库,将其需要的函数也链接到程序。
        extern声明外部文件中的函数
        C语言预处理预编译命令及宏定义详解
        文章图片


        1. 编译 — 预处理/预编译 test.c ---- test.i
        文本操作
        #include 头文件的包含
        注释删除:使用空格替换注释
        #define 替换,所以宏无法进行调试。
        ……

        2. 编译 — 编译 test.i ---- test.s
        把c语言代码翻译成汇编代码
        语法分析
        词法分析
        语义分析
        符号汇总

        3. 编译 — 汇编 test.s ---- test.obj
        把汇编代码转换成二进制代码(指令)。
        形成符号表。(符号+地址)

        4. 链接 test.obj ---- test.exe
        合并段表
        符号表的合并和重定位
        C语言预处理预编译命令及宏定义详解
        文章图片


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

        预处理/预编译详解 预定义符号
        本来就有的符号
        __FILE__//进行编译的源文件__LINE__//文件当前的行号__DATE__//文件被编译的日期__TIME__//文件被编译的时间__STDC__//如果编译器遵循ANSI C,其值为1,否则未定义

        应用
        printf("data: %s\n time: %s" ,__DATE__,__TIME__);

        输出
        data: Jul 13 2021time: 15:13:54


        #define 定义标识符

        宏和define区别,宏是有参数的。
        下面是宏的声明方式:
        #define name( parament-list ) stuff
        其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
        参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuf的一部分。
        例如
        #define SQUARE(X) (X)*(X)int main(){int ret = SQUARE(5); return 0; }

        宏的参数是替换的,不是传参的。
        在定义宏的时候不要吝啬括号。

        #和##
        #的作用
        使用#,把一个宏参数变成对应的字符串。
        把参数插入到字符串中
        #define PRINT(X) printf("the value of "#X" is %d\n", X)int main(){ int a = 10; int b = 20; PRINT(a); PRINT(b); return 0; }

        输出
        the value of a is 10
        the value of b is 20

        ##的作用
        ## 可以把位于他两边的符号合成一个符号,允许宏定义从分离的文本片段创建创建标识符。
        #define CAT(X,Y) X##Yint main(){ int class84 = 2021; printf("%d\n", CAT(class, 84)); }

        输出
        2021
        带副作用的宏参数
        #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); //输出的结果是什么?

        这里我们得知道预处理器处理之后的结果是什么:
        z = ( (x++) > (y++) ? (x++) : (y++));
        输出结果
        x=6 y=10 z=9
        宏和函数的对比
        对于上述的宏,也可以用函数实现其功能。
        使用宏的优点:
        1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
        2.函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之,这个宏可以用于整型、长整型、浮点数等等,宏是类型无关的。
        使用宏的缺点:
        1.每次调用宏,一份宏定义的代码插入程序中,除非宏比较短,否则可能会大幅度增加代码的长度。
        2.宏无法调试。在预编译(预处理)阶段,已经把 # define 给替换了,已经不再是宏了。
        3.宏由于类型无关,也就不够严谨。
        3.宏可能会带来运算符优先级的问题,更容易导致程序出错。
        inline 内联函数

        命名约定
        函数和宏语法相似,语言本身没法帮我们区分二者。把宏名全部大写,函数名不要全部大写。
        #undef 移除宏定义
        这条指令用于移除宏定义。
        如果现存的一个名字需要被重新定义,那么他的旧名字首先要被移除。
        #undef NAME


        命令行定义
        许多C的编译器提供了一种能力,允许在命令行中定义符号,用于在启动编译过程。例如:当我们根据一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。假设某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。

        条件编译
        C语言预处理预编译命令及宏定义详解
        文章图片

        #define DEBUG#ifdef DEBUG#endif


        常见的条件编译指令
        #if 常量表达式//...#endif

        举例子:为真参与编译,为假 (0)不参与编译。
        #if 1 printf("balabala...."); #endif

        二、多个分支的条件编译
        #if 常量表达式//...#elif 常量表达式//...#else//....#endif

        举例子
        #if 1==1#elif 2==1#else#endif

        三、判断是否被定义
        #if defined(symbol)#ifdef symbol#if !defined(symbol)#ifndef symbol

        四、嵌套指令
        #if defined(OS_UNIX) #ifdef OPTION1unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif#elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif#endif


        文件包含
        我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。
        头文件包含的方式:
        1.本地文件包含:#include "Filename"
        查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
        2.库文件包含:#include
        查找策略:查找头文件直接去标准路径下去查找,如果找不到就返回错误信息。
        这样是不是可以说,对于库文件也可以使用“”的形式包含?
        答案是肯定的,可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
        wwww想到自己经常重复包含,留下了悔恨的泪水~~
        出现嵌套文件包含解决方法 :条件编译
        每个头文件开头这样写:
        #ifndef __TEST__H__#define __TEST__H__ //头文件的内容#endif//__TEST__H__

        【C语言预处理预编译命令及宏定义详解】或者
        #pragma once

        就可以避免头文件的重复引入。
        总结一下:预处理阶段的预处理指令:条件编译指令 / #include / #define / #error /#pragma / ……

        offsetof(宏类型,成员名字)偏移量模拟实现
        C语言预处理预编译命令及宏定义详解
        文章图片

        #include #include #include struct S{ char c1; int a; char c2; }; #define OFFSETOF(struct_name, member_name) (int)&(((struct_name*)0)->member_name)int main(){ printf("%d\n", OFFSETOF(struct S, c1)); printf("%d\n", OFFSETOF(struct S, a)); printf("%d\n", OFFSETOF(struct S, c2)); return 0; }

        以上就是C语言预处理预编译命令及宏定义详解的详细内容,更多关于C语言预处理预编译命令及宏的资料请关注脚本之家其它相关文章!

          推荐阅读