也就是说,C 是一种相当有限的语言,它在 CPython 中的大部分用法都属于一小组语法规则。达到理解代码的程度比能够有效地编写 C 语言要小得多。本教程针对第一个目标,但不是第二个目标。
C 预处理器
顾名思义,预处理器在编译器运行之前在源文件上运行。它的功能非常有限,但您可以在构建 C 程序时充分利用它们。
预处理器生成一个新文件,这是编译器实际处理的文件。预处理器的所有命令都从一行的开头开始,并带有#
符号作为第一个非空白字符。
预处理器的主要目的是在源文件中进行文本替换,但它也会执行一些基本的条件代码#if
或类似的陈述。
您将从最常见的预处理器指令开始:#include
.
#include
#include
用于将一个文件的内容拉入当前源文件。没有什么复杂的#include
。它从文件系统读取文件,对该文件运行预处理器,并将结果放入输出文件中。这个做完了递归地对于每个#include
指示。
例如,如果您查看 CPythonModules/_multiprocessing/semaphore.c 文件,然后在顶部附近您将看到以下行:
#include "multiprocessing.h"
这告诉预处理器提取整个内容multiprocessing.h
并将它们放入输出文件的这个位置。
您会注意到有两种不同的形式#include
陈述。其中之一使用引号(""
)指定包含文件的名称,另一个使用尖括号(<>
)。区别在于在文件系统上查找文件时搜索的路径。
如果你使用<>
对于文件名,那么预处理器将仅查看系统包含文件。在文件名周围使用引号将强制预处理器首先查找本地目录,然后回退到系统目录。
#define
#define
允许您进行简单的文本替换,并且还可以发挥作用#if
您将在下面看到指令。
最基本的是,#define
允许您定义一个新符号,该符号将替换为预处理器输出中的文本字符串。
继续在semphore.c
,你会发现这一行:
这告诉预处理器替换每个实例SEM_FAILED
低于此点的文字字符串NULL
在代码发送到编译器之前。
#define
项目也可以采用参数,就像这个特定于 Windows 的版本一样SEM_CREATE
:
#define SEM_CREATE(name, val, max) CreateSemaphore(NULL, val, max, NULL)
在这种情况下,预处理器将期望SEM_CREATE()
看起来像一个函数调用并具有三个参数。这通常被称为宏。它将直接将三个参数的文本替换为输出代码。
例如,在第 460 行semphore.c
, 这SEM_CREATE
宏的使用方式如下:
handle = SEM_CREATE(name, value, max);
当您针对 Windows 进行编译时,该宏将被扩展,使得该行看起来像这样:
handle = CreateSemaphore(NULL, value, max, NULL);
在后面的部分中,您将看到该宏在 Windows 和其他操作系统上的定义有何不同。
#undef
该指令将从中删除任何先前的预处理器定义#define
。这使得有可能有一个#define
仅对文件的一部分有效。
#if
预处理器还允许条件语句,允许您根据某些条件包含或排除文本部分。条件语句以#endif
指令,也可以利用#elif
和#else
用于微调。
有以下三种基本形式#if
您将在 CPython 源代码中看到:
-
#ifdef <macro>
如果定义了指定的宏,则包括后续的文本块。你也可能会看到它写成#if defined(<macro>)
.
-
#ifndef <macro>
如果指定的宏是,则包括后续的文本块not定义的。
-
#if <macro>
如果定义了宏,则包括后续文本块and它评估为True
.
请注意,使用“文本”而不是“代码”来描述文件中包含或排除的内容。预处理器对 C 语法一无所知,也不关心指定的文本是什么。
#pragma
编译指示是对编译器的指令或提示。一般来说,您可以在阅读代码时忽略这些,因为它们通常涉及代码的编译方式,而不是代码的运行方式。
#error
最后,#error
显示一条消息并导致预处理器停止执行。同样,您可以放心地忽略这些内容来阅读 CPython 源代码。
Python 程序员的基本 C 语法
本节不会涵盖all本书重点介绍了 C 语言的各个方面,也不是为了教您如何编写 C 语言。它将重点关注 Python 开发人员第一次看到时感到不同或感到困惑的 C 语言方面。
一般的
与 Python 不同,空格对于 C 编译器来说并不重要。编译器并不关心您是否跨行分割语句或将整个程序塞进一行很长的行中。这是因为它对所有语句和块使用分隔符。
当然,解析器有非常具体的规则,但一般来说,您只需知道每个语句以分号结尾就能够理解 CPython 源代码(;
),所有代码块都用大括号 ({}
).
此规则的例外是,如果一个块只有一条语句,则可以省略花括号。
C 中的所有变量都必须是宣布,这意味着需要有一个单独的语句来表明类型该变量的。请注意,与 Python 不同,单个变量可以保存的数据类型不能更改。
这里有一些例子:
/* Comments are included between slash-asterisk and asterisk-slash */
/* This style of comment can span several lines -
so this part is still a comment. */
// Comments can also come after two slashes
// This type of comment only goes until the end of the line, so new
// lines must start with double slashes (//).
int x = 0; // Declares x to be of type 'int' and initializes it to 0
if (x == 0) {
// This is a block of code
int y = 1; // y is only a valid variable name until the closing }
// More statements here
printf("x is %d y is %d\n", x, y);
}
// Single-line blocks do not require curly brackets
if (x == 13)
printf("x is 13!\n");
printf("past the if block\n");
一般来说,您会发现 CPython 代码的格式非常清晰,并且通常在给定模块中坚持单一风格。
if
声明
在C中,if
工作原理与 Python 中的工作方式大致相同。如果条件为真,则执行以下块。这else
和else if
Python 程序员应该非常熟悉语法。请注意,Cif
声明不需要endif
因为块是由{}
.
C 里有一个简写if
… else
声明称为三元运算符:
condition ? true_result : false_result
您可以在以下位置找到它:semaphore.c
其中,对于 Windows,它定义了一个宏SEM_CLOSE()
:
#define SEM_CLOSE(sem) (CloseHandle(sem) ? 0 : -1)
该宏的返回值将是0
如果函数CloseHandle()
回报true
和-1
否则。
笔记:CPython 源代码的部分支持和使用布尔变量类型,但它们不是原始语言的一部分。 C 使用一个简单的规则解释二进制条件:0
或者NULL
是假的,其他一切都是真的。
switch
声明
与Python不同,C还支持switch
。使用switch
可以看作是扩展的快捷方式if
… elseif
链。这个例子来自semaphore.c
:
switch (WaitForSingleObjectEx(handle, 0, FALSE)) {
case WAIT_OBJECT_0:
if (!ReleaseSemaphore(handle, 1, &previous))
return MP_STANDARD_ERROR;
*value = previous + 1;
return 0;
case WAIT_TIMEOUT:
*value = 0;
return 0;
default:
return MP_STANDARD_ERROR;
}
这对返回值执行切换WaitForSingleObjectEx()
。如果值为WAIT_OBJECT_0
,然后执行第一个块。这WAIT_TIMEOUT
value 结果在第二个块中,其他任何内容都与default
堵塞。
请注意,正在测试的值,在本例中为返回值WaitForSingleObjectEx()
,必须是整数值或枚举类型,并且每个case
必须是一个常数值。
循环
C语言中共有三种循环结构:
-
for
循环
-
while
循环
-
do
… while
循环
for
循环的语法与 Python 完全不同:
for ( <initialization>; <condition>; <increment>) {
<code to be looped over>
}
除了循环中要执行的代码之外,还有三个代码块控制循环for
环形:
-
这<initialization>
当循环开始时,该部分只运行一次。它通常用于将循环计数器设置为初始值(并且可能用于声明循环计数器)。
-
这<increment>
代码在每次通过循环的主块后立即运行。传统上,这将增加循环计数器。
-
最后,<condition>
在之后运行<increment>
。当此条件返回 false 时,将评估此代码的返回值并中断循环。
这是一个来自模块/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
循环不会被检查,直到后第一次执行循环体。
有很多这样的例子for
循环和while
CPython 代码库中的循环,但是do
… while
未使用。
功能
C 中函数的语法类似于在Python中,此外还必须指定返回类型和参数类型。 C 语法如下所示:
<return_type> function_name(<parameters>) {
<function_body>
}
返回类型可以是 C 中的任何有效类型,包括内置类型,例如int
和double
以及自定义类型,例如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 *
,它是一个指向双精度浮点数的指针。
指针
如上所述,指针是保存值地址的变量。这些在 C 中经常使用,如下例所示:
static PyObject *
semlock_release(SemLockObject *self, PyObject *args)
{
<statements of function body here>
}
在这里,self
参数将保存地址,或一个指向, ASemLockObject
价值。另请注意,该函数将返回一个指向PyObject
价值。
笔记:要深入了解如何在 Python 中模拟指针,请查看Python 中的指针:有什么意义?
C中有一个特殊的值叫做NULL
这表明指针没有指向任何东西。您将看到分配给的指针NULL
并检查NULL
贯穿 CPython 源代码。这很重要,因为对于指针可以具有的值几乎没有限制,并且访问不属于程序一部分的内存位置可能会导致非常奇怪的行为。
另一方面,如果您尝试访问内存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>;
...
};
这部分示例来自模块/arraymodule.c显示了一个struct
宣言:
struct arraydescr {
char typecode;
int itemsize;
...
};
这将创建一个新的数据类型,称为arraydescr
它有很多成员,其中前两个是char typecode
和int 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;
.