【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

相关文章
|
1天前
|
C语言 编译器 开发者
【C语言基础】:预处理详解(二)
【C语言基础】:预处理详解(二)
|
1天前
|
编译器 C语言 C++
【C语言基础】:预处理详解(一)
【C语言基础】:预处理详解(一)
|
1天前
|
存储 自然语言处理 编译器
C语言——环境与预处理
C语言——环境与预处理
|
5天前
|
编译器 C语言
C语言环境处理收尾
C语言环境处理收尾
7 0
|
5天前
|
编译器 C语言
C语言收尾 预处理相关知识
C语言收尾 预处理相关知识
5 0
|
10天前
|
程序员 C语言 C++
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
10 0
|
10天前
|
存储 编译器 C语言
C语言学习记录——调试技巧(VS2019环境下)
C语言学习记录——调试技巧(VS2019环境下)
16 2
|
16天前
|
C语言
c语言循环设计程序结构
c语言循环设计程序结构
16 0
|
2天前
|
算法 Unix Linux
C语言随机数的产生(rand、srand、time函数细节讲解)
C语言随机数的产生(rand、srand、time函数细节讲解)
|
1天前
|
安全 C语言
【C语言基础】:内存操作函数
【C语言基础】:内存操作函数