本教程的目的是使经验丰富的Python程序员快速掌握C语言的基础知识以及如何在CPython源代码中使用它。假定您已经对Python语法有一定的了解。
也就是说,C是一种相当有限的语言,CPython中的大多数用法都属于一小套语法规则。与能够有效地编写C语言相比,理解代码要小得多。本教程的目标是第一个目标,而不是第二个目标。
Python和C的最大不同之处之一是C预处理器。您先来看一下。
注意:本教程改编自《 CPython内部知识:Python解释器指南》中的附录“ Python程序员C入门”。
免费下载:从CPython Internals:Python 3解释器指南中获取示例章节,向您展示如何解锁Python语言的内部工作原理,如何从源代码编译Python解释器以及参与CPython的开发。
顾名思义,预处理器在编译器运行之前在源文件上运行。它的功能非常有限,但是您可以在构建C程序时使用它们,从而发挥巨大的优势。
预处理器会生成一个新文件,编译器将实际处理该文件。预处理器的所有命令均从一行的开头开始,并以#符号作为第一个非空白字符。
预处理器的主要目的是在源文件中进行文本替换,但它还将使用#if或类似语句执行一些基本的条件代码。
#include用于将一个文件的内容拉入当前源文件。 #include并不复杂。它从文件系统读取文件,在该文件上运行预处理器,然后将结果放入输出文件中。这是对每个#include指令递归完成的。
例如,如果您查看CPython的Modules / _multiprocessing / semaphore.c文件,则在顶部附近将看到以下行:
这告诉预处理器提取multiprocessing.h的全部内容,并将它们放入此位置的输出文件中。
您会注意到#include语句的两种不同形式。其中一个使用引号("")指定包含文件的名称,另一个使用尖括号(<>)。区别来自在文件系统上查找文件时搜索路径。
如果您使用<>对于文件名,预处理器将仅查看系统包含文件。相反,在文件名周围使用引号将迫使预处理器先在本地目录中查找,然后退回到系统目录。
#define允许您执行简单的文本替换,还可以使用下面将看到的#if指令。
最基本的说来,#define允许您定义一个新符号,该符号在预处理器输出中将被文本字符串替换。
这告诉预处理器在将代码发送给编译器之前,将低于此点的SEM_FAILED的每个实例替换为文字字符串NULL。
在这种情况下,预处理器将期望SEM_CREATE()看起来像一个函数调用并具有三个参数。通常将其称为宏。它将三个参数的文本直接替换为输出代码。
当您使用Windows进行编译时,此宏将被展开,因此该行如下所示:
在下一节中,您将看到在Windows和其他操作系统上如何对该宏进行不同的定义。
此伪指令从#define中删除所有以前的预处理程序定义。这样就可以仅对文件的一部分使用#define。
预处理器还允许条件语句,使您可以根据某些条件包括或排除文本的各个部分。条件语句使用#endif指令关闭,也可以使用#elif和#else进行微调。
在CPython源代码中会看到#if的三种基本形式: #ifdef< macro> 如果定义了指定的宏,则包括后续文本块。 您可能还会看到它写为#if defined(< macro>)。 #ifndef< macro> 如果未定义指定的宏,则包括后续文本块。 #if< macro> 如果定义了宏,则包含随后的文本块,并且其结果为True。 请注意,使用“文本”而非“代码”来描述文件中包含或排除的内容。 预处理程序不了解C语法,也不关心指定的文本是什么。 语法是对编译器的指令或提示。 通常,您在阅读代码时可以忽略这些内容,因为它们通常处理代码的编译方式,而不是代码的运行方式。 最后,#error显示一条消息,并使预处理器停止执行。 同样,您可以放心地忽略这些内容,以阅读CPython源代码。
本节不会涵盖C的所有方面,也无意教您如何编写C。它将重点介绍C方面的不同之处,这些方面对于Python开发人员来说是第一次或第一次感到困惑。
与Python不同,空白对于C编译器并不重要。编译器不在乎是将语句拆分为多行还是将整个程序阻塞为一个很长的行。这是因为它对所有语句和块使用定界符。
当然,解析器有非常特定的规则,但是总的来说,只要知道每个语句以分号(;)结尾,并且所有代码块都用花括号( {})。
该规则的例外是,如果一个块只有一个语句,则可以省略花括号。
必须声明C中的所有变量,这意味着需要一个语句来指示该变量的类型。请注意,与Python不同,单个变量可以保存的数据类型不能更改。
/ *注释包括在斜线-星号和星号-斜线之间* / / *这种注释样式可以跨越多行-因此该部分仍然是注释。 * / //注释也可以在两个斜杠之后// //这种注释仅持续到行尾,因此新行必须以双斜杠(//)开头。 int x = 0; //声明x为' int'类型如果(x == 0){//这是一个代码块,则将其初始化为0 int y = 1; //直到结束时y才是有效的变量名} //这里有更多语句printf(" x是%d y是%d \\ n",x,y); } //如果(x == 13)printf(" x为13!\\ n");则单行块不需要大括号。 printf("粘贴if块\\ n");
通常,您会看到CPython代码的格式非常整洁,通常会在给定模块中坚持单一样式。
在C中,如果可以正常工作,就像在Python中一样。如果条件为真,则执行以下块。 else和else if语法应该对Python程序员足够熟悉。请注意,C if语句不需要endif,因为块用{}分隔。
如果…else语句称为三元运算符,则C中有一个简写形式:
您可以在semaphore.c中找到它,对于Windows,它为SEM_CLOSE()定义了一个宏:
如果函数CloseHandle()返回true,则此宏的返回值为0,否则为-1。
注意:布尔变量类型在CPython源代码的某些部分中受支持和使用,但它们不是原始语言的一部分。 C使用一个简单的规则解释二进制条件:0或NULL为false,其他所有条件为true。
与Python不同,C还支持switch。如果…elseif链,可以将使用开关视为扩展的快捷方式。此示例来自semaphore.c:
开关(WaitForSingleObjectEx(handle,0,FALSE)){case WAIT_OBJECT_0:if(!ReleaseSemaphore(handle,1,& previous))返回MP_STANDARD_ERROR; *值=上一个+ 1;返回0;情况WAIT_TIMEOUT:*值= 0;返回0;默认值:返回MP_STANDARD_ERROR; }
这将对WaitForSingleObjectEx()的返回值进行切换。如果值为WAIT_OBJECT_0,则执行第一个块。 WAIT_TIMEOUT值导致第二个块,而其他任何块均与默认块匹配。
请注意,要测试的值(在这种情况下为WaitForSingleObjectEx()的返回值)必须为整数值或枚举类型,并且每种情况下均必须为常量。
除了要在循环中执行的代码外,还有三个控制for循环的代码块:
<初始化>该部分在循环开始时仅运行一次。通常用于将循环计数器设置为初始值(并可能声明循环计数器)。
< increment>每次通过循环的主要块后,代码立即运行。传统上,这将增加循环计数器。
最后,< condition>在< increment>之后运行。此条件的返回值为false时,将评估此代码的返回值,并中断循环。
此循环将运行8次,i从0递增到7,并且在检查条件且i为8时终止。
while循环实际上与Python对应的循环相同。但是,do…while语法略有不同。只有在第一次执行循环主体之后,才会检查do…while循环的条件。
CPython代码库中有许多for循环和while循环的实例,但是…while未使用。
C语言中的函数语法类似于Python语言,此外还必须指定返回类型和参数类型。 C语法如下所示:
返回类型可以是C语言中的任何有效类型,包括诸如int和double之类的内置类型以及诸如PyObject之类的自定义类型,如本示例中的semaphore.c:
在这里,您可以看到几个C特有的功能。首先,请记住,空格并不重要。许多CPython源代码将函数的返回类型放在函数声明其余部分的上方。那就是PyObject *部分。稍后,您将仔细研究*的用法,但现在重要的是要知道可以在函数和变量上放置多个修饰符。
静态是这些修饰符之一。有一些复杂的规则来控制修饰符的操作方式。例如,这里的static修饰符与将其放在变量声明之前的含义大不相同。
幸运的是,在尝试阅读和理解CPython源代码时,通常可以忽略这些修饰符。
函数的参数列表是用逗号分隔的变量列表,类似于您在Python中使用的列表。同样,C需要为每个参数指定特定类型,因此SemLockObject * self表示第一个参数是指向SemLockObject的指针,称为self。请注意,C中的所有参数都是位置性的。
让我们看看该语句的“指针”部分的含义。
为了提供一些上下文,传递给C函数的参数都按值传递,这意味着该函数对值的副本进行操作,而不对调用函数中的原始值进行操作。要解决此问题,函数通常会传入一些可以修改的数据地址。
这些地址称为指针,并且具有类型,因此int *是指向整数值的指针,并且与double *是双精度浮点数的指针,其类型不同。
如上所述,指针是保存值地址的变量。这些在C中经常使用,如本例所示:
在这里,self参数将保存SemLockObject值的地址或指针。还要注意,该函数将返回一个指向PyObject值的指针。
C中有一个称为NULL的特殊值,它指示指针没有指向任何东西。您会在整个CPython源代码中看到分配给NULL并针对NULL进行检查的指针。这一点很重要,因为对于指针可以具有的值几乎没有限制,并且访问不属于程序的内存位置可能会导致非常奇怪的行为。
另一方面,如果您尝试以NULL访问内存,则程序将立即退出。这看起来似乎不太好,但是通常来说,如果访问NULL则比发现随机内存地址更容易发现内存错误。
C没有字符串类型。有一个约定,围绕该约定编写了许多标准库函数,但没有实际的类型。而是将C中的字符串存储为char(对于ASCII)或wchar(对于Unicode)值的数组,每个值都包含一个字符。字符串用空终止符标记,该终止符的值为0,通常在代码中显示为\\ 0。
基本的字符串操作(例如strlen())都依赖此null终止符来标记字符串的结尾。
因为字符串只是值的数组,所以不能直接复制或比较它们。标准库具有strcpy()和strcmp()函数(及其wchar表亲),用于执行这些操作以及更多操作。
您在C的这种迷你浏览中的最后一站是如何在C:结构中创建新类型。 struct关键字允许您将一组不同的数据类型组合在一起,成为一个新的自定义数据类型:
这将创建一个名为arraydescr的新数据类型,该数据类型具有许多成员,其中前两个是char类型代码和int项目大小。
通常,结构将用作typedef的一部分,该类型为名称提供了简单的别名。在上面的示例中,必须使用全名struct arraydescr x;声明所有新类型的变量。
typedef struct {PyObject_HEAD SEM_HANDLE句柄; 无符号长last_tid; 整数计数; int maxvalue; int种类; 字符*名称; } SemLockObject; 这将创建一个新的自定义结构类型,并将其命名为SemLockObject。 要声明这种类型的变量,您可以简单地使用别名SemLockObject x;。 这总结了您的C语法快速入门。 尽管此描述几乎没有触及C语言的表面,但是您现在已经拥有足够的知识来阅读和理解CPython源代码。 熟悉C之后,您可以通过探索CPython源代码来加深对Python内部工作的了解。 快乐的Pythoning! 注意:如果您喜欢从CPython Internals:Python解释器指南中学习到的样本内容,请务必阅读本书的其余部分。