程序环境和预处理

一:程序的翻译环境和执行环境

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

 1. 翻译环境详解

翻译环境可以分为编译链接两部分

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

编译又可以分为预编译,编译,汇编三部分

下面将从这三个方面来解释翻译环境是如何把代码翻译成可执行的机器指令的

1.1 编译

(1) 预编译

在预编译阶段,会进行以下操作:

1.注释的替换和删除

2.头文件的包含

3.#define 符号的替换

上代码看实例:

本次示例用的是VS2019的环境

#include<stdio.h>
#define M 10
int main()
{
	int a = 3;
	int b = M;
	printf("%d", a + b);
	return 0;
}

为了方便看预编译后产生了什么影响,在项目--属性--c/c++中把预处理到文件打开,这时我们运行代码,会产生一个名为test.i的文件,我们打开这个文件:

我们发现,这个文件竟然有一万多行代码,翻到文件最后,发现我们写的代码也在这里,但是和我们写的代码不同的是,注释被删掉了,并且我们预定义的M被替换成了10,而在test.i文件中这一万行代码,就是stdio库中所有信息的声明

(2) 编译

将c语言代码翻译成汇编代码,在此过程也会进行复杂的处理,会进行

词法分析 语法分析 语义分析 符号汇总 等过程

(3) 汇编

把汇编代码翻译成二进制指令,生成.o文件(目标文件)

在此阶段会进行生成符号表的操作

1.2 链接

链接目标文件和链接库生成可执行程序

1.合并段表

2.符号表的合并和重定位

我们在一个文件中定义了一个函数,想在另一个文件中使用,我们需要在这个文件中声明一下这个函数,但是为什么声明了就可以用了呢?我们来探讨一下这个问题:

在编译阶段会进行符号汇总(一般汇总的符号都是全局的,因为只有全局的在不同文件中才有价值),在add.c的文件中就汇总出了 add 的符号, add函数在定义会有一个地址我们假设为0x1010,在汇编阶段会进行生成符号表,两者会被一起管理起来在add.o文件中形成符号表

同理在test.c的文件中汇总出了 add main 等符号,由于add在test.c文件中只是声明了一下,并不知道add函数的具体地址是多少,我们先付给他一个无意义地址0x0000,假设main的地址为0x1111,在test.o文件中生成符号表

而像 add.o,test.o文件都是由格式的

在gcc编译器生成的目标和二进制可执行文件都是按照elf这种文件的形式组织的,在链接阶段会把多个源文件链接到一起,其实就是把两个文件中相同段的信息进行合并,这个过程就叫做合并段表

同时符号表也会合并和重定位

由于在符号表中有两个add,同时也有两个地址,因为在test.o文件中add的地址是虚假的,符号表合并我们会保存add.o文件中add函数的地址,这样当我们想调用add函数时,就可以通过这个地址来找到它

了解了这些以后,如果说我们在写add函数时把名字写成了Add,例如以下代码:

通过编译链接,最终形成的符号表就为

真正的add函数地址为0x1010,由于两文件中add函数的名字不同,最终add真正的地址并没有把虚假的Add地址给覆盖掉,所以通过Add的虚假地址就找不到add函数

2. 运行环境

程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用 main 函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈( stack ),存储函数的局部变量和返回
地址。程序同时也可以使用静态( static )内存,存储于静态内存中的变量在程序的整个执行过程
一直保留他们的值。
4. 终止程序。正常终止 main 函数;也有可能是意外终止。

3.总结

二:预处理详解

1.预定义符号

__FILE__       //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__     //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的,例如:
#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

2. #define

2.1 #define定义标识符

语法:
#define name stuff
例如:
#define M 100
#define N 200
等等
除了这样用我们也可以发挥想象力:
#include<stdio.h>
#define CASE break; case

int main()
{
	int n = 0;
	scanf("%d", &n);
	switch (n)
	{
	case 1:
	CASE 2:
    //相当于break;case 2:
	CASE 3:
	//相当于break;case 3:
	}
	return 0;
}

这里还有一个问题:

#define后边要不要加分号呢?

答案是:建议不要加,因为有些情况加的分号会导致错误发生

例如:

要注意#define会把MAX替换为1000;再加上原本语句还有一个分号,这里的两个分号就相当于两条语句,因为if后面没有加大括号,那后边的else就检测不到if了,所以就报错了

2.2 #define定义宏

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

下面是宏的定义方式:

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

int main()
{
	int a = 10;
	int b = 0;
	printf("%d", mul(a, b));
    //相当于printf("%d", a*b);
	return 0;
}
这里宏接收参数a b,在程序中,预处理器会把mul(a,b)替换为a*b
但是这个宏有一个问题:
如果代码如下:
int a = 5;
printf("%d\n" ,mul(a+1,a+1) );

这里我们是想求(a+1)+(a+1)的值,但是预处理器会将其替换为: a+1*a+1 运算顺序会改变,结果会发生错误,想要解决这个问题就要尽可能的多加有效的括号: mul(a,b)   (a)*(b) 

这样替换完后,就变成(a+1)*(a+1)

再例如:

#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

这里我们是想求10*(a+a)的值,预期答案应该为100,但是替换完后:10*(a)+(a),答案为55,要解决这个问题只要在外边加一个大括号就可以了: #define DOUBLE(x) ( (x) + (x) )

提示:
所以用于对数值表达式进行求值的宏定义都应该尽可能多加有效的括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

2.3 #define替换规则

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

2.4 #和##

引例:

我们发现字符串是有自动连接的特点的, 打印字符串不论在一个引号中,或者在多个引号中,只要内容一样是不会影响输出结果的


那我们是不是可以这样写:

#define PRINT(FORMAT, VALUE)\
 printf("the value is " FORMAT "\n", VALUE);

PRINT("%d", 10);

这样替换完后:

 printf("the value is " %d" "\n", 10);  就可以正常输出了

ps:这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中

还有一个技巧是可以使用#把一个宏参数变成其对应字符串

例如:

#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
#include<stdio.h>
int main()
{
	int i = 10;
	PRINT("%d", i + 3);
	return 0;
}
代码中的 #VALUE 会预处理器处理为: "VALUE"


## 的作用:
## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) \
sum##num += value;
...
ADD_TO_SUM ( 5 , 10 ); // 作用是:给 sum5 增加 10
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如:
x+1;  //不带副作用
x++;  //带有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++); //本来是想求x++和y++谁最大
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

预处理后MAX宏将会被替换为:

z = ( ( x ++ ) > ( y ++ ) ? ( x ++ ) : ( y ++ ));
(x++)>(y++)为假,x和y都自增一次,x变为6,y变为9,在执行y++,把y++的执行结果赋值给z,y又会自增一次,即x=6,y=10,z=9
如果使用函数的话就不会出现这样的情况,因为函数只有被调用时参数只会执行一次
int MAX(int a,int b)
{
  int z= ( (a) > (b) ? (a) : (b) );
  return z;
}

2.6 宏和函数的对比

#define 定义宏
函数
每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每
次使用这个函数时,都调用那个
地方的同一份代码
更快
存在函数的调用和返回的额外开
销,所以相对慢一些
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生
不可预料的后果,所以建议宏在书写的时候多些括 号。
函数参数只在函数调用的时候求
值一次,它的结果值传递给函
数。表达式的求值结果更容易预
测。
参数可能被替换到宏体中的多个位置,所以带有副作
用的参数求值可能会产生不可预料的结果
函数参数只在传参的时候求值一
次,结果更容易控制
宏的参数与类型无关,只要对参数的操作是合法的,
它就可以使用于任何参数类型
函数的参数是与类型有关的,如
果参数的类型不同,就需要不同
的函数,即使他们执行的任务是
相同的。
宏是不方便调试的
函数是可以逐语句调试的

 

宏是不能递归的                                                 

 

 总结:  宏通常被应用于执行简单的运算

2.7 命名规则

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
  • 把宏名全部大写
  • 函数名不要全部大写

2.8 #undef

这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

3.条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#if 常量表达式  
//代码
#endif
条件编译常常用于头文件的书写中
比如:
        在test_1.h文件中调用了stdio.h
        在test_2.h中又调用了test_1.h和stdio.h
        这样在test_2.h中就两次调用了stdio.h发生了重复
这时就可以在文件开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

当 tset_2.h 调用 test_1.h 时就把 __TEST_H__ 定义上了,第二次调用stdio.h时if语句为假就不会编译该头文件的内容

现在大多数编译器在创建头文件时就会自动在第一行补充:

#pragma once

这个语句也是防止头文件重复引用的

        
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

张呱呱_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值