程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第2种是执行环境,它用于实际执行代码。
详解编译+链接
翻译环境
![在这里插入图片描述](https://img-blog.csdnimg.cn/04bd03e2cb554aa298fb6a8349722f89.png
上图截取自比特科技免费课程
。组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
。每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
。链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
Linux环境方便观察编译过程:
1、预处理 选项gcc -E test.c -o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
2、编译 选项gcc -S test.c编译完成之后就停下来,结果保存在test.s中。
3、汇编 gcc -c test.c汇编完成之后就停下来,结果保存在test.o中。
比特科技推荐网站:
VIM学习资料
简明VIM练级攻略:
https://coolshell.cn/articles/5426.html
给程序员的VIM速查卡
https://coolshell.cn/articles/5479.html
运行环境
程序执行的过程:
1、程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2、程序的执行便开始。接着便调用main函数。
3、开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4、终止程序。正常终止main函数;也有可能是意外终止。
问:在编译过程中实现的内存堆栈和通过exe运行程序时进行的内存堆栈是不是一样的?
ChatAI答:
在编译过程中实现的内存堆栈和通过exe运行程序时进行的内存堆栈是不一样的。
在编译过程中,内存堆栈是由编译器实现的,用于在程序执行时为函数、变量等分配内存空间和管理内存使用情况。编译器会根据程序的代码框架和变量的作用域等因素,对内存空间进行分配和释放,并且对程序和函数的调用栈进行跟踪和管理。
在执行程序时,内存堆栈也存在,但实现方式是由操作系统和运行时环境决定的。当程序运行时,操作系统会为程序分配一定的内存空间,这些内存空间被划分为不同的存储区域,包括进程虚拟地址空间、栈、堆、全局/静态数据区等。程序使用内存空间时,根据内存空间的访问类型和权限进行对应的操作。
因此,编译器实现的内存堆栈和运行时环境实现的内存堆栈,并不是完全相同的。它们的实现方式、管理机制、内存分配和释放方式等都可能存在差异。这也是为什么在一些情况下,程序在编译时可以通过编译,但在执行时却会出现运行时错误的原因之一。
《程序员的自我修养》
预处理详解
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义(PS:VS2015无效)
__FUNCTION__ //打印函数名
这些预定义符号都是语言内置的。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
printf("file:%s line:%d\n",__FILE__,__LINE__);
printf("date:%s time:%s\n",__DATE__,__TIME__);
return 0;
}
用于写日志文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
//写日志文件
int i = 0;
int arr[10] = { 0 };
FILE* pf = fopen("log.txt", "w");
for (i = 0; i < 10; i++)
{
arr[i] = i;
fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\n",
__FILE__, __LINE__, __DATE__, __TIME__,i);
}
fclose(pf);
pf = NULL;
for (i = 0; i < 10; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
预处理指令:
#define
#include
#pragma
#if
#endif
#ifdef
#line
#define
#define定义标识符
语法:
#define name stuff
举个例子:
#define MAX 1000
#define reg register //为 register 这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把break写上
//如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加上一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n, \
__FILE__,__LINE__, \
__DATE__,__TIME__)
#define定义宏
#defne 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的声明方式:
#define name( parament-list ) stuff其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
如:
#define SQUARE( x ) x*x
这个宏接收一个参数x,如果在上述声明之后,你把
SQUARE( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5*5
#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
int ret = SQUARE(5);
printf("%d\n",ret);
return 0;
}
输出:25
#include <stdio.h>
#define SQUARE(X) X*X
int main()
{
int ret = SQUARE(5+1);
printf("%d\n",ret);
return 0;
}
输出:11
注意:宏是用来替换的而不是传参的。上面被替换成了:
5+1*5+1 = 11
建议优化写法:
#define SQUARE(X) ((X)*(X))
define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1、在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2、替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1、宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
2、当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#和##
如何把参数插入到字符串中?
#include <stdio.h>
#define PRINT(X) printf("the value of "#X" is %d\n",X)
int main()
{
int a = 10;
int b = 20;
PRINT(a);
PRINT(b);
return 0;
}
另一个技巧是:使用#,把一个宏参数变成对应的字符串。比如:
#include <stdio.h>
#define PRINT(FORMAT,VALUE)\
printf("the value of "#VALUE" is "FORMAT"\n",VALUE)
int main()
{
int a = 10;
int b = 20;
PRINT("%d",a+8);
PRINT("%d",b+3);
return 0;
}
##的作用
##可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
#include <stdio.h>
#define CAT(X,Y) X##Y
int main()
{
int chung1991 = 2023;
printf("%d\n", CAT(chung, 1991));
return 0;
}
输出:2023
注:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。