一.程序的翻译环境和执行环境

在 ANSI(国际标准) C 的任何一种实现中,存在两个不同的环境。

  1. 翻译环境,在这个环境中源代码被转换为可执行的二进制指令。
  2. 执行环境,它用于实际执行代码。

    1.1程序编译过程:

    解析:每个源文件经过编译器单独处理,生成目标文件,并且不同编译器的实现有所差异(例:linux环境下目标文件为xxx.o), 所有的目标文件加上链接库经过链接器处理形成了可执行程序     

    编译过程又可细分为四个环节:

    在预处理阶段,会完成一些文本操作:

    一、基础清洗(必做)

    1. 去除特殊符号:标点、乱码、特殊字符、emoji、非文字符号
    2. 去除无用字符:多余空格、换行符、制表符、空白字符
    3. 去除数字 / 保留数字:根据任务剔除无意义纯数字
    4. 去除网页杂质:HTML 标签、URL 链接、@提及、话题标签、广告话术

    二、标准化处理

    1. 大小写统一:英文全部转小写 / 大写
    2. 全角半角转换:全角字符转半角,统一格式
    3. 繁简转换:繁体转简体(中文场景常用)
    4. 错别字修正:纠正常见错别字、形近字

    等等

    在预处理阶段:把C语言代码转换成汇编代码(过程:词法分析,语法分析,符号汇总,语义分析)

    在汇编阶段:把汇编代码转换成二进制指令、形成符号表

    在链接阶段:

    1. 合并段表
    2. 符号表的合并和符号表的重定位

    二.预处理详解

    2.1预定义符号

    __FILE__     //进行编译的源文件
    __LINE__    //文件当前的行号
    __DATE__    //文件被编译的日期
    __TIME__    //文件被编译的时间
    __STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义
    

    使用示例:

    int main()
    {
    int i=0;
    for(i=0;i<=9;i++)
    {
    printf("file:%s  line:%d  date:%s  time:%s i=%d\n",_FILE_,_LINE_,_DATE_,_TIME_,i);
    }
    return 0;
    }

    对于_STDC_:

    printf("%d",_STDC_);

    若打印出来的数为1,则说明遵循ANSI C,否则不遵循

    2.2#define

    2.2.1关于宏的结构:

    #define  SQUARE(X)     ((X)*(X))
                宏名(参数)   宏体

    注意:参数列表的左括号必须与宏名紧邻,如果两者间有任何空白,就会变成宏体的一部分

       参数可在主函数中赋值,然后参与宏体的计算中

    2.2.2宏的替换规则:

    #define M 100
    #define DOUBLE(X) ((X)+(X))
    int main()
    {
    DOUBLE(M+2)//1.先将M转换为100,DOUBLE(100+2)
    ((100+2)+(100+2));//2.再将X=100+2代入到宏体中
    return 0;
    }

    在预处理过程中会将宏定义全部转化,例:

    #define MAX 100
    int main()
    {
    printf("%d",MAX);
    return 0;
    }

    在预处理过程中,会将MAX全部转换成100

    当宏定义过长,导致一行写不下要换行时,应该在末尾加一个续行符\,如图:

    注意:

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

    例:

    #define M 100
    #define DOUBLE(X) ((X)+(X))
    int main()
    {DOUBLE(M);}//可以出现其他#define定义的符号
    • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

    例:

    #define M 100
    #define DOUBLE(X) ((X)+(X))
    int main()
    {"M";
    "DOUBLE(2)";//这两种都不会被替换
    return 0;}

    2.2.3#和##

    #的作用:把宏参数直接转换成字符串常量,也叫 “字符串化”。

    则#a相当于"a",例:

    #define PRINT(N) printf("the value of "#N" is %d\n", N)
    
    int main()
    {
        int a = 10;
        PRINT(a);
    
        int b = 20;
        PRINT(b);
    
        return 0;
    }

    ##的作用:把两个宏参数拼接成一个新的标识符 / 记号,也叫 “标记粘贴”。

    举个例子:

    #define CONCAT(a, b) a##b
    
    int CONCAT(foo, bar) = 100; 
    // 展开后变成:int foobar = 100;
    
    CONCAT(123, 456); 
    // 展开后变成:123456

    2.2.4带副作用的宏参数

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

    例:x+1//无副作用

           x++//有副作用

    示例举例:

    #define MAX(x,y) ((x)>(y)?(x):(y))
    
    int main()
    {
        int a = 5;//6  7
        int b = 4;//5
        int m = MAX(a++, b++);
    相当于 int m=(a++)>(b++)?(a++):(b++)
                  5      4    6
        printf("m=%d ", m);//6
        printf("a=%d b=%d\n", a, b);//7  5
        return 0;
    }
    

    如果(a++)>(b++),则只会计算?后的a++

    2.2.5宏与函数

    #define MAX(a,b) ((a)>(b)?(a):(b))

    对于以上比较a,b大小的轻量级运算代码,优先选择宏

    原因:

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

    • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。

    宏的缺点:

    • 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
    • 宏是没法调试的。
    • 宏由于类型无关,也就不够严谨。
    • 宏可能会带来运算符优先级的问题,导致程序容易出现错误。

    2.3#undef

    作用:用于移除一个宏定义

    使用示例:

    #define M 100
    int main()
    {
    printf("%d",M);
    #undef
    printf("%d",M);//此时关于M的宏定义被取消,该条代码会报错
    return0;
    }

    2.4条件编译

    常见的条件编译指令: 

    1. #if 常量表达式
      ......
      #endif

      如果常量表达式为真,那么......里的内容就参与编译,否则就不参与编译                                   

    2. 多个分支的条件编译(可使用#elif,#else来实现条件语句中else if,else的作用)                   格式:
      #if
      ......
      #elif
      ......
      #else
      ......
      #endif

         使用示例:

      #define M 3
      int main()
      {
      #if M<5
      printf("M<5\n");
      #elif M==5
      printf("M==5\n");
      #else
      printf("others\n");
      #endif
      return 0;
      }
      

                                                                                                                                                              

    3. 判断是否被定义                                                                                                                          格式:                                                                                                                                        写法一:
      #if defined(symbol)//如果定义了symbol那么就编译
      #ifdef symbol
      
      #if !defined(symbol)//如果没有定义symbol那么就不编译
      #ifndef symbol
      写法二:
      #ifdef symbol
      #endif
       
      #ifndef symbol
      #endif
                                                                                                                                                                               
    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
      

      与if条件语句的嵌套相同

    2.5文件包含 

    每次包含文件都会将头文件的内容拷贝进来,如果多次包含,则会导致代码行数增多;

    为了防止这种情况发生,我们通常会给头文件加上同一模板:

    方法一:

    #ifndef TEST_H
    #define TEST_H
    ......
    #endif

    方法二:

    在最开头加上#pragma once

    2.6文件查找

    对于#include<stdio.h>和#include"test.h"

    <>"" 的区别是查找的策略不同。

    #include <stdio.h><> 查找策略:

    1. 直接去库目录下查找。

    #include "test.h""" 查找策略:

    1. 先去代码所在的路径下查找;
    2. 如果上面找不到,再去库目录下查找。

    2.7其他预处理指令

    #error

    作用:在编译阶段主动抛出自定义错误信息,并强制停止编译。几乎总是和 #if / #ifdef 配合,校验宏是否正确定义

    格式:#error  自定义错误信息

    • 不需要引号
    • 错误信息可以是任意文本
    • 预处理阶段直接触发

    使用示例:

    // 如果没有定义 CONFIG_VERSION 宏,就报错
    #ifndef CONFIG_VERSION
    #error 必须定义 CONFIG_VERSION 版本宏!
    #endif
    
    int main() 
    {
        return 0;
    }

    在预处理阶段直接触发

    和#pragma message的区别

    指令 作用 是否停止编译
    #error 抛出错误 是,强制停止
    #pragma message 打印提示信息 否,继续编译

    #line

    作用:手动修改编译器的「行号」和「文件名」,常和预定义符号一起使用

    格式:

    // 1. 仅修改行号
    #line 行号
    
    // 2. 修改行号 + 自定义文件名
    #line 行号 "自定义文件名"

    使用示例:

    #include <stdio.h>
    
    // 下一行行号=1,文件名="my_generated_code.c"
    #line 1 "my_generated_code.c"
    int main() {
        printf("文件名:%s\n", __FILE__);  // 输出:my_generated_code.c
        printf("行号:%d\n", __LINE__);    // 输出:1
        // 后续行号自动递增
        printf("行号:%d\n", __LINE__);    // 输出:2
        return 0;
    }

    特性:

    • 只影响预处理器:编译后不会保留在目标文件中
    • 行号自动递增:指定行号后,后续代码行号依次 + 1
    • 配合宏调试:解决宏展开后行号错乱的问题
    • 代码生成工具必备:如 YACC/Bison、Protocol Buffers 等生成的代码,用它映射回原始源文件

    #pragma

    作用:向编译器发送「自定义编译指令」,控制编译行为、优化、警告、平台特性等。

    Logo

    AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

    更多推荐