C语言程序环境和预处理

embedded/2024/10/19 3:23:41/

系列文章目录

第一章 C语言基础知识

第二章 C语言控制语句

第三章 C语言函数详解

第四章 C语言数组详解

第五章 C语言操作符详解

第六章 C语言指针详解

第七章 C语言结构体详解

第八章 详解数据在内存中的存储

第九章 C语言指针进阶

第十章 C语言字符函数和字符串函数

第十一章 C语言结构体,枚举,联合

第十二章 C语言程序环境和预处理


文章目录

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

1.1 翻译环境

1.2 执行环境

1.3 代码过程示例

1. 预处理阶段

2. 编译阶段

3. 汇编阶段

4. 链接阶段

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 嵌套文件包含


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

C语言的程序开发涉及两个关键环境:翻译环境和执行环境。这两个环境分别定义了C程序从源代码到可执行代码的转换过程以及程序的运行过程。

1.1 翻译环境

翻译环境是C程序从源代码转换成可执行代码的过程。在这个环境中,编译器执行以下几个步骤:

  1. 预处理(Preprocessing):这是编译过程的第一步,处理源代码文件中的预处理指令,如宏定义(#define)、条件编译(#ifdef、#ifndef)和文件包含(#include)等。

  2. 编译(Compilation):在预处理之后,预处理器输出的结果被送往编译器。编译器分析和转换代码,检查语法错误,并将代码转换成中间代码或直接转换成机器语言。

  3. 汇编(Assembly):中间代码通常需要进一步转换成汇编代码,然后由汇编器转换成机器可执行的二进制格式。

  4. 链接(Linking):链接器接着将多个对象文件和库合并成一个单一的可执行文件。在这个阶段,外部函数和变量的引用被解决。

1.2 执行环境

执行环境则是指编译后的程序实际运行的环境。它从程序开始执行的那一刻起直到程序运行结束。在执行环境中,操作系统和硬件提供必要的资源和平台支持。执行环境主要涉及以下方面:

  1. 加载(Loading):操作系统负责将可执行文件加载到内存中。

  2. 执行(Execution):CPU按照程序指令顺序执行操作,处理数据。

  3. 运行时库支持(Runtime Library Support):C标准库提供一系列标准函数,如输入输出处理、内存管理等,这些都是在执行时调用和执行的。

  4. 资源管理(Resource Management):操作系统管理程序所需的所有资源,如内存、文件处理、并发执行线程等。

  5. 终止(Termination):程序执行完毕后,操作系统回收所有分配给程序的资源,并清理环境,准备下一次程序的运行。

1.3 代码过程示例

#include <stdio.h>#define A 10
#define B 20int main() {int result = A + B;printf("The result is %d\n", result);return 0;
}

1. 预处理阶段

预处理器处理源代码中的预处理指令。在我们的示例中,预处理器将会执行以下操作:

  • 处理#include <stdio.h>,将标准输入输出库的内容包含进来,这样程序中的printf函数才能被正确识别和使用。
  • 处理#define A 10 和 #define B 20,这两个宏定义将在源代码中的所有A和B被替换为10和20。

2. 编译阶段

在预处理后,预处理过的代码被送到编译器。编译器做的工作包括:

  • 语法分析:编译器检查代码是否符合C语言的语法规则。
  • 语义分析:检查表达式和赋值操作等是否语义合法。
  • 代码优化:编译器可能会进行一些优化,例如简化算术运算。
  • 生成中间代码:编译器将C代码转换成中间代码(通常是汇编代码)。

例如,int result = A + B;在预处理后变成int result = 10 + 20;,然后可能会被编译器进一步优化为int result = 30;。

3. 汇编阶段

中间代码接下来被转换成机器语言对应的汇编代码。这些汇编代码是特定平台的指令,例如x86指令集。

4. 链接阶段

最后,汇编代码被转换成机器语言,并和其他必需的代码或库链接在一起形成最终的可执行文件。这包括解决外部库函数(如printf)的引用和地址分配。

2. 预处理详解

2.1 预定义符号

  • __FILE__:这个宏在程序编译时,会被替换成当前源文件的名称。它是一个字符串字面量。
  • __LINE__:在源代码中的任何位置使用这个宏,它会被替换为一个整型字面量,代表宏所在行的行号。
  • __DATE__:这个宏提供了一个字符串字面量,内容是源文件被编译的日期,格式为"MMM DD YYYY"(月 日 年)。
  • __TIME__:这个宏会被替换成源文件编译时的具体时间,格式为"HH:MM:SS"(时:分:秒)。
  • __STDC__:如果程序遵循ANSI C标准,这个宏会被定义。通常,如果__STDC__被定义,它会被赋值为1。

这些宏可以直接用在程序中,例如在打印语句中使用,在运行时输出文件名和行号。

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

当这条printf语句被执行时,它会打印出当前源代码文件的名称和这条printf语句的行号。如果在main.c文件的第10行有这条语句,它的输出是:

file:main.c line:10

2.2 #define

2.2.1 #define 定义标识符

#define是一种宏定义,它告诉预处理器在实际编译之前将所有出现的宏名称替换为指定的代码片段或值。

#define name stuff

name是宏的名称,stuff是当宏被调用时要替换成的代码或值。

代码示例:

#define MAX 1000           //定义了一个常量MAX,它将在代码中替换为1000。
#define reg register          //为关键字register定义了一个别名reg,在声明寄存器变量时可以使用。
#define do_forever for(;;)     //定义了一个宏do_forever,在代码中扩展为一个无限循环的for语句。
#define CASE break;case        //定义了一个宏CASE,它在switch语句中用来替换多个连续的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;  // 错误:宏定义不应包含分号
#define MAX 1000   // 正确:宏定义应该没有分号

2.2.2 #define 定义宏

#define不仅可以定义常量,还可以定义带有参数的宏,这些宏被称为宏函数。它们看起来和实际的函数调用非常相似,但是在预处理阶段发生文本替换,没有函数调用。

宏函数的定义格式如下:

#define name(parameter_list) stuff

parameter_list是参数列表,而stuff是当宏被调用时展开的代码。参数在stuff中使用时,每次出现都会被相应的实参替换。

#define SQUARE(x) (x * x)
//这个宏函数,计算参数x的平方。

使用注意:

因为宏展开是基于文本的替换,可能会产生预期之外的行为。比如:

int a = 5;
printf("%d\n", SQUARE(a + 1));

期望输出的是36,但由于宏展开为(a + 1 * a + 1),实际输出的是11。这是因为*的优先级高于+,所以先进行了乘法运算。

为了避免这种情况,应该在宏定义中使用括号包围参数和整个宏体:

#define SQUARE(x) ((x) * (x))
#define DOUBLE(x) ((x) + (x))

在这个例子中,无论传入的是变量、常量还是表达式,宏都会正确地展开,过程为:printf ("%d\n",(a + 1) * (a + 1) );

下面另一个例子说明了操作符优先级可能导致的问题:

int a = 5;
printf("%d\n", 10 * DOUBLE(a));

如果没有适当的括号,期望输出的是100,但由于宏展开为10 * (a) + (a),实际输出的是55。

所以解决方法为:

#define DOUBLE( x)   ( ( x ) + ( x ) )

2.2.3 #define的撤销

当用#define定义了一个宏,它就会在源文件中剩余的部分一直有效。但是,如果你想在某一点后不再使用这个宏,你可以使用#undef指令来取消它的定义。

使用#undef的一些情况:

  • 避免命名冲突:当有多个库被包含在一个项目中,可能会出现宏命名冲突的情况。使用#undef可以撤销先前定义的宏,以确保不会发生意外的宏展开。
  • 限制宏的作用域:如果你只想在源文件的某个特定部分使用宏,可以在宏不再需要时使用#undef来取消它,这样可以避免它在文件的其余部分产生影响。
  • 重定义宏:有时可能需要根据不同的条件改变宏的定义。你可以先#undef掉旧的宏定义,然后重新用#define来定义新的。

注意事项:

  • 你不能在宏定义中使用#undef来取消宏本身的定义。因为#undef只能用于全局范围,不能在宏定义的展开中使用。
  • 即使一个宏之前没有被定义,使用#undef也不会产生错误,编译器会忽略对未定义宏的撤销请求。

例如,如果你先定义了一个宏,然后在文件中的后续部分不再需要它,可以这么做:

#define PI 3.14159
/* 使用PI的代码 */
#undef PI
/* PI宏在这里不再有效 */

2.2.4 带副作用的宏参数

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

代码示例,宏定义:计算两个值中的最大值:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

如果宏的参数中包含了诸如x++或y++这样的副作用操作(在C语言中,x++表示x的值会在使用后增加1),使用这个宏可能会导致意外的结果。因为在宏替换过程中,这些带副作用的表达式可能会被求值多次。

int x = 5;
int y = 8;
int z = MAX(x++, y++);

本意是要计算xy之间的最大值,然后将它们各自增加1。但是,由于宏展开,这行代码变成了:

int z = ((x++) > (y++) ? (x++) : (y++));

这导致x或y可能增加了两次,而不是一次,因为>运算符先比较了x和y,然后选择的分支再次对x或y进行了自增操作。这就产生了副作用。因此,实际结果与预期不同,变量的最终值可能是:

x = 6,y = 10,z = 9

这是因为x首先增加到6,然后y增加到9,因为此时y大于x,所以y被选择为z的值,然后y再次增加,变为10。

2.2.5 宏和函数对比

宏(Macro)和函数(Function)在C语言中都用于代码重用,但它们在编译过程和运行时的行为有本质的不同。

宏是预处理器概念,是一种文本替换工具。宏不关心数据类型,因为它们在编译前就被文本替换了。

函数是编程的基本结构,用于封装代码以执行特定的任务。

属性
#define定义宏
函数
代码长度
每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长
函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度
更快
存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级
宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括
号。
函数参数只在函数调用的时候求值一次,它的结果值传递给函
数。表达式的求值结果更容易预测。
带有副作用的参数
参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。
函数参数只在传参的时候求值一次,结果更容易控制。
参数类型
宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
调试
不方便调试
可以逐语句调试
递归
不能递归
可以递归

2.2.6 条件编译

条件编译是C语言预处理器提供的一种功能,它允许程序在编译时根据特定的条件来包含或排除代码部分。条件编译通常是用预处理指令来实现的,最常用的有#if、#ifdef、#ifndef、#else、#elif和#endif。

#if指令允许根据一个常量表达式的值来包含或排除代码段。如果表达式的结果为真(非零),则编译接下来的代码,否则跳过。

#if defined(WINDOWS)// Windows平台特有的代码
#elif defined(LINUX)// Linux平台特有的代码
#else// 其他平台的代码
#endif

#ifdef 和 #ifndef 是 #if defined(...) 和 #if !defined(...) 的简写形式。它们用于检查一个宏是否已定义。

#ifdef DEBUG// 仅在DEBUG模式下编译的代码
#endif#ifndef PI
#define PI 3.14159
#endif
//如果DEBUG宏已定义,则编译与调试相关的代码。如果PI宏未定义,则定义它。

组合条件编译

可以组合这些指令来实现更复杂的条件编译逻辑。例如可以根据多个宏的定义来包含或排除代码。

#if defined(USE_FEATURE_X) && !defined(USE_FEATURE_Y)// 仅当USE_FEATURE_X定义而USE_FEATURE_Y未定义时编译这部分代码
#endif

2.2.7 嵌套文件包含

嵌套文件包含是指在一个头文件中包含另一个头文件的情况。这通常用于管理和组织大型项目中的代码依赖。通过嵌套包含,开发者可以确保在编译源文件时,所有必要的声明和宏定义都是可用的。

代码示例

假设有两个头文件:file1.h和file2.h,以及一个源文件main.c。

// file1.h
#include "file2.h"
// 其他的声明和宏定义...// file2.h
// 一些声明和宏定义...// main.c
#include "file1.h"
// main函数和其他代码...

在这个例子中,当main.c包含file1.h时,file1.h又会包含file2.h,从而形成了嵌套包含。这意味着在main.c中,来自file2.h的声明和宏定义也将可用。

头文件保护示例

// file2.h
#ifndef FILE2_H
#define FILE2_H
// 一些声明和宏定义...
#endif // FILE2_H// file1.h
#ifndef FILE1_H
#define FILE1_H
#include "file2.h"
// 其他的声明和宏定义...
#endif // FILE1_H

在这个示例中,FILE1_H和FILE2_H就是这样的预处理宏,它们确保了相关头文件的内容只被包含一次,无论它们在项目中被包含了多少次。


http://www.ppmy.cn/embedded/10029.html

相关文章

计算机网络原原理学习资料分享笔记---第一章/第四节/第五节(为有梦想的自己加油!)

第四节 计算机网络性能 第四节 计算机网络性能 第四节 计算机网络性能 1 、速率&#xff1a; 速率&#xff1a;网络单位时间内传送的数据量&#xff0c;用以描述网络传输数据的快慢。 速率基本单位&#xff1a;bit/s&#xff08;位每秒&#xff09; Kbit/s、 Mbit/s、 Gbit/…

IDEA更换新版本启动没反应

目前安装了新的IDEA(压缩包方式)&#xff0c;由于老版本的IDEA还在用&#xff0c;所以并没有删除&#xff0c;但是安装完后发现点击idea64.exe后没有反应&#xff0c;于是网上找了好多方法最后解决了 下面是我的解决过程 新版本&#xff1a;IntelliJIdea2024.1 老版本: Intelli…

GPT-SoVITS声音训练报错ZeroDivisionError: division by zero

环境: GPT-SoVITS-0421 问题描述: GPT-SoVITS声音训练报错ZeroDivisionError: division by zero Traceback (most recent call last):File "E:\GPT-SoVITS-0421\GPT-SoVITS-0421\GPT_SoVITS\s2_train.py", line 600, in <module>main()File "E:\GPT…

Python 函数

文章目录 Python 函数函数的简单定义函数传参详解位置参数关键字参数默认参数任意参数 函数注解 Python 函数 函数的简单定义 def fib(n): # write Fibonacci series up to n"""Print a Fibonacci series up to n."""a, b 0, 1while a <…

C++设计模式:中介者模式(十五)

1、定义与动机 定义&#xff1a;用一个中介对象来封装&#xff08;封装变化&#xff09;一系列的对象交互。中介者使各个对象不需要显示的相互引用&#xff08;编译时依赖 -> 运行时依赖&#xff09;&#xff0c;从而使其耦合松散&#xff08;管理变化&#xff09;&#xff…

# 设计模式 #5.6 Memento备忘录,行为型模式

在您提供的备忘录模式的笔记中&#xff0c;已经很好地概述了该模式的主要概念和参与者。为了进一步优化这些笔记&#xff0c;我们可以确保术语的一致性&#xff0c;并清晰地定义每个组件的作用。以下是优化后的笔记内容&#xff1a; 备忘录模式&#xff08;Memento Pattern&am…

JavaScript进阶部分知识总结

作用域 局部作用域 作用域规定了变量能够被访问的范围&#xff0c;离开了这个范围变量就不能被访问作用域分为&#xff1a;局部作用域和全局作用域 局部作用域分为函数作用域和块作用域 1.函数作用域&#xff1a; 在函数内部声明的变量只能在函数内部被访问&#xff0c;外…

索引【MySQL】

文章目录 什么是索引测试表 磁盘和 MySQL 的交互了解磁盘MySQL 的工作原理Buffer Pool 理解索引引入Page 的结构页内目录&#xff08;Page Directory&#xff09;多页情况B 树和 B树聚簇索引和非聚簇索引 主键索引创建 唯一索引主要特点与主键索引的区别使用场景创建 联合索引工…