当前位置: 首页 > news >正文

程序环境和预处理

程序环境和预处理

  • 前言
  • 一、程序环境?
    • 运行环境
    • 翻译环境
      • 预处理阶段
      • 编译阶段
      • 汇编阶段
      • 链接阶段
  • 预处理详解
    • 预定义符号
      • #define定义的宏
        • #define定义的常量
        • #define定义的宏函数
        • #与##
      • 宏和函数的对比
      • #undef
      • 命令行定义
      • 条件编译
      • 头文件的包含
        • 如何防止头文件的重复包含?


前言

程序环境和预处理

一、程序环境?

在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

运行环境

1、程序会被加载进内存,在有操作系统的环境中,将程序加载进内存的“动作”是由操作系统来完成的,比如我们双击电脑图标的本质就是将程序加载进内存;在没有操作系统的环境下,则需要我们手动操作将程序加载进内存;
2、程序加载进内存过后,便会开始调用main函数;
3、从main函数开始执行代码,这个时候程序将使用一个运行时堆栈(stack)(也就是函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4、程序终止;当然程序可能正常终止,也有可能异常终止(比如电脑突然死机、突然停电);

翻译环境

翻译环境主要分为两个大板块:编译、链接
翻译环境主要做些什么呢?
在这里插入图片描述

1、组成一个程序的每个源文件通过编译过程分别转换成目标代码
2、每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执程序。
3、链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

实际上编译环境还可以细分为:预处理、编译、汇编这三个阶段;
接下来我们来着重了解一下翻译这个环境;
首先先介绍一下博主用的编译器:VS2019集成开发环境(IDE)
为什么叫集成开发环境?
早期编译器:调试、编译、链接都是分开的单独使用的;
但是VS2019不是这样,它给你把调试器、编译器、连接器等全部给我们一条龙整好了,我们只需要按一下Ctrl+F5就能运行程序了,可以说是非常方便了,但是呢,由于封装的太好,对于编译阶段的一些细节,对我们来说观察起来不是很方便;
在这里插入图片描述

预处理阶段

预处理阶段:
1、会进行注释的清除;
2、对define定义的宏进行替换;
3、对#include包含的文件进行展开;

这些都是一些文本操作;

编译阶段

编译阶段:把C语言转换位汇编代码
1、语法分析;
2、词法分析;
3、词法分析;
4、符号汇总;(比如:将一个工程里面的每个.C文件里面的函数名、全局变量名进行统计给自统计各自的(每个.C文件统计自己的))

汇编阶段

汇编阶段:将汇编代码转换为二进制代码;
1、形成符号表;
比如:
在这里插入图片描述

我现在有两个文件:Code_10_12.c和Add.c
在Code_10_12.c文件里面有符号:Add、main
在Add.c文件里面有符号:Add
在上一个阶段(也就是编译阶段)我们只是将这些符号汇总在一起,如果只是单纯聚集在一起没有任何意义,于是我们在汇编阶段,就会为这些符号,添加对应上的地址;
比如:Code10_12.c:main 0x100;Add 0x000(由于在Code_10_12.c里面我们只是告诉编译器有这么一个函数,我在别的地方实现了,这是编译器就会给Add一个标记地址给Add);但是在Add.c中我只有Add一个符号,同时也找到了Add的实现,那么我们就会给这个文件里面的Add符号给一个真实Add的地址给它比如给个0x300;

链接阶段

链接阶段:
在汇编阶段我们不是形成了符号表吗?那么这些符号表中一定有重复的符号,在链接阶段,我们就会对这些重复的符号进行重定义并合并符号表;
比如:Add不是有连个地址吗,在链接阶段,编译器会自动将无效的Add函数的那个地址去除掉(至于怎么去除的,我们不必关心,编译器会帮我们完成)最终形成一个符号表:main 0x100 Add 0x300;
当然如果我们在main函数中将Add写成add那么在链接阶段编译器就会找不到add函数,就会报链接错误:
在这里插入图片描述

预处理详解

预定义符号

FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
eg:
在这里插入图片描述

#define定义的宏

#define定义的宏是文本替换,就是直接的替换,一般我们可以用define定义常数、字符串等也可以用来定义宏函数;

#define定义的常量

在这里插入图片描述

#define定义的宏函数

#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

比如我们可以利用一个宏函数来帮我们运算两数间的最大值:
在这里插入图片描述
我们还可以用它来实现求一个数的平方:
在这里插入图片描述
我们再来算算(5+1)的平方数,这算出来应该是36:
在这里插入图片描述
诶,我们发现宏函数算出来居然是11???
这是怎么回事?
前面我们说了#define进行的操作就是文本替换:
既int ret=SQUR(5+1);实际替换过来就是:int ret=5+1*5+1;
这样的话算出来不就是11吗?哪有什么办法能达到预期?
就是假括号:
在这里插入图片描述
我们发现这样就能达到预期效果了;

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
    注意:
    1、 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
    2、 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
    在这里插入图片描述

#与##

#加一个宏参数会把宏参数转换为字符串;
比如现在我要实现一个
int a=10;
printf(“The val of a is %d\n”,a);
float b=3.14 f;
printf(“The val of b is %f\n”,b);
如果用函数去实现的话,会变得比较麻烦,同时我们会发现一个函数完成不了;
这是我们就可以利用宏函数去完成:
在这里插入图片描述

##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
eg:
在这里插入图片描述
当然这只能对已经定义的变量进行使用,对于没有定义的变量,即使拼接完成了编译器也会找不到它儿报错:
在这里插入图片描述

宏和函数的对比

int Max(int a, int b)
{
	return a > b ? a : b;
}
#define MAX(a,b) (a > b ? a : b)
int main()
{
	int a = 40;
	int b = 99;
	int ret1 = Max(a, b);//利用函数来实现求两数的最大值
	int ret2= MAX(a,b);//利用宏定义来实现来实现求两数的最大值
	printf("%d\n", ret1);
	printf("%d\n",ret2);
	return 0;
}

在这里插入图片描述
通过上面的对比我们可以发现,有时候函数和宏定义函数,可以干相同的事情,那么使用那种方式更好呢?
实际上,这没有绝对答案,只有相对来说,有时候宏定义能实现函数实现不了的事情,函数又能实现宏实现不了的事情;

使用宏的好处:
1、对于同一间事情(宏和函数都能实现)来说,效率方面的话,宏其实是更占优势的;就比如上面求两数最大值的代码,用函数来实现的话,时间开销和空间开销上会比宏大一些;
函数的函数:建立栈帧 计算 销毁栈帧
宏函数步骤:计算
宏函数从实现上就省去了建立栈帧和销毁栈帧的时间和空间开销;
从汇编指令数量上我们也可以说明这一点:
在这里插入图片描述
2、宏函数对参数没有类型限制,可以处理不同类型的数据,比如:
上文提到的 printf(“the value of a is %d\n”,a);
我们利用一个函数是不能实现的,必须对不同的类型设计不同的函数;
但是我们可以轻松利用宏函数将该需求轻松搞定;

任何事物都有两面性,宏虽然方便,但是也有缺陷:

宏的缺陷:
1、宏虽然速度快,但是无法调试,因为编译器在预编译阶段就把宏给替换了;
2、大量使用宏的话会使代码变得冗长,使用函数则不会;我们知道宏使替换,如果我们对一个宏引用了10000次,那么我的代码在预编译阶段就会替换至少1000行的代码,;但是如果我们使用函数的话就不会出现这个问题,函数使调用的时候才创建,调用结束就销毁,不管我们调用多少次函数,实际上都是对同一份代码调用,不会造成代码冗长;
3、由于宏函数对于类型没有要求,这多少会造成代码不够严谨;
4、使用宏的话容易出现优先级问题,需要多次添加小括号,保证其优先级;

宏和函数对比
在这里插入图片描述

#undef

我们既然可以利用#define 定义一个宏,那我们可不可以取消一个宏?
当然可以:
在这里插入图片描述

我们可以发现在#define和#undef之间都可以使用宏,但是出了这个范围就不行了;

命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

#include <stdio.h>
int main()
{
  int array [SZ];
  int i = 0;
  for(i = 0; i< SZ; i ++)
 {
    array[i] = i;
 }
  for(i = 0; i< SZ; i ++)
 {
    printf("%d " ,array[i]);
 }
  printf("\n" );
  return 0;
}
//linux 环境
gcc -D SZ=10 programe.c

在这里插入图片描述

条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令;
比如:有些代代码,我需要在windows环境下运行,有些代码我需要在Linux环境下运行;那么在windows环境下运行的时候我们屏蔽掉Linux环境下的代码,选择编译windows环境下的代码就行了;Linu同理;

常见的条件编译指令:
1、
#if 常量表达式
//选择编译的代码
#endif//一定要用#endif结尾,不然会报错
eg:
在这里插入图片描述
在这里插入图片描述
2、条件编译也支持多条分支:
# if 常量表达式
//代码
# elif 常量表达式
//代码
# elif 常量表达式
\代码
··················
#else
//代码
#endif
eg:
在这里插入图片描述
3、判断是否被定义:
#ifdef 宏名
//代码
#endif
在这里插入图片描述
我们可以看到,#ifdef~#endif 之间的代码暗下去了,因为在此之前,我们从未定义过D这个符号,因此编译器不会编译此段代码;
在这里插入图片描述

头文件的包含

头文件的包含一般有两种表达形式:
#include<stdio.h>//1
#include"stdio.h"//2
这两种有啥区别呢?
用尖括号(< >)的头文件,编译器会去专门存放库函数文件夹下搜索叫stdio.h的文件,如果没找到,编译器就会报错;
而使用双引号引起来(“ ”),编译器会首先去当前源文件所在目录下搜寻是否存在stdio.h这个文件,如果不存在就再去专门存放库函数的文件夹下面搜索,如果还没搜索到的话,就会报错;

如何防止头文件的重复包含?

我们知道#include的作用就是在预编译阶段将其所包含的文件全部展开,但是我们有时会对同一个头文件,进行多次包含?这时在预编译阶段就会展开许多重复的代码,会使得代码变得冗长;
如何解决这个问题?
1、利用条件编译 :
eg:
在头文件里面写上:
#ifndef MAX
#define MAX
//头文件内容
#endif
解释:如果定义了MAX标识符,下面的代码就不会参与编译;
如果没定义的话,我感觉定义一个,下一次如果在包含这个头文件的时候,由于定义了MAX就不会在展开了,这样就避免了对同一个头文件进行多次包含;//这是比较古老的编译器的写法
2、在头文件第一行输入#pragam once//当下流行的写法

相关文章:

  • 如果在网站做推广连接/5118网站查询
  • 腾讯云 部署wordpress/百度关键词seo排名优化
  • 旅游小镇网站建设方案/b站推广链接
  • 租网站服务器价格/网络营销有哪些手段
  • 工作室网站建设方案模板/seo优化与品牌官网定制
  • 连衣裙一起做网站/线上推广的渠道和方法
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • 【AI】Hill Climbing 爬山算法
  • 【甄选靶场】Vulnhub百个项目渗透——项目三十三:Money-Heist-catch-me-if-you-can(密码学)
  • 02_MySQL环境搭建
  • codeblock 常见问题
  • 网络版本计算器(再谈“协议“)
  • C++类和对象详解(下篇)
  • Intel汇编-把内存块的值加载回FPU环境中
  • 做网赚项目那么多,最好最擅长的!
  • 在本地安装CentOs虚拟机的过程
  • 牛客JS刷题
  • 5.MongoDB系列之索引(二)