C语言程序环境与预处理

  • Post author:
  • Post category:其他




C语言程序环境与预处理



1. 程序环境

在C语言执行的过程中,存在两个环境(翻译环境,执行环境)


每个.c源文件都会经过编译器形成各自的.obj目标文件,最后再由目标文件和链接库经过连接器形成一个.exe可执行程序

image-20220310203231404



2 翻译环境 + 执行环境



2.1 编译

编译期间,又被划分为三个小部分:

预编译,编译,汇编

下面的过程在VS中很难查看到,需要查看的可以去Linux下试试(基于test.c文件进行演示)



2.1.1 预编译(文本操作)

  • 指令:

    gcc -E test.c -o test.i

  • 作用

    1. 加载

      头文件

      里面的所有内容
    2. 注释的删除(用空格替换)
    3. #define定义的值进行替换
    4. 条件编译(保留符合条件的代码,往下看)

    注意:

    // 结论:预编译的时候先进行去注释,再进行宏替换
    // 这里的 // 直接被当做注释去掉了,BSC就替换 空 了
    #define BSC //    
    int main()
    {
        BSC printf("Hello World\n");
        // 屏幕上的结果:Hello World
        // 说明BSC并没有被 // 替换
        return 0;
    }
    



2.1.2编译

把C代码翻译成汇编代码,推荐书籍《编译原理》,《程序员的自我修养》

  • 指令:

    gcc -S test.c
  • 作用:

    1. 语法分析
    2. 词法分析
    3. 语义分析
    4. 符号汇总(全局变量,函数名)



2.1.3 汇编

把汇编代码转换成二机制编码,并且形成符号表(

符号名

对应

地址

  • 指令:

    gcc -c test.c

    ,执行完成后就生成了目标文件,.obj

前面的就是编译期间的符号汇总,后面就是汇编的符号表

image-20220310205218911



2.2 链接

所有的目标文件(.obj文件)+ 链接库 通过

链接器

,然后生成一个可执行文件.exe

  1. 合并段表(elf文件格式):.obj文件会有一定的格式存放数据的(很多块/段),链接时会讲多个.obj文件对应的每个段进行合并处理
  2. 符号表的合并和重定位
  3. readelf -s test.o(可查看目标文件源码,但里面是二进制的)

在链接期间,就会通过符号表来查看来自外部引用的符号是否真实存在(不存在,就会报链接错误(无法找到外部符号))



2.3 执行环境

  1. 加载到内存
  2. 从main函数开始执行
  3. 使用一个运行时堆栈,存储函数的局部变量和返回地址
  4. 终止程序:正常运行完终止;意外终止

VIM学习资料

简明VIM练级攻略:

https://coolshell.cn/articles/5426.html

给程序员的VIM速查卡

https://coolshell.cn/articles/5479.html



3. 预处理详解



3.1 预定义符号

//1.__FILE__:文件的绝对地址
//2.__LINE__:当前的行号
//3.__DATE__:当天的日期
//4.__TIME__:当前的时间



3.2 日志案例

// 日志文件
int i = 0;
int arr[10] = {0};
FILE* pf = fopen('log.txt','w');
for(i=0;i<10;i++)
{
    arr[i] = i;
    fprintf(pf,"file:%s line:%d date:%s",__FILE__,__LINE__,__DATE__);
}
for(i=0;i<10;i++)
{
    printf("%d ",arr[i]);
}



3.3. #define



3.3.1 #define定义标识符

  • #define 标识符 值(注意:千万不能加分号)
#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__ )  



3.3.2 #define定义宏

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

  1. 源文件的任何地方都可以定义宏,与是否在函数内外无关
  2. 宏的作用范围:从定义处开始,往后都是有效的(若函数在宏上面定义的,函数里面将不会进行宏替换)

#define name(参数列表) 值

  • #define sqt(X) X*X – 宏的值是替换的(字符串里面的除外),不是传参的,int ret = sqt(5) – 输出ret结果为:25
  • int ret = sqt(5+1); – ret结果即为11 – 5+1*5+1 = 11
  • 注意:如果参数是表达式,最好加一个括号(#define sqt(X) (X)*(X))
  • 宏里面的参数可以是#define 定义标识符的值

image-20220310214933400

// 多条语句的宏建议用 do-while-zero模式(经常简单的函数就可以写成这样的)
#define INI_VAL(a,b) \
do{ \
	// 语句块  -  每个语句后需要续行
}while(0)



3.3.3 命令行定义宏

  • Linux中:gcc test.c -D 标识符 = 值
  • Windows中:在控制台设置的地方 – 预处理器 – 预处理器定义直接加上就可以了



3.3.4 #undef 取消宏

#undef 标识符(可以取消定义的宏)

int main()
{
    #define X 3
    #define Y X*2
    #undef X
    #define X 2
    
    int z = Y;  // z = Y = 2*2; // 使用的是最后一个宏
    return 0;
}



3.3.5 带有副作用的宏的参数

#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
    int a = 10;
    int b = 11;
    int max = MAX(a++,b++);
    // int max = MAX((a++)>(b++)?(a++):(b++))
    //      执行时: a=10 b=11(后++),10>11,执行b++,此时b = 12
    printf("%d\n",max);//12
    printf("%d\n",a);//11
    printf("%d\n",b);//13
    return 0;
}



3.3.6 #和##

#输入预处理指令,放在宏里面,就是将传入的参数变成字符串类型(若传的是变量,改的则是变量名 – —-> define替换是在预编译阶段进行的,而变量定义则是到了汇编了—–>优先顺序问题)

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

#define PRINT(X) printf("the value of "#X" is %d\n",X)

int main()
{
	int a = 10;
	int b = 20;
	PRINT(a);  // #X 将替换成值所对应的变量,然后将变量转换为字符串,相邻字符串具有连接属性(相邻的字符串就是一个连在一起的字符串)
	PRINT(b);
	return 0;
}
#define Combine(X,Y) X##Y
int main()
{
    int Class84 = 2020;
    //两个的结果一样
    printf("%d\n",Class84);
    printf("%d\n",Combine(Class,84));
    //printf("%d\n",Class##84);
    //printf("%d\n",Class84);
}



3.3.7 宏和函数的差别

属性 函数
代码长度 没使用一次就会加载一下代码,所以代码冗余 实现一次即可,后面每次使用,都是直接调用该函数
执行速度 更快 存在函数调用+返回值的额外开销
操作符优先级 因为是直接替换,所以很容易有优先级的问题,所以建议多加括号 函数参数只能在调用时计算一次,表达式的结果更容易预料
副作用参数 参数可能被替换到宏体中的多个位置,结果难以预料 参数在调用的时候会进行计算,函数里面的参数就不会有副作用了
参数类型 宏的参数与类型无关,只要操作合法就行,可以是任何参数 形参有类型检查的
调试 不支持 支持
递归 不支持 支持



3.3.8 宏的参数可以为 类型

// 函数就不能传递参数为 类型
#define SIZEOF(type) sizeof(type)

int main()
{
    int ret = SIZEOF(int);
    // int ret = sizeof(int)
}
#define MALLOC(count,type) ((type*)malloc(count*sizeof(type)))

int main()
{
    int* p = (int*)malloc(10*sizeof*(int)); //开辟10个整形空间
    int* p = MALLOC(10,int);
}



3.4 条件编译

当满足某条件时进行编译里面的代码(其他的代码不加载)(例如:付费版和普通版)

可以嵌套

// 1.
#define __DEBUG__ 1
#if __DEBUG__  // #if 需要后面赋值,来判断真假
	//……
#endif

// 2.
#if 常量表达式
        //……
#elif 常量表达式
        //……
#else 常量表达式
        //……

// 3. - 双分支(#ifdef == #if defined;#ifndef == #if ndefined)   定义就行,不需要值来判断真假
#ifdef
	// ……
#else
	//……
#endif

// 4.
#if (defined(C) && defined(CPP)) //- 两个宏都要被定义
#define DEBUG  // 这是定义了,但没有真假
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int i = 0;
    for(i=0;i<10;i++)
    {
        arr[i] = 0;
        //如果DEBUG定义了,就打印,若没定义,就不打印
        #ifdef DEBUG //或者#if defined(DEBUG)
                printf("%d\n",arr[i]);
        #endif
    }
    return 0;
}

#define DEBUG
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int i = 0;
    for(i=0;i<10;i++)
    {
        arr[i] = 0;
        //如果DEBUG没定义,就打印,若没定义,就不打印
        #ifndef DEBUG //或者#if !defined(DEBUG)
                printf("%d\n",arr[i]);
        #endif
    }
    return 0;
}
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int i = 0;
    for(i=0;i<10;i++)
    {
        arr[i] = 0;
        //如果条件为真就编译,否则就不编译
        #if 1
                printf("%d\n",arr[i]);
        #elif 条件
                ……
        #endif
    }
    return 0;
}



3.4.1 #ifdef与#if的区别

  1. #ifdef 只有双分支 – #ifdef …… #else (ifdef判断的是宏定义即可,可以不用赋值,不用判断真假)
  2. #if 是多分支,#if……#elif……#else (必须给宏赋值,需要判断真假)



3.5 头文件包含

  1. 本地头文件包含(自己写的.h文件) – #include “filename”

    查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。

  2. 库头文件包含 —- #include “filename”

    查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。



3.5.1 头文件重复包含问题

预编译时,会将头文件多次进行加载,造成代码的大量冗余



3.5.2 解决办法

// 1.
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

// 2.
#pragma once



版权声明:本文为qq_47799626原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。