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天前
|
编译器 C语言 C++
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
4 0
|
1天前
|
安全 编译器 C语言
【C语言进阶篇】offsetof宏的介绍 及其实现
【C语言进阶篇】offsetof宏的介绍 及其实现
|
1天前
|
编译器 程序员 C语言
【C语言进阶篇】assert宏 使用详解
【C语言进阶篇】assert宏 使用详解
|
1天前
|
存储 C语言 C++
【C语言刷题系列】水仙花数的打印及进阶
【C语言刷题系列】水仙花数的打印及进阶
|
2天前
|
程序员 C语言 C++
【C语言】:柔性数组和C/C++中程序内存区域划分
【C语言】:柔性数组和C/C++中程序内存区域划分
4 0
|
2天前
|
C语言 图形学 C++
|
15天前
|
存储 C语言
C语言进阶 文件操作知识(下)
C语言进阶 文件操作知识(下)
14 2
|
15天前
|
数据库 C语言
C语言进阶 文件操作知识(上)
C语言进阶 文件操作知识(上)
12 3
|
20天前
|
C语言
C语言进阶——sprintf与sscanf、文件的随机读写(fseek、ftell、rewind)
C语言进阶——sprintf与sscanf、文件的随机读写(fseek、ftell、rewind)
8 0
|
20天前
|
C语言
C语言进阶——文件的读写(文件使用方式、文件的顺序读写、常用函数、fprintf、fscanf)
C语言进阶——文件的读写(文件使用方式、文件的顺序读写、常用函数、fprintf、fscanf)
9 0