Дополнение A. Введение в C для программистов Python

Данное введение имеет целью ознакомить опытного программиста Python с основами языка программирования C и его применением в исходном коде CPython. Предполагается, что у вас уже имеются средние навыки синтаксиса Python.

Тем не менее, C достаточно ограниченный язык и бо́льшая часть его применения в CPython подпадает в небольшой набор синтаксических правил. Достичь того момента чтобы разбираться в коде, это гораздо меньший шаг нежели умение эффективно писать на C. Это руководство направлено на первую цель, но не предназначено для второй.

Одним из первых отличительных моментов Python от C является препроцессор C. Давайте вначале рассмотрим именно его.

Препроцессор C

Препроцессор, как и предполагает его название, запускается на ваших исходных файлах перед исполнением их компиляции. Он обладает очень ограниченными возможностями, но вы можете пользоваться им для великолепных преимуществ при построении программ на C.

Сам препроцессор производит некий новый файл, который и является тем, что и будет обрабатывать на самом деле компилятор. Все команды для препроцессора начинаются в самом начале строки с символа # в качестве самого первого не пробельного символа.

Основная цель такого препроцессора состоит в выполнении подстановок текста в его исходном файле, но он также выполняет и некий код базового условия при помощи #if или аналогичных предложений.

Давайте начнём с наиболее часто применяемой директивы препроцессора, #include.

  #include

#include применяется для вытаскивания содержимого одного файла в ваш текущий исходный файл. Нет ничего заумного в #include. Он считывает файл из своей файловой системы, запускает соответствующий препроцессор для этого файла и помещает полученные результаты в файл своего вывода. Это выполняется рекурсивно для каждой директивы #include.

Например, если вы рассматриваете свой файл Modules/_multiprocessing/semaphore.c, то в пределах его вершины вы увидите такую строку:


#include "multiprocessing.h"
 	   

Она сообщает своему препроцессору вытащить всё содержимое multiprocessing.h и поместить его в получаемый на выходе файл в этом месте.

Вы отметите две различные формы для такого предложения #include. Одна из них использует кавычки ("") для определения названия вкладываемого файла, а другая применяет угловые скобки (<>). Основное отличие состоит в том, в каком пути выполняется поиск такого файла в файловой системе.

Если вы применяете для названия файла <>, тогда препроцессор будет искать только файлы вложения системы. Применение же кавычек вокруг имён файлов вместо этого заставит ваш препроцессор вначале просматривать локальный каталог, а затем возвращаться обратно к каталогам системы.

  #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.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:

  1. #ifdef <macro>: содержит идущий следом блок текста, когда предписанный макро определён. Вы можете обнаружить также написание #if defined(<macro>).

  2. #ifndef <macro> содержит следующим далее блок текста, когда заданный макро не определён.

  3. #if <macro> содержит далее блок текста, если его макро определён и он вычисляется как True.

Обратите внимание на применение слова "текст" вместо "код" при описании того, что включается в соответствующий файл или исключается из него. Препроцессор абсолютно ничего не знает о синтаксисе C и не заботится о том, что предписывает текст.

  #pragma

Прагмы это инструкции или советы компилятору. Обычно вы можете игнорировать их при чтении своего кода, поскольку они, как правило, относятся к тому, как этот код компилируется, а не к тому как этот код исполняется.

  #error

Наконец, #error отображает некое сообщение и вызывает останов исполнения препроцессора. И снова, вы безопасно можете игнорировать их при чтении своего исходного кода CPython.

Базовый синтаксис 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. Когда его условие истинно, тогда исполняется следующий блок. Синтаксис else и elseif должен быть достаточно знаком программистам Python. Обратите внимание н то, что предложения if C не нуждаются в неком endif по той причине, что блоки ограничены {}.

Для предложений if … else в C имеется сокращённая форма, именуемая тернарным (троичным) оператором:


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() значению. Когда этим значением является WaitForSingleObjectEx(), выполняется первый блок. Значение WAIT_TIMEOUT в результате приводит ко второму блоку, всё остальное помечается блоком default.

Обратите внимание на то, что подлежащие проверке значения, в данном случае это возвращаемые из 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> Возвращаемое значение этого кода будет вычисляться и прервёт данный цикл когда такое условие возвращает ложь.

Вот некий образец из 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 типом, включая такие встроенные типы как 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 будет содержать значение адреса или, некий указатель на значение SemLockObject. Кроме того, обратите внимание на то, что данная функция возвратит некий указатель на значение PyObject.

В C присутствует особое значение, носящее название NULL, которое указывает на то, что данный указатель не указывает ни на что. Вы будете наблюдать назначенные в NULL указатели и выполнять проверки относительно NULL на протяжении исходного кода CPython. Это важно, так как существует считанное число ограничений относительно того, какие значения могут иметь указатели, а доступ к местоположению в памяти, которое не является частью вашей программы способно вызывать очень странное поведение.

С другой стороны, если вы попробуете получить доступ в памяти по значению NULL, тогда ваша программа выполнит выход немедленно. Это может показаться не лучшим вариантом, однако обычно проще обнаружить ошибку при работе с памятью при достижении NULL, чем когда подвергается изменению случайный адрес в памяти.

  Строки

C не обладает типом строк. Существует некое соглашение относительно множества написанных функций стандартной библиотеки, но в реальности нет такого типа. Точнее, строки в C хранятся как массивы значений char (для ASCII) или как wchar (для Unicode), причём каждое содержит единственны символ. Строки помечаются терминатором null, который обладает значением 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 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;.

Выводы

На этом мы сворачиваем ваше краткое знакомство с синтаксисом C. Хотя данное описание лишь поверхностно касается описания самого языка программирования C, теперь вы обладаете достаточными сведениями для чтения и понимания самого исходного кода CPython.