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日志并进行多维度分析。
目录
相关文章
|
1月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
20天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
22 2
|
1月前
|
自然语言处理 编译器 Linux
C语言中抽象的编译和链接原理
C语言中抽象的编译和链接原理
20 1
|
1月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
9天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
25 6
|
29天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
35 10
|
22天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
28天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
54 7
|
28天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
29 4