目录
前言:
1.程序的翻译环境和执行环境
在ANSIC的任何一种实现中,存在两个不同的环境:
- 翻译环境:在这个环境中源代码被转换为可执行的机器指令。
- 执行环境:他用于实际执行代码。
示意图如下所示:
2.详解编译+链接
2.1翻译环境
- 组成一个程序的每一个源文件通过编译过程分别转换成目标代码。
- 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准才C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2编译本身也分为几个阶段
2.2.1预编译(预处理)
在预编译阶段主要做以下的事情:
- 头文件的包含
- 注释的删除
- #define符号的替换
2.2.2编译
汇编阶段是将C语言代码转换成了汇编代码。
主要有以下的步骤:
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
2.2.3汇编
汇编阶段主要是将汇编指令转换成二进制指令。
2.2.4链接
主要有以下的步骤:
- 合并段表
- 符号表的合并和重定位
2.3运行环境
程序执行过程:
- 程序必须载入内存中,在有操作系统的环境中,一般这个操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码,这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址,程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程中一直保留他们的值。
- 终止程序,正常终止main函数,也有可能意外终止。
3.预处理详解
3.1预定义符号
- --FILE--:进行编译的源文件
- --LINE--:文件的当前的行号
- --DATE--:文件被编译的日期
- --TIME--:文件被编译的时间
- --STDC--:如果编译器遵循ANSIC,其值为1,否则就是未定义。
这些预定义符号都是语言内置的。
代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
//printf("%d\n", __STDC__);//不遵循ANSIC,所以未定义
system("pause");
return 0;
}
结果如下所示:
3.2#define
3.2.1#define定义标识符
语法:#define name stuff
在#define定义标识符的时候建议不要加上分号(;)这样容易导致问题。
3.2.2#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏(#define macor)。
宏的声明方式:#define name (parament-list)stuff。
注意:
- 参数列表的左括号必须与name紧邻。
- 如果两者之间有任何空白存在,参数列表就会被解释被stuff的一部分。
- 在定义最好给每一个参数都加上括号:#define ADD(x,y)((x) + (y))
3.2.3#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
- 在调用宏的时,首先对参数进行检查,看看是否包含任何理由#define定义的符号,如果是,他们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置,对于宏参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程。
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不能被搜索。
3.3#和##
3.3.1 #号
如何把参数插入到字符串中?
我们可以只用#,把一个宏参数变成对应的字符串。
代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define PRINT(val,format) printf("the value of " #val " is " format "\n",val)
int main()
{
int a = 10;
PRINT(a, "%d");
int b = 20;
PRINT(b, "%d");
return 0;
}
结果如下所示:
字符串是有自动连接的特点的。
如:printf(“hello ”“bit\n”);
输出为:hello bit
3.3.2 ##号
作用:
- 可以把位于它两边的符号合成一个符号。
- 它允许宏定义从分离的文本片段创建标识符。
注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define CAT(A,B) A##B
int main()
{
int Class1 = 100;
printf("%d\n", CAT(Class, 1));
return 0;
}
结果如下所示:
3.4带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用的这个宏的时候,就可能会出现危险,导致不可预测的后果,副作用就是表达式求值的时候出现永久性效果。
例如:
- x + 1;//不带有副作用
- x++;//带有副作用
3.5宏和函数的对比
宏通常被应用于执行简单的运算,比如在两个数中找到最大值,那么为什么不用函数来解决这个问题呢?
原因:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用,反之这个宏怎么可以用于整形、长整形、浮点型等可以用于>来比较的类型。
宏的缺点:
- 每次使用宏的时候,一份宏定义的代码便会插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于是与类型无关的,也就不够严谨。
- 宏可能会带来运算优先级的问题 ,导致程序容易出错。
3.5.1命名约定
- 宏名一般是都要大写。
- 函数名一般是不要全部大写。
3.6#undef
#undef是用于移除一个宏定义的。
如果出现一个名字需要重新被定义,那么它的旧名字首先要被移除掉。
3.7命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
3.8条件编译
在编译一个程序的时候,我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令。
常见的条件编译 指令:
1.
#if
#endif
2.多个分支的条件编译
#if
#elif
#else
#endif
3.判断是否被定义
#if defined(symbol)
#ifed symbol
或者:
#if !deifned (symbol)
#ifndef symbol
4.嵌套定义
#if defined (OS_UNIX)
#ifdef OPTION
unix_version_option1();
#endif
#ifed OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifed OPTION3
unix_version_option3();
#endif
#endif
4.文件包含
我们已经知道#include指令可以使另外一个文件被编译,就像它实际出现与#include指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
4.1头文件包含方式
4.1.1本地包含
#include“filename”:
采用“”括起来的就是本地包含。
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
4.1.2库文件包含
#include<filename>
采用<>括起来的就是库文件包含。
查找策略:
查找头文件直接区标准路径下去查找,如果找不到就会提示编译错误。
总结:
按照查找策略来讲对于库文件也可以使用“”的形式包含,但是这样做的查找效率就会低一些,当然这样也不容易区分到底是库文件还是本地文件。
4.2嵌套文件包含
如何避免头文件的重复引入:
我们可以使用条件编译来处理这类问题。
每个头文件的开头写:
#ifndef __TEST_H__
#deifne __TEST_H__
//头文件的内容
#endif //__TEST_H__
或者#pragma once
就可以避免头文件的重复引入了。
结束语:
这节中小编主要讲解了在C语言程序执行的过程以及程序的环境和预处理指令,希望对大家有所帮助,想要学习的同学记得关注小编和小编一起学习吧!如果文章中有任何错误也欢迎各位大佬及时为小编指点迷津(在此小编先谢过各位大佬啦!)