目录
一.程序的两大环境
1.翻译环境
2.执行环境
二.C语言程序的编译+链接
1.翻译环境
前面我们已经提到翻译环境和执行环境.
而对于翻译环境,从一个源文件得到可执行程序,又可以进一步细分为很多个过程.
源文件首先通过编译器编译,生成目标文件.(编译又可划分为预编译和编译两个过程.)
最后,每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
PS:同时,在链接的过程,链接器同时也会引入标准C函数库中任何被该程序所用到的函数(像
我们引入的头文件<stdio.h>中的printf,scanf函数)而且它可以搜索程序员个人的程序库,将其需
要的函数也链接到程序中.
2.翻译环境简略过程介绍
这里细节非常多,就不一一进行介绍,只简单介绍汇编中符号表的形成.
类似全局变量g_val,函数名Add,还有我们main函数的main,在机器看来都是一个个符号.
对于每一个源文件,都会形成相应的符号表,也就是把它们全部收集起来,并分配相应的地址.
而在链接过程中,我们会把符号表重新进行合并,比如我们在Add.c这个源文件中有Add函数的声
明,在test.c这个源文件也有Add函数的调用,因此两个源文件生成的符号表,都会有Add这个符
号,在合并的时候,就会将两个合成一个.
在执行的时候,最终的符号表就只会有单一一个Add符号,这样也实现了,在test.c中调用Add函数的基础.
三.预定义符号介绍
在C语言中,事先已经预定义了一些符号,如下面所示.
四.预处理指令 #define
4.1 define定义标识符
4.2 define定义宏
比方说我定义了一个宏SQUARE,用作将传进去的数字平方.
在实际运算过程中,宏定义中所有出现的x都会被替换成5,而整个SQUARE(5)又会等效为5*5,
最后输出25的结果.
注意:
但是实际上运用,还有很多需要注意的地方,比如说同样是上面的例子.
这次我没有直接传进去一个5,而是传进去4+1,因为在我们理解中,4+1得到5,然后再被传入.
但实际上呢,并不是如此,答案输出了9.
因此实际上,4+1并没有进行计算,而是直接代入了式子中
而乘法优先级高于加法,因此最后输出才会是9.
为了解决这个问题,需要在x外面都加上一个括号.
但这实际上还不够,我们再看下面这个例子.
按我们预期,DOUBLE(5)会得到5的2倍,也就是10,乘以10,这样最终输出就是100.
但实际上输出值是55,差别还是蛮大的.
这是因为DOUBLE(5)会被直接替换,而不是输出一个值,然后再返回回去.
为了解决这个问题,需要在整个表达式外面再加一个括号.
因此,对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
1.程序更容易读懂
程序中假如可能会存在各式各样的数据,而程序一旦变长,这些数字对我们来说,很多时候就变成
了魔法数,假如我们使用宏,则会给这个数字,相应对应的含义,使程序可读性更强.
2.程序易于修改
这在我们以后实现小游戏的时候,就会更加深刻体会到,假设五子棋程序中,有很多位置都出现了
5,代表棋盘大小的含义,如果我们要改变棋盘的大小,一个个改,是非常不方便的.但假如一开
始,我们就定义一个#define SIZE 5,后面直接调整宏定义的标识符即可.
3.避免前后大小不一致或键盘输入错误
比如程序中有pi=3.14158625,那经常使用,则可能会输入错误,直接定义一个宏,
则不会出现这样的问题.
4.3 宏定义替换规则
1. 宏参数和#define 定义中可以出现其他#define定义的符号.
但是对于宏,不能出现递归.
4.4 宏与函数对比
宏与函数相比,既有优点,也有明显的缺点,如何看待宏,还需要多方位思考和使用.
4.4.1 优势
1.程序可能会稍微快一些
因为define是直接在预处理阶段,就直接全部替换,而函数的使用,还需要函数栈帧的开辟等等一
系列的动作,因此在简单功能实现,比如比较两数大小上,使用宏速度上可能更高效一些.
2.宏是类型无关的
函数的声明需要指定传进去参数的类型,但我们可以很轻易发现,宏定义是类型无关的,比如同样
是比较两个数的大小,用MAX宏来实现,则可以实现long,double,float不同类型的数的比较.
4.4.2 劣势
1.编译后的代码通常会变得很大,可能大幅增加程序的长度,宏使用越频繁,问题往往也就越严重
那对应实际上就是这样一段代码,程序的长度大幅度增加.
2.宏无法调试,同时也没有类型检查
宏没有类型检查,即是它的优点,同时也是缺点.
没有类型检查,程序有时候出错,因为无法编译,往往也会成为一个头痛的问题.
3.无法用指针指向宏
我们可以用指针指向函数,也就是我们常说的函数指针,这在特定编程条件下会非常有用.
但我们知道宏在预处理阶段,就已经全部被删除了,所以不存在类似指针的说法.
4.宏可能不止一次计算它的参数,会带来诸如运算符优先级的问题,导致程序容易出现错误.
当i>j,i++会在式子中执行两次,造成我们不想要的结果.
4.5 #与##运算符
C语言中,宏定义可以包含两个专用的运算符#,##,两个运算符都会在预处理阶段被处理掉,
下面将分别介绍它们.
4.5.1 #运算符
#字符串能够将宏的一个参数,转化为字符串常量,俗称"字符串化(stringzation)".
我们知道在C语言中,相邻字符串会被合并.
而#操作符可以把stuff,转变为字符串
比如上面这个例子,#VALUE会被转成i+3的字符串,然后粘合起来.
4.5.2 ##运算符
##则可以把两个记号"粘合"起来,形成一个记号.
我们先通过一段和宏定义,确定max函数的具体框架,只需要往里面传入不同类型,比如float,
double等等,就可以实现两行代码,定义多个不同的比较函数.
4.6 命名约定
对于宏,一般所有字母都为大写
对于函数,一般以字母小写为主
4.7 宏可以使用#undef指令取消定义
五.条件编译
1.单分支条件编译
2.多分支条件编译
3.判断是否被定义
假如DEBUG被宏定义过,则执行代码,否则不执行.
而上述代码还可以进一步进行简化,写成下列的形式,效果是相同的.
同理,我们也可以猜出#ifndef的作用效果.
4.嵌套指令
将我们上面学到的三种情况,全部结合起来,就是嵌套指令.
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
六.条件编译的应用
6.1 文件包含
1.本地文件包含
但这样会存在一个问题,处理包含问题时,预处理器往往是先删除这条指令,并直接用包含文件的内容替换.
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif
那检测到__TEST_H__ 已经被定义过,则不会再重新编译这一段头文件.
而#pragma once也同样可以实现这个功能.
在vs2019中,在创立头文件时,会自动包含这一句代码.
6.2 编写在多台机器或多种操作系统可移植的程序
在程序开头定义这样一个宏(有且只有一个),则可以指明程序运行在哪个操作系统上.
6.3 为宏提供默认定义
6.4 临时屏蔽含注释的代码
我们不能用注释,注释掉含注释的代码,而采用条件编译,则可以轻松实现这个功能.
七.其它指令
#error
当机器遇到ERROR指令的时候,它会显示一条包含消息的错误消息.
有些编译器底下,甚至会直接终止编译,而不再检查其它错误.
通常它时配合条件编译进行使用,比如说我们上面提到得不同操作系统的例子.
#pragma
我们学过利用它调整默认对齐数,保证头文件只被调用一次,自定义编辑信息等等.
而实际上,在《C语言程序设计现代方法》一书中这样说到:
#line
改变程序行编号方式的.(通常程序行编号是从1,2到...最大整型所能表示的数)
一般有两种用法
使用后,它的后一行则从n开始进行编码.
指令后面的行会被认为来自文件,行号从n开始,n可以用宏来指定.