详解C程序环境和预处理

本文章将详解C语言的翻译环境和执行环境,并且介绍C程序的运行过程,在此基础上,介绍#define/#if预处理指令的讲解和应用等等。

目录

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

1、分类简介

2、详解编译+链接

2.2编译器的编译过程(编译的几个步骤)

2.3运行环境

二、预处理详解

1、预定义符号

2、define定义标识符

3、define定义宏

3.1宏的介绍

3.2定义宏涉及的几个步骤

3.3解释一下#和##

3.4带副作用的宏参数

3.5宏和函数的区别

3.6#undef

4、条件编译

4.1条件编译的介绍

4.2条件编译的应用

5、宏的例题

5.1模拟实现ofsetoff

5.2写一个宏,可以将一个整数的二进制位的奇数位和偶数互换


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

1、分类简介

ANSI C 的任何一种实现中,存在两个不同的环境。
1 种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
2 种是执行环境,它用于实际执行代码。

2、详解编译+链接

2.1翻译环境

C代码运行后会经过预处理,编译,汇编,链接等过程形成运行程序,这也是C代码翻译的过程。对于不同的C代码,他们各自的源文件,在编译器(预处理,编译,汇编)作用下形成各自的目标文件。各个目标文件和链接库结合链接形成最终的C程序。

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

2.2编译器的编译过程(编译的几个步骤)

编译过程分为几个步骤,预处理,编译,汇编

预处理过程:预处理过程主要是1、头文件的包含(比如stdio.h库的包含)2、注释的删除(编译过程是不看注释的,预处理过程把注释删去)3、#define符号的替换。ps:这里define符号的替换是是整体替换,下文define讲解处会详细解释。

编译过程:编译的过程主要是把C语言代码转换成汇编代码。也包括语法分析,词法分析,语义分析,符号汇总等。

汇编过程:把汇编代码转换成机器可以读懂的二进制语言。并借用上一步的符号汇总形成符号表。下一步就是链接过程,这里也再介绍一下。

链接过程:合并段表。并把符号表合并和定位。

2.3运行环境

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

二、预处理详解

上面我们大致讲了程序如何从C代码到运行的过程,下面我们着重讲解与我们关系比较密切的预处理阶段。

1、预定义符号

常见的预定义符号:

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

这些都是c语言内置的。

下面我们实操一下:

在vs输入以下代码

 运行结果是

 发现可以显示此源文件所在的位置以及该行代码所在的行数,这些都是CC语言内置的预定义符号。

2、define定义标识符

语法:

#define name stuff

这个我们并不陌生,我们常常会在前几行预定义一些全局变量的值,比如

#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__ )

这里我们给出了大致的几种情况。注:请勿加分号(;),容易造成语法错误。

3、define定义宏

3.1宏的介绍

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏( macro 或定义
宏( define macro )。
声明方式:
#define name( parament-list ) stuff
//其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:
参数列表的左括号必须与 name 紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分
这里的替换就是把原来的替换到新名称下,不需要额外的加括号等!!!
举个例子, 如:
#define SQUARE(x) x*x
int main()
{
int a=SQUARE(5);
//这里就相当于a=5*5
//把宏完全替换掉就可以,不用加额外的括号
}

 易错点:

宏是整体带入的,请看以下代码:

#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

}

这里的结果可能有人会认为是a是36,其实大错特错!!!

整体代入后,a=5+1*5+1为11.并不需要人为的添加括号等行为!!!!

所以,我们在写宏的时候往往要添加括号,如果上述目标结果是36,那么我们需要将宏改成:

#define SQUARE(x) ((x)*(x))

防止引发歧义。

3.2定义宏涉及的几个步骤

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上 述处理过程。
注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。

3.3解释一下#和##

(1)#的作用
首先我们看一下以下代码:
char* p = "hello ""world\n";
printf("hello"," world\n");
printf("%s", p);
这里输出的是不是
hello world 
答案是确定的:是。
我们发现字符串是有自动连接的特点的。虽然是两个双引号内的内容,但自动的连接到了一起。
#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE);
int main()
{

PRINT("%d", 10);
}
结果是

这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
下面我们写一个更详细的版本:

int i = 10;
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
int main()
{
	PRINT("%d", i + 3);//产生了什么效果
}

结果是

 这里可以把i+3打印出来。

这也就展示了#的作用可以把一个宏参数变成对应的字符串

(2)##的作用

##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) sum##num += value;
int main()
{
	int sum5=0;
int a= ADD_TO_SUM(5, 10);//作用是:给sum5增加10
printf("%d\n", a);
}

结果是

sum##num在这里就是sum5的意思

 因此我们就理解了##的作用。再重申理解一次:

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

3.4带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
比如下面的例子:
#include<stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{


	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	return 0;
}

大家认为的结果是什么呢?6,9,9???

不不不,请看下面的运行结果 

这是因为x++先运行一遍,然后x=6,?前面的b执行了一次y++,y=9,然后x>y为假,执行b,也就是y++,y=10,但是z=y++是在y自增1之前也就是9.

绕来绕去,发现代码并不是我们平常认为的结果,所以得出以下结论:

x+1;//不带副作用
x++;//带有副作用,尽量使用x+1这样不会对x本身起作用的表达式!

3.5宏和函数的区别

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于 > 来比较的类型。
宏是类型无关的。
宏的缺点: 当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。 
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏做大的优点就是:
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现 类型 ,但是函数做不到
举个栗子:
#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

这里的MALLOC宏就可以包含int类型,然后开辟空间返回首地址给int*的指针。此外也可以是char等类型,这里的类型是函数没办法传的参数。

注:我们为宏起名字通常用 大写字母

3.6#undef

这条指令用于移除一个宏定义。
#define NAME 123
#undef NAME//它的名字就会被移除。

4、条件编译

4.1条件编译的介绍

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件 编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
举个例子:
#include <stdio.h>
#define __DEBUG__
int main()
{
 int i = 0;
 int arr[10] = {0};
 for(i=0; i<10; i++)
 {
 arr[i] = i;
 #ifdef __DEBUG__
 printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
 #endif //__DEBUG__
 }
 return 0;
}
常见的条件编译指令:
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

这里有人会问为什么不直接用if语句?

其实,用这种方法是想避免多余的代码参与编译,if语句会参与程序编译的,而在#if中如果不满足条件就不会参与编译和下面的步骤了!

4.2条件编译的应用

下面我们以文件包含为例

 上面的程序我们包含了多次stdio.h的库,那么每写一次就会编译一次吗?

答案是:是的,每写一次就会编译一次,大大浪费了时间空间。

当然,平时我们不会这样写,但是有些库会包含其他库,我们就可以用这样的一段条件编译的代码解决:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

这样就只保证只定义一次__TEST_H__头文件了

当然,随着时代进步,也有专门的头文件可以做到相同的效果:

#pragma once

在写头文件前写一个pragma once就会避免重复引入头文件了。

5、宏的例题

5.1模拟实现ofsetoff

#include<stddef.h>
struct Stu
{
	char name[10];
	int age;
	char add[20];
};


int main()
{
	printf("%d\n", offsetof(struct Stu, name)); 
	printf("%d\n", offsetof(struct Stu, age));
	printf("%d\n", offsetof(struct Stu, add));
	return 0;
}



ofsetoff是计算结构体偏移量的,下面我们用宏实现:

/模拟实现
#define OFFSETOF(TYPE,NAME)  (int)&(((TYPE*)0)->NAME) 
int main()
{
	printf("%d\n", OFFSETOF(struct Stu, name));
	printf("%d\n", OFFSETOF(struct Stu,age));
	printf("%d\n", OFFSETOF(struct Stu, add));
	return 0;
}

大致就是取一个地址为0的TYPE*类型的地址,然后指向结构体元素,然后强制类型转换成int类型的值就是该元素的偏移量。

5.2写一个宏,可以将一个整数的二进制位的奇数位和偶数互换

#include <stdio.h>
#define CHANGE(num) ((((num) & 0xaaaaaaaa) >> 1) | (((num)& 0x55555555) << 1))  

int main()
{
	int num = 0;
	scanf("%d", &num);
	printf("%d\n", CHANGE(num));
	return 0;
}

交换奇偶位,需要先分别拿出奇偶位。既然是宏,分别拿出用循环不是很现实,那就用&这些位的方式来做。奇数位拿出,那就是要&上010101010101……,偶数位拿出,就是要&上101010101010……,对应十六进制分别是555……和aaa……,一般我们默认是32位整数,4位对应一位16进制就是8个5,8个a。通过& 0x55555555的方式拿出奇数位和& 0xaaaaaaa的方式拿出偶数位。奇数位左移一位就到了偶数位上,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。

文尾,头文件是多种多样的,比如还有

#error #pragma #line #pragma pack()等等,更多地学习还得看大家自己,共同加油吧!

猜你喜欢

转载自blog.csdn.net/m0_67821824/article/details/127350746
今日推荐