前言:在C语言的实现过程中从源文件到可执行程序的的环境是翻译环境,生成exe到执行的环境是执行环境。在翻译环境中将源代码(文本文件)转换为可执行程序(二进制文件)。然后是执行环境执行代码

翻译环境+链接

在源文件翻译为可执行程序的过程中,每一个.c文件都会经过编译器编译为一个目标文件(xxx.obj)。这些目标文件再经过链接器连接上链接库共同生产可执行程序。

所以一个可执行程序的产生是:

源文件-编译->目标文件-链接器(加链接库)->可执行程序

//链接器会自动从C语言标准库中引入所需要的库函数,也可以引用个人数据库里的函数。

翻译:

翻译的环境下包括三个主要步骤:1.预编译 2.编译 3.汇编

预编译

预编译的过程中编译器会将注释都用空格替代、包含头文件中的内容以及将#define的内容替换。以上的一系列操作都是文本操作。

编译

这个过程将C语言的代码经过一系列分析(语法、词法、语义、符号)分析为汇编代码。

汇编

在这个过程中会将汇编代码转换成计算机可识别的二进制指令,并且形成符号表

int main()
{
	int a = 0;
	int b = 0;
	printf("%d\n", a);
	return 0;
}
int Add(int a)
{
	return a;
}

如上面代码,在汇编阶段会形成符号表,符号表类似于函数的地址表

链接

在经过编译阶段后就到链接的阶段,在编译阶段结束会产生一个目标文件(XXX.OBJ文件)。在链接阶段,需要进行的是合并段表和符号表合并以及重定位。

1.合并段表,即使是不同的源文件产生目标文件的文件格式都是elf,所以在合并段表阶段,链接器会将不同的目标文件的段表内容对应链接在一起。

2.符号表的合并和重定位,在这阶段,链接器会将符号表的内容进行合并,并且将一些无效的符号地址进行重定义。

在经过链接阶段后就会产生exe可执行文件(也是elf文件格式)

运行环境

将生成的exe文件进行运行的环境是运行环境,主要运行过程分为4个阶段

1.将可执行程序载入内存中

2.程序开始,从main函数进入

3.向内存申请所需要的栈、堆、静态区空间

4.程序结束,main函数正常结束

预处理

预定义符号

预定义符号是一些C语言定义好的符号,如___FILE__(显示当前文件位置)和__DATE__(显示当前日期) 和__TIME__(显示当前时间)

例如:

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		return 0;
	}
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
		fprintf(pf, "NAME:%s TIME:%s DATE:%s",__FILE__,__TIME__,__DATE__);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

而还有一个预定义符号__STDC__,这个符号用于判断当前编译器是否严格遵循C语言标准,如遵循,则返回1,否则为未定义

int main()
{
	printf("%d\n", __STDC__);//VS2019....未定义
	return 0;
}

#define

#define是C语言预处理指令,可以用于替换后面的内容(在预处理阶段编译器会自动换回来)

#define Max 100//将Max定义为100

除此以外,#define的机制中还允许它将参数替换到文本中,这种实现称为宏或者定义宏

它的一般格式为#define name(参数) 参数实现内容//注意参数的括号要紧挨着name

#define Add(x,y) x+y

int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

而#define在预处理阶段主要经过三个步骤

1.预编译器对文本内#define定义的宏或者变量进行扫描

2.将扫描出来的宏或者参数进行替换到原来位置

3.再次扫描,重复上面步骤

注意:

1.宏不能递归,但#define定义的变量可以出现在宏或者#define定义的内容里面

2.替换#define定义的内容时不会替换常量字符串里的符号

#define Max 100
"Max"//不会被换
#和##

对于#define 预定义的符号,通过#可以将参数直接以传过来的形式用字符串形式替换到后面。即#可以将一个宏参数替换为对应字符串形式

#define FUNCTION(a) printf(#a)

int main()
{
	int a = 10;
	FUNCTION(e);//输出e
	return 0;
}

而##可以将两边的符号合成同一个符号,允许创立新的标识符

#define Add(x,y) x##y

int main()
{
	int apple = 5;
	Add(app, le);//就是Add(apple)就是5
	return 0;
}
宏和函数

宏和函数两者比较相似,但两者都有各自的的优缺点

首先,宏相对函数的优点:

1.宏没有类型的限制,比较灵活。2.调用函数需要花费一定时间,当函数体内的语句编译花费的时间比调用和结束函数所需时间要少时,使用函数就没有时间上的好处。3.宏可以传递类型。

而函数相对宏的优点:

1.宏需要反复替换,容易造成代码量过大。2.宏不能调试。3.宏没有指定类型,不够严谨。4.宏的使用需要考虑宏参数的副作用/运算符优先级。

#undef

#undef用于移除一个宏定义

#define M 100
int main()
{
	printf("%d\n",M );
#undef M
	printf("%d\n", M);//用不了
	return 0;
}

命令行定义

这是在很多C语言编译器上都支持的一种功能,允许在命令行中添加定义,用于启动编译过程。

条件编译

这用于条件判断是否编译

ifdef
#define __DEBUG__ //定义__DEBUG__

int main()
{
	int a = 0;
#ifdef __DEBUG__ //是否定义__DEBUG__
	printf("%d\n", a);
#endif
	return 0;
}
#if defined(...)
int main()
{
	int a = 0;
#ifdef __DEBUG__
	printf("%d\n", a);
#endif
#ifndef __DEBUG__//与ifdef对立
	printf("%d\n", a);
#endif
#if defined(__DEBUG__)
	printf("%d\n", a);
#endif
#if !defined(__DEBUG__)//与defined对立
	printf("%d\n", a);
#endif
	return 0;
}

#if
int main()
{
#if 1==2
	printf("e");
#elif 2==4
	printf("p");
#else
	printf("a");
	return 0;
#endif

#include

这个指令可以使它包含的文件被编译,预处理会将这个指令删除并且换成相对应的文件编译

而对于头文件的包含有<>或者""的形式,但两者的查找范围是不同的。<>直接去标准位置查找所需的头文件,而""会先去当前路径下寻找文件,找不到才去标准路径底下寻找.

因为每引一次头文件就会将头文件内容编译一次,所以难免会造成头文件多次被编译,为解决这个问题有以下两种方法

//1
#pragma once

#define MAX 100

//2
#ifndef __LED_H_
#define __LED_H_

#define MAX 100

#endif