预处理(十二-终)


预处理(十二-终)

翻译环境和执行环境

翻译环境

在这个环境中源代码被转换为可执行的机器指令,就是把(.c)文件翻译成(.exe)文件。
一个C语言程序需要经过的四个步骤:编辑(.c)、编译(.obj)、链接(.exe)、运行。
而其中编译又分为:预编译(预处理)、编译、汇编。
过程
预编译(预处理)
源码中的所有预处理语句(#号开头的语句便是预处理语句,例如:#include)得到处理并删除注释。
编译
就是把C语言代码翻译成汇编代码。
汇编
把汇编代码翻译成二进制代码。
链接
但机器代码还是不能直接运行。所以链接器将处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。

执行环境

这个环境用于实际执行代码。

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

预编译(预处理)详解

预定义符号

1
2
3
4
__FILE__//源文件的绝对路径
__LINE__//文件当前的行号
__DATE__//文件被编译的日期
__TIME__//文件被编译的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
printf("%s\n",__FILE__);
printf("%d\n",__LINE__);
printf("%s\n",__DATE__);
printf("%s\n",__TIME__);
return 0;
}
/*
d:\vc++2010\prac\prac\ss.c
5
Feb 7 2021
22:29:26
*/

预处理指令

1
2
#开头的就是预处理指令例如:  
#define,#include,#pragma pack(),#if,#endif,#ifdef,#line

#define定义标识符

#define name stuff
例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define MAX 100
#define STR "blog"
int main()
{
int a=MAX;
//预处理完后变为
//int a=100;
printf("%d\n",a);//100
printf("%s\n",STR);//blog
return 0;
}

:在define定义标识符时,最后不要加;

#define定义宏

#define可以把参数替换到文本中,这种实现称为宏(macro)或定义宏(define macro)。
宏的申明方式
#define name(parament-list) stuff
parament-list是一个由逗号隔开的符号表,这些符号可能出现在stuff中。
参数列表的左括号必须与name紧邻
例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define SQU(x) x*x
//接收一个参数x
int main()
{
int a=SQU(3);
//预处理器会将3*3代替SQU(3)
printf("%d\n",a);//9
return 0;
}

:宏的参数不是传参,而是替换
例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define SQU(x) x*x
int main()
{
int a=SQU(3+1);
//预处理将 3+1*3+1 代替SQU(3+1),并不是(3+1)*(3+1)
//所以a的值为7,而不是16
printf("%d\n",a);//7
}

所以为了避免这个问题,我们要给stuff加上括号(stuff)以及stuff中每个参数也加上括号。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define SQU(x) ((x)*(x))
int main()
{
int a=SQU(3+1);
//预处理将 ((3+1)*(3+1)) 代替SQU(3+1)
//所以这次a的值为16
printf("%d\n",a);//16
return 0;
}

:

  1. 宏参数和#define定义中可以出现其他#define的变量,但宏不能出现递归。
  2. 预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索,通俗点讲,就是字符串常量的内容不会被替换。

#和##的区别
#是将其后面的宏参数进行字符串化操作,并不替换。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#define PRINT(x) printf(#x"的值为%d\n",x)
int main()
{
int a=10;
int b=20;
PRINT(a);
//printf(“a”“的值为%d\n”,x)替换PRINT(a)
PRINT(b);
//printf(“b”“的值为%d\n”,x)替换PRINT(a)
return 0;
}
/*
a的值为10
b的值为20
*/

##是将它两边的符号合成一个符号。
例:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define CON(x,y) x##y
int main()
{
int myblog2021=100;
printf("%d\n",CON(myblog,2021));//100
//CON(myblog,2021)
//myblog##2021
//myblog2021
return 0;
}

宏和函数

区别

  1. 宏的参数与类型是无关的,只要对参数的操作合法,就可以使用于任何类型;函数的参数类型是固定的,如果参数的类型不同,就需要使用不同的函数,即使他们执行的任务是相同的。
  2. 宏可能会带来运算优先级的问题,导致运算结果出错;
  3. 宏的参数替换是直接替换的;而函数调用时会将实参的值传给形参;
  4. 宏是不方便调试的,因为宏是在编译之前进行(先用宏体替换宏名,再进行编译);而函数是可以逐语句调试;
  5. 宏的参数是不占内存空间的,因为只做字符串的替换;而函数调用时参数之间的传递,所以占用内存;
  6. 宏的速度比函数速度快,因为函数有调用和返回时间的开销;
  7. 宏在传参时可以传类型,但是函数不能传类型;
  8. 宏不能递归,函数可以递归;
  9. 宏的代码长度很长(除去非常小的宏),每次使用时,宏代码都被插入到程序中,使得程序的长度大幅度增加;

命名约定
宏名全部大写,函数名不要全部大写。

#undef

用于移除一个宏定义。
#undef name
例:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define MAX 100
int main()
{
printf("%d\n",MAX);//100
#undef MAX
return 0;
}

条件编译

选择性的按照某种条件编译代码。
常见的条件编译指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//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

文件包含

#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。
替换方式:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含方式

本地文件包含
#include "filename.h"
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
库文件包含
#include <filename.h>
编译器直接在标准位置查找头文件。如果找不到就提示编译错误。


Author: ljs
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source ljs !
评论
  TOC