前言
C语言终于复习完了,这一个多月的学习,使用C语言编程的能力明显得到了提升。最后一篇是关于C编译运行过程,它是C语言最难理解的了,但不用学的太深,接下来让我们一起来学习它吧。一、程序的翻译环境和执行环境
C语言代码是如何生成我们想要的代码呢?
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码。
C语言开发过程大致分为如图所示几个步骤
虽然整体过程比较复杂,但是我们关心的主要就是预处理这个步骤。
各步骤对程序的处理过程:
预处理:
- 展开包含的头文件,即#include后包含的头文件,全部复制到当前程序展开。
- #define定义的符号替换
- 删除所有注释
编译:
- 将C语言代码转换成汇编代码,还有一些更复杂的操作。
汇编:
- 主要将汇编代码转换成二进制指令(机器指令)
链接:
- 将所有源文件经前面几个步骤形成的文件链接在一起,多文件工程时体现这一点。
运行:
- 生成可执行文件
二、预处理
预定义符号介绍
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
预处理指令 #define
#define 定义标识符常量
语法:
#define <标识符> <字符串>
在编译预处理时,源程序中的该标识符均以指定的字符串来代替。
需要提醒的是:宏定义在处理时仅仅做符号替换,而不做任何类型或语法检测,仅仅时一个符合而不是任何类型。
#define MAX 100
//MAX - 标识符
//100 - 字符串
int main()
{
printf("%d\n", MAX);
return 0;
}
#define 定义带参宏
语法
#define <标识符> (<参数列表>) <字符串>
#define ADD(a,b) a-b
int main()
{
int ret = ADD(4, 3);
printf("%d\n", ret);
return 0;
}
但有时替换的到的效果并不是我们想要的
比如:
#define SQUARE(x) x*x
int main()
{
int ret = SQUARE(5+5);//实际上换成了5+5*5+5=35
printf("%d\n", ret);
return 0;
}
所有很多时候要注意使用,也可以加上括号来分隔
#define SQUARE(x) (x)*(x)
但仍不能完全解决这类问题,我们需要在定义的时候将使用场景考虑在内,需要记住宏定义仅仅只是符号替换,不做运算处理。
我们可以来讨论typedef和#define的区别
#define ptr_t int*
typedef int* ptr_t2;
int main()
{
ptr_t p1, p2;
//预处理后替换为int* p1,p2
int *p1, p2;//p1是指针,p2是整型
ptr_t2 p3, p4;//p3和p4都是指针类型 int* p1和int* p2
return 0;
}
总结用法:
在程序中扩展#define定义符号和带参宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
宏和函数的对比
宏和函数对比
宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个。
#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));
这里给出一张大佬总结的宏和函数的区别的图片
命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:
把宏名全部大写 函数名不要全部大写
#和##的介绍
我们先来看一段代码
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);
...
PRINT("%d", 10);//替换为:printf("the value is "%d"\n", 10);
//打印:the value is 10
另外一个技巧是: 使用 # ,把一个宏参数变成对应的字符串。 比如:
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i+3);//替换为:printf("the value of i+3 is "%d"\n", 10);
//打印:the value of i+3 is 13
//使用#号将宏参数变成了字符串i+3
return 0;
}
那##有什么作用呢?
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define ADD(num, value) num##value
int main()
{
int inum = 10;
int i = 10;
printf("%d\n", ADD(i, num));//替换成inum 替换后的字符需要是已定义的,否则无法替换
printf("%d\n", ADD(1, 2));//替换成常量12
return 0;
}
注: 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。所有一般也比较难以使用
#undef
这条指令用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
条件编译
满足条件代码就参与编译,不满足条件,代码就不参与编译,类似于条件语句
- #if 常量表达式
如果常量表达式为真就继续
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
//实例
#define DEBUG 1 //用于调试
//非调试时值可改为0
int main()
{
#if 0
printf("1:%d\n", 10);//无法编译,预处理时直接删除
#endif
#if 1
printf("2:%d\n", 10);//打印10
#endif
#if DEBUG
printf("我是调试代码\n");
#endif
return 0;
}
2.多个分支的条件编译
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif
- 判断是否被定义
两组等价
#if defined(symbol)
#endif
#ifdef symbol
#endif
两组等价
#if !defined(symbol)
#endif
#ifndef symbol
#endif
#define MAX 0
int main()
{
//判断符号是否被定义,不管定义的标识符是否为真
#if defined(MAX)
printf("defined:MAX\n");
#endif
#if !defined(MAX)
printf("undefined:MAX\n");
#endif
//也可简写
#ifdef MAX
printf("defined:MAX\n");
#endif
#ifndef MAX
printf("undefined:MAX\n");
#endif
return 0;
}
- 嵌套指令
上述几种嵌套在一起使用
#if defined(MULT)
#ifdef OPTION1
//
#endif
#ifdef OPTION2
//
#endif
#elif defined(ADD)
#ifdef OPTION2
//
#endif
#endif
#include
我们已经知道, #include 指令可以使另外一个文件被编译。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
头文件被包含的方式:
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
本地文件包含
#include “filename”
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。
双引号也要去标准库文件目录下查找,那么是不是可以也用双引号包含库文件?
答案是肯定的,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
有时我们写一个程序创建多个源文件,可能就会发生某些头文件被重复包含,导致编译速率变慢,代码冗余。那怎么解决呢?
- 条件编译
每个头文件的开头写:
#ifndef TEST_H //如果有标识符没有定义,则继续
#define TEST_H //定义一个大写的头文件名标识符
//头文件的内容
#endif //TEST_H- #pragma once //直接开头使用即可
就可以避免头文件的重复引入。