【C语言进阶】程序环境和预处理

目录

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

1.1 翻译环境

1.1.1编译过程

1.2运行环境 

2.预处理详解

2.1预定义符号 

 2.2#define

2.2.1#define定义的标识符 

2.2.2#define 定义的宏 

2.2.3#define 替换规则 

2.2.4#和##

2.2.5带副作用的宏参数

 2.2.6 宏和函数对比

2.2.7命名约定

2.3#undef 

2.4条件编译 

 2.5 文件包括

2.5.1 头文件被包含的方式 

2.5.2 嵌套文件包含


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

在ANSI C的任何一种实现中,存在两个不同的环境

        第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

        第二种是执行环境,它用于实际执行代码。

1.1 翻译环境

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

1.1.1编译过程

        编译过程从整体来看可以分为下图两个阶段:

又可细分为一下阶段:

        在编译中 符号汇总的含义是 将各个.c文件里的全局的符号如函数名等。汇编阶段的形成符号表是指将汇总的符号制成表单。链接里符号表的合并和符号表的重定位是将相同名字的合并,这个过程可以发现哪些是未定义的或者为声明的。

1.2运行环境 

程序执行的过程:

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

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

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

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


2.预处理详解

2.1预定义符号 

__FILE__    进行编译的源文件

__LINE__    文件当前的行号

__DATE__   文件被编译的日期

__TIME__   文件被编译的时间

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

使用:

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

结果:

 2.2#define

2.2.1#define定义的标识符 

        语法:#define name stuff (名字  内容)

        如 #define MAX 100 

        #define 定义的标识符在预编译阶段会被替换。

        int a = MAX 经过预编译之后就是 a = 100。

        这里有一个问题 define定义标识符时需要在最后加上 “ ;”吗?

        比如 #define MAX1 100;

                #define MAX2 100

        其实加不加 “ ;”在定义标识符的这行代码上是没有错误的,但是预编译替换时“;”也会加上,例如int  a = MAX1; 这条语句在预编译之后就会变为 int a = 100;; ,可以看到这里就会有两个“;”,这是错误的。所以建议不要加上“;”,这样容易导致问题。

2.2.2#define 定义的宏 

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

宏的声明方式:

 #define  name(参数)内容 ;

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

注意:参数的左括号必须与name紧邻;

           如果宏是一个数值表达式那么每个参数以及整体都要加上括号;

例: #define SQUARE(x)  x * x

        int x = 2;

        那么 SQUARE(x + 1)的值是什么呢? 我们预期的答案是 9;但是经过预编译之后他将变为:

x + 1 * x + 1 也就是2 + 1 * 2 + 1 =5;这就出现了错误。

所以用于对数值表达式进行求值的宏定义都应该将参数和整体加上括号,避免在使用宏时由于参数中的操作符或者邻近操作符之间不可预料的相互作用。

2.2.3#define 替换规则 

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

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

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

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

注意:

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

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

2.2.4#和##

# 可以把宏的参数变成对应的字符串 

 如:

#include<stdio.h>
#define PRINT(FORMAT, VALUE) printf("the value of "#VALUE" is " FORMAT "\n", VALUE)

int main()
{
	int i = 1;
	PRINT("%d", i + 1);
	return 0;
}

结果: 

    

##可以把位于两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符

如:#define ADD_TO_SUM(num,value)  sum##num += value

则ADD_TO_SUM( 5, 10); 作用就是sum5 增加10。

注意装:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

2.2.5带副作用的宏参数

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

例如:x + 1 不带副作用;

           x++ 带有副作用。 

下面看看宏参数具有副作用引起的问题。

#include <stdio.h>
#define MAX(x, y) (( x ) > ( y ) ? ( x ) : (y ))
int main()
{
	int x = 5;
	int y = 8;
	int a = 5;
	int b = 8;
	int z = MAX(x, y);
	int c = MAX(a++, b++);
	printf(" x = %d, y = %d, z = %d\n", x, y, z);
	printf(" a = %d, b = %d, c = %d\n", a, b, c);
	return 0;
}

结果:

 2.2.6 宏和函数对比

执行简单的运算常常用宏而不用函数;有以下原因

        1. 用于调用函数和从函数返回的代码可能比实际执行小型计算工作所需要的时间更长。所以宏比函数在程序的规模和速度方面有优势。

        2. 更为重要的是函数参数必须声明为特定类型,所以函数只能在类型合适的表达式上使用,反之这个宏可以适用于整型、长整型、浮点型等。宏与类型无关。

当然宏和函数比也有缺点:

        1.每次使用宏的时候,一分宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度。

        2.宏没办法调试。

        3.宏由于与类型无关,也就不够严谨。

        4.宏可能会带来运算符优先级的问题,导致程序出现错误。

2.2.7命名约定

        一般来讲函数的宏的使用语法很相似,所以语言本身没办法帮我们区分二者。那我们平时有一个习惯是:

        把宏名全部大写。

        函数名不要全部大写。 

2.3#undef 

这条指令用于移除一个宏定义。

#undef NAME // 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

2.4条件编译 

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者不编译,这是我们就要用到条件编译指令。比如:调试性代码,删除可惜,保留又碍事,所以我们可以选择性的编译。 

#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
	}

	return 0;
}

常见的条件表达式:

1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值
如:
#define __DEBUG__ 1
#if __DEBUG__
//...
#endif

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

3.判断是否被定义
#if define(sumbol)
#ifdef symbol

#if !define(symbol)
#ifndef symbol

4.嵌套指令
#if define(OS_NUTIX)
	#ifdef OPTION1
		nuix_version_option1();
	#endif
	#ifdef OPTION2
		nuix_version_option2();
	#endif
#elif define(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif

 2.5 文件包括

        我们知道,#include指令可以使另外一个文件被编译。预处理器先删除这条指令,并且包含文件的内容替换,这样一个源文件被包含几次,那就实际被编译几次。

2.5.1 头文件被包含的方式 

 头文件被包含的方式有两种:

1. 本地文件包含

        #include “test.h”

        查找策略:先在源文件所在目录下查找,如果该头文件未被找到,编译器就像查找库函数头文件一样在标准位置查找头文件。

2.库文件包含

        #include<stdio.h>

        查找头文件直接去标准路径下去查找,如果找不到就提示编译错误;

        大家可以发现无论是 < > 还是“ ”包括头文件他们都会在标准位置查找,那么是不是可以说我们以后就可都用“ ”来包含头文件。其实是可以的, 但是如果是包含库文件那他也会先在源文件目录下查找,如果没有才会去标准位置查找,这样就会带来效率问题,也不容易区分是库文件还是本地文件了。

2.5.2 嵌套文件包含

 如果出现这样的场景:

这种情况下程序就会包含两次comm.h的内容,这样就造成了文件内容的重复,那该怎样解决呢?

使用条件编译:

每个头文件的开头这样写

#ifndef  __TEST_H__

#define __TEST_H__

//头文件的内容

#endif

或者 #pragma once

这样就可以避免头文件的重复引用了。


本篇博客就到这里了,感谢大家的观看,以上若用错误恳请各位指正,感激不尽!!! 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值