015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令

001、ANSI C实现的“翻译环境”和“执行环境”

(1)翻译环境

在这个环境中,源代码被转化为可执行的机器指令(二进制指令)

  • 单文件简易版本
    015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第1张图片

  • 多文件简易版本
    015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第2张图片

  • 编译链接详细版本
    015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第3张图片

    • VS2022集成IDE(windows下)的编译器叫cl.exe,链接器叫link.exe
    • gcc编译器(windows下)的几个有关编译环境的命令
----预处理gcc 文件.c -E -o 文件.i,但直接放在终端了,故需要-o(output)重定向到文件内部
----编译gcc -S 文件名,生成.s文件
----汇编gcc -c 文件名,生成.o文件

----直接gcc 文件名字,直接经过整个翻译环境(预处理、编译、汇编、链接)生成可执行程序a.exe

015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第4张图片

(1)符号表会把全局变量和具有外部链接的函数等标识符和地址记录下来(局部变量是程序在运行的时候才会创建,而创建符号表还在编译阶段),汇总成表(函数声明部分会给予一个无效的地址,以便后续和函数定义整合在一起)
(2)链接库是属于库函数的
(3)目标文件是二进制文件,这个文件是有格式的,在linux环境下,gcc产生的目标文件和可执行程序的格式都是elf,而这种文件格式将.o文件分为一个一个段,因此在有多个.o文件的情况下,链接器将这些.o文件对应的、相同数据的段合并在一起
(4)再将之前有多个源文件生成的多个符号表合并,重定位指的是:链接的时候整合出一份新的符号表的同时查看标识符和地址是否正确(比如,这个时候如果找不到函数定义,则根据之前给予函数声明的无效地址就会提示编译器“不存在该函数(即链接错误)”),因此,实际上调用函数的时候,就是在链接的过程后才能实现的
015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第5张图片

(2)运行环境

在这个环境中,主要是实际执行代码的过程

  • 程序必须要载入内存
    • 在有操作系统的环境中:一般这个由操作系统完成
    • 在独立的环境中:程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
  • 程序开始调用主函数
    • 找到程序入口,即调用main函数
  • 开始执行程序代码
    • 按住程序逻辑顺序开始执行编写的代码
    • 这个时候程序将使用一个运行时堆栈(即“函数栈帧”),存储函数的局部变量和函数的返回地址
    • 程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直被保留
  • 终止程序
    • 正常终止main函数(也有可能是意外终止)

002、预编译的详细解读

(1)预定义符号

__FILE__    //进行编译的源文件名称
__LINE__    //显示文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器严格遵循ANSI C,其值为1,否则未定义
__FUNCTION__    //程序预编译时预编译器将用所在的函数名,返回值是字符串
__FUNCDNAME__   //和上面的宏类似,都是显示重命名后的函数
//演示代码
#define _CRT_SECURE_NO_WARNINGS 1
#include 
int main()
{
    printf("%s\n", __FILE__);   //进行编译的源文件名称
    printf("%d\n", __LINE__);   //显示文件当前的行号
    printf("%s\n", __DATE__);   //文件被编译的日期
    printf("%s\n", __TIME__);   //文件被编译的时间
    //printf("%d\n", __STDC__); //当编译器严格遵循ANSI C时,则其值为1,否则未定义
    return 0;
}
//实际上,编译器在代码编译的时候,会对函数和变量名重命名
//在C语言中重命名的规则是加上下划线
//在C++中重命名的规则 会更加复杂
#define _CRT_SECURE_NO_WARNINGS 1
#include 
int main()
{
    printf("%s\n", __FUNCDNAME__);
    printf("%s\n", __FUNCTION__);
    return 0;
}

(2)预编译指令(宏编译指令):#define和#undef

①宏命名约定

一般来讲把宏名全部大写

②宏标识符和定义宏

  • 宏标识符
#include 
#define NUM 10
int main()
{
    printf("%d", NUM);
    return 0;
}
  • 定义宏体
#include 
#define ADD(X, Y) ((X) + (Y))//注意定义宏名和参数列表之间不能有空格,否则就会将()一起作为替换内容
int main()
{
    printf("%d", ADD(2, 3));
    return 0;
}
  • 取消宏定义
#define _CRT_SECURE_NO_WARNINGS 1
#include 
#define NUMBER 10
#define ADD(X,Y) ((X) + (Y))
int main()
{
    printf("%d", NUMBER);
#undef NUMBER
    printf("%d", NUMBER);//这个语句是没办法识别NUMBER这个宏的
    
    printf("%d", ADD(1, 2));
#undef ADD
    printf("%d", ADD(1, 2));//这个语句是没办法识别ADD这个宏的
    
    return 0;
}

③宏替换规则

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

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

    • 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
    • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不会被搜索,因此不会被替换

④定义宏和函数的比较

实际上在后来的C/C++里面引入了关键字内联(inline)具有函数的优点又具有宏的优点,可以把函数像宏一样直接替换在后续的代码中,可以理解为“加强版的宏”,不过这个以后再学…
015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第6张图片

//举例一个传标识符参数的宏
#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
#define MALLOC(num, type) ((type*)malloc(num * sizeof(type)))//传递了int过来,这是函数不能做到的
int main()
{
    int* p = MALLOC(10, int);
    if (!NULL)
    {
        return 0;
    }
    p[0] = 2;
    p[1] = 0;
    p[2] = 2;
    p[3] = 3;
    for (int i = 0; i < 4; i++)
    {
        printf("%d ", p[i]);
    }
    free(p);
    return 0;
}

⑤宏处理辅助操作符“\”、“#”、“##”

  • 使用“\”可以进行代码换行操作(只是物理换行而不是逻辑换行)
  • #可以达到保留原本传递字符串的效果,而不是作为替换值,即“把一个宏参数变成对应的字符串”,或者说“把参数名字直接插入”
  • ##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,效果就是“两个字符串合成”,而这种字符串合成要更加强大
//示范一
#include 
#define ADD(X, Y) \
((X) + (Y))
int main()
{
    printf("%d", ADD(2, 3));
    return 0;
}
//示范二
#include 
#define PRINTF(num, format)\    //这里也用了换行符
        printf("The value of "#num" is "format, num)
int main()
{
    int a = 100;
    char b = 'c';
    double c = 3.14;
    PRINTF(a, "%d\n");
    PRINTF(b, "%c\n");
    PRINTF(c, "%f\n");
    return 0;
}
//示例三
#include 
#define FUN(X, Y) X##Y
int main()
{
    int XY = 10;
    printf("%d\n", FUN(X, Y));//依旧可以打印出10,X和Y被FUN连接为一串字符串了,并且整体作为变量名
    return 0;
}

⑥使用注意事项

  • 加分号and不加分号,需要使用者自己抉择,一篇情况下,最好是不在宏定义末尾加上分号,这样出现bug的概率会变低
  • 要警惕使用带副作用的宏参数
#include 
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
int main()
{
    int a = 3;
    int b = 5;
    int c = MAX(a++, b++);
    //((a++)>(b++)?(a++):(b++)),出现加加两次的情况,使用者容易忽略
    printf("%d\n", c);//6
    printf("%d\n", a);//4
    printf("%d\n", b);//7
    return 0;
}
//但是函数在使用的时候就不会出现这种错误,两者之间最大的区别就在于“替换”

(3)命令行定义

有很多C编译器都提供了一种能力,允许直接在命令行中定义符号用于启动编译过程,例如下面这一串代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include 
int main()
{
    int arr[NUM];//NUM是未定义的,因为不确定运行该代码的机器是否内存足够…
    for (int i = 0; i < NUM; i++)
    {
        arr[i] = i;
    }
    for (int i = 0; i < NUM; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

本次采用的是VS2022自带的编译器cl.exe,其输入命令行的输入命令是:

cl 需要编译的源文件名 -D NUM=100

015+limou+C语言深入知识——(7)编译环境和运行环境以及预处理指令_第7张图片
若是在gcc编译器下,其命令行定义的命令就是:

gcc 需要编译的源文件名 -D NUM=100

这里就不再演示; ,可以在VScode里面进行演示

(4)预编译指令(条件编译指令):#if和#else和#endif等

//单分支条件编译指令
#define _CRT_SECURE_NO_WARNINGS 1
#include 
//#define NUM1 200
int main()
{
#ifdef NUM1/*或者写成#if也是可以的*/
    printf("定义了NUM1");
#endif
    return 0;
}
//多分支条件编译指令
#define _CRT_SECURE_NO_WARNINGS 1
#include 
//#define NUM1 200
#define NUM2 100
int main()
{
#ifdef NUM1/*或者写成#if也是可以的*/
    printf("定义了NUM1");
#elif NUM2
    printf("定义了NUM2");
#else
    printf("NUM1和NUM2均未定义\n");
#endif
    return 0;
}
  • 条件编译指令的使用和if-else是类似的,最大的区别是这是预编译指令,是在预编译的时候进行判断的,因此需要注意的是不要放入变量(变量是在程序运行的时候才创建好的,在预编译的过程根本不存在)
  • 条件编译指令的判断语句一般选用常量表达式,而不要使用变量
//正确的写法
#define _CRT_SECURE_NO_WARNINGS 1
#include 
#define NUM1 200
int main()
{
#ifdef NUM1 == 200
    printf("定义了NUM1");
#endif
    return 0;
}
//错误的写法
#define _CRT_SECURE_NO_WARNINGS 1
#include 
//#define NUM1 200
int main()
{
    int NUM1 = 200;//这在预编译的时候是不可见的!NUM1还未被创建
#ifdef NUM1 == 200
    printf("定义了NUM1");
#endif
    return 0;
}
  • 另外条件编译的写法较多,以下是常见的预处理指令
1.单分支的条件编译
#if 常量表达式,由预处理器求值
    //...
#endif

2.多个分支的条件编译
#if 常量表达式
    //...
#elif 常量表达式
    //...
#else
    //...
#endif
3.判断是否被定义
#if defined(symbol)//等价于#ifdef symbol
#if !defined(symbol)//等价于#ifndef symbol

4.嵌套编译指令
#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

(5)预编译指令(文件包含编译指令):#include

①库文件的包含

#include 
#include 

②自定义头文件的包含

//在别的地方自定义了一个function_1.h
//在别的地方自定义了一个function_2.h
#include "function.1"
#include "function.2"
//尽管对于头文件也可以使用引号来包含但是查找效率会变低,并且无法区分是库文件还是自定义头文件
//在VS2022里,如果没有在当前源文件路径下找到function_1或function_2头文件的话,就会到标准头文件路径下去找:
//(1)VS是在安装路径中, 一个叫做“include”的文件夹中
//(2)linux是在路径“/usr/include”中

③避免头文件重复(嵌套)包含的方法,这样写头文件会更加规范(而库文件在包含的使用已经经过该处理了)

//写法一:
#ifndef __FUNCTION_H__
    #define __FUNCTION_H__
    //其他代码
#endif

//写法二:
#pragme once
    //其他代码

(6)预编译指令(其他编译指令):#error、#pragma、#line

可以到《C语言深度解剖》的第三章里学习

003、宏的编写练习

(1)模拟实现offsetof宏

#define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName)

(2)使用宏交换一个二进制数的奇数位和偶数位

#define SwapIntBit(n) (((n) & 0x55555555) << 1 | ((n) & 0xaaaaaaaa) >> 1)

你可能感兴趣的:(C语言学习笔记,c语言,windows,ide)