一、程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码
1.1翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人
的程序库,将其需要的函数也链接到程序中。
1.2 编译期间的步骤
预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2. 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。
3. 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
预处理阶段:
头文件的包含,去注释,宏替换
编译阶段:
语法分析,词法分析,语义分析,符号汇总
汇编阶段:
把汇编指令转化为二进制指令,生成符号表
链接阶段:
合并段表,符号表的合并和重定位
1.3 运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
#include <stdio.h>
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", i);
}
return 0;
}
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
二、预处理符号详解
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
__FILE__:显示文件的所在绝对路径,前后是两个_并不是一个。
__LINE__:显示这一行代码所在的行数
__DATE__:显示现在的年月日
__TIME__:显示现在的时间
__STDC__:验证本机是否遵循C语言标准,如果遵循则输出1
#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义
宏(define macro)
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
提示:
用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中
的操作符或邻近操作符之间不可预料的相互作用
#define 替换规则:
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
两个特殊的符号#和##
是把一个宏参数变成对应的字符串
是起到连接作用 我们来看代码:
int main()
{
int a = 10;
printf("this value of a is %d\n", a);
int b = 20;
printf("this value of b is %d\n", b);
float c = 90.5f;
printf("this value of b is %f\n", c);
return 0;
}
当我们有大量如上的代码要打印,而每次重新printf函数打印又觉得太麻烦,那么如果写一个函数只需要每次传值和类型就能打印呢,答案是只有宏能做到,如下图:
#define PRINT(n,format) printf("this value of "#n" is "format"\n",n)
int main()
{
int a = 10;
PRINT(a, "%d");
int b = 20;
PRINT(b, "%d");
float c = 90.5f;
PRINT(c, "%f");
return 0;
}
这就用到了#的作用
以下是##的使用:
#define AND(a,b) a##b
int main()
{
int cod666 = 100;
printf("%d\n", AND(cod, 666));
return 0;
}
如图所示##能将两个符号连接起来并且使用。
宏的副作用
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 10;
int b = 20;
int m = MAX(++a, ++b);
printf("m=%d a=%d b=%d\n", m, a, b);
return 0;
}
像这样的运算在宏替换后就变成int m = ((++a) > (++b) ? (++a) : (++b));也就是每进行一次运算变量都会自增两次。
宏和函数对比
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。那为什么不用函数来完成这个任务?
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以
用于>来比较的类型。宏是类型无关的
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是把宏名全大写,函数名不要全大写。
条件编译
条件编译是在预处理的时候就已经将要编译的代码留下,不要的代码删除。
int main()
{
#if 1>2
printf("1\n");
#elif 2>5
printf("2\n");
#else
printf("WU\n");
#endif
return 0;
}
与if语句的区别就在于#if在预处理的时候就已经将要编译的代码留下,不要的代码删除。而if仅仅是不执行那个不满足条件的语句。
#define MAX 10
int main()
{
#ifdef MAX
printf("1\n");
#endif
return 0;
}
int main()
{
#ifndef MAX
printf("1\n");
#endif
return 0;
}
如果定义了MAX就打印1
第二段代码是如果没定义MAX就打印1
#define MAX 10
#undef MAX
int main()
{
#ifdef MAX
printf("1\n");
#endif
return 0;
}
#undef是取消一个定义,取消后原来定义的就不能在使用。
防止头文件重复包含的两种方法:
#pragma once
在程序一开始就输入#pragma once
第二种方法:
#ifndef TEST_H
#define TEST_H
//头文件的内容
#endif
用条件编译的方式,如果没有定义就定义一个。
两个用宏写出的习题:
写一个宏,计算结构体中某变量相对于首地址的偏移
先上代码然后我们一步一步讲解:
//offsetof宏需要两个参数,第一个是要计算什么类型的偏移量,第二个是其指向的成员变量
//假设第一个成员变量是放在0地址处,将0强制转换为要计算的类型,然后让这个类型指向其成员变量
//然后取地址得到这个成员变量的地址,然后将地址强制转化为int类型
//#define OFFSETOF(type_s,name_t) (int)&(((type_s*)0)->name_t)
//struct s
//{
// char name[20];
// int n;
// short t;
//};
//int main()
//{
// struct s a = { 0 };
// OFFSETOF(struct s, n);
// printf("%d\n", OFFSETOF(struct s,t));
// return 0;
//}
首先我们不能保证我们创建的结构体是在哪个地址放着,其次计算偏移量实际上是结构体成员所在的地址减去首地址的大小,如果我们直接将手地址看成0,那么计算偏移量只需要让其他成员的地址减去0就可以,所以我先将0强制转化为要计算的指针类型,然后由这个结构体指针指向其要计算偏移量的成员,然后取地址拿到这个成员的地址最后因为打印的是整形我们将其整个强制转化为整形,在这里不减去首地址的原因是首地址为0减和不减都一样。
第二道题:
写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。
#define SWAP(n) n=((n&0xaaaaaaaa)>>1)+((n&0x55555555)<<1)
首先我们取得整数的奇数位,从右边开始第一个为奇数位,第二个为偶数位
n=11的时候11的二进制为 00000000 00000000 00000000 00001011
得到这个二进制的奇数位只需要让这个数&在奇数上是1的数
00000000 00000000 00000000 00001011
01010101 01010101 01010101 01010101
&后: 00000000 00000000 00000000 00000001
然后我们将得到的奇数位向左移动一位让其到偶数位上
00000000 00000000 00000000 00000010
同理让偶数位上的数&在偶数上是1的数
00000000 00000000 00000000 00001011
10101010 10101010 10101010 10101010
&后: 00000000 00000000 00000000 00001010
然后我们将得到的偶数位向右移动一位让其到奇数位上
00000000 00000000 00000000 00000101
将移好的位相加:
00000000 00000000 00000000 00000010
00000000 00000000 00000000 00000101
00000000 00000000 00000000 000001111
int main()
{
int a = 10;
printf("%d\n", SWAP(a));
return 0;
}
交换奇偶位,需要先分别拿出奇偶位。既然是宏,分别拿出用循环不是很现实,那就用&这些位的方式来做。奇数位拿出,那就是要&上010101010101……,偶数位拿出,就是要&101010101010……,对应十六进制分别是555……和aaa……,一般我们默认是32位整数,4位对应一位16进制就是8个5,8个a。通过& 0x55555555的方式拿出奇数位和& 0xaaaaaaa的方式拿出偶数位。奇数位左移一位就到了偶数位上,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。这个宏只能完成32位以内的整形,要想完成64位的,那就将5和a的数量翻倍即可。
总结
本节的重点就是头文件的去重以及用宏实现相应的功能,在使用宏的时候一定要注意宏的副作用,并且使用宏一定要注意多加括号,这样才能保证算出的数据是正确的,只有合理的使用宏才能发挥出宏的优势。