18.2 基础 C 语法

本节不会涵盖 C 的所有内容,也不打算教你如何编写 C。它将重点介绍 C 的不同方面,或者在 Python 开发人员第一次看到它们时感到困惑的方面。

通用知识

与 Python 不同,空格对 C 编译器并不重要。编译器不在乎你是跨行拆分语句,还是把整个程序挤成一条很长的行。这是因为它对所有语句和块都使用分隔符。

当然,解析器有非常具体的规则,但一般来说,只要知道每个语句以分号(;)结尾,以及所有代码块都用大括号({})包围,你就能理解 CPython 源码。

此规则的例外情况是如果块只有一条语句,则可以省略大括号。

C 中的所有变量都必须被声明,这意味着需要有一条语句表示该变量的类型。请注意,与 Python 不同,单个变量可以容纳的数据类型不能更改。

让我们来看看一些例子:

代码不翻译

一般来说,你看到的 CPython 代码格式会非常整洁,并且通常在给定的模块中会坚持单一风格。

if 语句

在 C 中,if 的工作方式通常与 Python 中的工作方式相同。如果条件为 true,则执行下面的代码块。Python 程序员应该非常熟悉 elseelseif 语法。请注意,C 中的 if 语句不需要使用 endif,因为代码块由 {} 来分隔。

if...else 语句在 C 中的简写被称为三元运算符

condition ? true_result : false_result

你可以在 semaphore.c 中找到它,对于 Windows 而言,它定义了 SEM_CLOSE() 宏:

#define SEM_CLOSE(sem) (CloseHandle(sem) ? 0 : -1)

如果函数 CloseHandle() 返回 true,则此宏的返回值将为 0,否则为 -1

布尔变量类型在部分 CPython 源码中受支持和使用,但它们不是原始语言的一部分。C 使用简单的规则解释二进制条件:0 或 NULL 为假,其他一切都为真。

switch 语句

与 Python 不同的是,C 还支持 switch。使用 switch 可以被视为扩展 if...else 链的快捷方式。此示例来自 semaphore.c:

代码不翻译

这将基于 WaitForSingleObjectEx() 的返回值执行切换。如果该值为 WAIT_OBJECT_0,则执行第一个代码块。WAIT_TIMEOUT 值会执行第二个代码块,其他任何内容都与 default 代码块匹配。

需要注意的是 switch 中的值都会被测试校验。在本例中,来自 WaitForSingleObjectEx() 的返回值必须是整数值或枚举类型,并且每一个 case 都必须是常量值。

循环

C 语言中有三种循环结构:

  1. for 循环;

  2. while 循环;

  3. do ... while 循环。

让我们依次看一下每一个循环结构:

for 循环的语法与 Python 完全不同:

for ( <initialization>; <condition>; <increment>) {
<code to be looped over>
}

除了要在循环中执行的代码外,还有三个代码块控制 for 循环:

  1. 当循环启动时,<initialization> 部分正好运行一次。它通常用于将循环计数器设置为初始值(可能还用于声明循环计数器)。

  2. <increment> 代码在每次通过循环的主代码块后立即运行。一般情况下,这将增加循环计数器。

  3. 最后,<condition><increment> 之后运行。将计算此代码的返回值,当此条件返回 false 时,循环将中断。

下面是 Modules/sha512module.c 中的一个示例:

for (i = 0; i < 8; ++i) {
    S[i] = sha_info->digest[i];
}

此循环将运行 8 次,i 从 0 递增到 7,当条件被检查且 i 为 8 时将终止。

while 循环实际上与 Python 对应的循环相同。但是,do...while 语法有点不同。到第一次执行循环体之后才会检查 do...while 循环中的条件。

在 CPython 代码基线中,有许多 for 循环和 while 循环实例,但是 do..while 没有被使用到。

函数

C 中的函数语法与 Python 中的语法相似,只是必须指定返回类型和参数类型。C 语法是这样的:

<return_type> function_name(<parameters>) {
    <function_body>
}

返回类型可以是 C 中的任何有效类型,包括内置类型,如:intdouble,以及自定义类型,如:PyObject,如来自 semaphore.c 中的示例所示:

static PyObject *
semlock_release(SemLockObject *self, PyObject *args)
{
    <statements of function body here>
}

在这里,你可以看到一些 C 相关的特性正在发挥作用。首先,记住空格并不重要。CPython 源代码的大部分内容都将函数的返回类型放在函数声明的上方。这就是 PyObject * 部分。稍后,你将仔细研究 * 的用法,但现在重要的是要知道,你可以在函数和变量上放置一些修饰符。

static 是这些修饰符之一。有一些复杂的规则来管理修饰符的操作方式。例如,这里的 static 修饰符的含义与将其放在变量声明前面时非常不同。

幸运的是,在尝试阅读和理解 CPython 源代码时,你通常可以忽略这些修饰符。

函数的参数列表是一个逗号分隔变量列表,类似于你在 Python 中使用的。同样,C 需要每个参数的具体类型,因此 SemLockObject *self 表示第一个参数是指向 SemLockObject 的指针,并被称为 self。请注意,C 中的所有参数都是位置参数。

让我们来看看该语句的“指针”部分是什么意思。

为了给出一些上下文,传递给 C 函数的参数都是按值传递的,这意味着函数在调用函数中的值的副本上操作,而不是在原始值上操作。为了解决这个问题,函数将经常传入函数可以修改的一些数据的地址。

这些地址被称为指针,并具有类型,因此 int * 是指向整数值的指针,与 double * 类型不同,double * 是指向双精度浮点数的指针。

指针

如上所述,指针是保存值地址的变量。这些在 C 中经常使用,如本示例所示:

static PyObject *
semlock_release(SemLockObject *self, PyObject *args)
{
    <statements of function body here>
}

在这里,self 参数将保存 SemLockObject 值的地址或指向 SemLockObject 值的指针。另外请注意,该函数将返回指向 PyObject 值的指针。

在 C 中,有一个名为 NULL 的特殊值,它表示指针不指向任何东西。在整个 CPython 源码中,你将看到分配给 NULL 并根据 NULL 进行检查的指针。需要注意的一点是指针可以具有什么值的限制很少,而且访问不属于程序的内存位置可能会导致非常奇怪的行为。

另一方面,如果你尝试访问 NULL 的内存,则你的程序将会立即退出。这似乎不太好,但访问 NULL 通常比修改随机内存地址更容易找出内存漏洞。

字符串

C 没有字符串类型。有一个惯例:许多标准库函数都是围绕它编写的,但没有实际的类型。相反,C 中的字符串存储为 char (对于 ASCII 而言)或 wchar (对于 Unicode 而言)值的数组,每个值都包含一个字符。字符串用空终止符标记,其值为 0,通常在代码中显示为\0

基本的字符串操作,如 strlen() 依赖于此空终止符来标记字符串的结尾。

因为字符串只是值的数组,所以不能直接复制或比较它们。标准库有 strcpy()strcmp() 函数(及其wchar 表亲)用于执行这些操作等。

结构体

在这个 C 迷你之旅的最后一站是如何在 C 中创建新的类型:结构体struct 关键字允许你将一组不同的数据类型组合在一起,形成一个新的自定义数据类型:

struct <struct_name> {
    <type> <member_name>;
    <type> <member_name>;
    ...
};

Modules/arraymodule.c 中的部分示例展示了一个 struct 声明:

struct arraydescr {
    char typecode;
    int itemsize;
    ...
};

这将创建一个名为 arraydescr 的新数据类型,它有许多成员,其中前两个是char typecodeint itemsize

结构体通常被用作 typedef 的一部分,其为结构体名提供了一个简单的别名。在上面的示例中,新类型中的所有变量都必须被一个全名进行声明:struct arraydescr x;

你会经常看到这样的语法:

typedef struct {
    PyObject_HEAD
    SEM_HANDLE handle;
    unsigned long last_tid;
    int count;
    int maxvalue;
    int kind;
    char *name;
} SemLockObject;

这将创建一个新的自定义结构类型,并为其命名为 SemLockObject。要声明此类型的变量,你可以简单地使用别名:SemLockObject x;

Last updated