C的追忆 (7)——程序环境和预处理

终于,我们在这里迎来了本系列最后一章,这里是可以说是小编C语言学习的一个里程碑,下面,我们就来看一下这最后的程序环境和预处理吧!

目录

一. 程序的翻译环境

二. 程序的执行环境

2.1 翻译环境

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

2.3 运行环境

三. 预处理详解

3.1 预定义符号

3.2 #define

3.2.1 作用

3.2.2 命名约定

3.2.3 #define 替换规则

3.2.4 #define 定义标识符

3.2.5 #define 定义宏

 3.2.6 #和##

(1)# 的作用

(2)## 的作用

 

3.2.7 带副作用的宏参数

 

3.2.8 宏和函数对比

3.3 #undef

3.4 命令行定义

3.5 条件编译

3.6 文件包含

3.6.1 头文件被包含的方式:

(1)本地文件包含

(2)库文件包含

3.6.2 嵌套文件包含

四. 其他预处理指令


 

一. 程序的翻译环境

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

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

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

c78a8aab9fa3492181eb5eb8c166d5d6.png

二. 程序的执行环境

2.1 翻译环境

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

编译过程可以参照下图:

b64f887197b1423a96a19fd62bc0be12.png

 

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

e4b04f0e37b3450dbc8c4c6b08a7367e.png
 
见下图:
5ba2d18eb2aa4c37b3c458085b355684.png
8c7164e5c09e43e9991ad4f6ee4d037e.png
 

 

2.3 运行环境

程序执行的过程:

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

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

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

 

三. 预处理详解

3.1 预定义符号

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

注意: 以上横杠都是双横杠。

这些预定义符号都是语言内置的。

大家有兴趣可以试试这个案例代码:

printf("file:%s line:%d\n", __FILE__, __LINE__);

 

3.2 #define

3.2.1 作用

#define是C/C++编程语言中的预处理指令,主要用于定义常量、宏以及函数等。它的作用主要有以下几点:

  1. 定义常量:使用#define可以定义一个标识符(通常是一串有意义的名字),并且给它赋一个确定的值。例如,#define PI 3.14159就是定义了一个名为PI的常量,其值为3.14159。

  2. 定义宏:宏是一种在编译阶段进行文本替换的预处理指令。例如,#define MAX(a, b) ((a) > (b) ? (a) : (b))定义了一个求最大值的宏。在代码中使用MAX(x, y)时,预处理器会将其替换为((x) > (y) ? (x) : (y))

  3. 条件编译:#define还可以用于条件编译,通过判断某个宏是否被定义来决定是否编译某段代码。例如,#ifdef DEBUG#endif之间的代码只有在DEBUG被定义时才会被编译。

  4. 函数声明与定义:虽然不常见,但有时我们也可以使用#define来定义简单的函数。

 

3.2.2 命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

 

3.2.3 #define 替换规则

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

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

        3.由于#define是直接替换,替换过去后代码会受符号优先级影响导致与本来结果不同,所以用于对数值表达式进行求值的宏定义都应该用按照所需要进行运算的优先级加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

 

3.2.4 #define 定义标识符

定义示例代码:

#define name stuff

注意:

        #define最好不要不要在最后加上 ; ,否则#define会将 ; 一起替换过去 , 这样容易导致问题。

 

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


 3.2.6 #和##

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

(1)# 的作用

使用 # , 把一个宏参数变成对应的字符串
运行以下代码:
int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);
代码中的 #VALUE 会预处理器处理为:
"VALUE" .
最终的输出的结果应该是:
 
the value of i+3 is 13

(2)## 的作用

##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) \
    sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
注:
        这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

 

3.2.7 带副作用的宏参数

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

例如:

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

注意:我们编写是尽量不要出现这类有副作用的代码,否则很容易出问题。

 

3.2.8 宏和函数对比

函数和宏各有各的有点下面我们来看一下两者的区别。

 

#define定义宏

函数

代  码  长  度

每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长

函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码

执 行 速 度

 

更快

 

存在函数的调用和返回的额外开 销,所以相对慢一些

操 作 符 优 先 级

 

宏参数的求值是在所有周围表达式的上下文环境里,  除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括

号。

函数参数只在函数调用的时候求 值一次,它的结果值传递给函

带 有  副  作  用  的  参  数

  

参 数 类 型

  

调 试

  

递  归

宏是不能递归的

函数是可以递归的

 

3.3 #undef

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

 

3.4 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

其中最典型的就是我们耳熟能详的linux了

a5bf2496981c4b1c981c23cf068e832c.png

 

3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
 
比如说:
        调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
例:
#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__
     }
     return 0;
}
常见的条件编译指令:
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.6 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。

3.6.1 头文件被包含的方式:

(1)本地文件包含

例:
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。 如果找不到就提示编译错误。

(2)库文件包含

例:

#include <filename.h>
查找策略:直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的, 可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

 

3.6.2 嵌套文件包含

如果出现这样的场景:
 
7fe32b9256f44a3f8a5d7988c55171fc.png
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

 

如何解决这个问题?
答案:条件编译。
每个头文件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

或者:

#pragma once
就可以避免头文件的重复引入。


四. 其他预处理指令

#error
#pragma
#line
#pragma pack()  \\在结构体部分介绍过。
...
 
等等还有很多预处理指令就不在给大家详细介绍,好了,C语言的学习到这就算告一段落了,谢谢大家阅读!
 

 

 

 

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值