C 语言学习笔记 06 - 预处理

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) {
        result *= i;
    }
    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) 的函数。

但宏函数也存在缺陷,在如上定义时,很有可能出现结果与预期不相符的情况。

例如

MAX(1, MAX(2, 4));

// 期望 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)

MAX(1, MAX(2, 4)); => 4

宏函数只进行简单的替换,如果传入的参数为表达式,它在替换后也会变为表达式,并不会预先求值,因此会导致多次重复调用影响函数结果。

此外,我们也可以定义多行的宏函数,尽管宏函数原则上仅允许一行,但我们可以通过以下方法定义。

#define IS_HEX_CHAR(ch) \
((ch) >= '0' && (ch) <= '`9') || \
((ch) >= 'a' && (ch) <= '`f') || \
((ch) >= 'A' && (ch) <= '`F')

此外,宏函数中也不存在返回类型的概念,返回值可以是任何类型。宏函数仅仅进行代码替换的操作,最终能否成功通过编译不一定。

comments powered by Disqus