【C语言】程序环境预处理 -- 详解(上)

简介: 【C语言】程序环境预处理 -- 详解(上)

一、程序的翻译环境和执行环境

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

  1. 翻译环境,在这个环境中源代码被转换为可执行的机器指令。
  2. 执行环境,它用于实际执行代码。

1、翻译环境

  • 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
  • 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准 C 函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

编译本身也分为几个阶段:

⚪sum.c

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

⚪test.c

#include <stdio.h>
int main()
{
    extern void print(char *str);
    extern int g_val;
    printf("%d\n", g_val);
    print("hello world\n");
 
    return 0;
}

解析图(VS2019):


2、执行环境

程序执行的过程:

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  2. 程序的执行便开始。接着便调用 main 函数。
  3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
  4. 终止程序。正常终止 main 函数;也有可能是意外终止。
int Add(int x, int y)
{
    return( x + y);
}
 
int main()
{
    int a = 10;
    int b = 20;
    int ret = Add(a, b);
 
    return 0;
}


二、预处理详解

1、预处理符号

__FILE__    //进行编译的源文件
__LINE__    //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
 
// __STDC__在VS2019下测试,为未定义。说明其并不遵守ANSI 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()
{
    int i = 0;
    FILE* pf = fopen("test.txt", "a+"); // 追加的形式,每运行一次就追加
    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    for (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");
    }
    fclose(pf); 
    pf = NULL;
 
    return 0;
}


2、#define

(1)#define 定义标识符
// 语法:
#define name stuff
#include <stdio.h>
 
#define day 100
 
int main()
{
    int t = day;
    printf("%d\n", t);
 
    return 0;
}

在预处理阶段就会day 替换为 100。预处理结束后 int t = day 这里就没有 day 了,会变为 int t = 100

// 预处理前
int t = day;
 
// 预处理后
int t = 100;
#define MAX 1000
#define reg register           //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上
 
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,\
                          __DATE__,__TIME__ )
 
int main()
{
    register int num = 0;
    reg int num = 0; // 这里reg就等于register
 
 
    do_forever // 预处理后替换为 for(;;); 
        ; // 循环体循环的是一条空语句
 
    do_forever; // 那么可以这么写,这个分号就是循环体,循环的是一个空语句
 
 
    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:
        // 最后一个case没有break
    }
 
    return 0;
}

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

后果:

#include <stdio.h>
 
#define day 100;
int main()
{
    int t = day; // int t = 100;;
 
    // int t = 100;
    // ;
 
    return 0;
}


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

结论:在 #define 定义标识符时,虽然语法支持,但是尽量不要在末尾加分号!


(2)#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)

下面是宏的申明方式:

#define name( parament - list ) stuff 其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在 stuff中。

注意 参数列表的左括号必须与 name   紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为 stuff   的一部分。

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

如果 SQUARE(3),预处理就会用 3 来替代 X*X 的内容,替换后为 3*3。

SQUARE (3+1) 的结果是什么?
#include <stdio.h>
 
#define SQUARE(X) X*X
 
int main()
{
    printf("%d\n", SQUARE(3+1));
 
    return 0;
}

这里将 3+1 替换成 X,那么 X 就是 3+1, 3+1*3+1, 根据优先级算得结果为 7。要看作为一个整体,完全替换。宏的参数是完成替换的,他不会提前完成计算,而是替换进去后再计算。替换是在预处理阶段时替换,表达式真正计算出结果是在运行时计算。

// 以下为正确代码:
#include <stdio.h>
 
// 整体再括一个括号,更加严谨
#define SQUARE(X) ((X)*(X))
 
int main()
{
    printf("%d\n", SQUARE(3+1));
 
    return 0;
}
// error
#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;
}
 
// correct
#define DOUBLE(X) ((X)+(X))
 
int main()
{
    printf("%d\n", 10 * DOUBLE(3+1));
 
    return 0;
}

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


3、#define 替换规则

在程序中扩展 #define  定义符号和宏时,需要涉及以下几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。

注意

  1. 宏参数#define 定义中可以出现其他 #define 定义的变量。但是对于宏,不能出现递归
  2. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索
#include <stdio.h>
 
#define M 100
 
int main()
{
    printf("M = %d\n", M);
    return 0;
}


4、# 和 ##

#include <stdio.h>
#define PRINT(X) printf("变量"#X"的值是%d\n", X);
// #X 就会变成 X内容所定义的字符串
 
int main()
{
    int a = 10;
    PRINT(a); // printf("变量""a""的值是%d\n", a);
 
    int b = 20;
    PRINT(b); // printf("变量""b"的值是%d\n", b);
 
    int c = 30;
    PRINT(c); // printf("变量""c""的值是%d\n", c);
 
    return 0;
}

#X 替换成参数所对应的字符串。

改进:可以打印其他类型的数字。

#include <stdio.h>
#define PRINT(X, FORMAT) printf("变量"#X"的值是 "FORMAT"\n", X);
 
int main()
{
    int a = 10;
    PRINT(a, "%d");
 
    float f = 5.5f;
    PRINT(f, "%.1f"); //printf("变量""f""的值是 ""%.1f""\n", f);
 
    return 0;
}

⚪## 的作用

## 可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

#include <stdio.h>
 
#define CAT(X,Y) X##Y
 
int main()
{
    int VS2019 = 100;
    printf("%d\n", CAT(VS, 2019)); // printf("%d\n", VS2019);
 
    return 0;
}

注意:## 也可以将多个符号合成一个符号,比如 X##Y##Z

【C语言】程序环境预处理 -- 详解(下)https://developer.aliyun.com/article/1514535?spm=a2c6h.13148508.setting.26.4b904f0ejdbHoA

相关文章
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
74 5
|
2月前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
78 4
|
2月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
67 1
|
2月前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
54 2
|
2月前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
45 1
|
2月前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
32 2
|
3月前
|
存储 文件存储 C语言
深入C语言:文件操作实现局外影响程序
深入C语言:文件操作实现局外影响程序
|
1月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
70 10
|
1月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
53 9
|
1月前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
44 8