# C 语言程序编译(预处理)
# 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
- 翻译环境,源代码被转换为可执行的机器指令:编译器
- 执行环境,用于实际执行代码:操作系统
# 翻译环境
翻译是由 编译+链接 两个大的过程组成,而 编译 又可以分解为:预处理、编译、汇编三个过程。
编译+链接:
![image-20230829162933330](/Users/mac/Library/Application Support/typora-user-images/image-20230829162933330.png)
编译链接阶段的过程:
![image-20230829164000129](/Users/mac/Library/Application Support/typora-user-images/image-20230829164000129.png)
# 运行环境
程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
程序的执行便开始。接着便调用main函数。
开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
终止程序。正常终止main函数;也有可能是意外终止。
# 预处理详解
# 预定义符号
C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。
// __FILE__ //进⾏编译的源⽂件
// __LINE__ //⽂件当前的⾏号
// __DATE__ //⽂件被编译的⽇期
// __TIME__ //⽂件被编译的时间
// __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
printf("file:%s line:%d\n", __FILE__, __LINE__);
# #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__ )
# 定义宏
基本语法
#define name( parament-list ) stuff
举个例子
#define SQUARE( x ) x * x // bad
#define SQUARE(x) (x) * (x) // good
// why? try
printf ("%d\n",(a + 1) * (a + 1) );
#define DOUBLE(x) (x) + (x) // bad
#define DOUBLE( x) ( ( x ) + ( x ) ) // good
// why? try
printf("%d\n" ,10 * DOUBLE(a))
# 宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。
- 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先 被替换。
- 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
# #和##
# #运算符
#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化“。
举个例子:
#define PRINT(n, format) printf("the value of "#n" is "format"\n", n);
int a = 10;
double pai = 3.14;
PRINT(a, "%d"); // the value of a is 10
PRINT(pai, "%lf"); // the value of pai is 3.14
注意:printf("the value of ""a" " is %d", a); 可以这样打印多段字符串在一起。
# ##运算符
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称 为记号粘合。
举个例子:
#define CAT(name, num) name##num
int class105 = 105
printf("%d\n", CAT(class, 105)); // 105
注意:这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
# 带副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可 能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。
举个例子:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);/
# 宏和函数对比
宏通常被应⽤于执⾏简单的运算。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。
1 #define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不⽤函数来完成这个任务?
原因有⼆:
- ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之 这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型⽆关的。
和函数相⽐宏的劣势:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序 的⻓度。
- 宏是没法调试的。
- 宏由于类型⽆关,也就不够严谨。 4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
...
//使⽤
int *p = MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int )malloc(10 sizeof(int));
# #undef 这条指令⽤于移除⼀个宏定义。
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
举个例子:
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
...
//使⽤
int *p = MALLOC(10, int);//类型作为参数
#undef MALLOC
MALLOC(10, int) // 编译出错
# 命令⾏定义
许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
gcc -D ARRAY_SIZE=10 programe.c
..
int array [ARRAY_SIZE];
...
# 条件编译
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。
举个例子:
...
#define NUM 1
// #if 常量表达式
#if NUM == 1
printf("hehe");
#elif NUM == 2
printf("haha");
#else
printf("heihei");
#endif
// #if defined(常量表达式) 和 #ifdef 常量表达式
// #define MAX 1
#if defined(MAX)
// #if !defined(MAX)
printf("%d", MAX);
#endif
#ifdef MAX
// #ifndef MAX
printf("%d", MAX);
#endif
// 嵌套指令
#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
...
# 头⽂件的包含
// 本地文件包含
#include "filename"
// 库文件包含
#include <filename.h>
本地文件包含查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。如果找不到就提⽰编译错误。
库文件包含查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。 这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含? 答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件 了。
# 嵌套文件包含
#include 指令可以使另外⼀个⽂件被编译。如果⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
如何解决头⽂件被重复引⼊的问题?答案:条件编译。
每个头⽂件的开头写:
test.h
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif
或者:
test.h
#pragma once
就可以避免头⽂件的重复引⼊。