面试题答案
一键面试避免符号重复定义错误的方法
- 使用
#ifndef
、#define
和#endif
预处理指令:- 在头文件开头使用
#ifndef
检查某个宏是否已定义,如果未定义,则执行后续代码。例如:
- 在头文件开头使用
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件内容
#endif
- 当第一次包含该头文件时,`MY_HEADER_H` 未定义,于是定义该宏并处理头文件内容。再次包含时,`MY_HEADER_H` 已定义,`#ifndef` 条件不成立,头文件内容被忽略,避免了重复定义。
2. #pragma once
:
- 这是一种现代编译器支持的方式,它告诉编译器该头文件只应被包含一次。例如:
#pragma once
// 头文件内容
- 编译器会确保头文件无论被包含多少次,其内容都只处理一次。不过它不是标准 C++ 的一部分,某些较老的编译器可能不支持。
3. 将定义放在实现文件中:
- 头文件只进行符号声明,而将符号的定义放在对应的实现文件(.cpp
文件)中。例如,函数声明放在头文件:
// myheader.h
void myFunction();
- 函数定义放在实现文件:
// myheader.cpp
#include "myheader.h"
void myFunction() {
// 函数实现
}
符号在头文件和实现文件编译时的解析与处理过程
- 预处理阶段:
- 头文件包含:预处理器会将
#include
指令指定的头文件内容插入到包含它的源文件中。例如,#include "myheader.h"
会把myheader.h
的内容直接插入到该#include
指令所在位置。 - 宏替换:预处理器会对源文件中的宏进行替换。例如,定义了
#define PI 3.14159
,则在源文件中所有PI
都会被替换为3.14159
。
- 头文件包含:预处理器会将
- 编译阶段:
- 词法分析:编译器将预处理后的源文件看作字符流,把它分割成一个个单词(token)。例如,
int num = 10;
会被分割为int
(关键字)、num
(标识符)、=
(运算符)、10
(常量)、;
(界符)等单词。 - 语法分析:基于词法分析得到的单词序列,编译器构建语法树,检查源文件的语法是否正确。例如,对于
int num = 10;
会构建一棵表示变量声明和初始化的语法树。如果语法错误,如int num 10;
(少了=
),编译会报错。 - 语义分析:编译器检查语法结构的语义,比如类型检查。对于
int num = "hello";
,语义分析时会发现将字符串赋值给整型变量是不合法的,从而报错。在这个阶段,符号(变量、函数等)的声明会被记录到符号表中。 - 代码生成:编译器将语法树转换为目标机器的汇编代码或中间代码。例如,对于
int num = 10;
会生成相应的汇编指令来分配内存并初始化变量。
- 词法分析:编译器将预处理后的源文件看作字符流,把它分割成一个个单词(token)。例如,
- 链接阶段:
- 编译器将每个源文件编译生成目标文件(
.obj
或.o
),目标文件包含编译后的机器代码和符号表。当多个源文件包含同一个头文件时,头文件中的声明在每个源文件编译时都被记录到各自的符号表中。 - 链接器将多个目标文件以及可能用到的库文件链接成一个可执行文件或库文件。在链接过程中,链接器会检查符号的定义,确保每个符号只有一个定义。如果出现重复定义,如两个源文件都定义了
void myFunction()
,链接器会报错。通过前面提到的避免符号重复定义的方法,能确保在链接阶段不会出现符号重复定义错误。
- 编译器将每个源文件编译生成目标文件(