程序中被隐藏了的过程

被隐藏的过程

起初计算机并没有高级语言
在这里插入图片描述
计算机能够直接看懂并能够执行命令的是机器语言(二进制语言)。我们只要将这些文件链接起来就能够生成可执行文件。

但是对于程序员来说机器语言并不是那么地友好,毕竟它使用的是二进制来表示某一个命令,例如:加法的机器指令是00000011。因此人们将这些功能包装成了一个个程序员能够看懂的标签,从而形成了汇编语言,加法的汇编指令是ADD,而高级语言的加法指令则是“ + ”。但是随着语言更加地高级,计算机并不能直接读懂,因此它内部会进行翻译,看懂之后再进行执行。就好比我们使用中文去命令一个不懂中文的外国人,必须要翻译成外国人能够听懂的语言,然后他才能够按照你的命令去做。
因此在汇编语言及高级语言所写的程序中,存在着翻译环境和执行环境。

我们平时在编写程序的时候都是一气呵成,但是实际上它是怎样从源文件产生出目标文件的呢?

在翻译和执行的环境下,它是这样产生的:
在这里插入图片描述
实际上一个编译链接的步骤有四个:预编译、编译、汇编、链接。
预编译又被称为预处理。
在GCC编译器下,源文件经过预处理产生.i文件,.i文件经过编译生成.s文件,.s文件经过汇编生成.o文件,最后.o文件经过链接生成可执行文件

那么这些步骤中每一步都具体干了些什么呢?
我们先来看看预处理。

预处理

预处理阶段主要处理的内容:

  • 展开宏定义,并删除# define
  • 处理# include 预编译指令
  • 处理条件预编译指令
  • 删除所有的注释
  • 添加行号和文件标识
  • 保留# pragram编译器指令

那么预编译指令具体又有哪些?每个指令都是在命令其做什么?

预编译指令含义
# define宏定义
# undef撤销已定义的宏
# include将文件插入该指令处
# if 、# else、# elif、#endif这几个指令提供了多种编译选择,当#if后面为真时,编译#if~#endif之间代码,如果为假则跳过,实际上和if……else的用法比较相似
#ifdef、#ifndef条件编译指令,表示“如果有定义”和“如果没有定义”
#line改变文件当前的函数和文件名,形式:#line number[“Filename”]
#error编译程序时,只要遇到#error会生成一个编译错误提示信息并停止编译
#pragram设定编译器的一些状态,因此预处理阶段会保留它

此外,编译器中还预定义了一些宏

_FILE_ //在编译中的文件名
_LINE_//在编译中的文件的行号

_DATE_//文件在编译时的日期
_TIME_//文件在编译时的时间

_STDC_//查看所用编译器是否遵循ANSI C,如果是则值为1,否则未定义

注意这些预定义的宏标识符两边是两个下划线。

例如:

# include <stdio.h>
int main(void)
{
	printf("flie:%s\n", __FILE__);
	printf("line:%d\n", __LINE__);
	printf("date:%s\n", __DATE__);
	printf("time:%s\n", __TIME__);
	//printf("%d\n", __STDC__); //由于该编译器不遵循ANSI C,所以显示未定义

	return 0;
}

在我这的VS2019下运行效果:
在这里插入图片描述

定义标识符常量

#define能够定义一些常量,比如数值常量、字符常量……等。这意味着你可以将任何的文本给替换到程序中,毕竟所有的文本都可以看作是字符。
形式如下:

#define name stuff

#define只是进行了简单地替换,即将name全部替换成stuff!!!
例如:

#define PI 3.14159265
//将该宏生命周期内的PI全部替换成3.14159265

如果有时候后面定义的字符太长呢?例如将一段语句插入程序中(可以把它单纯地看作是字符常量),那么写起来非常地别扭,这个时候就可以使用续接符,实际上它是一个反斜杠,但是它所在的那一行后面什么字符都没有。
例如:

//续接符
#define PROCESS \
   for(int i = 0; i < 12; i++){\
      sum += i; \
       if (i % 2 == 0){\
         count++;\
       }\
   }

虽然从理论上来说,#define可以替换所有的文本及符号,但是你不能定义注释符。因为实际上注释符的处理先于预编译指令的处理。

宏定义表达式

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

下面是宏的申明方式:

//宏的声明方式
#define name( parament-list ) stuff 

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

例如:

#define SQUARE( x ) x * x
#define SUM(X, Y) X + Y
#define SUB(X, Y, Z) X - Y - Z

需要注意大部分定义的宏名采用的是大写。

拿第一个定义的宏来说:

它接收了一个参数,如果在#define SQUARE( x ) x * x声明之下,将SQUARE( 3 ) 置于程序之中,那么在预处理阶段,就会将该宏生命周期内的程序中的SQUARE( 3 )替换成3 * 3

虽然看上去这个宏定义一点问题都没有,但是问题可大着呢!

#define SQUARE( x ) x * x

# include <stdio.h>
int main(void)
{
	int result = 0;
	result = 2 + SQUARE(2 + 3) + 5;
	printf("%d\n", result);
	return 0;
}

你的预期结果是多少呢?
是不是2+25+5=32,那打印的结果就是32呢?
这是错误的!
#define只是简单地进行了替换,原封不动地替换到程序中
实际上:
result = 2 + 2 + 3 * 2 + 3 + 5;
故而结果为18
在这里插入图片描述
如何解决这类问题呢?
只要我们别太小气,在该有括号的地方加上括号就可以了。

此外当#define与类型挂钩的时候

//#define和类型挂钩时
#define INT int //简单地替换

typedef int INT_T; //INT_T本质上就是int,别忘记分号


#define PTR_T int*
typedef int* PTR_R; //别忘记分号

PTR_T a, b;
PTR_R c, d;

//宏定义只是简单地将PTR_T替换成int*,但实际上*默认跟的是后面的标识符
//int *a, b;  所以b只是整型

//而重定义,只是把它换了一个名字,本质不变,PTR_R本质上是int*,是一个完完整整的类型
//int* a, b; //所以a, b都是int*类型

#undef

#undef
用于撤销已经定义过的宏名。
例如:
在这里插入图片描述
MAX的生命周期在#define 与#undef之间。

文件包含

#include 是将它包含的文件插入到该指令处以取代这条指令。
#include 指令包含文件的两种格式:

//格式一
# include <file>
//格式二
# include "file"

这两种格式的区别在于:

  • 格式一在预处理时到系统规定的路径下寻找(可以理解为库文件)
  • 格式二先在当前目录下查找,找不到再从系统规定的路径下查找(可以理解为先找本地文件,再找库文件)

这样是不是可以说,对于库文件也可以使用 “” 的形式包含? 答案是肯定可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

如果一个程序中同一个文件包含了N多次,这种情况会发生什么呢?
那么它就会将这个文件插入N多次,导致很多重复且无用的内容占用了空间。
如何防止多次重复包含同一文件呢?
不知道你有没有注意过这个情况。当我们创建一个头文件时,文件的开头都会有这样一个预处理指令:
在这里插入图片描述

#pragram once
//可以防止头文件的重复引用

另外一种方法就是:条件编译

#ifndef __TEST_H__ 
#define __TEST_H__ 
//头文件的内容
#endif __TEST_H__ 

上述条件编译可以理解为:如果没有定义文件__TEST_H__ ,则定义文件__TEST_H__ ,下一次在碰到__TEST_H__ 文件的包含命令,则就不会进行编译了。

条件编译

条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。

形式一

#ifdef 标识符
程序段 1
#else
程序段 2
#endif

如果定义了标识符,则编译程序段1,否则就编译程序段2

可以没有#else
形式二:

#ifdef 标识符
程序段
#endif

如果定义了标识符,则编译程序段

形式三:

#ifndef 标识符
程序段 1
#else
程序段 2
#endif

如果没有定义标识符,则编译程序段1,否则编译程序段2

形式四:

#if 常量表达式
程序段 1
#else
程序段 2
#endif

如果表达式常量为真则编译程序段1,否则编译程序段2

而#elif 命令意义其实与 else if 差不多

#line

#line 预处理指令的作用是改变当前文件的行数和文件名称,它们是在编译程序中预先定义的标识符。
定义形式如下
命令的基本形式如下

#line linenumber["filename"]

例如:

#line 10 test.c

其中的文件名也就是test.c可以省略

这条指令可以改变当前的行号和文件名,例如上面的这条预处理指令就可以改变当前的行号为 10,文件名是 test.c。另外,编译器对 C 源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名不会被这些中间文件代替,有利于进行分析。

#pragram

详见《C语言深度解剖》

编译

汇编

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小酥诶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值