目录
前言👨💻
本篇博客介绍主要介绍预处理环节下需要处理的内容,包括符号常量的替换,宏的替换以及条件编译等,最后列出offsetof和交换奇数偶位的宏实现!
1.程序的翻译环境与执行环境👨💻
翻译环境:在这个环境下源代码被翻译成可执行的机器指令
执行环境:用于执行代码
在编辑后得到一个源程序文件f.c,经过编译得到目标程序文件.obj,再将所有的目标模块输入计算机,与系统提供的库函数等进行连接,得到可执行程序f.exe
2.编译与链接👨💻
2.1编译🧗♂️
编译中又可以分成三个阶段;
设存在test.c这样一个文件
一、预编译(预处理)–>生存test.i文件
- 此阶段会将头文件的内容读进去,如将stdio.h中内容读进去与其他程序组成一个完整的源程序
- define定义的标示符常量会在此阶段进行替换
- 会将注释进行删除
二、编译–>生成test.s文件
将C语言代码转换成汇编语言;
并进行语法分析、词法分析、语义分析以及符号汇总
三、汇编–>生成test.o文件
此阶段会将汇编语言转换为二进制码,即机器指令
将编写汇总的符号会整合形成符号表
2.2链接🧗♂️
此阶段会合并段表并进行符号表的合并与重定位
最终我会将test.o与add.o以及链接库整合到一起形成可执行文件即.exe文件
3.预处理详解👨💻
3.1预定义符号🧗♂️
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //是否遵循ANSI C
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* pf = fopen("test1.txt", "w");
if (pf == NULL)
{
perror(pf);
return EXIT_FAILURE;//返回1
}
for (int i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
return 0;
}
此时在该项目文件中就生成了一个test的文件,里面存储的就是相关信息。
3.2define🧗♂️
3.2.1#define定义标识符💨
语法:
#define name Stuff
对于#define定义的表示符常量,在前面我提到过在预处理阶段就会直接将标识符所代表的常量替换成其所指的常数值。
需要注意的是#define不需要加上";"(分号)。
3.2.2#define定义的宏💨
语法:
#define name(parament->list) stuff
其中parament->list是一个由逗号隔开的符号表,他们可能出现在stuff中;
需要注意的是:
参数列表的左括号必须与name紧邻,否则如果存在空白的话,参数列表就会被解释为stuff的一部分!
例如:
#include<stdio.h>
#define source(x) x*x
#define double(x) ((x)+(x))
int main()
{
printf("%d", source(5));
//输出25
printf("%d", source(5 + 1));
//输出5+1*5+1=11,因此宏进行运算时,需要注意优先级
printf("%d\n", double(3*2));
//输出12
printf("%d\n", 10 * double(3 * 2));
//输出120
return 0;
}
因此在使用宏定义的时候需要注意优先级以及各类括号的使用。
3.2.3#define替换规则💨
-
在调用宏的过程中,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,则它们首先被替换;
-
宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不可以出先递归;
-
当预处理搜索#define定义的符号时,字符串常量的内容并不被搜索。
3.2.4#和##💨
1️⃣#
void print(int n)
{
printf("The value n is %d", n);
}
int main()
{
int a = 10;
print(a);
return 0;
}
上述代码它并不能将打印语句中“The value n is %d”中的n变换成a,而宏却可以解决这个问题
#define print(N) printf("The value " #N " is %d", N);
int main()
{
int a = 10;
print(a);
return 0;
}
这种做法就是将参数所对应的字符串通过 #N放到字符串当中
而面对不容类型的数据我们又该如何进行处理呢?
#define print(N,FORMAT) printf("The value of" #N " is " FORMAT,N);
int main()
{
int a = 10;
print(a,"%d\n");
float b = 11.333;
print(b,"%f\n");
}
此时,向宏中传递两个参数,第二个就是其数据对应的格式化字符,我们对其传递参数,因为其本身就是字符串,而多个字符串是可以进行连接的,所以用这种办法就可以输出处理不同类型的数据了!
2️⃣##
#define Add_Data(name,value) name##value
int main()
{
int class106 = 100;
printf("%d", Add_Data(class, 106));
return 0;
}
##可以将位于它两边的符号合并成一个符号
3.2.5带副作用的宏参数💨
当宏参数在宏定义中出现超过一次的时候如果参数带有副作用,那么此时这个宏就存在危险,导致不可预测的后果,副作用就是表达式求值的时候出现永久性的效果。
#include<stdio.h>
#define MAX(a,b) ((a)>(b))?(a):(b)
int main()
{
int a = 5;
int b = 8;
int z = MAX(a++, b++);
//(a++)>(b++)?(a++):(b++)
//5<8 -->a=6,b=9
//此时z=9,而b还需要进行++,所以b=10
printf("a=%d b=%d z=%d", a, b, z);
return 0;
}
//此时输出a,6,b=10,z=9
这是因为存在++,从而改变了a、b的值,造成永久性效果,所以会产生以上效果。
3.2.6函数与宏的对比💨
宏的优点:
- 在规模和速度是比函数更胜一筹的;
- 宏与类型无关,可适用与各种类型。
宏的缺点:
- 每次使用宏时,一份宏定义的代码就会插入到程序当中,除非宏很短,否则会造成程序冗长;
- 宏不能进行调试且不能递归,不能调试是因为在预处理阶段已经将宏替换,所以它是不能进行调试的;
- 宏会带来优先级的问题,很容易导致错误;
- 与类型无关,因此不够严谨。
但是呢,宏可以完成函数不能完成的一个任务,就是传递类型
#include<stdio.h>
#define MALLOC(num,type) (type*)malloc((num)*sizeof(type))
int main()
{
int* p = MALLOC(10, int);
return 0;
}
3.2.7#undef与命名约定💨
- #undef用来移除宏定义,如果一个名字需要重新进行宏定义,那么首先需要对其进行移除;
- 一般,宏命名以全大写字母命名,函数大小写混合命名。
3.3条件编译🧗♂️
1️⃣常量表达式
#if 常量表达式
//....
#endif
2️⃣多分支的条件编译
#if 常量表达式
//....
#elif 常量表达式
//...
#else
//....
#endif
3️⃣判断是否被定义
#ifdef symbol <----> #if defined (symbol)
#endif
//
#ifndef symbol <----> #if !defined (symbol)
#endif
4️⃣嵌套指令
#if defined (os_unix)
#ifdef option1
test1();
#endif
#ifdef option2
test2();
#endif
#elif defined (ox_msdos)
#ifdef option2
test3();
#endif
下面给出具体例子:
利用常量表达式进行条件编译
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
#if 1
printf("hehe\n");
#elif 4==5
printf("haha\n");
#else
printf("heihei\n");
#endif
return 0;
}
ifdef的使用
#define MAX 100
int main()
{
#ifdef MAX
printf("max\n");
#elif defined (MIN)
printf("min\n");
#else
printf("mid\n");
#endif
}
3.4文件包含🧗♂️
我们知道,#include可以使另外一个文件被编译,也就是说是编译程序将另一源文件嵌入到带有#include的源文件当中;
- #include"filename"是先去源文件所在的目录下查找,如果查找不到再去库目录中查找
- #include<filename>,则是直接去库目录中查找(标准路径下查找)
- 当然一般库文件用<>包含,本地文件一般使用""包含
在了解过条件编译与预处理后,我们知道一般在预处理时,系统会将头文件所包含的内容嵌入到使用该头文件的的源程序当中,而包含次数越多,嵌入的代码也就越多,为了防止这种情况的产生,因此我们可以利用条件编译和#pargma指令。
#if !defined TEST
#define TEST
//头文件内容
#endif
3.5offsetof的宏实现🧗♂️
offsstof是用来计算结构体成员变量的偏移量的一个函数。
在自定义类型的博客中,我提到过内存对齐是与偏移量有关的,所以接下来我就利用宏实现该函数。
#define OFFSETOF(type,m_name) (size_t)(&(((type*)0)->m_name))
struct S
{
char c1;
int i;
char c2;
};
int main()
{
struct S s = { 0 };
printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, i));
printf("%d\n", OFFSETOF(struct S, c2));
return 0;
}
分析:这个结构体在之前博客中有提到过偏移量的计算以及存储的总字节大小,在此我就不再进行赘述了。将0强制类型转换成type类型的指针,此时0也就是个地址(指针),也就是上述代码中结构体的地址,也就可以指向结构体的成员;指向成员后对其取地址,因为偏移量是地址的差值,而起始地址是0x000000,所以其他成员的地址也就是其偏移量的数值,因此对其地址进行强制类型转换成无符号整型从而可以得出偏移量的大小!
3.6利用宏实现交换奇偶位🧗♂️
交换奇偶位也就是交换这个数字的二进制的奇偶位,我们不妨先假设存在一个数其二进制是01001101 10100100 01011010 01100100
#define SWAP_BIT(N) ((N&0x55555555)<<1)+((N&0xAAAAAAAA)>>1)
int main()
{
int n;
scanf("%d", &n);
printf("%d", SWAP_BIT(n));
return 0;
}
Ending👨💻
本篇博客就结束了,下次见🙋♂️