C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(上)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)

C语言的最后这些知识考得都少,相比于文件,该部分校招考点多一些。(文件是倒数第一这个是倒数第二吧)该部分为C语言的扩展学习,旨在完善C语法的同时,了解C语法之外的其他周边特性。学习完成该阶段,会了解到程序从“文本”到“二进制程序”的过程,接触到其他C标准头文件。


与标准库函数,甚至摸到C语言和操作系统/体系结构等学科的某些关联,整体达到完成C语言的学习,最后对于C语言的任何知识和代码,能读,能写,会查。

1.程序环境

程序环境是什么?我们都 "经历" 过,但不曾感知到 "他" 的存在。我们其实在不知不觉中早就已经接触到了程序环境…… 第一次创建了一个文件(test.c),敲下那句 "hello world" 随后保存后点击运行后编译出可执行文件(test.exe)时,其实就已经接触到了 "他" 了。


我们只是按下了运行,然后好像所有东西都像变魔术一样直接就产生了,这一切都似乎是理所当然的事。但是你是否思考过他是如何变成 "可执行程序" 的呢?在这一章,我们将简单地探讨一个 "源程序"是如何变成 "可执行程序" 的,作一个大概了解。

1.1 ANSI C 标准

ANSI C是由美国国家标准协会(ANSI)及国际化标准组织(ISO)推出的关于C语言的标准。

ANSI C 主要标准化了现存的实现, 同时增加了一些来自 C++ 的内容 (主要是函数原型)

并支持多国字符集 (包括备受争议的三字符序列)。

1.2程序的翻译环境和执行环境

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

① 翻译环境:在该环境中,源代码被转换为可执行的机器指令。

执行环境:用于实际执行代码。


1.2.1 翻译环境

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

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

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

举个例子:test.cadd.cminu.c

再举个例子:

sum.c

 
int global_val = 2023;
void print(const char* string) 
{
    printf("%s\n", string);
}

test.c

 
#include <stdio.h>
int main() 
{
    extern void print(char* string);
    extern int global_val;
    printf("%d\n", global_val);
    printf("Hello,World!\n");
 
    return 0;
}
挺重要的图:

main.c

 
extern int sum(int, int);
int main(void) 
{
    sum(1, 2);
    return 0;
}

sum.c

 
int sum(int num1, int num2) 
{
    return( num1 + num2);
}
解析图(VS2019):

1.2.2运行环境

程序执行过程:

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


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


③ 开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),


内存函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,


存储与静态内存中的变量在整个执行过程中一直保留他们的值。


④ 终止程序。正常终止 main 函数(也有可能是意外终止)。


举个例子:这段代码的执行过程

 
int Add(int x, int y) {
    return( x + y);
}
 
int main(void) {
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
 
    return 0;
}

【百度百科】C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。

2. 预处理详解

什么是预处理:

【百度百科】程序设计领域中,预处理一般是指在 程序源代码被翻译为目标代码的过程中,
生成二进制代码之前的过程。
典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,

但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)

预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

2.1 预定义符号

下面的__ 都为两个下划线。

__FILE__ //进行编译的源文件

__LINE__ //文件当前的行号

__DATE__ //文件被编译的日期

__TIME__ //文件被编译的时间

__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义(VS2019,VS2022都未定义)

介绍:在预处理阶段被处理的已经定义好的符号为预定义符号。这些符号是可以直接使用的,

是在C语言中已经内置好的。代码演示:

 
#include <stdio.h>
int main() 
{
    printf("%s\n", __FILE__);     // 返回使用行代码所在的源文件名,包括路径
    printf("%d\n", __LINE__);     // 返回行号
    printf("%s\n", __DATE__);     // 返回程序被编译的日期
    printf("%s\n", __TIME__);     // 返回程序被编译的时间
    printf("%s\n", __FUNCTION__); // 返回所在函数的函数名
    return 0;
}

这些预定义符号有什么用?

如果一个工程特别复杂,这时去调试时可能会无从下手。所以需要代码在运行的过程中记录一些日志信息,通过日志信息分析程序哪里出了问题,再进行排查就很简单了。

举个例子:

 
#include <stdio.h>
int main() 
{
    FILE* pf = fopen("log.txt", "a+"); //追加的形式,每运行一次就追加
    if (pf == NULL) 
    {
        perror("fopen");
        return 1;
    }
    for (int i = 0; i < 5; i++)
    {
        printf("* 错误日志 ");
        printf("%d *\n", i + 1);
        printf("发生时间:%s  %s\n", __DATE__, __TIME__);
        printf("具体位置:%s,函数名为%s,第%d行。\n", __FILE__, __FUNCTION__, __LINE__);
        printf("\n");
    }
    for (int i = 0; i < 5; i++)//写到文件记录下来
    {
        fprintf(pf,"* 错误日志 ");
        fprintf(pf, "%d *\n", i + 1);
        fprintf(pf, "发生时间:%s  %s\n", __DATE__, __TIME__);
        fprintf(pf, "具体位置:%s,函数名为%s,第%d行。\n", __FILE__, __FUNCTION__, __LINE__);
        fprintf(pf, "\n");
    }
    fclose(pf);
    pf = NULL;
    return 0;
}

2.2 #define

(#define定义的标识符和宏和枚举一样,习惯用大写)(程序员的约定俗成)

2.2.1#define 定义标识符

代码演示:#define 定义标识符的方法

 
#include <stdio.h>
#define TIMES 100
int main() 
{
    int t = TIMES;
    printf("%d\n", t);//100
    return 0;
}

解析:在预处理阶段就会把 TIMES 替换为 100。

预处理结束后 int t = TIMES 这里就没有TIMES 了,会变为 int t = 100;

当然了, #define 定义的符号可不仅仅只有数字,还可以用来做很多事,比如:

 
#define REG register        //给关键字register,创建一个简短的名字
#define DEAD_LOOP for(;;)   //用更形象的符号来替换一种实现(死循环的实现)

这里假设一个程序里 switch 语句后面都需要加上break,但是某人原来不是写C语言的,

他以前用的语言 case 后面是不需要加 break 的,因为他不适应每个 case 后面都要加上 break,

所以总是会忘。这时可以妙用 #define 来解决:

 
#define CASE break;case     // 在写case语句的时候自动字上break
int main() 
{
    int n = 0;
    //switch (n) 
    //{
    //    case 1:
    //        break;
    //    case 2:
    //        break;
    //    case 3:
    //        break;
    //}
    switch (n) 
    {
        case 1: // 第一个case不能替换
        CASE 2: // 相当于 break; case 2:
        CASE 3: // 相当于 break; case 3:
    }
    return 0;
}

如果定义的 stuff 过长,可以分行来写,除了最后一行外,每行的后面都加一个续行符即可 \

 
#include <stdio.h>
#define DEBUG_PRINT printf("file:%s\nline:%d\n \
                        date:%s\ntime:%s\n" , \
                        __FILE__,__LINE__ , \
                        __DATE__,__TIME__ )
 
int main() 
{
    DEBUG_PRINT;
    return 0;
}

#define 定义标识符时,为什么末尾没有加上分号?

举个例子:加上分号后,预处理替换的内容也会带分号 100;

 
#include <stdio.h>
#define TIMES 100;
int main(void) 
{
    int t = TIMES; // int t = 100;;
    // 等于两个语句
    // int t = 100;
    // ;
    return 0;
}

举个例子:加上分号,代码会出错的情况

 
#include <stdio.h>
#define TIMES 100;
int main(void) 
{
    int a, b;
    if (a > 10)
        b = TIMES; // b = 100;;
    else // else不知道如何匹配了
        b = -TIMES; // b = 100;;
    return 0;
}

所以:在 #define 定义标识符时,尽量不要在末尾加分号(必须加的情况除外)

2.2.2 #define 定义宏

介绍:#define 机制允许把参数替换到文本中,这种实现通常被称为宏(macro)或


定义宏(define macro),parament-list 是一个由逗号隔开的符号表,他们可能出现在 stuff 中。


注意事项:


① 参数列表的左括号必须与 name 紧邻。


② 如果两者之间由任何空白存在,参数列表就会将其解释为 stuff 的一部分。


代码演示:3×3=9

 
#include <stdio.h>
#define SQUARE(X) X*X
int main() 
{
    printf("%d\n", SQUARE(3)); //替换成printf("%d\n", 3 * 3);
    return 0;
}

那么SQUARE (3+1) 的结果是什么?


答案:7 。这里将 3+1 替换成 X,那么 X 就是3+1, 3+1 * 3+1, 根据优先级结果为 7。


要看作为一个整体,完全替换。宏的参数是完成替换的,他不会提前完成计算,


而是替换进去后再计算。替换是在预处理阶段时替换,表达式真正计算出结果是在运行时计算。


如果想获得 3+1 相乘(也就是得到 4×4 = 16) 的结果,我们需要给他们添加括号:

 
#include <stdio.h>
#define SQUARE(X) ((X)*(X))
// 整体再括一个括号,严谨
int main() 
{
    printf("%d\n", SQUARE(3 + 1)); //替换成printf("%d\n", ((3+1)* (3+1)));
    return 0;
}

另外,整体再套一个括号。让代码更加严谨,防止产生不必要的错误。举个例子,我们DOUBLE实现两数相加,我希望得到 10* DOUBLE,也就是 "10*表达式相加" 的情况:

 
#include <stdio.h>
#define DOUBLE(X) (X)+(X)
int main() 
{
    printf("%d\n", 10 * DOUBLE(3+1));
    // printf("%d\n", 10 * (4) + (4)); 
    // 我们本意是想得到80,但是结果为44,因为整体没带括号
    return 0;
}

解决方案:整体再加上一个括号

 
#include <stdio.h>
#define DOUBLE(X) ((X)+(X))
int main() 
{
    printf("%d\n", 10 * DOUBLE(3+1));
    return 0;
}

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


#define 替换规则

在程序中扩展 #define 定义符号或宏时,需要涉及的步骤如下:

① 检查:在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。

如果包含,它们首先被替换。

② 替换:替换文本随后被插入到程序中原来的文本位置。对于宏,函数名被它们的值替换。

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

如果包含,就重复上述处理过程。

举个例子:

 
#define M 100
#define MAX(X, Y) ((X)>(Y) ? (X):(Y));
int main(void) {
    int max = MAX(101, M);
 
    return 0;
}

注意事项:

宏参数 和 #define 定义中可以出现 #define 定义的变量。但是对于宏绝对不能出现递归

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

 
#include <stdio.h>
#define M 100
int main() 
{
    printf("M = %d", M);
    return 0;
}

C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中):https://developer.aliyun.com/article/1513279

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