C для программистов на Python

Оглавление

Цель этого руководства - познакомить опытного программиста на Python с основами языка Си и с тем, как он используется в исходном коде CPython. Предполагается, что у вас уже есть среднее представление о синтаксисе Python.

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

В этом уроке вы узнаете:

  • Что такое препроцессор языка Си и какую роль он играет в создании программ на Си
  • Как можно использовать директивы препроцессора для управления исходными файлами
  • Как Синтаксис C сравнивается с синтаксисом Python
  • Как создавать циклы, функции, строки и другие возможности языка Си

Одним из первых существенных различий между Python и C является препроцессор C. На это вы посмотрите в первую очередь.

Препроцессор языка Си

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

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

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

Вы начнете с наиболее часто используемой директивы препроцессора: #include.

#include

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

Например, если вы посмотрите на файл CPython 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.

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

#pragma

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

#error

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

Базовый синтаксис языка Си для программистов на Python

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

В языке Си есть сокращение для коротких 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 равно false, а все остальное равно true.

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

<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>
}

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

Примечание: Более подробно о том, как имитировать указатели в 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>;
    ...
};

Этот неполный пример из 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;.

Заключение

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

В этом уроке вы узнали:

  • Что такое препроцессор языка Си и какую роль он играет в создании программ на Си
  • Как можно использовать директивы препроцессора для управления исходными файлами
  • Как Синтаксис C сравнивается с синтаксисом Python
  • Как создавать циклы, функции, строки и другие возможности языка Си

Теперь, когда вы знакомы с C, вы можете углубить свои знания о внутренней работе Python, изучив исходный код CPython. Счастливого использования Python!

Back to Top