一、翻译环境
1.1 概况
概念:源代码被转换成可执行的机器指令(二进制指令)
机器只能执行二进制指令
1.2 预处理
(1) 预处理符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义 VS2019不遵循,gcc遵循
__FUICTION__ //打印当前函数
printf("file:%s line:%d\n", __FILE__, __LINE__);
(2) 预处理指令
#define
❤️#define 替换规则
- 替换步骤
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
- 注意
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归(即自己调用自己)
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
扩展 ------------------->如何把参数插入到字符串中? -------------------> # 的作用
要点:(1)字符串有自动连接的功能 (2)使用 # ,把一个宏参数变成对应的字符串
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
#include <stdio.h>
int main()
{
PRINT("%d", 10);
return 0;
}
#include <stdio.h>
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i + 3);
return 0;
}
扩展------------------->##的作用
##可以把位于它两边的符号合成一个符号。允许宏定义从分离的文本片段创建标识符
注:这样的连接必须产生一个合法的标识符,不然就是未定义的,无法编译过去
#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
int stu109 = 2023;
printf("%d\n", CAT(stu, 109));
return 0;
}
❤️#define 定义标识符
- 格式:#define name stuff
后面不需要加上“;”
因为#define定义标识符是替换的效果,可能会把一个语句分开来造成语法错误
- 具体实例
#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 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏
- 格式
- #define name( parament-list ) stuff
- parament-list 是一个由逗号隔开的符号表,可能会出现在stuff中,可以理解为形参
- 参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分 - 所以用于对数值表达式进行求值的宏定义都应该在整体外加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
#define SQUARE( x ) x * x
int main()
{
int a = 5;
printf("%d\n" ,SQUARE( a + 1) ); //结果是11,而不是36
return 0;
}
#define DOUBLE(x) (x) + (x)
int main()
{
int a = 5;
printf("%d\n", 10 * DOUBLE(a)); //是55而不是100,乘法先于宏定义的加法
return 0;
}
- 带有副作用的宏参数
- 概念:这个宏在使用的时候,无形之中会影响其他参数的数值,且是永久性的那种
- 举例:
#include <stdio.h>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++); //z = ( (x++) > (y++) ? (x++) : (y++));
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
#include
- 概念:
- #include 指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样
- 这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
- 使用方式:
- 本地文件
- 格式:#include “filename”
- 查找策略:
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误
- 库文件
- 格式:#include <filename.h>
- 查找策略:直接去标准路径下去查找,如果找不到就提示编译错过
- 注意:库文件也是可以用“#include “filename””的方式进行查找的,但是按照查找策略来看,需要查找两次,效率太低,所以分开
- 本地文件
- 嵌套文件包含的情况
- 概念:
包含了多个头文件,那么预处理后,就会有多个头文件的内容,从而造成变量重复臃肿 - 解决方法:条件编译
- #ifndef TEST_H
#define TEST_H 如果__TEST_H__没被定义过,
//头文件的内容 就执行下面的内容(参与编译)
#endif //_TEST_H - #pragma once 规定某个头文件只能被用一次
- #ifndef TEST_H
- 概念:
#undef
- 概念:用于移除一个宏定义
- 格式:#undef NAME
- 使用场景:如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
- 举例:
1.3 命令行定义
- 概念:许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。即,编出不同版本的程序,变成动态的,适应各种情况
- 举例:
- 当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
gcc -D ARRAY_SIZE=10 programe.c //编译指令,这里定下ARRAY_SIZE的大小为10
1.4 条件编译
- 概念:有选择性地进行编译,而不是一味地单独删除代码
- 常见的编译指令:
-
#if 常量表达式 常量表达式由预处理器求值,为真就执行
//… 下面的语句
#endif -
#if 常量表达式 预处理时,把不该要的都会被删掉(即
//… 编译过程中不会出现)
#elif 常量表达式
//…
#else
//…
#endif -
#if defined(symbol) 等价于下面,含义:如果symbol被
#ifdef symbol 定义了,就执行下面的语句(参与编译)
#if !defined(symbol) 等价于下面,如果symbol没被定义,
#ifndef symbolsymbol 定就执行下面的语句(参与编译) -
#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
-
二、运行环境
- 概念
- 用于实际执行代码
- 过程
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
- 程序开始执行,接着便调用main函数
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(函数栈帧stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执法过程中一直保留他们的值
- 终止程序。正常终止main函数,也有可能是意外终止