• C语言程序环境和预处理



    1.程序的翻译环境和执行环境

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

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

    2.详解编译 + 链接

    2.1翻译环境

    在这里插入图片描述

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

    为什么需要多个源文件?
    对于目前没有接触过项目的程序员来说,你完全可以将所有的代码都塞在一个源文件中,这是因为一个人需要写的代码量非常的少。
    但你可以设想一下,现在你需要和一个团队进行配合来完成一个大项目,每个人都有不同的分工,但现在只有一个源文件,这会出现什么样的局面?
    只有一个源文件意味着什么?这意味着你的团队只能有一人写代码,而其他人只能干看着;如果有多个源文件,那每个人都能在不同的源文件上写代码来完成协同工作。

    2.2编译的三个阶段

    先看代码:
    test.c

    #include
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int c = Add(a, b);
    	printf("%d\n", c);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    add.c

    #include
    
    int Add(int x, int y)
    {
    	return x + y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    编译链接的画图解释:
    在这里插入图片描述

    2.3运行环境

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

    3.预处理详解

    3.1预定义符号

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

    在这里插入图片描述

    3.2 #define

    3.2.1 #define定义标识符

    例如:

    #define MAX 1000
    #define STR "hello bit"
    
    int main()
    {
    	int a = MAX;
    	printf("%d\n",MAX);
    	printf("%s\n",STR);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:#define定义的标识符将会在预处理阶段进行替换:

    int main()
    {
    	int a = 1000;
    	printf("%d\n",1000);
    	printf("%s\n","hello bit");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,我们也可以在VS下看到这一现象,下面是操作步骤:

    1.右键点击
    在这里插入图片描述
    2.右键点击属性
    在这里插入图片描述
    3.将预处理到文件修改为 是
    在这里插入图片描述
    4.执行程序:此时会报错,但不用理会
    5.找到该程序的Debug文件夹,并打开
    在这里插入图片描述
    6.用VS打开test.i文件
    在这里插入图片描述
    7.将文件拉到末尾查看
    在这里插入图片描述

    警告:
    在define定义标识符时,末尾不要添加;否则可能出现错误
    错误演示:

    #define RET 1000;
    int main()
    {
    	if (RET)
    	{
    		;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    3.2.2 #define 定义宏

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

    #define name(parament-list) stuff
    
    • 1

    例如:

    #define SQUARE(X) X*X
    
    int main()
    {
    	int r = SQUARE(5);
    	printf("%d\n",r);
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    注意:
    参数列表的左括号必须与name紧邻
    如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

    警告:
    这个宏存在着一个问题:宏是完成替换的!
    下面的代码将会与我们想要的结果相差甚远

    #define SQUARE(X) X*X
    
    int main()
    {
    	int r = SQUARE(5+1);
    	printf("%d\n", r);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    上面的代码在预处理阶段将会替换为以下代码

    int main()
    {
    	int r = 5 + 1 * 5 + 1;
    	printf("%d\n", r);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样就比较清晰了,由宏替换产生的表达式并没有按照预想的次序进行求值。
    那么,在宏定义上加上两个括号,这个问题就能解决了:

    #define SQUARE(X) (X)*(X)
    
    • 1

    在这里插入图片描述
    但这样仍然会存在问题,如下代码:

    #define DOUBLE(X) (X) + (X)
    
    int main()
    {
    	int r = 10 * DOUBLE(3);
    	printf("%d\n", r);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码在进行宏替换时会变成:

    int main()
    {
    	int r = 10 * 3 + 3;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但我们实际想要的结果是 : 10*(3+3)
    要想解决这个问题,同样的,也需要再加上一个括号:

    #define DOUBLE(X) ((X) + (X))
    
    • 1

    在这里插入图片描述
    提示:

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

    3.2.3 #define 替换规则

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

    例如:

    #define MAX 100
    #define DOUBLE(X) ((X) + (X))
    
    DOUBLE(MAX + 2);
    //1.替换MAX -> DOUBLE(100 + 2)
    //2.替换宏  -> ((100+2) + (100+2))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    例如:

    #define MAX 100
    int main()
    {
    	char* ch = "MAX";
    	//MAX为常量字符串,不能被宏替换
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.2.4 #和##

    现在先来想想一个问题:如何把参数插入到字符串中?
    然后再来看看这样的代码:

    char* p = "hello""world\n";
    printf("hello""world\n");
    printf("%s\n",p);
    
    • 1
    • 2
    • 3

    这里输出的是不是:hello bit?
    答案是确定的:是!
    在这里插入图片描述
    那么再来想想下面的代码该如何改进?

    int main()
    {
    	int a = 10;
    	printf("the value of a is %d\n",a);
    
    	int b = 20;
    	printf("the value of b is %d\n", b);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们可以发现这段代码在打印时只有一个字符的不同(a和b),这时就可以引入 #来进行宏定义
    # 就是把一个宏参数转换为字符串。

    #define PRINT(N) printf("the value of "#N " is %d\n",N)
    
    int main()
    {
    	int a = 10;
    	PRINT(a);
    
    	int b = 20;
    	PRINT(b);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    在预处理阶段宏替换:

    int main()
    {
    	int a = 10;
    	printf("the value of " "a" " is %d\n",a);
    
    	int b = 20;
    	printf("the value of " "b" " is %d\n",b);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再如:打印不同类型的变量

    #define PRINT(N,FORMAT) printf("the value of " #N " is " FORMAT,N)
    
    int main()
    {
    	int a = 10;
    	PRINT(a,"%d\n");
    
    	float b = 3.14f;
    	PRINT(b, "%f\n");
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    ## 的作用

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

    代码演示:

    #define ADD_TO_SUM(num,value) sum##num += value
    //sum##5 -> sum5
    
    int main()
    {
    	int sum5 = 10;
    	ADD_TO_SUM(5, 10);//给sum5加上10
    	printf("%d\n", sum5);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.2.5 带副作用的宏参数

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

    x+1;//不带副作用
    x++;//带副作用
    
    • 1
    • 2
    #define MAX(a,b) ((a)>(b)?(a):(b))
    
    int main()
    {
    	int a = 5;
    	int b = 4;
    	int m = MAX(a++, b++);
    
    	printf("%d", m);
    	printf("a=%d b=%d\n", a, b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果上面的宏换成函数,那么我们希望得到的结果会与实际结果相同;但是,宏的参数是完全替换的!替换后的代码如下:

    int main()
    {
    	int a = 5;
    	int b = 4;
    	int m = ((a++) > (b++) ? (a++) : (b++));
    	
    	printf("%d", m);
    	printf("a=%d b=%d\n", a, b);
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.2.6 宏和函数对比

    宏通常被应用于执行简单的运算
    比如在两个数中找出较大的一个

    #define MAX(a,b) ((a)>(b)?(a):(b))
    
    • 1

    为什么不用函数来完成这个任务呢?

    原因有二:
    1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以宏比函数在程序的规模和速度方面更胜一筹。
    2.更为重要的是函数的参数必须声明为特定的类型。
    所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整型、长整型、浮点型等可以用于比较的类型。宏是类型无关的

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

    宏有时候可以做到函数做不到的事情,比如:宏的参数可以出现类型,但是函数做不到

    #define MALLOC(num,type) (type*)malloc(num * sizeof(type))
    //使用
    MALLOC(10,int);//类型做参数
    
    //预处理替换之后:
    (int*)malloc(10 * sizeof(int));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.2.7命名约定

    一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。
    那我们平时的一个习惯是:
    把宏名全部大写
    函数名不要全部大写

    3.3 #undef

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

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

    代码演示:

    #define M 100
    
    int main()
    {
    	printf("%d\n", M);
    #undef M
    	printf("%d\n", M);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    3.4 条件编译

    在编译一个程序的时候我们如果要将一条(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
    比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

    #include
    #define __DEBUG__
    int main()
    {
    	int i = 0;
    	int arr[10] = { 0 };
    	for (i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    	}
    #ifdef __DEBUG__ // 如果出现 __DEBUG__标识符则下列代码执行
    	printf("%d\n", arr[i]);
    #endif __DEBUG__ // #ifdef 和 #endif 必须成对出现
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    常见的条件编译指令有:

    1.
    #if 常量表达式   //常量表达式为真,执行下述代码,否则不执行直到endif
     	//....
    #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
    • 40

    3.5文件包含

    我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于
    #include指令的地方一样。
    这种替换方式很简单:
    预处理器先删除这条指令,并用包含文件的内容替换。
    这样一个源文件包含10次,那就实际被编译10次

    3.5.1头文件被包含的方式

    本地文件包含

    #include "filename"
    
    • 1

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

    VS环境的标准头文件路径:

    C:\program Files (x86)\Microsoft visual Studio 12.0\vc\include
    
    • 1

    注意按照自己的安装路径去找

    库文件包含

    #include
    
    • 1

    查找头文件直接去标准路径下去查找,如果找不到就编译错误。
    这样是不是可以说,对于库文件也可以使用" " 的形式包含?
    答案是肯定的:可以!
    但是这样查找效率就低了点,当然这样也不容易区分是库文件还是本地文件了。

    3.5.2嵌套文件包含

    如果出现这样的场景:
    在这里插入图片描述

    comm.h和comm.c是公共模块。
    test1.h和test.c使用了公共模块。
    test2.h和test2.c使用了公共模块。
    test.h和test.c使用了test1模块和test2模块。
    这样最终程序中就会出现两份comm.h的内容,这样就造成了文件内容的重复。

    利用条件编译就可以解决这个问题:

    #ifndef __TEST_H__   //如果__TEST_H__被定义就不进行头文件包含
    #define __TEST_H__   //只有第一次包含头文件才被定义
    //头文件内容
    #endif 
    
    • 1
    • 2
    • 3
    • 4

    或者:

    #pragma once
    
    • 1

    4.模拟实现offsetof

    注意:offsetof是一个宏

    思路:
    结构中总是将第一个成员元素放在偏移量为0的地址处,其余成员则按照内存对齐规则存放。
    那么每个成员的偏移量其实就是这个成员的起始地址减去第一个成员的地址的差。
    我们便可以假设第一个成员的地址放在0地址处,然后依次存放其他成员;
    这样取出你所要求偏移量的成员的地址(减去地址为0的第一个元素的地址),便是它的偏移量

    #define OFFSETOF(type,name) (size_t)&(((type*)0)->name)
    
    struct S
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    int main()
    {
    	printf("%zu\n", OFFSETOF(struct S, c1));
    	printf("%zu\n", OFFSETOF(struct S, i));
    	printf("%zu\n", OFFSETOF(struct S, c2));
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

  • 相关阅读:
    实在智能受邀参加第14届珠中江数字化应用大会,AI赋能智能制造,共话“湾区经验”
    【Pygame实战】你说神奇不神奇?吃豆人+切水果结合出一款你没玩过的新游戏!(附源码)
    读取resources 目录资源文件的方法
    深入理解JS作用域链与执行上下文
    Talk预告 | 中科院自动化研究所尼博琳:拓展语言-图像模型用于视频识别
    STM32CubeMX:串口DMA
    day-58 代码随想录算法训练营(19)单调栈 part 01
    【Linux】WSL2与VMware切换使用
    LeetCode(36)旋转图像【矩阵】【中等】
    Linux返回上级目录命令
  • 原文地址:https://blog.csdn.net/m0_64240499/article/details/125874041