C-程序环境和预处理

一.程序的环境

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

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

二.翻译环境

用过编译软件的你们肯定都有过这样一个流程

建立test.c文件-->写main函数-->输出“hello world”-->生成解决方案.exe

那么有没有产生过疑问?test.c究竟是怎么生成test.exe的,编译器干了什么?

这篇文章接下来会阐述我知道的内容,但不要指望一篇文章就能全部讲通,如果想要有更深的了解,建议去看看编译原理相关书籍

每个.c文件生成.exe文件要先经过编译生成目标文件.obj(vs下),然后再链接到一起生成可执行程序.exe

编译过程

编译分为三个阶段:预处理,编译,汇编

可以参考下图理解

现在建立了三个文件----test.c用来运行程序,add.c用来定义函数,add.h用来声明add.c中的add函数


注:头文件是不会参与编译的


预处理和编译的过程很好理解(实际上要比上图复杂的多),汇编生成的符号表有什么用呢,里面的内容是什么呢?

我们知道,每个全局变量都有自己的一块空间,每个函数也有自己的空间,这块空间的地址就在它们被定义的文件里,也就是说x和add函数的地址被存放在add.c文件中,形成了类似于下面的这个表

而test.c文件需要使用全局变量x和函数add,于是它声明了x和函数add(分别用extern和展开的add.h文件声明的),相当于告诉编译器现在有一个int类型的变量x和一个函数add,于是test.o文件的符号表先给这两个“外来人员”填上了虚假的地址

链接过程

如何把多个目标文件汇总到一起生成一个可执行文件呢?这就是链接要做的工作了

test.o和add.o文件在Linux下是一种elf的文件格式,也就是它们被划分成一段一段的,每一段都有自己的功能,然后在链接的时候段表就会合并,而符号表就会合并成这样

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,比如test.c 用到的printf函数

三. 运行环境

程序执行的过程:

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

四.预处理详解

4.1 预定义符号

__FILE__

进行编译的源文件

__LINE__

当前行号

__DATE__

文件被编译的日期

__TIME__

文件被编译的时间

__STDC__

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

__FUNCTION__

查看当前执行函数的函数名

可以用下面的代码进行验证

int sub(int a, int b)
{
    printf("当前执行函数为%s\n", __FUNCTION__);
    return a - b;
}
int main()
{
    printf("file is %s,line is %d\n",__FILE__,__LINE__);
    printf("date is %s,time is %s\n", __DATE__, __TIME__);
    printf("当前执行函数为%s\n", __FUNCTION__);
    sub(1, 2);
    return 0;
}

运行结果

4.2 #define的使用

4.2.1 #define定义标识符

定义格式

#define name stuff

举例

#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //表示死循环
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

在预处理的时候这些被定义的标识符都会被stuff替换

int main()
{
    printf("%d", MAX);//printf("%d",1000);
    do_forever;//for(;;);
    return 0;
}

注意:#define定义标识符的时候,最好不要加;

来看下面这段代码

#define X 3;

printf("%d",X); //会被替换成printf("%d",3;);
//编译器报错

#define定义标识符是纯文本替换,所以为了避免导致问题,最好不加;

4.2.2 #define定义宏

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

语法格式

#define name(符号表)stuff

其中符号表的符号可能会出现在stuff中

注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

举例如下

#define SQUARE( x ) x * x

如果下面有SQUARE(5),就会被替换成

5*5

但是这个宏有个问题

如果我想要计算6*6,采用下面的代码

SQARE(5+1)

打印的结果并不是36,而是11

实际上,上面这段代码被替换成了

5+1*5+1

所以在定义宏替换时,一定不要吝啬使用括号()

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

4.2.3 #define替换规则

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

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

注意

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

#define MAX MAX*MAX //不能递归使用

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

#define MAX 100
printf("MAX=%d",MAX); // 第一个MAX不会被替换

4.2.4 #和##

首先看这样一段代码

printf("hello ""world");
//运行结果
hello world

实际上,编译器在处理的时候会把上面的两个字符串拼成一个字符串打印输出

在敲代码生涯中,你们是不是经常遇到这样的输出

double wang = 96;
double zhang = 89;
printf("wang的分数是 % lf\n", wang);
printf("zhang的分数是 % lf\n", zhang);

每次输出一位学生的成绩,都要输出它的姓名,既然输出格式一样,可不可以直接把姓名加到字符串中直接输出呢?

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

#define PRINT(x) printf(#x"的分数是%lf\n",x);
int main()
{
double wang = 96;
double zhang = 89;
PRINT(wang);
PRINT(zhang);
return 0;
}

实际上,#x会被替换成“x”,也就是printf("x""的分数是%lf\n",x)

又因为字符串的自动连接属性,所以就打印出了我们想要的结果

wang的分数是96.000000
zhang的分数是89.000000

##可以把位于它两边的符号合成一个符号

见下例

#define CAT(x,y) x##y

int main()
{
    double wangqiang = 96;
    printf("%lf\n", CAT(wang,qiang));
    return 0;
}
//打印结果
96.000000

4.2.5 宏和参数的对比

宏通常被用于简的运算

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

为什么上面这串代码不写成一个函数:

1.用于调用函数和返回值的执行时间可能比计算工作要多的多,因此宏在效率上更胜一筹

2.函数的参数必须声明类型,一个函数不能同时实现int之间的比较和double之间的比较,而宏是和类型无关的

当然宏也有缺点

1.宏在编译的时候就会被替换,调试是在程序运行期间进行的,因此宏不能进行调试

2.宏会被直接替换到文件中,会增加文件的大小

宏也可以做到函数做不到的事情,比如宏的参数可以出现类型

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

int *p=(int*)MALLOC(10,int);

总结

#define定义宏

函数

除非宏非常小并且使用次数少,否则代码长度会大幅增加

函数代码只出现在一个地方

执行速度更快

函数调用和接收返回值需要时间和空间,执行速度比较慢

参数与类型无关

参数必须有特定类型

不方便调试

可以逐语句调试

不能递归

可以递归

优先级容易出错,需要多加()

不需要考虑优先级问题,更容易预测表达式结果

命名约定

1.宏的名字要全部大写

2.函数名不要全部大写

这是程序员平时的命名习惯,当然也可以不遵守,但是结果会怎样比较难说

4.3 #undef

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

#define MAX 1000
#define MIN -1000
printf("%d",MAX); //打印1000
printf("%d",MIN); //打印-1000
#undef MAX
printf("%d"MAX); //报错,MAX未定义
printf("%d",MIN); //打印-1000

4.4 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号,用于编译过程。

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处.(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器
内存大些,我们需要一个数组能够大些.)

见下例

#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}

可以在编译的时候给ARRAY_SIZE赋值

//linux 环境演示
gcc -D ARRAY_SIZE=10 programe.c

4.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。

因为我们有条件编译指令

常见的条件编译指令有以下几种

1.条件编译

#if 常量表达式//如果常量表达式的值非0,则执行中间代码块

代码块

#endif

2.多个分支的条件编译

#if 常量表达式1
代码块1

#elif 常量表达式2
代码块2

#elif ...
...
#else
代码块n

#endlif

3.判断是否被定义

如果symbol被定义,就执行代码块

#if defined(symbol)
//或者#ifdef symbol

代码块

#endlif

如果symbol未被定义,就执行代码块

#if !defined(symbol)
//或者#ifndef symbol

代码块

#endlif

4.上面的预处理指令可以嵌套使用

#define FLG 0
#ifdef FLG

#if FLG
printf("FLG==0\n");
#else
printf("FLG!=0\n");
#endif

#endif

结果是输出FLG!=0

4.6 文件包含

4.6.1文件被包含的方式

#include预处理指令用于包含头文件,可以使得这个文件的内容被编译。就像它实际出现于 #include 指令的地方一样

包含方式有下面两种

本地文件包含

#include “filename”

库文件包含

#include <filename>

包含方式的不同决定了不同的查找策略

""的查找方式:

先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标

准路径查找头文件

<>的查找方式:

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

这里的标准路径是安装的路径

由上面的查找策略我们可以知道

1.<>只能包含库函数,""是通用的

2.包含库函数时最好使用<>,""会使编译器多查找一次,效率降低

4.6.2 文件嵌套包含

很多时候,一个头文件会被多个源文件包含,也就意味着在编译的时候头文件会在多个位置重复展开,使代码长度大幅增加

可以用条件编译指令解决头文件被重复包含的问题

头文件

#ifndef __TEST_H__
#define __TEST_H__

头文件内容

#endif

或者

#pragma once

表示头文件只能被引入一次

而库里面的头文件则不需要我们担心,它们早就解决了这个问题

下图是stdio.h的部分内容

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 会敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值