C-程序的环境和预处理

文章详细阐述了程序从源代码到可执行文件的编译和链接过程,包括预编译/预处理、编译、汇编和链接各阶段的任务。同时,讨论了宏的定义、使用和潜在问题,以及条件编译和文件包含等预处理指令的作用。

程序的翻译环境

在翻译环境中,源代码会被转换为可执行的机器指令

test.c会被转换为test.exe,而test.exe是以二进制格式存储着的(类似二进制文件)

test.c转换为test.exe时,需要通过编译和链接

每一个.c文件(源文件)都会单独经过编译器进行处理,处理后会生成对应的.obj文件(目标文件)

然后所有.obj文件统一经过链接器捆绑处理生成一个.exe文件(可执行程序)

链接器会引入标准C函数库中任何被该程序所用到的函数(即从链接库中链接引用),也可以搜索程序员个人的程序库,将其需要的函数也链接到程序中

编译

编译过程可分为预编译/预处理、编译、汇编

  • 预编译/预处理:在Linux环境中,使用gcc -E test.c即可对test.c进行预编译/预处理,会生成一个.i文件,文件中就是预编译/预处理的结果,预编译结果会包含头文件中的函数,预编译即是进行文本操作

  • 预编译会展开(包含)头文件(执行#include)

  • 删除文件中的注释(使用空格进行替换)

  • 执行#define,会进行替换

  • 编译:在Linux环境中,使用gcc -S test.i即可对test.i进行编译,会生成一个.s文件

  • 语法分析

  • 词法分析:编译原理

  • 语义分析

  • 符号汇总:汇总全局变量、函数名……

  • 将C语言代码转换为汇编代码

  • 汇编:在Linux环境中,使用gcc -c test.s即可对test.s进行汇编,会生成一个.o文件[ 即Windows环境下的.obj文件 ]

  • 将汇编代码转换为二进制代码

  • 形成符号表:形成一张存放符号及符号地址的表格

链接

汇编后生成的.o[ .obj ]文件经过链接器链接生成可执行程序.exe

  • 合并段表.o文件有固定格式[ elf文件格式 ],分为几段,链接期间会将目标文件[ .o文件 ]链接在一起,对应段上的数据也会合并链接在一起,最终生成的可执行文件的格式也是elf格式

  • 符号表的合并和符号表的重定位:将符号表合并,并将一些符号表内的符号的地址进行重定位

PS.链接期间会去符号表中查找符号位置,若地址错误会发生链接错误,程序执行终止

程序的执行环境

用于实际执行代码

程序执行过程

  1. 程序载入内存中

  1. 程序开始执行,调用main函数

  1. 开始执行程序代码

  1. 程序执行终止:正常终止main函数或者意外终止

预编译/预处理

预定义符号

__FILE__ 进行编译的源文件

__LINE__ 文件当前的行号

__DATE__ 文件被编译的日期

__TIME__ 文件被编译的时间

__FUNCTION__ 当前执行的函数名

__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义

#include <stdio.h>
int main()
{
    printf("%s\n", __FILE__);//打印当前代码文件位置
    printf("%d\n", __LINE__);//打印当前行代码行数:5
    printf("%s\n", __DATE__);//打印当前日期
    printf("%s\n", __TIME__);//打印当前具体时间
    printf("%d\n", __STDC__);//VS直接报错:__STDC__未定义
    return 0;
}

#define

是预处理指令[ 前面加上#的都是预处理指令 ]

#define定义标识符

#include <stdio.h>
#define 佰 100
//可以是一个数值,也可以是字符串,还可以是关键字
int main()
{
    printf("%d\n", 佰);
    return 0;
}
#include <stdio.h>
#define 打印 printf("hello, world")
int main()
{
    打印;
    return 0;
}

#define定义标识符定义的可以是一个数值,也可以是字符串,还可以是关键字,也可以是一段代码

#define定义标识符时,后面尽量不加" ; ",加了后定义的标识符会带上" ; "

#define 佰 100; //即是 佰==100;

#define定义宏

#define允许将参数替换到文本中

定义宏的声明

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

PS①. parament-list的左括号要与name相连,中间如果有空格,参数列表会被识别为stuff的一部分

PS②. 宏是替换而不是传参

#include <stdio.h>
#define S(x) x*x
int main()
{
    int S1 = S(5);//S(5)替换成5*5
    int S2 = S(5 + 1);//替换成5+1*5+1=11
    //上面的定义宏改为(x)*(x)就不容易出错
    printf("%d %d\n", S1, S2);
    return 0;
}
#include <stdio.h>
#define A(x) x+x
int main()
{
    int A1 = 10 * A(5);
    //替换成 10*5+5=55
    //宏改成 ((x)+(x))就不容易出错
    printf("%d\n", A1);
    return 0;
}

用于对数值表达式进行求值的宏定义都应该适当地加上括号,避免使用宏时参数中的操作符或邻近的操作符之间发生不可预料的相互作用

#define替换规则

程序中使用#define定义标识符和宏时涉及到的步骤

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果有,它们首先被替换

  1. 替换文本随后被插入到程序中原来文本的位置。对于宏、参数名被他们的值替换

  1. 最后,再次对结果文件进行扫描,检查是否还包含由#define定义的标识符和宏,如果有,重复上述步骤

注意

  • 宏和标识符中可以出现其他由#define定义的变量,但对于宏不能出现递归

  • 当预编译器搜索#define定义的标识符和宏时,字符串变量的内容不会被搜索

#的作用

#include <stdio.h>
void print(int a)
{
    printf("the value of a is %d\n", a);
}
int main()
{
    int a = 10;
    int b = 20;
    print(a);
    print(b);
//这里也会打印the value of a is
//但本需要打印the value of b is
    return 0;
}

此时函数无法解决此类问题,则使用宏

#include <stdio.h>
#define PRINT(x) printf("the value of " #x " is %d\n", x)
//#x 就是 #参数,即是转换为 "参数"
int main()
{
    int a = 10;
    PRINT(a);
//printf("the value of " "a" " is %d\n", a);
    return 0;
}

使用#可以把一个宏参数变成对应的字符串

##的作用

##可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符
#include <stdio.h>
#define MERGE(X, Y, Z) X##Y##Z
int main()
{
    int half_life = 2;
    printf("%d\n", MERGE(half, _, life));
    //打印2
//printf("%d\n", half_##life);
//printf("%d\n", half_life);
    return 0;
}

PS.这里使用##连接必须产生一个合法的标识符,否则结果就是未定义的

带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果

#include <stdio.h>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
//传参Z++ - 有副作用
//传参Z+1 - 无副作用
int main()
{
    int a = 0;
    int b = 1;
    int max = MAX(a++, b++);
//进行替换:(a++) > (b++) ? (a++) : (b++);
//比完大小后a+1、b+1,此时b>a,直接跳到最后的(b++),max变为b后b+1
    printf("%d %d %d\n", max, a, b);
    //输出2 1 3
    return 0;
}

PS.宏参数是进行替换

宏和函数

  • 宏和参数很相似但不完全一样,宏作用范围更大更容易产生副作用,函数作用范围更小

#include <stdio.h>
#define MAX(A, B) ((A) > (B) ? (A) : (B))
int max(int a, int b)
{
    return (a > b ? a : b);
}
int main()
{
    float a = 0.123456;
    float b = 1.654321;
    float max1 = MAX(a, b);
//宏MAX:任何类型的数值均可以代入计算
    float max2 = max(a, b);
//函数max:会将原本的float类型的数值转为int类型再输出,结果会有失精度
    printf("%f %f", max1, max2);
    return 0;
}
  • 宏的效率更高,在预处理阶段就进行了替换,而函数调用时会有函数调用和返回的开销

  • 使用宏时,每使用一次宏,宏定义的代码就会被插入程序中,当宏比较长时会使程序变得臃肿

  • 宏可能会带有运算符优先级的问题,导致程序出错

  • 宏有时候可以做函数做不到的事情,宏可以传类型

#include <stdio.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
    int* p = MALLOC(10, int);
    free(p);
    p = NULL;
    return 0;
}

  • 宏无法调试,可读性会比较差

  • 宏无法递归而函数可以递归

内联函数可完全替代宏和函数,可以看作是两者的合体版

#undef

用于移除一个宏定义

#undef NAME//宏定义的名字
#include <stdio.h>
#define PRINT(X) printf("the value of " #X " is %d", X)
int main()
{
    int a = 10;
    PRINT(a);
#undef PRINT//或者写成 PRINT(X)
    PRINT(a);//此时报错:PRINT未定义
    return 0;
}

命令行定义

许多C编译器提供了一种功能,允许在命令行中定义符号,来用于启动编译过程

命令行定义也是预编译过程

#include <stdio.h>
int main()
{
    for (int i = 0; i < NUM; i++)
    {
        printf("%d ", i);
    }
    return 0;
}
//gcc编译器可以用命令行定义:
//gcc 文件名 -D NUM=数字
//gcc -D NUM=数字 文件名

条件编译

在编译一个程序的时候,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃

使用条件编译指令实现条件编译

#include <stdio.h>
//#define DEBUG - 定义DEBUG,不定义值也算定义
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
#ifdef DEBUG//没定义DEBUG,此句为假,不编译printf
        printf("%d ", arr[i]);
#endif
    }
    return 0;
}

条件编译指令

#if /*常量表达式,条件为真就编译,为假就不编译*/
    //……
#endif
//多个分支的条件编译
#if /*常量表达式*/
    //...
#elif /*常量表达式*/
    //...
#else
    //...
#endif
//判断是否被定义
//定义了symbol -> 编译
#if defined(symbol)
#ifdef symbol
    //……
#endif

//未定义symbol -> 编译
#if !defined(symbol)
#ifndef symbol
    //……
#endif
//嵌套条件指令
#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

嵌套条件指令使用例:

#include <stdio.h>
#define DEBUG
#define x64
int main()
{
    int arr[10] = { 0 };
    int i = 0;
    for (i = 0; i < 10; i++)
    {
#if !defined(DEBUG)
    #ifdef x86
        arr[i] = i;
    #elif defined(x64)
        arr[i] = (i + 1);
    #endif
#elif !defined(RELEASE)
    #ifdef x86
        printf("%d ", arr[i]);
    #elif defined(x64)
        printf("%%d ");//最后执行这一项
    #endif
#endif
    }
    return 0;
}

文件包含

预处理器会删除文件包含指令,并用包含文件的内容替换

即使是相同的文件,包含多少次就会替换多少次

头文件包含

#include <filename.h> //C语言标准库文件包含
#include "filename" //本地头文件包含&标准库文件也可使用这种方法包含
  • #include "filename"查找头文件会现在源文件所在目录下查找,若未找到会去标准头文件的路径下去找,若仍未找到会报错

  • #include <filename.h>会直接去标准头文件的路径下去找,若未找到会直接报错

不同编译环境的标准头文件路径不一定相同

嵌套文件包含

在编写程序时很容易发生嵌套文件包含[ 即重复包含相同的文件 ]

解决方法一

#ifndef __TEST_H__//若未定义test.h
#define __TEST_H__//则定义test.h
//头文件内容
#endif

解决方法二

#pragma once//添加在头文件中开头

其他预处理指令

#error
#pragma
#pragma pack()
#line
…………

部分预处理器指令 -> 预处理指令讲解

更多可参考《C语言深度解剖》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值