hi,各位,让我们开启今日份博客~
目录
一、翻译环境和运行环境
在ANSI C(ANSI C 是美国国家标准协会创立的一套C标准,于1989年完成,这个版本的语言常被叫做C89.) 的任何一种实现中,存在两个不同的环境。
第一种是翻译环境,在这个环境中,源代码被转换成可执行的机器指令(二进制指令)。
第二种是运行环境,它用于实际执行代码。
1、翻译环境
翻译由编译和链接两大部分组成,其中编译又分为预处理、编译、汇编三个过程。
在编译器中一个C语言的项目可能有多个.c
文件一起构建,那多个.c
文件如何生成可执行程序呢?
- 首先多个
.c
文件单独经过编译器,编译处理成对应的目标文件(在Windows环境下的目标文件后缀是.obj
,Linux环境下目标文件的后缀是.o
) - 然后多个目标文件和编译库一起经过链接器处理生成最终的可执行程序(链接库是指运行时库【它是支持程序运行的基本函数集合】或者第三方库)。
如果我们再把编译器分成预处理、编译、汇编这三个过程,那就变成了以下过程:
1.1预处理(预编译)
在预处理阶段,源文件和头文件会被处理成.i
为后缀的文件.
在gcc
下观察对test.c
文件预处理后的test.i
文件,命令如下:
gcc test.c -E -o test.i
-E
选项:提示编译器执行完当前命令后就停下来,后面的编译、汇编和链接暂不执行-S
选项:提示编译器执行完编译停下来,汇编、链接暂不执行-c
选项:提示编译器执行完汇编就停下来
预处理过程进行的操作:
1、对#include
头文件进行包含
2、删除代码中的注释(使用空格替换)
3、#define
定义的符号进行替换,使用完后,符号删除
1.2编译
编译是将C语言程序转换成了汇编代码
在gcc
下观察对test.i
文件编译后的test.s
文件,命令如下:
gcc test.i -S -o test.s
1.2.1词法分析
词法分析是使用一种叫做lex
的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。
我们以以下表达式为例进行词法分析:
array[index] = (index+4)*(2+6);
1.2.2语法分析
语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。这些语法树是以表达式为节点的树,对于不同的语言,只是其语法规则不一样。
如下:
1.2.3语义分析
语义分析是由语义分析器来完成的,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段还会报告错误的语法信息。
如下:
1.3汇编
汇编过程是通过汇编器来完成的,汇编器将汇编代码转变成机器可执⾏的指令(2进制的指令),每⼀个汇编语句⼏乎都对应⼀条机器指令。它是根据汇编指令和机器指令的对照表一一 的进行翻译的,不做指令优化。
gcc
下汇编命令如下:
gcc -c test.s -o test.o
1.4链接
链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序。链接过程主要包括:地址和空间分配,符号决议和重定位等。
符号解析: 目标文件中可能包含一些未定义的符号引用,如函数调用、全局变量引用等。链接器会在所有的目标文件和库文件中查找这些符号的定义,将符号引用与对应的符号定义进行匹配,确保每个符号都有正确的定义。
重定位:编译生成的目标文件中的地址通常是相对地址或未确定的地址。链接器会根据最终可执行文件或库文件的布局,对目标文件中的代码和数据进行重定位,将相对地址转换为绝对地址,使程序在运行时能够正确地访问代码和数据。
合并段:目标文件通常包含多个段,如代码段、数据段、只读数据段等。链接器会将各个目标文件中的相同类型的段进行合并,形成最终可执行文件或库文件中的相应段,并为每个段分配合适的内存地址。
在生成输出文件时,还会添加一些必要的头部信息,如程序入口点、段的属性等,以便操作系统能够正确的加载和执行程序。
2.运行环境
程序必须载入内存中,在有操作系统的环境中,一般由操作系统来完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。程序载入内存之后,执行才能开始,开始后首先调用main函数,开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),储存函数的局部变量和返回的地址,程序同时也可以使用静态(static)内存,储存于静态内存中的变量在程序的整个执行过程一直保留他们的值,正常终止main函数时,程序终止,也有可能是意外终止。
如上图,我们双击以.exe
结尾的可执行文件就会进入运行环境,此时程序已经被加载到内存中。
二、预处理
1、预定义符号
C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号直接在C语言中就可以使用:
这些预定义符号也的确是在预处理阶段就被替换掉的:
图片的右侧是在预处理阶段生成的test.i
文件,我们可以看到这些预定义符号的位置已经被替换展开了。
2、#define
定义常量
#define
是一种预处理指令,它定义的符号在预处理阶段就会被替换展开。
基本语法:#define name stuff
name
是常量的名字stuff
是常量的内容
基本用法:
#define MAX 100
int main()
{
printf("%d\n", MAX);
return 0;
}
图片的右侧依旧是预处理阶段产生的test.i
的文件,可以看出已经MAX
被替换掉了。
在#define
定义标识符的时候,不要在最后加上;
,因为这样很容易造成问题。
假设你定义了#define MAX 100;
并且执行了语句int a=MAX;
此时就会替换成int a=100;;
替换之后会产生一个空语句;
这是很容易造成语法错误的,比如:
if(a>0)
max = MAX;
else
max = 0;
如果是加了分号的情况,等替换后,if
和else
之间就是2
条语句,而没有大括号的时候,if
后边只能有⼀条语句。这里会出现语法错误。
3、#define
定义宏
规范用法示例:
#include <stdio.h>
#define MAX(x,y) (x)*(y)
int main()
{
printf("%d", MAX(3, 5));
return 0;
}
结果:
这就是宏的规范用法了,有人很疑惑为什么要加那么多的括号,这是因为它是直接替换的,如果不加括号可能会造成我们不想要的后果,例如我要计算MAX(2+1,3+2)
,假设没有加括号,直接是x*y
,它进行替换会变成2+1*3+2
,这就不是我们想要的结果了,所以我们要多加括号,保证正确性。
其实此时还会存在错误,假设我们要计算15/MAX(2+1,3+2)
时,它会替换成15/(2+1)*(3+2)
这也不是我们想要的结果,我们想的是15
除以MAX
计算出的整体结果,所以我们在外部还应该加上括号。即#define MAX(x,y) ((x)*(y))
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
3.1 带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。例如x+1;
不带副作用,x++;
带有副作用,假设我们以上面的宏为例子,我们执行下面的代码:
#include <stdio.h>
#define MAX(x,y) (x)*(y)
int main()
{
int a = 2;
printf("%d\n", MAX(a + 1, a + 1));
printf("%d\n", MAX(a++, a++));//((a++)*(a++))
printf("a=%d\n", a);
return 0;
}
运行结果:
这就是执行的结果,在执行第二步的时候虽然a
的确加了两次但和我们预想的不一样,最终结果不是9
,也不是6
,而是4
。这就是上面所说的不可预料的后果。
3.2 宏参数可以出现类型
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
int main()
{
//int* p = (int*)malloc(10 * sizeof(int));
int* p = MALLOC(10, int);
//int* p = (type*)malloc(n*sizeof(type));
return 0;
}
这是函数所不能比的。
宏一般的使用场景:执行的运算比较简单。
4、#
和##
运算符
#
运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#
运算符所执行的操作可以理解为字符串化。
假设我写了这样的代码:
int a = 10;
printf("the value of a is %d\n", a);
double h = 3.14;
printf("the value of h is %f\n", h);
我们发现这两句话很像,我们这时候就可以借助宏实现这样的打印。
#define PRINT(n,he) printf("the value of " #n " is " he "\n",n)
#n
就是将n
字符串化,假设我们调用PRINT(a, "%d");
此时#n
它会被替换成"a"
,而he
会被替换成"%d"
。printf
中的多个字符串会被拼接在一起。
相关代码:
int a = 10;
printf("the value of a is %d\n", a);
double h = 3.14;
printf("the value of h is %f\n", h);
PRINT(a, "%d");
PRINT(h, "%f");
执行结果:
##
可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ##
被称为记号粘合。这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。
##
的简单用法:
#include <stdio.h>
#define AD(X,Y) X##Y
int main()
{
int heihei = 100;
//AD(hei,hei)-》heihei
printf("%d\n", AD(hei, hei));
return 0;
}
运行结果:
##
的复杂用法:
假设我们现在要写两个函数,一个比较int
类型中的较大值,一个比较double
类型中的较大值。我们会这样书写:
int int_max(int x, int y)
{
return x > y ? x : y;
}
double double_max(double x, double y)
{
return x > y ? x : y;
}
但是有了##
我们就可以用宏来实现了。
#include <stdio.h>
// \是续行符
//产生函数的模具
#define BIGMAX(type) \
type type##_max(type x,type y)\
{ \
return x>y?x:y; \
}
//产生一个比较int类型的函数
BIGMAX(int)
//产生一个比较double类型的函数
BIGMAX(double)
int main()
{
printf("%d\n", int_max(2, 3));
printf("%f\n", double_max(1.5, 3.5));
return 0;
}
运行结果:
5、命名约定
⼀般来讲函数、宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的⼀个习惯是:把宏名全部大写,函数名不要全部大写
特例:offsetof
也是宏但是是小写。
6、#undef
这条指令用于移除⼀个宏定义。
#include <stdio.h>
#define MAX 100
int main()
{
#undef MAX
int n = MAX;
return 0;
}
7、条件编译
#if 常量表达式
//...
#endif
两个必须配对使用。
#include <stdio.h>
int main()
{
#if 1+1==2//条件为真
printf("heihei");
#endif
return 0;
}
结果:
如果条件为假,就不会打印,其中的表达式必须是常量表达式,因为这些指令是在预处理阶段就处理的,而变量在预处理阶段还没有产生,所以表达式会无效。
多个分支的条件编译:
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
代码:
#include <stdio.h>
#define M 100
int main()
{
#if M<10
printf("heihei");
#elif M>=50&&M<100
printf("haha");
#else
printf("jeijei");
#endif
return 0;
}
结果:
判断是否被定义
//两者等价
#if defined(symbol)
#ifdef symbol
//两者等价
#if !defined(symbol)
#ifndef symbol
使用:
#include <stdio.h>
#define M 100
int main()
{
#if defined(M)
printf("heihei");
#endif
#if !defined(M)
printf("haha");
#endif
return 0;
}
相同的替换:
#include <stdio.h>
#define M 100
int main()
{
#ifdef M
printf("heihei");
#endif
#ifndef M
printf("haha");
#endif
return 0;
}
结果:
嵌套指令:
#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
总结:
以上就是本期博客分享的全部内容啦!技术的探索永无止境。
道阻且长,行则将至!后续我会给大家带来更多博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~