C语言—程序环境和预处理

翻译环境和执行环境

翻译环境:在此环境中源代码被转换为可执行的机器指令

执行环境:用于实际执行代码

C语言程序的编译和链接

以下简单介绍C语言的编译和链接,更详尽的知识可参照编译原理相关书籍。

每个源文件(后缀为.c)会单独通过编译器形成对应的目标文件(后缀为.obj),所有目标文件和链接库通过链接器形成可执行程序(后缀为.exe)。

其中编译包括预编译(预处理)、编译以及汇编。

VS是集成开发环境,当点击编译后其实完成了全部编译和链接的过程直接生成了可执行程序。而在Linux环境gcc这个编译器下,可通过相应命令选择执行的步骤。以下讨论Linux为例讨论编译和链接。

如 在终端输入 gcc test.c -E是执行完预编译就停止,如果输入 gcc test.c -E-o test.i则是将预编译的结果生成一个test.i的文件。预处理时会进行一些文本操作如头文件的包含,define定义符号的替换,注释的删除,形成新的文件即test.i。

在终端输入 gcc test.c -S或 gcc test.i -S,即进行到编译这一步就停止。编译这一步将C语言代码转换成汇编代码生成一个test.s的文件。这是一个复杂的过程包括语法分析,词法分析,符号汇总,语义分析。其中,符号汇总会将全局的符号汇总出来,不论是函数名还是全局变量名等,不包括局部变量。

在终端输入 gcc test.s -c会进行汇编,汇编是在test.s文件的基础上进行处理生成test.o文件,将汇编指令转化为二进制指令同时形成符号表,会给编译汇总的符号分配地址。

链接又分为合并段表和符号表的合并和重定位。每一种文件都有自己的特殊的格式,在Linux中后缀为.o文件的格式为elf格式。最终生成可执行程序也是elf格式。

程序的运行环境

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

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

3.开始执行程序代码

2.终止程序

详解预处理

1.预定义符号

__FILE__   进行编译的源文件

__LINE__  文件当前的行号

__DATE__  文件被编译的日期

__TIME__  文件被编译的时间

__STDC__  如果编译器遵循ANSI  C,其值为1,否则未定义

例如:

int main()
{
    int i = 0;
    for(i = 0; i < 10; i++)
    {
        printf("file:%s i=%d\n",__FILE__,i)
    }
    return 0;
}

2. #define 定义的(宏)在预处理阶段会被替换

#define NUM 1000
#define STR "hello"

有如下替换规则:

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

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

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

例如:

#define M 100
#define DOUBLE(X) ((X)+(X))

int main()
{
    DOUBLE(M+2);
    return 0;
}

会先检查参数,将参数M先替换成100。

注意:1.在#define定义的这一行最后不要加分号!

           2.宏是替换,而不会计算比如

#define SQUARE(X) X*X

int main()
{
    int r = SQUARE(5+1);
    printf("%d\n",r);
    return 0;
}

     输出的结果为5+1*5+1=11,而非6*6=36。所以在定义宏的时候最好加上括号:

#define SQUARE(X) ((X)*(X))

        3.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

        4.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

#和##

 如何把参数插入到字符中?

#define PRINT(N) printf("the value of"#N" is %d\n",N)

inr main()
{

    int a = 10;
    PRINT(a);
//相当于printf("the value of""a" "is %d\n",N);
    int b = 20;
    PRINT(b);
//与a同理
    return 0;
}

以上代码用函数无法实现。,其中#N的作用是把传入的参数加双引号,变成字符。即把参数插入了字符串中。

以下代码的作用是把两个参数合并到一起打印。

#define CAT(Class,Num) Class##Num

带副作用的宏

 使用宏时要避免出现副作用

#define MAX(x,y) ((x)>(y)?(x):(y))

int main()
{
    int a = 5;
    int b = 4;
    int m = MAX(a++,b++);
// 实际是这个式子 int m = ((a++)>(b++)?(a++):(b++))
    printf("%d",m);
    printf("%d",a);   
    printf("%d",b);   
//正确答案为 6,7,5

 因为结果为第二个a++,所以第二个b++不会执行。导致a++执行了两次,而b++只执行了一次。

        在函数与宏的对比上,对于小型的轻量的计算用于调用函数和从函数返回的代码可能比实际执行这个小型的计算工作所需要的时间更多,所有有时候宏比函数在程序的规模和速度方面更胜一筹。更为重要的是,函数的参数必须声明为特定的类型。而宏与类型无关。同时,宏也有缺点。因为调用一次宏就会将宏定义的代码插入,可能大幅增加代码长度,而且宏无法调适。因为宏也不具有类型,不够严谨。

此外宏还可以实现一些函数无法实现的操作,如:

#define MALLOC(num,type) (type*)malloc((num)*sizeof(type))

int main()
{
    //malloc(40);想开辟一个四十个字节的空间
    //malloc(10,int);这样的表示更直观清晰,可是malloc函数不支持
    //于是利用宏
    int*p = MALLOC(10,int);
    //等同于int*p = (int*)malloc((10) * sizeof(int));
    return 0;
}

函数是无法做到传类型的,而宏可以。 

条件编译

在编译程序时,可用条件编译,编译或放弃编译一条(一组)语句 。

在代码中有一些是为了方便调试,实际并不需要。我们就可以使用条件编译。例如:

#include<stdio.h>
#define __DEBUG__

int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0;i < 10;i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功
#endif //__DEBUG__  这个注释是为了说明和#ifdef是一对
	}

	return 0;
}

当定义了__DEBUG__时,就会编译printf语句。当把#define __DEBUG__注释掉时,printf语句则不会被编译。 

以下是常用的条件编译指令

//1. 
#if  常量表达式//if后为真,中间代码编译,为假在预处理阶段中间代码就被删除了
//....
#endif

//2 多个分支的条件编译
#if 常量表达式
	//....
#elif 常量表达式
	//...
#else
	//...
#endif

//3 判断是否被定义
#if defined(symbol)
#ifdef symbol //这两句意义相同

#if !defined(symbol)
#ifndef symbol

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值