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

简介: 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

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
268 1
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
1242 14
|
C语言
【C语言】全局搜索变量却找不到定义?原来是因为宏!
使用条件编译和 `extern` 来管理全局变量的定义和声明是一种有效的技术,但应谨慎使用。在可能的情况下,应该优先考虑使用局部变量、函数参数和返回值、静态变量或者更高级的封装技术(如结构体和类)来减少全局变量的使用。
360 5
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
887 4
|
编译器 C语言
【C语言】宏定义在 a.c 中定义,如何在 b.c 中使用?
通过将宏定义放在头文件 `macros.h` 中,并在多个源文件中包含该头文件,我们能够在多个文件中共享宏定义。这种方法不仅提高了代码的重用性和一致性,还简化了维护和管理工作。本文通过具体示例展示了如何定义和使用宏定义,帮助读者更好地理解和应用宏定义的机制。
768 2
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
372 2
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
1177 5
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
867 23
|
8月前
|
存储 C语言
`scanf`是C语言中用于按格式读取标准输入的函数
`scanf`是C语言中用于按格式读取标准输入的函数,通过格式字符串解析输入并存入指定变量。需注意输入格式严格匹配,并建议检查返回值以确保读取成功,提升程序健壮性。
1465 0
|
10月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
450 15