C 语言的头文件
C 语言的编译过程主要分为以下步骤:预处理、编译、链接。这一节首先介绍预处理。
头文件
在编写代码中,多处出现了 #include <xxx>
的引用头文件,这就是一个宏。
调用一个函数前需要先找到函数的原型,而头文件则引入了函数的原型。
换句话说,可以通过手动复制标准库中的函数原型以使用它们。
使用头文件可以批量导入多个函数原型,避免了上面这样的繁琐操作。
头文件中还包含变量与结构体等复杂结构。
预处理后,头文件的内容就会在目标位置 (调用函数的位置, …) 展开,即使用头文件中的内容替换所需内容,头文件展开的过程同时也是递归的过程,中间可能有多个头文件参与展开。
自定义头文件
既然标准库中提供了头文件,我们也可以编写自己的头文件供他人引用。
在项目目录下创建 /include
文件夹用于存放头文件,/src
存放实现代码
例如一个 factorial
的头文件与代码实现:
factorial.h
//
// Created by NatsuYuki on 2022/8/5.
//
#ifndef UNTITLED_FACTORIAL_H
#define UNTITLED_FACTORIAL_H
unsigned int Factorial(unsigned int n);
unsigned int FactorialByIteration(unsigned int n);
#endif //UNTITLED_FACTORIAL_H
factorial.c
//
// Created by NatsuYuki on 2022/8/5.
//
#include "../include/factorial.h"
unsigned int Factorial(unsigned int n) {
if (n == 0) {
return 1;
} else {
return n * Factorial(n - 1); // f(n) = n*f(n-1)
}
}
unsigned int FactorialByIteration(unsigned int n) {
unsigned int result = 1;
for (unsigned int i = n; i > 0; --i) {
*= i;
result }
return result;
}
上面使用了双引号的形式引用路径文件,<>
的形式只能引用工程路径下的头文件,而不支持引用路径文件。
编写完后,还需要在 CMakeList.txt 里面声明头文件与实现,不过 CLion 在创建这些文件时会自动帮你完成。例如:
add_executable(src/factorial.c)
接下来,创建一个新的 .c 文件,引用我们先前编写好的头文件,就可以直接调用声明好的函数。
<>
与 ""
之别
“” 会首先查找当前源文件所在路径,接下来才会查找工程头文件搜索路径,而 `` 只会查找工程头文件搜索路径。
搜索路径可以在 CMake 配置文件中配置
(include_directories(...)
),配置后即可使用
<>
引入你的头文件。不同的编译器中也会提供不同的参数提供搜索目录。
大体来看,我们可以认为 ""
的范围大于
<>
,但开销也会较大于
<>
,不过一般情况下可以忽略不计。
宏函数
宏不仅是为了替换函数实现,常量,还可以接受参数。我们将有参数的宏称为宏函数。
#define MAX(a, b) a > b ? a : b
即定义了一个 MAX(a, b) 的函数。
但宏函数也存在缺陷,在如上定义时,很有可能出现结果与预期不相符的情况。
例如
(1, MAX(2, 4));
MAX
// 期望 MAX(1, MAX(2, 4)); -> MAX(1, 4); => 4
// 实际 替换为 1 > 2 > 4 ? 2 : 4 ? 1 : 2 > 4 ? 2 : 4 => 1
与函数不同,MAX 中的 a,b 如果是表达式时不会进行求值,我们通常会给 a,b 加上括号防止其未被正确求值。
#define MAX(a, b) (a) > (b) ? (a) : (b)
(1, MAX(2, 4)); => 4 MAX
宏函数只进行简单的替换,如果传入的参数为表达式,它在替换后也会变为表达式,并不会预先求值,因此会导致多次重复调用影响函数结果。
此外,我们也可以定义多行的宏函数,尽管宏函数原则上仅允许一行,但我们可以通过以下方法定义。
#define IS_HEX_CHAR(ch) \
((ch) >= '0' && (ch) <= '`9') || \
((ch) >= 'a' && (ch) <= '`f') || \
((ch) >= 'A' && (ch) <= '`F')
此外,宏函数中也不存在返回类型的概念,返回值可以是任何类型。宏函数仅仅进行代码替换的操作,最终能否成功通过编译不一定。