【C语言进阶】了解计算机的程序环境和预处理过程 掌握计算机预处理操作(上)

简介: 【C语言进阶】了解计算机的程序环境和预处理过程 掌握计算机预处理操作(上)

1.编译与链接

1.1 程序的翻译环境与执行环境:

在研究程序的编译与链接细节之前,我们首先要了解我们程序的翻译以及执行环境,我们要知道,在 ANSI C 的任何一种实现中,都存在着两种环境:

翻译环境:在该环境中,我们所写下的 .c 等源代码将被转化成可执行的机器指令

执行环境:在该环境下,将会真正执行经过转化后生成的 .exe 可执行文件

1.2 翻译环境:

在翻译环境中执行的操作,简单来说可以分为三个步骤:

一:组成一个程序的每个源文件通过编译过程分别转换成目标代码(.obj)。

二:每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。

三:链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。

链接库指的是链接我们的工程中所引用的库函数所依赖的函数库以及各种第三方函数库。

1.3 翻译阶段:

如果我们再细分下来,翻译阶段又可以分为两个阶段,即编译与链接:


①.编译:

编译阶段又可以细分为三个阶段:

1.预编译(预处理):头文件的包含、#define 定义符号的替换、注释的删除,均为文本操作。

2.编译:将 C 语言的代码翻译成汇编代码,其具体动作包含了语法分析、词法分析、语义分析、符号汇总等。

3. 汇编:将汇编指令翻译为二进制指令,具体动作包含了形成符号表等。


②.链接:

连接阶段负责各个文件的链接相关操作,其具体动作包括了:

1.合并段表

2.合并符号表

3. 重定位符号表

1.4 运行环境:

在这个环境下,我们的程序就真正进入了运行阶段。我们程序的执行过程可以简述为

下面四个步骤:

①. 程序载入内存中:在有操作系统的环境中该步骤通常由操作系统完成;而在独立环境中则必须由我们自己手动完成;独立环境中的程序也有可能通过执行可执行代码置入只读内存来完成。

②. 调用 main 函数:程序正式开始执行。

③. 顺序执行程序代码 :在这个阶段中,我们的程序会使用一个运行时堆栈来存储函数的局部变量和返回地址。同时程序也可以使用静态内存来存储变量,并且这些存储于静态变量中的变量在整个程序的执行过程中将始终保留它们的值。

④. 终止程序:一般情况下会正常终止 main 函数,但我们的程序也有可能会意外终止。

2. 预处理详解:

2.1 预定义符号:

在 C 语言中有一些内置的预处理符号:

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

这些符号都是语言内置的,可以直接使用:

#include<stdio.h>
int main()
{
  printf("进行编译的源文件:\n");
  printf("%s\n", __FILE__);
  printf("文件当前的行号:\n");
  printf("%d\n", __LINE__);
  printf("文件编译的日期:\n");
  printf("%s\n", __DATE__);
  printf("文件编译的时间:\n");
  printf("%s\n", __TIME__);
  return 0;
}

670e8df2f22e44bead4d5f29a952d84a.png

2.2 #define:

#define 的用处非常多,就比如我们常用的定义标识符常量、定义宏等等,而在这个过程中,也有一些细节值得我们去注意。

2.2.1 #define 定义标识符:

我们常常会使用 #define 去定义一些标识符来方便我们的代码书写。它很常用,使用格式也很简单:

#define name stuff
• 1

例如:

#define MAX 1000
#define reg register
//为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)
//用更形象的符号来替换一种实现
#define CASE break;case
//在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                           date:%s\ttime:%s\n",\
            __FILE__,__LINE__,\
            __DATE__,__TIME__) 

同时可以思考一下,在 #define 的最后,要不要加分号 “ ; ” ?

最好不要添加,这是因为有可能会导致语法错误:

#define MAX 1000;
#include<stdio.h>
int main()
{
  int a = 1;
  int max = 0;
  if (a)
  {
    max = MAX;
  }
  printf("max = %d\n", max);
  return 0;
}

例如这个例子,在预编译(预处理)阶段,会进行 #define 定义标识符的替换,于是 if 语句中的代码将会被替换为:

将被替换为:max = (MAX;);
即:max = MAX;;

此时我们发现,经过符号替换后,出现了语法错误

2.2.2 #define 定义宏:

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

宏的申明方式为:

#define name( parament-list ) stuff

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

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

例如我们定义一个宏 sqr 用来计算平方:

#define sqr(x) x*x
• 1

这个宏将会在执行时接受一个参数 x,如果在上述声明之后,将下面的代码置于程序中:

sqr( 5 );
• 1

则预处理器就会将上面的表达式替换为:

5 * 5;

警告:这个宏的书写仍是不严谨的,随时可能导致错误的产生。

例如下面这样情况:

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

看起来好像并没有什么问题,但事实上,这段代码在经过预编译的符号替换后将会变成:

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

原本想要计算 6 * 6,却在预编译的符号替换后出现了错误,打印了 5 + 1 * 5 +1。

解决方法很简单,只需要对宏定义进行简单修改即可:

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

通过小括号的使用,在预编译阶段,这段代码将被替换为:

printf("%d\n",(a+1)*(a+1));
• 1

但是其实这样写还有可能出现下面这样的错误:

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

经过了与编译后又将会被替换为:

printf("%d\n", 10 * (a)+(a));
• 1

由于操作符中 * 的优先级高于 +,计算结果又将出现差错。

解决方案是再使用一对小括号对宏定义进行简单修改即可:

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

这时的代码在经过预编译后将会被替换为:

printf("%d\n", 10 * ((a)+(a)));

总结 在用于对数值表达式进行求值的宏定义中,都应该用这种方式加上括号以避免在使用宏时由于参数中操作符或邻近操作符之间不可预料的相互作用,从而导致在经过预编译阶段的符号替换后出现错误。

2.2.3 #define 替换规则:

#define 在进行符号替换时,遵循以下规则:

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

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

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


注意

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

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

2.2.4 带副作用的宏参数:

这里所说的副作用是指表达式求值的时候出现的永久性效果

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

而这样的代码会通过下面这样的方式对结果造成影响:

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

这段代码的实际结果为:

984fb7d03f95467eb58ee638ba66d650.png由这个例子我们可以得知,当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就有可能导致不可预测的后果。

3. 宏与函数:

3.1 宏与函数对比:

宏 #define 常用于定义一些全局的宏常量等。宏还常常被应用于执行简单的运算,例如比较两数的大小可以这样用:

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

为什么我们不使用函数来完成这个过程呢?

在这里使用宏而不用函数的主要原因有两个:

①. 相比于函数宏在程序规模与速度方面更胜一筹:由于函数在调用时牵扯到函数栈帧的创建与销毁等操作,所以用于调用函数以及从函数返回的耗时可能比实际执行这个小型计算工作的耗时更多。

②. 宏是类型无关的:在声明、定义和使用函数时,函数的参数必须是特定的类型,所以函数只能在类型合适的特定表达式上使用。反观宏,无论是整形、长整型还是浮点型,都可以进行比较。

宏与函数相比也有劣势

①. 宏只能处理简单运算:我们知道在预处理阶段宏将会进行符号替换,这就意味着每次在使用宏的时候,一份宏定义的代码将插入到程序中。这样一来,如果宏比较长,就将会大幅度增加程序代码的长度。

②. 宏是没有办法进行调试的。

③. 由于宏具有类型无关的特点,因此也不够严谨。

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

宏也可以做到函数做不到的事,比如:

#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
#include<stdio.h>
#include<string.h>
int main()
{
  MALLOC(10, int);
  //预处理器替换后:(int*)malloc(10 * sizeof(int));
  return 0;
}


相关文章
|
24天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
46 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