程序环境和预处理

1. 程序的翻译环境和执行环境

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

2. 详解编译+链接

2.1 翻译环境

程序编译过程:AIt

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

2.2 编译本身也分为几个阶段

sum.c

int g_val = 2016;
void print(const char *str)
{
 printf("%s\n", str);
}

test.c

#include <stdio.h>
int main()
{
 extern void print(char *str);
 extern int g_val;
 printf("%d\n", g_val);
 print("hello bit.\n");
 return 0;
}


如何查看编译期间的每一步发生了什么呢?
test.c

#include <stdio.h>
int main()
{
 int i = 0;
 for(i=0; i<10; i++)
 {
 printf("%d ", i);
 }
 return 0;
}
  1. 预处理 选项 gcc -E test.c -o test.i
    预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。
  2. 编译 选项 gcc -S test.c
    编译完成之后就停下来,结果保存在test.s中。
  3. 汇编 gcc -c test.c
    汇编完成之后就停下来,结果保存在test.o中。

2.3 运行环境

程序执行的过程:

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

3. 预处理详解

3.1 认识预处理指令

  1. C语言中的预处理功能主要是指可以在C语言源程序中包含各种编译命令,用这些编译命令在代码编译前执行,所以这些命令被称为预处理命令。其实现的功能就是C语言的预处理功能
  2. 预处理命令实际上并不是C语言的一部分,而是C语言编译系统的一个组成部分。在C语言发明之初,用来保证C代码的效率和汇编语言接近,是让C语言编写的代码更加贴近机器语言的一个设计考量。在这之后,因为CPU效率越来越高,其他编程语言,基本上就不需要这个特色了
  3. C语言编译系统就像一个英文格式检查和翻译系统,其会先对源代码进行词法和语法分析,判断代码的格式是否符合规范。例如,在程序中对常量的赋值,变量的数据类型不匹配等语法上的错误,其都会检查出来,在确认没有格式上的错误后,再把内容翻译成计算机能识别的目标代码
  4. 预处理命令是编译系统的控制命令,编译系统根据其要求先对代码进行优化,然后再进行编译,使程序更加简练清晰,常用的预处理命令有
  • 文件包含命令
  • 条件编译命令
  • 宏定义命令
  1. 所有的预处理命令在程序中都是以“#”开始的,每一条命令单独占用一行,该行不再包含其他预处理命令和语句,比如已经接触很多的:
    #include //命令
    注意:当预处理命令比较长,需要分行时,在前一行的最后要加上“\”续行符。预处理命令可以放在程序文件中的任何位置,根据需要决定。

3.2#define

3.2.1 #define定义宏

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

下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

比如:

#define SQUARE( x ) x * x

这个宏接收一个参数 x .
如果在上述声明之后,你把

SQUARE( 5 );

置于程序中,预处理器就会用下面的表达式替换上面的表达式:

5 * 5

注意:
这个存在一个问题,观察下面的代码段:

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

大家来看看这个结果是多少,你们可能觉得这段代码将打印36这个值,但事实上它打印的是11。为什么?
因为在替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了
printf (“%d\n”,a + 1 * a + 1 );
这样就比较清晰了,因为替换产生的表达式并没有按照预想的次序进行求值。

那怎么才能解决这个问题呢?很简单,在在宏定义上加上两个括号,这个问题便轻松的解决了:

#define SQUARE(x) (x) * (x)

这样预处理后就产生了预期的结果:

printf ("%d\n",(a + 1) * (a + 1) );

这里还有一个宏定义:

#define DOUBLE(x) (x) + (x)

定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

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

这又将打印什么值呢?
注意:这里看上去我们加了括号避免了之前的问题,看上去结果应该是100才对,但是在计算的时候结果却是55

其实解决这个问题的思路跟上一个一模一样,就是总体加上括号就好了:

#define DOUBLE(x) ((x) + (x))

总结:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

3.2.2 #define替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

3.2.3 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

例如:

x+1;//不带副作用
x++;//带有副作用

MAX宏可以证明具有副作用的参数所引起的问题。

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

这里我们得知道预处理器处理之后的结果是:

z = ( (x++) > (y++) ? (x++) : (y++));

所以结果为:

x=6 y=10 z=9

所以对于带副作用的宏大家还是少用为好,一不小心就会犯错。

3.2.4 宏和函数对比

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?

有两点原因:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
    所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。
    所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
    宏是类型无关的

3.2.5 使用宏定义的优缺点

使用宏定义有如下几个优点

  • 使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量。
    例如:有时候在多个源文件中,都需要3.1415这个数值常量。如果不定义PI来代表,不仅麻烦,而且容易写错。而如果源程序中定义了PI来代替这个值,则只要记住一个宏名,这样就不容易出错
  • 使用宏名可以达到一改全改的效果。
    例如:在程序中只需要定义一个宏,就可以在程序中全部的地方使用这个宏所代表的值,而如果需要修改这个值,只需要在宏定义的地方修改一下,整个程序代码中所有使用该宏的地方都被修改,用起来十分的方便
  • 在使用宏定义时,可以引用已定义的宏名,这样可以层层替换

优点说完了,接下来说说宏定义的缺点

  • 宏定义的宏名只是一个字符串替换的置换,并不做语法检查
    例如:如果把PI写成如下形式
#define PI 3.i4i5

即把数字写成了小写字母i,预处理时一样可以使用,只有在编译时会自动报错

  • 宏并不是函数,宏并不是语句,宏并不是类型定义,因此其不能对参数进行有效性的检测
  • 使用宏次数很多时,宏展开后,源程序变大,而函数调用不会使程序变大

4. 条件编译

条件编译命令的作用是定义源文件中的某些编译代码要在一定的条件下才参与编译,如果条件不满足,则不编译。利用条件编译可以使同一个源文件在不同的编译条件下,产生不同的目标程序。

条件编译命令和条件语句非常相似,只有条件编译命令的前面有一个“#”。常用的条件编译命令的格式有如下3种:

  • 1.ifdef-else-endif 格式
#ifdef 标识符
       代码段1
#else 
       代码段2
#endif
或者
#ifdef 标识符
       代码段1
#endif             

其中,ifdef,else,和endif是关键字 ,代码段是由若干条C语句或者预处理命令组成的。其编译时,过程如下:
当标识符被宏定义过,则对代码段1进行编译,生成目标程序;如果没有被定义,则对代码段2进行编译;在省略#else时,只有当标识符被定义时,才对代码段1进行编译

  • 2.ifndef-else-endif 格式
#ifndef 标识符
        代码段1
#else    
        代码段2
#endif
或者
#ifndef 
        代码段1
#endif

其中,ifndef,else,endif是关键字,其他和前面的格式一样。不过,编译时的过程是不同的。其过程为:
当标识符未被宏定义过,则对代码段1进行编译,生成目标程序;如果标识符被宏定义过,则对代码段2进行编译;在省略#else时,只有当标识符未被定义时,才对代码段1进行编译

  • 3.if-else-endif 格式
#if 常量表达式1
      代码段1
#elif 常量表达式2
      代码段2
. . .
#elif 常量表达式n
      代码段n
#else
      代码段n+1
#endif

其中,if,else,elif,endif是关键字,#if只有一个,#elif可以有很多,也可以没有。#else可以有一个,也可以没有。其和if…else条件语句执行差不多,当常量表达式1为真时,编译代码段1,否则判断常量表达式2,如果为真,编译代码段2,如果常量表达式没有一个为真,则编译代码段n+1,这时如果没有#else,就编译整个代码后面的代码。

好了,今天博主就分享到这里,希望大家多多点赞评论支持博主。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值