C语言进阶-程序环境和预处理(1)

简介: C语言进阶-程序环境和预处理

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

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

1. 翻译环境,在这个环境中源代码被转换成可执行的机器指令

2. 执行环境,它用于实际执行代码。

计算机时能够执行二进制指令的,但是我们写出的C语言代码是文本信息,计算机不能直接理解。

所以,翻译环境将C语言代码翻译为二进制指令(可执行程序),执行环境执行二进制的代码。

2.翻译环境

翻译环境实际上就是编译链接的过程:

2.1 编译+链接

上图就是程序编译的过程,每个源文件(后缀为.c)都单独经过一个编译器,生成目标文件(后缀为.obj),然后这些目标文件一起和链接库在链接器的作用下生成可执行程序(后缀为.exe)

下面我们来看个例子:

我们写了add.c和test.c两个源文件,它们经过编译之后生成目标文件add.obj和test.obj:

经过链接后生成可执行程序:

2.2 编译本身也分为几个过程

编译也分为预编译(预处理)、编译、汇编三个过程:

预编译阶段主要进行:注释的删除、#include头文件的包含、#define符号的替换等文本操作:

编译阶段:把C语言代码翻译为汇编指令,进行语法分析、词法分析、语义分析和符号汇总

汇编阶段: 把汇编代码翻译为二进制指令

而在经历了编译过程的符号汇总,汇编过程的形成符号表后,链接时主要进行:合并段表、符号标的合并和重定位

在Linux环境下,gcc编译产生的目标文件(test.o),可执行程序(test)都是按照ELF这种文件格式来存储的,而按照ELF这种格式存储时,把内存划分为一个一个的段,不同的数据在不同的段里面存储:

下面我们用前面的代码,来举例说明一下:符号汇总→生成符号表→符号表的合并与重命名的过程:

链接中合并段表,add.c和test.c在编译期间都会产生目标文件,并以ELF的格式存储,合并段表就是将它们产生的段表合一:

以上就是翻译环境的过程。下面我们简单来说一下执行环境

2.3 运行环境

程序执行的过程:

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

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

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

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

3.预编译详解

3.1 预定义符号

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

我们来使用一下:

3.2 #define

3.2.1 #define定义标识符

语法:

#define name stuff

#define可以定义任何变量,整数、字符串甚至for循环都可以:

#include<stdio.h>
#define M 100
#define STR "abc"
#define FOR for(;;)
int main()
{
  printf("%d\n", M);
  printf("%s\n", STR);
  FOR
  {
    printf("hehe\n");//死循环打印hehe
  }
  return 0;
}

上述代码中用#define定义的标识符,实际上在计算机中的预处理阶段,代码已经被替换:

#include<stdio.h>
#define M 100
#define STR "abc"
#define FOR for(;;)
int main()
{
  printf("%d\n", 100);
  printf("%s\n", "abc");
  for(;;)
  {
    printf("hehe\n");//死循环打印hehe
  }
  return 0;
}

运行结果:

使用#define CASE break;case 可以在写case语句时自动把break带上,那我们的switch语句就可以写成下面的形式:

#include<stdio.h>
#define CASE break;case
int main()
{
  int i = 0;
  scanf("%d", &i);
  switch (i)
  {
  case 1:
  CASE 2 :
  CASE 3 :
  CASE 4 :
  }
  return 0;
}

下面再来看一段代码:

// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ , \
                          __DATE__,__TIME__ )

这段代码其实就是下面的代码:

#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,__FILE__,__LINE__ , __DATE__,__TIME__ )

只不过这样看起来太长了,我们就把它分成4行,斜杠\是续行符,确保每一行和下一行的代码能够续上。

现在来思考一个问题:#define定义的标识符后面能不能加上分号“;”呢?

像下面一样:

#define M 100;
#define M 100

建议还是不要加,因为极有可能造成错误,比如,

#include<stdio.h>
#define M 100;
int main()
{
  int i = 1;
  if (i = 1)
    i = M;
  else
    M = 0;
  return 0;
}

运行一下会发现报错了:

为什么呢?

如果我们在用#define定义标识符时在后面加上了分号,那此时计算机中的代码应该是:

所以else没有与之匹配的if,自然就报错了。

3.2.2 #define定义宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

下面是一段用#define定义的宏用来实现求两数较大值

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

这段代码在预处理阶段实际上是:

int main()
{
  int a = 10;
  int b = 8;
  int c = ((a) > (b) ? (a) : (b));
  printf("%d\n", c);
  return 0;
}

在预处理阶段,先将a,b传给MAX(x,y)得到MAX(a,b),然后将a,b传给宏体(x)>(y)?(x):(y)得到(a)>(b)?(a):(b)。最后将(a)>(b)?(a):(b)替换到调用宏的位置。

注意:

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

这个意思就是,当我们写 #define MAX(x,y)时,MAX和它右边的括号必须紧挨在一起,中间不能有空格,像下面写法就是错误的:

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

但是在调用宏的时候无所谓,可以加空格,也可以不加。

还有要注意的一点是:我们在用#define定义宏时,最好给参数都带上括号。

这是为什么呢?看下面一段代码(求一个数的平方):

#include<stdio.h>
#define SQUART(x) x*x
int main()
{
  int a = 3;
  int r = SQUART(a + 3);
  printf("%d\n", r);
  return 0;
}

我们期望得到的是(3+3)*(3+3)= 36,但是结果呢?

实际上将参数x替换成a+3进行的是:a+3*a+3的计算,3+9+3=15。

而要得到正确结果,只需要给参数加上括号就行了:

此时,请再来思考一个问题,仅仅给参数加上括号就能得到期望的值吗?

再看一段代码:

#include<stdio.h>
#define SQUART(x) (x)+(x)
int main()
{
  int a = 3;
  int r = 10 * SQUART(a);
  printf("%d\n", r);
  return 0;
}

我们期望得到的值是10*6=60,但是结果呢?

实际上替换后,进行的是10*(a)+(a) = 10*(3)+(3) = 33。

此时要得到期望的结果就要给宏体的整体加上括号:

总结一下:

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

3.2.3 #define 替换规则

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

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

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

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

   就重复上述处理过程。

关于#define的替换规则,我们在学了上述内容应该有所体会,

这里我们再拿上文中的一段代码举例说明一下:

#include<stdio.h>
#define SQUART(x) ((x)*(x))
int main()
{
  int a = 3;
  int r = SQUART(a + 3);
  printf("%d\n", r);
  return 0;
}

这里在替换时,是直接将a+3替换进去的,而不是:先算出a+3的值,再替换进去

#include<stdio.h>
#define SQUART(x) ((x)*(x))
int main()
{
  int a = 3;
  int r = ((a+3)*(a+3));
  printf("%d\n", r);
  return 0;
}

注意:

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

示例:

#include<stdio.h>
#define SQUART(x) ((x)*(x))
#define M 3
int main()
{
  int r = SQUART(M+3);
  printf("%d\n", r);
  return 0;
}

结果是:36

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

示例:

字符串常量中的M不被替换。

目录
相关文章
|
24天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
45 5
|
24天前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
57 4
|
23天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
54 1
|
24天前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
35 2
|
24天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
36 1
|
21天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
40 10
|
21天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
42 9
|
21天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
32 8
|
21天前
|
C语言 开发者
【C语言】数学函数详解
在C语言中,数学函数是由标准库 `math.h` 提供的。使用这些函数时,需要包含 `#include <math.h>` 头文件。以下是一些常用的数学函数的详细讲解,包括函数原型、参数说明、返回值说明以及示例代码和表格汇总。
41 6
|
21天前
|
存储 C语言
【C语言】输入/输出函数详解
在C语言中,输入/输出操作是通过标准库函数来实现的。这些函数分为两类:标准输入输出函数和文件输入输出函数。
122 6