• 【C语言】程序环境和预处理


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

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

    第1中是翻译环境,在这个环境中源代码被转换为可执行的机器指令

    第2中是执行环境,它用于实际执行代码

    2、详解编译与链接 

    2.1 翻译环境 

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

    例如:

    //sum.c
    int g_val = 2016;
    void print(const char* str)
    {
        printf("%s\n", str);

    //test.c
    #include
    int main()
    {
        extern void print(char* str);
        extern int g_val;
        printf("%d\n", g_val);
        printf("hello\n");
        return 0;
    }

     1、预处理 选项gcc test.c -E test.c -o test.i

    预处理完成之后就停下来,预处理之后产生的结果在test.i文件中。

    2、编译 选项 gcc -s test.c

    编译完成之后就停下来,结果保存在test.s中。

    3、汇编 gcc -c test.c

    汇编完成之后就停下来,结果保存在test.o中 

    2.3 运行环境 

    程序的执行过程:

    1、程序必须载入内存中。存在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

    2、程序便开始执行,接着便调用main函数。

    3、开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。

    4、终止程序。正常终止main函数;也有可能是意外终止。

    3、预处理详解 

     3.1 预定义符号

    __FILE__        //运行时编译的源文件

    __LINE__        //文件当前的行号

    __DATE__        //文件被编译的日期

    __TIME__        //文件被编译的时间

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

    这些预定义符号都是内置的。 

    1. #include
    2. int main()
    3. {
    4. int i = 0;
    5. FILE* pf = fopen("test.txt", "w");//记录日志
    6. if (pf == NULL)
    7. {
    8. perror("fopen");
    9. return 1;
    10. }
    11. for (i = 0; i < 10; i++)
    12. {
    13. fprintf(pf,"file:%s line=%d date=%s time=%s i=%d\n",__FILE__,__LINE__,__DATE__,__TIME__ ,i);//怎么知道是在哪个文件中哪一行什么时间打印的呢?我们可以记录一下
    14. }
    15. fclose(pf);
    16. pf = NULL;
    17. return 0;
    18. }

    3.2 #define 

     3.2.1 #define 定义的标识符

    语法:

            #define name stuff

    1. #include
    2. #define MAX 100
    3. #define STR "hello"
    4. int main()
    5. {
    6. printf("%d\n", MAX);
    7. printf("%s\n", STR);
    8. return 0;
    9. }

    注意:#define定义的语句后面不要加分号 

    3.2.2 #define定义宏 

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

    宏的申明方式:

    #define name(parament-list) stuff

    其中的parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中。

    注意:

    参数列表的左括号必须与name紧邻。

    如果两者之间有任何空白,参数列表就会被解释为stuff的一部分

    #define SQUARE( x ) x*x

    这个宏接受一个参数x

    1. #include
    2. #define SQUARE(x) x*x
    3. {
    4. int n = SQUARE(5);//宏定义是存在缺陷
    5. int r = SAUARE(5 + 1);//想要的是6*6,但结果却是11
    6. //发生替换r=5+1*5+1,结果为11
    7. printf("%d\n", n);
    8. return 0;
    9. }

     想要改变这种情况我们可以在宏定义中加上两个括号,这个问题便迎刃而解了!

    3.2.3 #define的替换规则

     在程序中扩展#define定义符号和宏时,需要设计几个步骤:

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

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

    3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就处理上述过程

    注意:

    1、宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归

    2、当预处理搜索#define定义的符号的时候,字符串常量的内容并不被搜索 

    3.2.4 #和## 

    如何把参数插入到字符串中? 

    首先我们看这样的代码:

    char* p="hello""world\n";

    printf("hello world\n");

    printf("%s\n",p);//输出结果:hello world 

    1. #include
    2. #define PRINT(N) printf("the value of "#N" is %d\n",N)
    3. int main()
    4. {
    5. int a = 10;
    6. PRINT(a);
    7. int b = 5;
    8. PRINT(b);
    9. return 0;
    10. }

    ##的作用:

    ##可以把位于它两边的符号合成一个符号

    它允许宏定义从分离的文本片段创建标识符 

    1. #include
    2. #define CAT(Grade, Num) Grade##Num
    3. int main()
    4. {
    5. int grade100 = 1;
    6. printf("%d\n",CAT(grade,100));
    7. return 0;
    8. }

    注意:这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

    3.2.5  带副作用的宏参数

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

    x+1        //不带副作用

    x++        //带有副作用 

    1. #include
    2. #define MAX(x,y) ((x)>(y)?(x):(y))
    3. int main()
    4. {
    5. int a = 5;
    6. int b = 4;
    7. int m = MAX(a++, b++);
    8. //int m=((a++)>(b++)?(a++):(b++))
    9. printf("%d\n", m);
    10. printf("a=%d b=%d\n", a, b);
    11. return 0;
    12. }//结果为6 7 5,产生了重复计算

    3.2.6 宏和函数对比 

    宏通常背应用于执行简单的运算;比如在两个数中找出较大的一个。之所以不用函数原因有二:

    1、用于调用函数和函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

    2、更为重要的是函数的参数必须声明为特定的类型。

    所以函数只能在类型合适的表达式上使用。反之这个宏就可以适用于整型、长整型、浮点型等用于>来比较的类型 

     宏的缺点:

    1、每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

    2、宏时无法调试的。

    3、宏由于类型无关,也就不够严谨。

    4、宏可能会带来运算符优先级的问题,导致程序容易出错。

    宏有时候可以做到函数做不到的事情。比如:宏的参数可以出现类型,但函数做不到。 

    3.2.7 命名约定 

    一般来讲函数的宏的使用语法很相似。那我们平时的习惯是:

    把宏名全部大写

    函数不要全部大写 

    3.3 #undef 

    这条指令用于移除一个宏定义 

    1. #include
    2. #define M 100
    3. int main() {
    4. #undef M
    5. printf("%d\n", M);
    6. return 0;
    7. }

     3.4 命令行定义

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

    比如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。) 

    3.5 条件编译 

     在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

    调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

    1. #include
    2. #define __DEBUG__
    3. int main()
    4. {
    5. int i = 0;
    6. int arr[10] = { 0 };
    7. for (i = 0; i < 10; i++)
    8. {
    9. arr[i] = i;
    10. #ifdef __DEBUG__
    11. printf("%d\n", arr[i]);//为了观察数组是否赋值成功
    12. #endif // !__DEBUG__
    13. }
    14. return 0;
    15. }

    3.5 命令包含 

    我们已经知道,#include 指令可以使用另一个文件被编译。就像它实际出现于#include 指令的地方一样。这种替换的方式很简单:

    预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际编译10次 

    为了防止头文件被多次重复的包含,有两种解决方案:

    1. //方案一
    2. #pragma once
    3. //方案二
    4. #ifndef __TEST_H__
    5. #define __TEST_H__
    6. //....
    7. #endif

    #include与#include"stdio.h"的区别是查找策略不同:

    <> 的查找策略:直接去库目录下查找

    " " 的查找策略:

    1、先去代码所在的路径下查找

    2、如果上面找不到再去库目录下查找

    4、其他预处理指令 

    #error

    #pragma

    #line

    ....... 

     

     

     

  • 相关阅读:
    Three.js-效果合成(EffectComposer)
    PCB元件创建
    使用python读写xlsx格式中的数据【xlrd、pywin32】
    【Python】json 格式转换 ① ( json 模块使用 | 列表转 json | json 转列表 | 字典转 json | json 转字典 )
    记-快两年了基本上没怎么写博客
    RemObjects Elements多用途软件开发工具链
    桥接模式(Bridge Pattern)
    Windows11 安装 chocolatey 包管理器
    【每日一题】Day37 选择题
    docker安装nacos和sentinel笔记
  • 原文地址:https://blog.csdn.net/2401_83283514/article/details/139602900