C语言——C程序编译过程

本文详细介绍了C语言的编译过程,包括预处理(宏定义、文件包含、条件编译)、编译、汇编和链接四个阶段。重点讲解了宏定义的使用,包括无参数和带参数的宏,以及宏定义与函数、typedef的区别。同时,提到了typedef在创建别名和简化类型声明中的应用。

C语言目录:

1. 概述

2. 数据类型

3. 量

4. 运算符

5. 流程控制

6. 函数

7. C程序编译过程

8. 文件

9. 内存管理


预处理
编译
汇编
链接
hello.c
hello.i
hello.s
hello.o
hello.exe
  1. 编写代码,保存后生成 hello.c 源文件

    # include<stdio.h>
    
    int main(int argc, char const *argv[]){
        printf("Hello World!");
        
        return 0;
    }
    

    在这里插入图片描述

  2. 对源文件 hello.c 进行预处理,生成预处理文件 hello.i 在这里插入图片描述

  3. 对预处理文件 hello.i 进行编译,生成汇编文件 hello.s
    在这里插入图片描述

  4. 对汇编文件 hello.s 进行汇编,生成二进制文件 hello.o 在这里插入图片描述

  5. 对二进制文件 hello.o 进行链接,生成可执行文件 hello.exe
    在这里插入图片描述

  6. 运行可执行文件 hello.exe
    在这里插入图片描述

7.1 预处理

C编译器在对源程序进行编译前,会将一些特殊指令作解释(如 #include),进而产生一个新的源程序,之后再进行通常的编译

  • 所有的预处理指令都以 # 开始,并且结尾不用分号

  • 预处理指令可以出现在程序的任何位置,它的作用范围从它出现的位置到文件尾,所以尽可能将预处理指令写在源程序开头

  • 包括:文件包含、宏定义、条件编译

7.1.1 文件包含处理

使用 <> ,仅在系统指定的磁盘路径下搜索所包含的文件

使用 "" ,现在当前工作目录中搜索,若找不到则在系统中寻找

7.1.2 宏定义

:用来替换重复出现的字符串

宏名:代替该字符串的标识符

宏代换(宏展开):在编译预处理阶段,对程序中出现的所有 宏名 用相应的字符串进行代换,宏代换由预处理程序自动完成

a. 不带参数的宏定义

# define 标识符 字符串

字符串可以是常数,表达式,格式串等

约定:宏名一般用大写字母作为标识符,以便与变量名区别开,但用小写也没有语法错误

#include <stdio.h>
#define PI 3.14

int main (){
    float S = PI * r * r;

    printf("%f", S);
    return 0;
}

若字符串中出现了某个宏名,不进行替换

#define PI 3.14

char *str = "PIE";//此时不会发生宏代换

宏代换只是简单的 字符串替换 ,不做语法检查。只有编译时才对源程序进行语法检查


宏定义的有效范围是从定义位置开始到文件结束。如果需要终止宏定义的作用域,可以用 #undef

#include <stdio.h>
#define PI 3.14

int main (){
    #undef PI
    float S = PI * r * r; //报错

    printf("%f", S);
    return 0;
}

宏定义之间可以相互引用

#define R  3.0
#define PI 3.14
#define C  2*PI*R
#define S  PI*R*R

可以用宏定义表示数据类型

# define String char*

int main(){
    String str = "The String";	
    
    return 0;
}
b. 带参数的宏定义

带参数的宏,在调用中,不仅要宏展开,而且要用实参数代替形参

#define 宏名(形参列表) 字符串

# define SUM(a,b) (a+b)

int main(){
    int sum = SUM(1,2);
    
    printf("%d\n",sum);
    return 0;
}
  • 宏标识符与形参列表之间不能有空格,否则会当被当做普通字符串

    # define SUM (a,b) (a+b)
    
    int main(){
        int sum = SUM(1,2);//会被替换成 (a,b) (a+b)(1,2)
        //编译不通过
        
        printf("%d\n",sum);
        return 0;
    }
    
  • 带参数的宏在宏代换时,只作简单的字符和参数的替换,不进行计算操作

  • 宏定义时,要用 () 将形参字符串括住

    # define Twice(a) 2*a
    
    int main(){
        int res = D(3+4);// 替换结果为:2*3+4
        
        printf("%d\n",sum);
        return 0;
    }
    
    # define Twice(a) 2*(a)
    
    int main(){
        int res = D(3+4);// 替换结果为:2*(3+4)
        
        printf("%d\n",sum);
        return 0;
    }
    
  • 宏定义时,要将计算结果也用 () 括住

    #  define Pow(a) (a)*(a)
    
    int main(){
        int res = Pow(10)/Pow(2);
        //宏代换后:10*10/2*2=100
        
        return 0;
    }
    
    #  define Pow(a) ((a)*(a))
    
    int main(){
        int res = Pow(10)/Pow(2);
        //宏代换后:(10*10)/(2*2)=25
        
        return 0;
    }
    

7.1.3 条件编译

程序中一部分代码在满足一定条件才进行编译,否则不参与编译

优点:按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件,有利于程序的移植和调试。生成的目标程序较短

#if 条件1
	...
#elseif 条件2
    ...
#else
    ...
#endif
#define SCORE 67
#if SCORE > 90
    printf("优秀\n");
	code 优秀
#elif SCORE > 60
    printf("良好\n");
	code 良好
#else
    printf("不及格\n");
	code 不及格
#endif

7.2 别名typedef

typedef 原类型名 新类型名;

  • 原类型名中含有定义部分,新类型名一般用大写表示

  • typedef 在编译时完成替换

7.2.1 typedef 的使用

a. 基本数据类型
typedef int Integer;
typedef Integer MyInteger;
int main(){
    Integer a;//等价于int a
}
b. 数组
typedef int NAME[20];
NAME a;//等价于 int a[20];
c. 结构体类型
 struct Person{
    int age;
    char *name;
};

typedef struct Person Person;
typedef struct Person{
    int age;
    char *name;
} Person;
typedef struct {
    int age;
    char *name;
} Person;
d. 枚举类型
enum Session{
    Spring,
    Summer,
    Autumn,
    Winter
};
typedef enum Session Session;
typedef enum Session {
    Spring,
    Summer,
    Autumn,
    Winter
}Session;
typedef enum {
    Spring,
    Summer,
    Autumn,
    Winter
}Session;
e. 指针

指向结构体的指针

typedef struct {
    float x;
    float y;
}Point;

typedef Point *PP;

指向函数的指针

int sum(int a, int b) {
    int c = a + b;
    printf("%d + %d = %d", a, b, c);
    
    return c;
}
typedef int (*Fn)(int, int);

// 定义一个指向sum函数的指针变量p
Fn p = sum;

7.3 宏定义与函数、typedef区别

7.3.1 宏定义与函数的区别

带参数宏定义与函数形式类似

  • 匹配问题:宏定义不涉及存储空间的分配、参数类型匹配、参数传递、返回值

  • 执行阶段:函数调用在程序运行时执行,而宏代换只在编译预处理阶段进行

  • 执行效率:带参数的宏比函数具有更高的执行效率

7.3.2 宏定义与typedef 区别

宏定义与typedef 都可对数据类型进行说明

  • 宏定义只是简单的字符串替换,在预处理阶段完成
  • typedef 在编译时处理,是对类型说明符的重新命名
typedef char *String;//给char *起了别名String

int main(){
    String str = "This is a String";
    return 0;
}

#define String char * //用String对char *进行宏代换

int main(){
    String str = "This is a String";
    
    return 0;
}
<think>嗯,用户现在想了解C++中extern "C"的使用,以及预处理和编译阶段的作用。看来用户之前已经接触过相关的知识,可能是在混合编程或者项目编译过程中遇到了问题。需要结合用户提供的引用内容来组织回答,确保覆盖extern "C"和预处理、编译阶段的关键点。 首先,用户提到的extern "C"在C++中用于解决C和C++之间的链接问题。根据之前的回答,extern "C"告诉编译器使用C的链接规则,避免名称修饰(Name Mangling),这样C++代码可以正确调用C语言编译的函数。需要详细说明extern "C"的语法和使用场景,比如在头文件中的条件编译,以及如何包裹函数声明。 然后,预处理阶段的作用。根据引用[3],预处理包括展开头文件、宏替换、去掉注释、条件编译。用户可能想知道预处理在extern "C"中的作用,比如头文件中的条件编译指令#ifdef __cplusplus和extern "C"是如何在预处理阶段被处理的。需要解释预处理如何影响最终的编译结果,特别是保证C和C++代码的兼容性。 编译阶段的作用,根据引用[3],是检查语法并生成汇编代码。这里需要强调编译器在编译阶段如何处理extern "C"修饰的函数,即禁止名称修饰,生成与C兼容的符号名。同时,需要区分C和C++编译器在名称修饰上的差异,以及为什么需要extern "C"来统一符号。 用户提供的引用中还提到了#include和#define的处理,这可能与预处理阶段相关。例如,在头文件中使用extern "C"时,通过#include包含的头文件会被预处理展开,确保函数声明被正确处理。同时,宏替换可能在预处理阶段影响extern "C"的使用,比如引用[1]中的例子,通过-D选项替换宏,可能导致函数名被替换,进而影响链接。 还需要注意用户之前的问题中提到的extern "C"在头文件中的条件编译,确保C和C++都能正确包含头文件。这可能涉及到预处理阶段的#ifdef __cplusplus判断,只有在C++编译时才会包含extern "C"块,从而避免C编译器报错。 用户可能存在的深层需求是理解如何在实际项目中正确使用extern "C"来混合C和C++代码,避免链接错误,以及预处理和编译阶段在此过程中的作用。需要结合实例说明,比如头文件的写法、编译命令的区别,以及预处理后的代码变化。 此外,用户可能对预处理和编译阶段的具体步骤不太清楚,需要分步骤解释,比如预处理生成.i文件,编译生成.s文件,汇编生成.o文件,最后链接成可执行文件。并指出extern "C"主要在编译阶段影响符号生成,而预处理阶段处理头文件和宏替换,间接影响编译阶段的符号处理。 还需要注意用户提到的引用中关于全局变量和函数的extern声明,这可能与如何在C++中正确声明C的全局变量有关,需要补充说明extern "C"不仅用于函数,也用于变量,并且需要在头文件中适当处理。 总结下来,回答需要结构清晰,分点讲解extern "C"的作用、预处理阶段的具体处理(如头文件展开、宏替换、条件编译)、编译阶段的名称修饰和符号生成,并通过示例和对比表格帮助用户理解。同时,结合用户提供的引用内容,引用相关的注释和例子,确保回答的准确性和相关性。</think>### C++ 中 `extern "C"` 的使用与预处理、编译阶段作用详解 #### 一、**`extern "C"` 的核心作用** 1. **解决符号链接问题** C++ 支持 **函数重载** 和 **命名空间**,编译器会对函数名进行 **名称修饰**(Name Mangling),生成唯一符号(如 `_Z3addii`)。而 C 语言无此机制,直接使用原始函数名(如 `add`)。 **`extern "C"`** 的作用是强制 C++ 编译器以 **C 语言的符号规则** 处理函数或变量,避免因名称修饰导致的链接错误[^1]。 **示例**: ```cpp // C++ 调用 C 函数 extern "C" { int add(int a, int b); // 声明 C 函数 } int main() { add(2, 3); // 调用时使用原始符号名 "add" return 0; } ``` 2. **兼容头文件的写法** 若 C 头文件需同时被 C 和 C++ 代码包含,需通过条件编译实现兼容: ```cpp #ifdef __cplusplus extern "C" { // 仅 C++ 编译器处理此块 #endif int add(int a, int b); // C 函数声明 #ifdef __cplusplus } #endif ``` - `__cplusplus` 是 C++ 编译器的预定义宏,确保 C 编译器忽略 `extern "C"`[^3]。 --- #### 二、**预处理阶段的作用** 预处理是编译过程的第一步,核心任务包括: 1. **展开头文件** 通过 `#include` 将头文件内容 **直接替换** 到代码中。 **示例**: ```cpp #include "add.h" // 替换为 add.h 中的声明 ``` - 若 `add.h` 包含 `extern "C"` 块,预处理后会直接插入到当前文件中。 2. **宏替换与条件编译** - `#define` 定义符号常量或宏: ```cpp #define PI 3.14 // 预处理后所有 PI 替换为 3.14 ``` - `#ifdef` 根据条件决定是否编译某段代码: ```cpp #ifdef DEBUG printf("Debug mode"); // 仅当定义了 DEBUG 时保留 #endif ``` 3. **特殊场景** 预处理指令可能改变函数名或逻辑(需谨慎使用): ```bash g++ -Dadd=rand main.cpp # 预处理时将 "add" 替换为 "rand" ``` - 如引用[1]中示例,可能导致函数行为异常[^1]。 --- #### 三、**编译阶段的作用** 编译阶段将预处理后的代码(`.i` 文件)转换为汇编代码(`.s` 文件),核心任务包括: 1. **语法检查与优化** - 检查类型、作用域、语法错误(如缺少分号)。 - 对代码进行优化(如删除未使用变量)。 2. **符号生成规则** - **C++ 默认行为**:生成修饰后的符号名(如 `_Z3addii`)。 - **`extern "C"` 作用**:禁止名称修饰,生成原始符号名(如 `add`)。 **验证方法**: ```bash nm add.o # 查看目标文件符号表 ``` - 输出中 `T add` 表示符号未修饰,`T _Z3addii` 表示已修饰。 --- #### 四、**完整流程示例** 1. **代码结构** - **C 函数文件 `add.c`**: ```c int add(int a, int b) { return a + b; } ``` - **C 头文件 `add.h`**(兼容写法): ```cpp #ifdef __cplusplus extern "C" { #endif int add(int a, int b); #ifdef __cplusplus } #endif ``` - **C++ 调用文件 `main.cpp`**: ```cpp #include "add.h" int main() { return add(2, 3); } ``` 2. **编译流程** - **预处理**: ```bash gcc -E add.c -o add.i # 生成预处理后的 C 代码 g++ -E main.cpp -o main.i # 生成预处理后的 C++ 代码 ``` - 检查 `main.i` 中是否包含 `extern "C"` 块和 `add` 声明。 - **编译与链接**: ```bash gcc -c add.c -o add.o # 编译 C 代码 g++ main.cpp add.o -o main # 编译 C++ 代码并链接 ``` --- #### 五、**对比表格** | 阶段 | 核心任务 | 对 `extern "C"` 的影响 | |--------------|----------------------------------|---------------------------------------| | **预处理** | 展开头文件、宏替换、条件编译 | 处理 `#ifdef __cplusplus` 条件块 | | **编译** | 生成符号、检查语法、优化代码 | 禁止名称修饰,生成 C 风格符号 | | **链接** | 合并目标文件、解析符号引用 | 确保 C++ 能找到未修饰的 C 符号 | --- ### 相关问题 1. **为什么 C++ 的头文件通常使用 `#ifndef` 防止重复包含?** - 预处理阶段通过 `#ifndef` 避免同一头文件被多次插入,防止重复定义错误[^3]。 2. **宏定义 `#define` 在预处理阶段有哪些潜在风险?** - 可能意外替换代码中的标识符(如引用[1]中的 `-Dsqrt=rand`),导致逻辑错误[^1]。 3. **如何查看预处理后的代码?** - 使用 `gcc -E` 或 `g++ -E` 生成 `.i` 文件,直接观察宏展开和头文件插入结果。 4. **`extern "C"` 能否用于类成员函数?** - 不能,`extern "C"` 仅适用于全局函数和变量,类成员函数隐含 `this` 指针,与 C 不兼容。 5. **混合编程时如何处理全局变量?** - 在头文件中用 `extern "C"` 声明变量,在 C 文件中定义,C++ 文件中通过 `extern` 引用[^2]。 [^1]: 预处理指令(如 `-D`)可动态修改代码逻辑,需谨慎使用。 [^2]: `extern` 关键字在 C 中用于声明全局变量,C++ 中需结合 `extern "C"` 确保兼容性。 [^3]: 预处理阶段处理头文件展开和条件编译,直接影响后续编译阶段的符号生成。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AmosTian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值