🐍 Язык C для программистов на Python

Если вы уже владеете синтаксисом Python, самое время познакомиться с основами языка C и посмотреть, как он используется в исходном коде CPython.

Перевод публикуется с сокращениями, автор оригинальной статьи Jim Anderson.

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

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

Здесь вы узнаете:

  • что такое C-препроцессор и какую роль он играет в создании программ;
  • как можно использовать директивы препроцессора для управления исходниками;
  • какова разница между синтаксисом C и Python;
  • как создавать циклы, функции, строки и другие объекты в C.

Перейдем к рассмотрению препроцессора C.

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

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

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

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

Начнем рассмотрение с самой частой директивы препроцессора: #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 перед отправкой кода компилятору.

Определенные с помощью #define элементы также могут принимать параметры, как в данном случае с SEM_CREATE для Windows:

#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);

Далее разберемся, как этот макрос определяется в различных ОС.

#undef

Директива стирает любое предыдущее определение препроцессора из #define, что позволяет использовать #define только для части файла.

#if

Препроцессор может использовать условные операторы, позволяющие включать или исключать куски текста на основе определенных условий. Условные операторы завершаются директивой #endif и могут использовать #elif и #else для точной настройки.

Есть три основные формы #if, которые вы могли встречать в исходнике CPython:

  • #ifdef <macro> включает в себя последующий блок текста, если задан указанный макрос. Иногда он выглядит как #if defined(<macro>);
  • #ifndef <macro> включает последующий блок текста, если указанный макрос не определен;
  • #if <macro> включает последующий блок текста, если макрос определен и имеет значение True.

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

#pragma

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

#error

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

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

Здесь не будут охвачены все аспекты языка программирования, и вы не научитесь писать код лучше, но будет уделено внимание аспектам C, которые отличаются или сбивают с толку разработчиков Python при первом знакомстве.

Основы

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

Есть, конечно, очень специфические правила для парсера, но в целом вы сможете понять исходник CPython, просто зная, что каждый оператор заканчивается символом (;), а все блоки кода окружены фигурными скобками ({}).

Исключение из этого правила – если блок имеет только один оператор, то фигурные скобки могут быть опущены.

Все переменные в C должны быть объявлены, то есть должен быть один оператор, указывающий тип этой переменной. Обратите внимание, что в отличие от Python, тип переменной в C не может измениться.

Вот несколько примеров:

/* Комментарии вставляются между символами /* и */       */
/* Этот стиль комментария может включать несколько строк – 
   так что эта часть все еще остается комментарием. */

// Такие комментарии создаются после символов //
// Этот тип комментария идет только до конца строки, поэтому новые
// строки должны начинаться с «//».

int x = 0; // Объявляет x типом int и присваивает ему 0

if (x == 0) {
    // Это блок кода
    int y = 1;  
    // Еще немного кода
    printf("x is %d y is %d\n", x, y);
}

// Однострочные блоки не требуют фигурных скобок
if (x == 13)
    printf("x is 13!\n");
printf("past the if block\n");

Код CPython хорошо отформатирован и придерживается одного стиля со всем проектом.

if

В C if работает аналогично Python – если условие истинно, то выполняется следующий блок. Синтаксис else и else if также должен быть знаком программистам Python. Обратите внимание, что операторы if не нуждаются в endif, поскольку блоки разделяются символом {}.

Как и в других языках, в C есть сокращение для if ... else – тернарный оператор:

condition ? true_result : false_result

Вы можете найти его в semaphore.c, где определяется макрос для SEM_CLOSE():

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

Возвращаемое значение этого макроса будет равно 0, если функция CloseHandle() возвращает true, иначе 1.

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

Если значение равно WAIT_OBJECT_0, то выполняется первый блок. Значение WAIT_TIMEOUT приводит ко второму блоку, а все остальное соответствует блоку по умолчанию.

Циклы

В C есть 3 вида циклов:

  • for
  • while
  • do … while

Синтаксис циклов for отличается от Python:

for ( <initialization>; <condition>; <increment>) {
    <code to be looped over>
}

Существует три блока кода, управляющие циклом:

  • <initialization> выполняется один раз при запуске цикла. Используется для установки счетчика в начальное значение (и, возможно, для объявления счетчика циклов).
  • <increment> запускается сразу после каждого прохождения через основной блок цикла. Традиционно это приведет к увеличению счетчика циклов.
  • <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 немного отличается – условие не проверяется до тех пор, пока тело цикла не будет выполнено в первый раз.

Функции

Синтаксис функций в C также похож на Python-овский, с тем отличием, что должен быть указан тип возвращаемого значения и типы параметров. Выглядит он следующим образом:

<return_type> function_name(<parameters>) {
    <function_body>
}

Возвращаемый тип может быть любым, включая встроенные типы, такие как int и double, а также пользовательские, вроде PyObject, как из примера ниже:

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

Здесь видим несколько специфичных для C особенностей. Помните, что пробелы не имеют значения, и большая часть кода CPython хранит возвращаемый тип функции в строке над остальной частью объявления функции. Теперь рассмотрим странный код с PyObject *.

Список параметров для функций представляет собой разделенный запятыми список переменных, подобный тому, что вы используете в Python. C требует определенных типов для каждого параметра, поэтому SemLockObject *self говорит, что первый параметр является указателем на SemLockObject и называется self.

Чтобы дать некоторый контекст, все передаваемые функциям C параметры передаются по значению, т. е. функция работает с копией, а не с исходным значением. Чтобы обойти это, функции часто передают адрес данных, которые функция может изменить.

Эти адреса называются указателями и имеют типы, поэтому int* является указателем на целочисленное значение и имеет другой тип, нежели double*, который является указателем на число с плавающей точкой.

Указатели

Как упоминалось выше, указатели – это переменные, содержащие адрес значения. Они часто используются в языке C, как показано в примере:

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

Здесь параметр self будет содержать адрес или указатель на значение SemLockObject. Обратите внимание, что функция вернет указатель на значение PyObject.

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

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

Строки

C не имеет строкового типа. Строки хранятся в виде массивов значений char (для ASCII) или wchar (для Unicode), каждый из которых содержит один символ. Из-за этого их нельзя напрямую копировать или сравнивать. Стандартная библиотека имеет функции strcpy() и strcmp() (и их двойники wchar) для выполнения этих операций и многого другого.

Структуры

Ключевое слово struct позволяет сгруппировать набор различных типов данных в новый пользовательский тип:

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

Этот пример из Modules/arraymodule.c частично демонстрирует объявление структуры:

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

Создается новый тип данных arraydescr, который имеет много членов: первые два из них являются char typecode и int itemsize.

Часто структуры будут использоваться как часть typedef, который предоставляет алиас для имени. В приведенном выше примере все переменные нового типа должны быть объявлены с указанием полного наименования структуры arraydescr х;.

Вы часто будете наблюдать такой синтаксис:

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

Создается новый пользовательский тип с именем SemLockObject. Для объявления переменной такого типа можно просто использовать псевдоним SemLockObject x;.

Заключение

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

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

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

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

Удачи в обучении!

Дополнительный материал:

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
28 июня 2018

3 самых важных сферы применения Python: возможности языка

Существует множество областей применения Python, но в некоторых он особенно...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...