【c语言】:程序的编译

先说在ANSIC任何一种实现中,存在两种不同的环境

  • 翻译环境,在这个环境下源代码被转换成可执行的机器指令
  • 执行环境,用于实际代码运行

一、程序编译与链接

 首先每一个源程序都会经过编译器转换成对应的目标代码,然后每个目标文件由链接器捆绑在一起,形成一个单一可执行的程序,同时也会引用c语言中任何被改程序所用到的函数,而且也可以搜索程序员的个人程序库,将其链接到其中。

1.1 编译的各个阶段

 

预编译阶段内容:

1.头文件包含

#include 预处理指令

2.define定义的符号替换

#define 预处理指令

3.注释删除

以上这些都是文本操作

编译阶段内容:

把c语言代码翻译成了汇编代码

1、语法分析

2、词法分析

3、语义分析

4、符号汇总

汇编阶段:

把汇编指令翻译成了二进制的指令

形成符号表,这样就能够找到源文件外部的符号(只能汇总全局符号)

链接阶段:

1、合并段表

2、符号表的合并和重定位

 将上述内容展现于下图:

1.2 运行环境

程序执行的过程:

1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

2. 程序的执行便开始。接着便调用main函数。

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

4. 终止程序。正常终止main函数;也有可能是意外终止。

二、预处理详解

2.1 预定义符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

c语言本身就有一些已经预定义的符号,例如上述符号。

2.2 #define

2.2.1 #define定义标识符

#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定义标识符号的时候,后面要不要加上分号;

答:最好不要,因为宏定义标识符,并不会进行计算,在编译阶段进行的是内容替换,比如以下代码

#define max 100;
int main()
{
    int a=max;//这里其实等价于a=100;;可以看出多了一个分号,很容易出现bug
    return 0;
}

2.2.2 #define定义宏

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

 

#define name(parament-list) stuff
//name表示名字
//parament-list 是以逗号隔开的参数
//宏的具体内容

例子
//#define max(a,b) a+b

需要注意,宏的括号一定要跟名字紧邻着,否则会被理解为stuff的一部分内容。

这里同样也要注意因为宏是直接进行文本替换,然后才在程序中发生计算,所以如果不按照标准规定写宏,可能会产生bug

#define SQUARE(x) x*x

int c=5SQUARE(5+1);
//我们预期这里的内容是36,但是最终结果是11,是因为实际计算的是
//5+1*5+1==11

所以在写的时候我们应该尽可能带上括号,防止因为优先级的问题出现bug

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

写成这样的模式基本上就不会有太大的问题

2.2.3 #define替换规则

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

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。

2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。

3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。.

注意:

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

2.2.4 #和##

 思考一个问题:如何把参数插入到字符串中?

char* p = "hello ""bit\n";
printf("hello"," bit\n");
printf("%s", p);

这里输出的是hello bit,字符串是具备自动连接的特点。

所以我们是不是可以这样写代码呢?



#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
int main()
{
	int i = 10;
		PRINT("%d", i + 3);
}

可以看到中间的i并没有替换成对应的数字,而是直接以i的形式插入到了字符串中,这就是#的作用,避免被转换成对应的字符,而是直接以参数本身形式插入字符串。

##的作用

##就是把name跟num连接起来,比如图中的代码,class105,在打印的时候我们选择了printf函数打印,CAT(class,105),这个宏的作用就是把class跟105-->class105,可能就是用在某些你需要进行拼接符号的场景。

3.2.5 带副作用的宏参数

简单来讲就是宏在执行的过程中,参数自身的值会发生变化,这个就叫做带副作用的宏的参数。

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

int main()
{
    int x=5,y=8;
    int c=MAX(x++,y++);
    printf("%d ",c);
    return 0;
}
//这里输出的是多少,9嘛?
//首先带入  ((5++)>(8++)?(a++):(b++)) 首先是进行ab大小的比较,在这里比较之后,
//a跟b跟别变成了6和9,然后执行后面的b++,最终的结果应该a=6 c=9 b=10

 

 为了简洁明确的代码,所依最好的建议不要写这种带有副作用的宏

3.2.6 宏与函数的对比

可以感受到宏跟函数有一定的相似性,接下来我们将进行系统比较

属性#define定义宏函数      
代码长度每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每 次使用这个函数时,都调用那个、地方的同一份代码
执行速度       更快存在函数的调用和返回的额外开 销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。表达式的求值结果更容易预 测。
带 有 副 作 用 的 参 数惨数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一 次,结果更容易控制。
参数类型 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。
调试无法调试涵数是可以逐语句调试的
递归无法递归可以递归

 

3.3 条件编译

1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分支的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
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

3.4 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方 一样。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个源文件被包含10次,那就实际被编译10次。

3.4.1 头文件的包含方式

本地文件

#include"test.h"

先从源文件所在目录进行查找头文件,然后再到标准函数库头文件所在目录下查找

#include<stdio.h>

直接从标准函数库头文件所在目录下查找

总的来说就是""的引用方式查找范围更广

3.4.2 避免头文件被重复引用

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

只要在每次引用头文件的时候就会避免头文件被重复引用,敲重点!!!

以上就是小编目前所学程序的编译模块知识梳理内容,希望对大家有帮助。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值