面向 Python 程序员的 C

举报
Yuchuan 发表于 2021/08/12 14:03:33 2021/08/12
【摘要】 您对 C 语法的快速浏览到此结束。虽然这个描述几乎没有触及 C 语言的表面,但您现在已经有足够的知识来阅读和理解 CPython 源代码。

Table of Contents

本教程的目的是让有经验的 Python 程序员快速掌握C 语言的基础知识以及如何在CPython 源代码中使用它。它假设您已经对 Python 语法有中级理解。

也就是说,C 是一种相当有限的语言,它在 CPython 中的大部分用法都属于一小组语法规则。与能够有效地编写 C 相比,理解代码的步骤要小得多。本教程针对第一个目标,而不是第二个目标。

在本教程中,您将学习:

  • 什么是C预处理器是和什么样的作用它对营造C程序
  • 如何使用预处理器指令来操作源文件
  • 如何C语法比较Python语法
  • 如何在 C 中创建循环函数字符串和其他功能

Python 和 C 之间最显着的区别之一是 C 预处理器。你先看看那个。

C 预处理器

顾名思义,预处理器在编译器运行之前在源文件上运行。它的功能非常有限,但是您可以利用它们在构建 C 程序时发挥巨大的优势。

预处理器生成一个新文件,编译器将实际处理该文件。预处理器的所有命令都从一行的开头开始,一个#符号作为第一个非空白字符。

预处理器的主要目的是在源文件中做文本替换,但它也会做一些带有#if或类似语句的基本条件代码。

您将从最常用的预处理器指令开始:#include.

#include

#include用于将一个文件的内容拉入当前源文件中。没有什么复杂的#include。它从文件系统中读取一个文件,对该文件运行预处理器,并将结果放入输出文件中。这是为每个指令递归完成的#include

例如,如果您查看 CPython 的Modules/_multiprocessing/semaphore.cfile,那么在顶部附近您将看到以下行:

#include "multiprocessing.h"

这告诉预处理器multiprocessing.h在这个位置拉入 的全部内容并将它们放入输出文件中。

您会注意到#include语句有两种不同的形式。其中一个使用引号 ( "") 来指定包含文件的名称,另一个使用尖括号 ( <>)。不同之处在于在文件系统上查找文件时搜索了哪些路径。

如果您使用<>for 文件名,那么预处理器将只查看系统包含文件。在文件名周围使用引号将强制预处理器首先查看本地目录,然后返回到系统目录。

#define

#define允许您进行简单的文本替换,还可以播放#if您将在下面看到的指令。

在最基本的情况下,#define您可以定义一个新符号,该符号在预处理器输出中被替换为文本字符串。

继续semphore.c,你会发现这一行:

#define SEM_FAILED NULL

这告诉预处理器在将代码发送到编译器之前SEM_FAILED用文字字符串替换此点以下的每个实例NULL

#define项目也可以采用参数,如此 Windows 特定版本中的SEM_CREATE

#define SEM_CREATE(name, val, max) CreateSemaphore(NULL, val, max, NULL)

在这种情况下,预处理器将期望SEM_CREATE()看起来像一个函数调用并具有三个参数。这通常称为。它将直接将三个参数的文本替换为输出代码。

例如,在第 460 行semphore.cSEM_CREATE宏是这样使用的:

handle = SEM_CREATE(name, value, max);

当您为 Windows 编译时,此宏将被展开,因此该行如下所示:

handle = CreateSemaphore(NULL, value, max, NULL);

在后面的部分中,您将看到该宏在 Windows 和其他操作系统上的定义有何不同。

#undef

该指令从#define. 这使得可以#define只对文件的一部分有效。

#if

预处理器还允许使用条件语句,允许您根据特定条件包含或排除文本部分。条件语句用#endif指令关闭,也可以使用#elif#else进行微调。

#if您将在 CPython 源代码中看到三种基本形式:

  1. #ifdef <macro>如果定义了指定的宏,则包括随后的文本块。你也可以看到它写成#if defined(<macro>).
  2. #ifndef <macro>包括:如果在指定的宏文本的后续块限定。
  3. #if <macro>如果宏已定义并且其计算结果为则包括随后的文本块True

请注意使用“文本”而不​​是“代码”来描述文件中包含或排除的内容。预处理器对 C 语法一无所知,也不关心指定的文本是什么。

#pragma

Pragma 是编译器的指令或提示。通常,您可以在阅读代码时忽略这些,因为它们通常处理代码的编译方式,而不是代码的运行方式。

#error

最后,#error显示一条消息并导致预处理器停止执行。同样,您可以安全地忽略这些来阅读 CPython 源代码。

Python 程序员的基本 C 语法

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

一般

与 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 中一样工作。如果条件为真,则执行以下块。在elseelse if语法应该熟悉足以Python程序员。请注意,Cif语句不需要 ,endif因为块由 分隔{}

在 C 中有一个简短的ifelse语句的简写,称为三元运算符

condition ? true_result : false_result

您可以在semaphore.c其中找到它,对于 Windows,它定义了一个宏SEM_CLOSE()

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

这个宏的返回值是0函数是否CloseHandle()返回true-1否则返回。

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

switch 声明

与 Python 不同,C 还支持switch. 使用switch可以看作是扩展ifelseif链的捷径。这个例子来自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值导致第二个块,其他任何内容都与该default块匹配。

请注意,被测试的值,在这种情况下,来自 的返回值WaitForSingleObjectEx()必须是整数值或枚举类型,并且每个case值都必须是常量值。

循环

C语言有3种循环结构:

  1. for 循环
  2. while 循环
  3. dowhile循环

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次数i0到递增7,并在条件检查且i为时终止8

while循环实际上与它们的Python 对应物相同。然而do……while语法有点不同。直到第一次执行循环体之后,才检查dowhile循环上的条件。

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说第一个参数是指向 a 的指针,SemLockObject并称为self。请注意,C 中的所有参数都是位置参数。

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

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

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

指针

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

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

在这里,self参数将持有的地址,或者一个指针,一个SemLockObject值。另请注意,该函数将返回一个指向PyObject值的指针。

注意:要深入了解如何在 Python 中模拟指针,请查看Python 中的指针:重点是什么?

在 C 中有一个特殊的值,NULL它表示一个指针不指向任何东西。您将看到在整个 CPython 源代码中分配NULL和检查的指针NULL。这很重要,因为对于指针可以具有的值几乎没有限制,并且访问不属于程序的内存位置可能会导致非常奇怪的行为。

另一方面,如果您尝试访问 处的内存NULL,那么您的程序将立即退出。这可能看起来不太好,但通常NULL比修改随机内存地址更容易找出内存错误。

字符串

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

像这样的基本字符串操作strlen()依赖于这个空终止符来标记字符串的结尾。

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

结构

这个 C 迷你之旅的最后一站是如何在 C 中创建新类型:structs。该struct关键字允许您将一组不同的数据类型组合到一个新的自定义数据类型中:

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

来自的这个部分示例Modules/arraymodule.c显示了一个struct声明:

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

这将创建一个名为的新数据类型arraydescr,它具有许多成员,其中前两个是 achar typecode和 an int itemsize

通常,结构将用作 a 的一部分typedef,它为名称提供了一个简单的别名。在上面的例子中,新类型的所有变量都必须用全名声明struct arraydescr x;

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

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

结论

您对 C 语法的快速浏览到此结束。虽然这个描述几乎没有触及 C 语言的表面,但您现在已经有足够的知识来阅读和理解 CPython 源代码。

在本教程中,您学习了:

  • 什么是C预处理器是和什么样的作用它对营造C程序
  • 如何使用预处理器指令来操作源文件
  • 如何C语法比较Python语法
  • 如何在 C 中创建循环函数字符串和其他功能

现在您已经熟悉了 C,您可以通过探索 CPython 源代码来加深对 Python 内部工作原理的了解。快乐的Python!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。