本文章将详解C语言的翻译环境和执行环境,并且介绍C程序的运行过程,在此基础上,介绍#define/#if等预处理指令和宏的讲解和应用等等。
目录
一、程序的翻译环境和执行环境
1、分类简介
2、详解编译+链接
2.1翻译环境
C代码运行后会经过预处理,编译,汇编,链接等过程形成运行程序,这也是C代码翻译的过程。对于不同的C代码,他们各自的源文件,在编译器(预处理,编译,汇编)作用下形成各自的目标文件。各个目标文件和链接库结合链接形成最终的C程序。
2.2编译器的编译过程(编译的几个步骤)
编译过程分为几个步骤,预处理,编译,汇编。
预处理过程:预处理过程主要是1、头文件的包含(比如stdio.h库的包含)2、注释的删除(编译过程是不看注释的,预处理过程把注释删去)3、#define符号的替换。ps:这里define符号的替换是是整体替换,下文define讲解处会详细解释。
编译过程:编译的过程主要是把C语言代码转换成汇编代码。也包括语法分析,词法分析,语义分析,符号汇总等。
汇编过程:把汇编代码转换成机器可以读懂的二进制语言。并借用上一步的符号汇总形成符号表。下一步就是链接过程,这里也再介绍一下。
链接过程:合并段表。并把符号表合并和定位。
2.3运行环境
二、预处理详解
上面我们大致讲了程序如何从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 name( parament-list ) stuff
//其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在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定义宏涉及的几个步骤
3.3解释一下#和##
char* p = "hello ""world\n";
printf("hello"," world\n");
printf("%s", p);
#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.
绕来绕去,发现代码并不是我们平常认为的结果,所以得出以下结论:
3.5宏和函数的区别
#define MAX(a, b) ((a)>(b)?(a):(b))
#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的方式拿出偶数位。奇数位左移一位就到了偶数位上,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。
文尾,头文件是多种多样的,比如还有