Перевод публикуется с сокращениями, автор оригинальной статьи 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, то в верхней части увидите следующую строку:
Это дает задачу препроцессору взять все содержимое multiprocessing.h и поместить его в выходной файл на указанную позицию.
Вы могли заметить две разные формы нотации оператора #include. Одна из них использует кавычки ("") для указания имени включаемого файла, а другая – угловые скобки (<>). Разница заключается в том, по какому адресу искать необходимый файл в файловой системе.
Если используются <>, то препроцессор будет смотреть только на системные включаемые файлы. Использование кавычек вокруг имени заставит препроцессор сначала заглянуть в локальный каталог, а потом вернуться к системным.
#define
#define
позволяет
выполнить простую подстановку текста и участвует в манипуляциях с директивой
#if
, которые описаны ниже.
В самой простой
ситуации #define
объявляет новую константу, которая заменяется текстовой
строкой в выходных данных препроцессора.
Вернемся к semphore.c и найдем такую строку:
Это заставляет
препроцессор заменить каждый экземпляр SEM_FAILED
перед отправкой кода
компилятору.
Определенные с помощью #define элементы также
могут принимать параметры, как в данном случае с SEM_CREATE
для Windows:
Здесь препроцессор
будет ожидать, что SEM_CREATE()
– функция с тремя принимаемыми параметрами
и будет заменять текст в выходном коде. Это называется макросом.
Например, в строке 460
semphore.c макрос SEM_CREATE
используется следующим образом:
После обработки препроцессором для Windows строка будет выглядеть следующим образом:
Далее разберемся, как этот макрос определяется в различных ОС.
#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 не может измениться.
Вот несколько примеров:
Код CPython хорошо отформатирован и придерживается одного стиля со всем проектом.
if
В C if
работает аналогично
Python – если условие истинно, то выполняется следующий блок. Синтаксис else
и
else if
также должен быть знаком программистам Python. Обратите внимание, что
операторы if
не нуждаются в endif
, поскольку блоки разделяются символом {}.
Как и в других языках, в C есть сокращение для if ... else – тернарный оператор:
Вы можете найти его в
semaphore.c, где определяется макрос для SEM_CLOSE()
:
Возвращаемое значение
этого макроса будет равно 0, если функция CloseHandle()
возвращает true, иначе 1.
switch
Использование switch
можно рассматривать как расширенный вариант if ... elseif. Взглянем на
пример из semaphore.c:
Если значение равно
WAIT_OBJECT_0
, то выполняется первый блок. Значение WAIT_TIMEOUT
приводит ко
второму блоку, а все остальное соответствует блоку по умолчанию.
Циклы
В C есть 3 вида циклов:
- for
- while
- do … while
Синтаксис циклов for отличается от Python:
Существует три блока кода, управляющие циклом:
<initialization>
выполняется один раз при запуске цикла. Используется для установки счетчика в начальное значение (и, возможно, для объявления счетчика циклов).<increment>
запускается сразу после каждого прохождения через основной блок цикла. Традиционно это приведет к увеличению счетчика циклов.<condition>
выполняется после <increment>. Возвращаемое значение этого кода будет вычислено, и цикл прерывается, когда это условие вернет false.
Пример из Modules/sha512module.c:
Этот цикл будет выполняться 8 раз с шагом i от 0 до 7 и завершится на i=8.
Циклы while похожи на Python-аналоги, но синтаксис do ... while немного отличается – условие не проверяется до тех пор, пока тело цикла не будет выполнено в первый раз.
Функции
Синтаксис функций в C также похож на Python-овский, с тем отличием, что должен быть указан тип возвращаемого значения и типы параметров. Выглядит он следующим образом:
Возвращаемый тип может быть любым, включая встроенные типы, такие как int и double, а также пользовательские, вроде PyObject, как из примера ниже:
Здесь видим несколько специфичных для C особенностей. Помните, что пробелы не имеют значения, и большая часть кода CPython хранит возвращаемый тип функции в строке над остальной частью объявления функции. Теперь рассмотрим странный код с PyObject *.
Список параметров для функций представляет собой разделенный запятыми список переменных, подобный тому, что вы используете в Python. C требует определенных типов для каждого параметра, поэтому SemLockObject *self говорит, что первый параметр является указателем на SemLockObject и называется self.
Чтобы дать некоторый контекст, все передаваемые функциям C параметры передаются по значению, т. е. функция работает с копией, а не с исходным значением. Чтобы обойти это, функции часто передают адрес данных, которые функция может изменить.
Эти адреса называются указателями и имеют типы, поэтому int* является указателем на целочисленное значение и имеет другой тип, нежели double*, который является указателем на число с плавающей точкой.
Указатели
Как упоминалось выше, указатели – это переменные, содержащие адрес значения. Они часто используются в языке C, как показано в примере:
Здесь параметр self будет содержать адрес или указатель на значение SemLockObject. Обратите внимание, что функция вернет указатель на значение PyObject.
В языке C есть специальное значение NULL, сообщающее, что указатель ни на что не указывает. Это важно знать, поскольку существует очень мало ограничений относительно того, какие значения может иметь указатель, а доступ к не являющейся частью программы ячейке памяти может сломать весь проект.
Если попытаться получить доступ к памяти и наткнуться на NULL, то программа немедленно завершится. При такой ситуации проще вычислить ошибку памяти, чем при изменении случайного адреса.
Строки
C не имеет строкового
типа. Строки хранятся в виде массивов значений char (для ASCII) или wchar (для
Unicode), каждый из которых содержит один символ. Из-за этого их нельзя
напрямую копировать или сравнивать. Стандартная библиотека имеет функции
strcpy()
и strcmp()
(и их двойники wchar
) для выполнения этих операций и
многого другого.
Структуры
Ключевое слово struct позволяет сгруппировать набор различных типов данных в новый пользовательский тип:
Этот пример из Modules/arraymodule.c частично демонстрирует объявление структуры:
Создается новый тип данных arraydescr, который имеет много членов: первые два из них являются char typecode и int itemsize.
Часто структуры будут использоваться как часть typedef, который предоставляет алиас для имени. В приведенном выше примере все переменные нового типа должны быть объявлены с указанием полного наименования структуры arraydescr х;.
Вы часто будете наблюдать такой синтаксис:
Создается новый пользовательский тип с именем SemLockObject. Для объявления переменной такого типа можно просто использовать псевдоним SemLockObject x;.
Заключение
Статья не претендует на звание всеобъемлющего мануала по языку C, но теперь у вас есть достаточные знания, чтобы прочитать и понять исходный код CPython.
В этом уроке вы узнали:
- что такое препроцессор и какую роль он играет в построении программ;
- как можно использовать директивы препроцессора для управления исходными файлами;
- отличие синтаксиса C и Python;
- как создавать циклы, функции, строки и другие структуры.
Теперь, когда вы познакомились с C поближе, можно углубить знания о внутренней работе Python, изучив исходный код CPython.
Удачи в обучении!
Дополнительный материал:
Комментарии