『C』程序的翻译执行
在ANSI C的任何一种实现中,存在两种不同的环境。第一种是翻译环境,在这个环境中,源代码被转换为可执行的机器指令。第二种是执行环境,它用于实际执行代码。标准明确说明,这两种环境不必位于同一台机器上。例如,交叉编译器就是在一台机器上运行,但它所产生的可执行代码运行于不同类型的机器上。
程序的翻译和执行
翻译
组成一个程序的每个源文件通过编译过程分别转换成目标代码。
每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序猿客人的程序库,将需要的函数链接到程序中。
执行
程序执行过程:
- 首先,程序必须载入到内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 然后,程序的执行便开始。接着便执行main函数。
- 然后,开始执行程序代码。这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
- 最后,终止程序,正常终止main函数,也有可能是意外终止。
程序的翻译流程
预处理
命令:gcc -E ×××.c -o ×××.i
主要工作:
拷贝头文件到×××.i。
去掉注释。
对宏进行展开。
处理条件编译。
代码演示
#include <stdio.h>#define N 5/*** 测试用例 ***/int main(){#if 1printf("hello, world! month: %d\n", N);
#elseprintf("hehe, world! month: %d\n", N);
#endifreturn 0;
}
预处理之后
835 # 943 "/usr/include/stdio.h" 3 4
836
837 # 2 "pretreatment.c" 2
838
839
840
841
842
843 int main(){844
845
846 printf("hello, world! month: %d\n", 5);
847
848
849
850
851 return 0;
852 }
前面800多行都是stdio.h头文件的展开。可以看到,预处理之后,注释被去掉了,宏也进行了展开,条件编译也被处理。
预定义符号
__FILE__ | 进行编译的源文件 |
__LINE__ | 文件当前的行号 |
__DATE__ | 文件被编译的日期 |
__TIME__ | 文件被编译的时间 |
__STDC__ | 如果编译器遵循ANSI C,其值为1,否则未定义 |
这些预定义符号都是语言内置的。
代码演示
#include <stdio.h>int main(){printf("files: %s\tline: %d\n", __FILE__, __LINE__);printf("date: %s\t\ttime: %s\n", __DATE__, __TIME__);printf("stdc: %d\n", __STDC__);return 0;
}
运行结果
[sss@aliyun order]$ !gcc
gcc pre_define_symbol.c -o pre_define_symbol
[sss@aliyun order]$ ./pre_define_symbol
files: pre_define_symbol.c line: 5
date: May 5 2019 time: 19:59:18
stdc: 1
宏
语法:
#define constant 100
#define reg register
#define do_forever for(;;)
#define CASE break;case
#define DEBUG_PRINT printf("File: %s. Line: %d."\"x = %d, y = %d, z = %d",\__FILE__, __LINE__,\x, y, z)
宏常量
语法:
#define name stuff
代码示例
#include <stdio.h>#define N 10int main(){printf("hello, world! %d\n", N);return 0;
}
运行结果
[sss@aliyun order]$ gcc macro_constant.c -o macro_constant
[sss@aliyun order]$ ./macro_constant
hello, world! 10
宏函数
语法:
#define name(parament_list) stuff
其中parament_list是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻。如果两者存在任何空白,参数列表就会被解释为stuff的一部分。
代码演示
#include <stdio.h>#define MAX(a, b) a > b ? a : bint main(){int a = 1;int b = 2;printf("max: %d\n", MAX(a, b));return 0;
}
运行结果
[sss@aliyun order]$ gcc macro_function.c -o macro_function
[sss@aliyun order]$ ./macro_function
max: 2
宏函数的一个注意事项
代码演示
#include <stdio.h>#define SQUARE(X) X * Xint main(){int num = 3;printf("square(num + 1): %d\n", SQUARE(num + 1));return 0;
}
运行结果
[sss@aliyun order]$ !gcc
gcc macro_function.c -o macro_function
[sss@aliyun order]$ ./macro_function
square(num + 1): 7
可见宏函数只是简单的文本替换。
SQUARE(num + 1)
num + 1 * num + 1
#undef
用于移除一个宏。
语法:
#define N 10
#undef N
#和##
首先来看一段代码
#include <stdio.h>int main(){char* p = "hello, ""world!\n";printf("hello, ""world!\n");printf("%s", p);return 0;
}
运行结果
[sss@aliyun order]$ gcc test.c -o test
[sss@aliyun order]$ ./test
hello, world!
hello, world!
从这里,我们可以看出字符串是有自动连接的特点的。
#用法示例
#include <stdio.h>#define PRINT(FORMAT, VALUE)\printf("the value is "FORMAT"\n", VALUE);/*** #的作用,把一个宏参数编程对应的字符串 ***/
#define PRINT2(FORMAT, VALUE)\printf("the value of "#VALUE" is "FORMAT"\n", VALUE);int main(){int i = 10;PRINT("%d", 10);PRINT2("%d", i + 3);return 0;
}
运行结果
[sss@aliyun order]$ !gcc
gcc test.c -o test
[sss@aliyun order]$ ./test
the value is 10
the value of i + 3 is 13
##用法示例
#include <stdio.h>#define PRINT(FORMAT, VALUE)\printf("the value is "FORMAT"\n", VALUE);/*** #的作用,把一个宏参数编程对应的字符串 ***/
#define PRINT2(FORMAT, VALUE)\printf("the value of "#VALUE" is "FORMAT"\n", VALUE);/* * #的作用,把位于它两边的符号合成一个符号,* 它允许宏定义从分离的文本片段创建标识符 */
#define ADD_TO_SUM(num, value) \sum##num += value;int main(){int i = 10;int sum5 = 0;PRINT("%d", 10);PRINT2("%d", i + 3);ADD_TO_SUM(5, 10);printf("sum5: %d\n", sum5);return 0;
}
运行结果
[sss@aliyun order]$ gcc macro_20190505.c -o macro
[sss@aliyun order]$ ./macro
the value is 10
the value of i + 3 is 13
sum5: 10
宏和函数
属性 | 宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长。 | 函数代码只出现在一个地方;每次使用这个函数时,都调用同一份代码。 |
执行速度 | 更快。 | 存在函数的调用和返回的额外开销,相对慢一些。 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写时多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何类型参数。 | 函数的参数是与类型有关的,如果函数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的。 | 函数是可以逐语句调试的。 |
递归 | 宏是不能递归的。 | 函数是可以递归的。 |
命令行定义
许多C编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出一个程序的不同版本时,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存很大,我们需要一个大些的数组。)
代码演示
#include <stdio.h>int main(){int arr[LEN];int i = 0;for(; i < LEN; ++i){arr[i] = i;}printf("The array is: \n");for(i = 0; i < LEN; ++i){printf("%d ", arr[i]);}printf("\n");return 0;
}
运行结果
[sss@aliyun order]$ gcc -DLEN=10 command_line_define.c -o command_line_define
[sss@aliyun order]$ ./command_line_define
The array is:
0 1 2 3 4 5 6 7 8 9
[sss@aliyun order]$ gcc -DLEN=5 command_line_define.c -o command_line_define
[sss@aliyun order]$ ./command_line_define
The array is:
0 1 2 3 4
条件编译
在编译一个程序的时候,我们如果要将一条语句编译或者放弃是很方便的。我们可以用条件编译指令。
代码演示
#include <stdio.h>
#define __DEBUG__int main(){int i = 0;int arr[10] = {0};for(; i < 10; ++i){arr[i] = i;#ifdef __DEBUG__printf("%d\n", arr[i]);#endif}return 0;
}
常见的条件编译指令
#if 常量表达式// ...
#endif/*** 多分支条件编译 ***/
#if 常量表达式// ...
#elif 常量表达式// ...
#else// ...
#endif/*** 判断symbol是否被定义 ***/
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol/*** 嵌套指令 ***/
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
文件包含
我们已经看到过,#include指令使另一个文件的内容被编译,就像它实际出现于#include指令的地方一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。
头文件包含方式
本地头文件包含
#include "×××.h"
该种包含方式,先在源文件所在目录下查找,找不到的话就到标准库文件所在路径进行查找。
库文件包含
#include <×××.h>
该种包含方式,直接去标准库文件所在路径进行查找,找不到报错。
Linux环境的标准头文件路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC/include
嵌套文件包含
comm.h和comm.c是公共模块。test1.h和test1.c使用了公共模块。test2.h和test2.c使用了公共模块。test.h和test.c使用了test1模块和test2模块。这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复包含。
如何解决这个问题?
条件编译。
#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif#pragma once
上面两种方法都可以避免头文件的重复包含。
其他预处理指令
#error
#pragma
#line
...
编译
命令:gcc -S ×××.i -o ×××.s
主要工作:
将预处理过的源代码文件转换为汇编代码。
演示
[sss@aliyun order]$ gcc -S hehe.i -o hehe.s
[sss@aliyun order]$ vim hehe.s
.file "pretreatment.c".section .rodata
.LC0:.string "hello, world! month: %d\n".text.globl main.type main, @function
main:
.LFB0:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl $5, %esimovl $.LC0, %edimovl $0, %eaxcall printfmovl $0, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)".section .note.GNU-stack,"",@progbits
汇编
命令:gcc ×××.s -o ×××.o
主要工作:将汇编代码变成二进制指令。
注意,×××.o还不能执行。
chmod +x ×××.o
加了权限也不能执行。
和可执行程序还差了一步链接。
演示
[sss@aliyun order]$ gcc -c hehe.s -o hehe.o
[sss@aliyun order]$ objdump -d hehe.ohehe.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 <main>:0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbp4: be 05 00 00 00 mov $0x5,%esi9: bf 00 00 00 00 mov $0x0,%edie: b8 00 00 00 00 mov $0x0,%eax13: e8 00 00 00 00 callq 18 <main+0x18>18: b8 00 00 00 00 mov $0x0,%eax1d: 5d pop %rbp1e: c3 retq
[sss@aliyun order]$ chmod +x hehe.o
[sss@aliyun order]$ ll
total 124
-rw-rw-r-- 1 sss sss 184 May 5 19:44 hehe.c
-rw-rw-r-- 1 sss sss 16918 May 5 19:55 hehe.i
-rwxrwxr-x 1 sss sss 1528 May 6 09:21 hehe.o
-rw-rw-r-- 1 sss sss 501 May 6 09:15 hehe.s
[sss@aliyun order]$ ./hehe.o
-bash: ./hehe.o: cannot execute binary file
链接
把二进制指令转换成最终的二进制代码。
将多个源代码生成的目标文件×××.o链接才能得到最终的可执行程序。
例如:printf的定义在printf.c文件中,printf.c经过编译器得到目标文件需要加上C语言标准库提供的函数定义。
『C』程序的翻译执行相关推荐
- 『 不老 』程序员之修炼指南
题图来源:www.pinterest.com 在程序员聚集的技术社区里面,你应该会经常看到下列帖子: 中年程序员的心酸 大龄大专程序员的出路在哪里? 程序员 30 岁之后何去何从? 50 岁以上的程序 ...
- 『教师节』程序猿用文心大模型带你一键加速祝福,祝老师们节日快乐
『教师节』文心大模型带你一键加速祝福 文心大模型助力教师节 猜猜他谁 教师节&&中秋节 中秋助力 大模型使用指南 教师节助力 安装wenxin_api第三方库 定义我们的祝福主题与内容 ...
- 『ssh』使用shell远程执行命令
经常需要远程到其他节点上执行一些shell命令,如果分别ssh到每台主机上再去执行很麻烦,因此能有个集中管理的方式就好了.所以介绍几种shell命令远程执行的方法. 前提条件:配置shell免密登录 ...
- 『操作系统』 进程的描述与控制 Part 1 前驱图与程序执行
文章目录 2.1 前趋图和程序执行 2.1.1 程序的顺序执行及其特征 1. 程序的顺序执行 2.程序顺序执行时的特征 2.1.2 前趋图 2.1.3 程序的并发执行及其特征 1. 程序的并发执行 2 ...
- 交大『云观CD-ROMIII』直接由CD-ROM执行之安装法(转)
交大『云观CD-ROMIII』直接由CD-ROM执行之安装法(转) 笔者很高兴看到交大一群朋友们的努力, 出版了一份本土的 "Plug & Play" Linux CD-R ...
- 程序环境——翻译环境与执行环境
本篇重点: 程序的翻译环境和执行环境 详解翻译环境中的 编译+链接 程序的翻译环境和执行环境 在ANSI的任何一种实现中,存在两个不同的环境. 翻译环境,在这个环境中源代码被转换为可执行的机器指令. ...
- [日推荐]『TheGolfGame』一个高端大气上档次的小程序
今天小编再来给大家推荐一个高端大气上档次的小程序 TheGolfGame 简介:The Golf Game是基于高尔夫教学的服务平台,为用户提供最专业的高尔夫教学指导,大量的教学视频,更有娱乐视频,让 ...
- 简单介绍程序的翻译环境和执行环境
前言:我们常常在程序中看到编译,链接,执行,但是你真的了解过它们吗?作为一名未来的合格的程序员,应当能够对它们有清晰的认知.本文就将对它们进行浓缩精炼的介绍,让你面对它们的时候不再感到迷惑. 想要深入 ...
- 『中秋赏月』程序员用文心大模型带你玩转不一样的中秋
『中秋赏月』文心大模型带你玩转不一样的中秋 中秋 文心大模型使用 安装wenxin_api 利用ERNIE-ViLG文生图模型生成图片 下载生成的图片 使用ERNIE 3.0模型,生成有关中秋的佳句 ...
最新文章
- python3运行报错:TypeError: Object of type ‘type‘ is not JSON serializable解决方法(详细)
- 深入浅出面向对象和原型【番外篇——重新认识new】
- 【效率】神器工具:新一代多系统启动 U 盘装机解决方案
- Linux 、shell 时间函数 - 获取七天前所在周
- Leetcode PHP题解--D56 637. Average of Levels in Binary Tree
- 放弃第三方?苹果正自研iPhone调制解调器芯片
- Linux系统实现ICMP ping功能,并计算时延
- 手动升级 Confluence - 规划你的升级
- Eclipse开发环境设置(Maven+Spring MVC+Flex)
- 面试题:老师生日分析过程,能否建模用程序解答?
- android 控件覆盖关系,Android设置viewGroup和其子控件两者之间的焦点关系【原创】...
- 计算方法实验(二):龙贝格积分法
- Shell判断字符串是否为空
- 如何自制自平衡云台基于mpu6050,arduino输出三维倾斜角度的方法(含源码,库)
- winform打印html文件,c# 如何实现web打印插件
- 计算机网络实验如何设置无线路由器密码,怎么设置路由器密码 路由器设置密码方法【图文】...
- 逆波兰式求值 —Java
- 1w存银行一年多少利息_在银行存定期一万块一年有多少利息?
- 替换Android手机的开机动画,安卓技术宅系列之修改手机开机动画
- Win10配置pytorch深度学习环境