КулЛиб - Классная библиотека! Скачать книги бесплатно 

Язык C. Мастерство программирования. Принципы, практики и паттерны [Кристофер Прешерн] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Прешерн К.

Язык С
Мастерство программирования
Принципы, практики и паттерны

Fluent C

Principles, Practices, and Patterns

Christopher Preschern

Beijing • Cambridge • Farnham • Köln • Sebastopol • Tokyo

Язык С
Мастерство программирования
Принципы, практики и паттерны

Прешерн К.

2023

УДК 004.4
ББК 32.372
П71

П71

Прешерн К.
Язык С. Мастерство программирования. Принципы, практики и паттерны / пер. с англ. А. Н. Слинкина – М.: ДМК Пресс, 2023. – 300 с.: ил.

ISBN 978-6-01810-340-7
В этом практическом руководстве начинающие и опытные програм­
мисты на C найдут наставления по принятию проектных решений, включая пошаговое применение паттернов к сквозным примерам.
Автор, один из ведущих членов сообщества паттернов проектирования,
объясняет, как организовать программу на C, как обрабатывать ошибки
и проектировать гибкие интерфейсы. В части I вы научитесь реализовывать проверенные практикой подходы к программированию на языке C;
часть II показывает, как паттерны программирования на C применяются
к реализации более крупных программ.

Copyright © 2023 Books.kz Limited Liability Partnership. All rights reserved.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без
письменного разрешения владельцев авторских прав.
Материал, изложенный в данной книге, многократно проверен. Но, поскольку
вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи
с этим издательство не несет ответственности за возможные ошибки, связанные
с использованием книги.

ISBN 978-1-49210-973-3 (англ.)
ISBN 978-6-01810-340-7 (казах.)

© Christopher Preschern, 2023
© Оформление, перевод на русский язык, издание,
Books.kz, 2023

Оглавление
Предисловие....................................................................................................8
ЧАСТЬ I. Паттерны на C............................................................................... 25
Глава 1. Обработка ошибок......................................................................... 26
Сквозной пример..............................................................................................27
Разбиение функции................................................................................................ 29
Проверка условий.................................................................................................... 32
Принцип самурая.................................................................................................... 35
Переход к обработке ошибки................................................................................. 39
Запись об очистке.................................................................................................... 42
Объектная обработка ошибок................................................................................ 45
Резюме................................................................................................................48
Для дополнительного чтения...........................................................................49
Что дальше.........................................................................................................50

Глава 2. Возврат информации об ошибке................................................. 51
Сквозной пример..............................................................................................52
Возврат кода состояния.......................................................................................... 54
Возврат существенной информации об ошибке................................................... 61
Специальное возвращаемое значение.................................................................. 67
Протоколирование ошибок.................................................................................... 70
Резюме................................................................................................................77
Для дополнительного чтения...........................................................................77
Что дальше.........................................................................................................77

Глава 3. Управление памятью..................................................................... 78

Хранение данных и проблемы с динамической памятью..............................80
Сквозной пример.................................................................................................... 83
Сначала стек............................................................................................................ 83
Вечная память......................................................................................................... 86
Последствия............................................................................................................. 88
Отложенная очистка............................................................................................... 90
Единоличное владение........................................................................................... 94
Обертка выделения................................................................................................. 97
Проверка указателя............................................................................................... 102
Пул памяти............................................................................................................ 105
Резюме..............................................................................................................111
Для дополнительного чтения.........................................................................111
Что дальше.......................................................................................................112

Глава 4. Возврат данных из C-функций................................................... 113
Сквозной пример............................................................................................115
Возвращаемое значение....................................................................................... 116

6

 Оглавление
Выходные параметры........................................................................................... 119
Агрегат................................................................................................................... 123
Неизменяемый экземпляр................................................................................... 128
Буфер, принадлежащий вызывающей стороне.................................................. 131
Вызываемая сторона выделяет память............................................................... 135

Резюме..............................................................................................................139
Что дальше.......................................................................................................140

Глава 5. Время жизни и владение данными.......................................... 141

Сквозной пример............................................................................................143
Программный модуль без состояния................................................................... 144
Программный модуль с глобальным состоянием............................................... 148
Экземпляр, принадлежащий вызывающей стороне.......................................... 152
Разделяемый экземпляр....................................................................................... 158
Резюме..............................................................................................................164
Для дополнительного чтения.........................................................................165
Что дальше.......................................................................................................166

Глава 6. Гибкие API...................................................................................... 167

Сквозной пример............................................................................................169
Заголовочные файлы............................................................................................ 169
Описатель.............................................................................................................. 172
Динамический интерфейс.................................................................................... 176
Управление функцией.......................................................................................... 179
Резюме..............................................................................................................183
Для дополнительного чтения.........................................................................183
Что дальше.......................................................................................................184

Глава 7. Гибкие интерфейсы итераторов................................................. 185

Сквозной пример............................................................................................187
Доступ по индексу................................................................................................. 188
Курсор..................................................................................................................... 192
Итератор обратного вызова................................................................................. 197
Резюме..............................................................................................................202
Для дополнительного чтения.........................................................................203
Что дальше.......................................................................................................204

Глава 8. Организация файлов в модульных программах.................... 205
Сквозной пример............................................................................................207
Охрана включения................................................................................................ 209
Каталоги программных модулей......................................................................... 212
Глобальный каталог include.................................................................................. 217
Автономный компонент....................................................................................... 221
Копия API............................................................................................................... 226
Резюме..............................................................................................................235
Что дальше.......................................................................................................235

Глава 9. Бегство из ада #ifdef.................................................................... 236
Сквозной пример............................................................................................238
Избегание вариантов............................................................................................ 240

Оглавление  7
Изолированные примитивы................................................................................ 243
Атомарные примитивы........................................................................................ 246
Уровень абстракции.............................................................................................. 250
Разделение реализаций вариантов...................................................................... 255

Резюме..............................................................................................................261
Для дополнительного чтения.........................................................................261
Что дальше.......................................................................................................262

ЧАСТЬ II. Истории о паттернах................................................................ 263
Глава 10. Реализация протоколирования............................................... 264
История о паттернах.......................................................................................264
Организация файлов............................................................................................. 265
Центральная функция протоколирования.......................................................... 266
Фильтрация источника сообщений..................................................................... 267
Условное протоколирование................................................................................ 269
Несколько мест протоколирования..................................................................... 270
Протоколирование в файл.................................................................................... 272
Кросс-платформенная обработка файлов........................................................... 273
Использование средства протоколирования...................................................... 277
Резюме..............................................................................................................277

Глава 11. Построение системы управления пользователями............. 279
История о паттернах.......................................................................................279
Организация данных............................................................................................ 279
Организация файлов............................................................................................. 281
Аутентификация: обработка ошибок.................................................................. 282
Аутентификация: протоколирование ошибок.................................................... 284
Добавление пользователей: обработка ошибок................................................. 285
Итерирование........................................................................................................ 287
Применение системы управления пользователями........................................... 290
Резюме..............................................................................................................291

Глава 12. Заключение................................................................................. 293
Чему вы научились..........................................................................................293
Для дополнительного чтения.........................................................................293
Заключительные замечания...........................................................................294

Об авторе..................................................................................................... 295
Об иллюстрации на обложке.................................................................... 295
Предметный указатель.............................................................................. 296

Предисловие
Вы купили эту книгу, чтобы поднять свои навыки программирования на новый
уровень. И это правильно, потому что вам, безусловно, пригодятся излагаемые
в ней практические знания. Если у вас имеется большой опыт программирования на C, то вы в деталях узнаете, как принимаются хорошие проектные решения и какие у них есть плюсы и минусы. Если вы только начинаете знакомиться
с C, то найдете здесь руководство по принятию решений и на примерах кода
увидите, как эти решения применяются для построения больших программ.
В книге есть ответы на вопросы о том, как структурировать C-программу,
как обрабатывать ошибки и как проектировать гибкие интерфейсы. Когда вы
больше узнаёте о программировании на C, начинают возникать разные вопросы, например:
• следует ли возвращать имеющуюся информацию об ошибке?
• следует ли использовать для этой цели глобальную переменную errno?
• что лучше: немного функций с большим числом параметров или наоборот?
• как построить гибкий интерфейс?
• как реализовать базовые вещи, например итератор?
Для объектно ориентированных языков на большую часть этих вопросов
почти исчерпывающий ответ дает книга «банды четырех»: Erich Gamma, Richard
Helm, Ralph Johnson, John Vlissides «Design Patterns: Elements of Reusable Object-Oriented Software»1. Паттерны проектирования дают программисту проверенные
опытом рекомендации, как должны взаимодействовать между собой объекты
и как они связаны отношением владения. Кроме того, они показывают, как
следует группировать объекты.
Однако на процедурных языках типа C большинство этих паттернов проектирования невозможно реализовать так, как описано «бандой четырех».
В С нет встроенных объектно ориентированных механизмов. Наследование
или полиморфизм можно эмулировать, но это не лучшее решение, потому что
такой код будет непонятен программистам, привыкшим к программированию
на C, но не владеющим программированием на объектно ориентированных
языках типа C++ и незнакомым с использованием таких концепций, как наследование и полиморфизм. Такие программисты хотели бы придерживаться
стиля программирования на C, к которому привыкли. Однако к нему применимы не все объектно ориентированные рекомендации или, по крайней мере,
конкретная реализация идеи паттерна проектирования не годится для не объектно ориентированного языка.
1

Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. «Паттерны объектно ориентированного проектирования». Питер, 2022.

Зачем я написал эту книгу  9
Итак, ситуация выглядит следующим образом: мы хотим писать на C, но не
можем напрямую использовать большую часть знаний, документированных в
виде паттернов проектирования. В этой книге показано, как преодолеть разрыв и практически реализовать эти знания на языке программирования C.

Зачем я написал эту книгу
Теперь я хочу рассказать, почему знания, собранные в этой книге, оказались
столь важными для меня и почему их так трудно отыскать.
В школе я изучал C в качестве первого языка программирования. Как и любой
начинающий программист на C, я удивлялся, почему нумерация элементов
массива начинается с 0, и наугад пытался поместить операторы * и & в нужное
место, чтобы заставить работать магию указателей.
В университете я узнал, как в действительности работают синтаксические
конструкции C и как они транслируются в аппаратные биты и байты. Вооруженный этими знаниями, я смог писать небольшие программы, которые работали очень хорошо. Но я по-прежнему не понимал, почему более длинный код
выглядит именно так, а не иначе, и, уж конечно, не мог сам придумать решения
вроде:

typedef
typedef
typedef
typedef

struct INTERNAL_DRIVER_STRUCT* DRIVER_HANDLE;
void (*DriverSend_FP)(char byte);
char (*DriverReceive_FP)();
void (*DriverIOCTL_FP)(int ioctl, void* context);

struct DriverFunctions
{
DriverSend_FP fpSend;
DriverReceive_FP fpReceive;
DriverIOCTL_FP fpIOCTL;
};
DRIVER_HANDLE driverCreate(void* initArg, struct DriverFunctions f);
void driverDestroy(DRIVER_HANDLE h);
void sendByte(DRIVER_HANDLE h, char byte);
char receiveByte(DRIVER_HANDLE h);
void driverIOCTL(DRIVER_HANDLE h, int ioctl, void* context);
При изучении этого кода возникает много вопросов:
• зачем нужны указатели на функции в struct?
• зачем функциям нужен этот DRIVER_HANDLE?
• что такое IOCTL и почему бы не написать вместо этого отдельные функции?
• зачем нужны явные функции создания и уничтожения?
Эти вопросы появились, когда я начал писать производственные приложения.

10

 Предисловие

Я то и дело сталкивался с ситуациями, когда понимал, что мне не хватает
знаний о C; например, как реализовать итератор или как обрабатывать ошибки
в функциях. Я осознавал, что синтаксис-то я освоил, но понятия не имею, как
им правильно воспользоваться. Я пытался чего-то добиться, но все получалось
коряво или не получалось вовсе. Мне были необходимы рекомендации,
показывающие, как решать конкретные задачи на языке C. Например:
• как проще всего захватывать и освобождать ресурсы?
• можно ли использовать goto для обработки ошибок?
• следует ли сразу проектировать интерфейс гибким или лучше изменять
его, когда возникнет необходимость?
• следует ли использовать макрос assert, или нужно возвращать код
ошибки?
• как реализовать итератор на C?
Для меня оказалось неожиданным открытием, что, хотя у моих опытных
коллег было много различных ответов на эти вопросы, никто не смог направить меня туда, где такие проектные решения были документированы вместе
с описанием их плюсов и минусов.
Поэтому я обратился к интернету и снова испытал удивление: оказалось
очень трудно найти убедительные ответы на эти вопросы, хотя язык C существует уже не один десяток лет. Я обнаружил, что, несмотря на изобилие литературы по основам и синтаксису языка C, нет почти ничего о продвинутом
программировании и о том, как писать на C красивый код, который выдержит
испытание производственным приложением.
И вот тут в игру вступает эта книга. Она поможет вам отточить свои навыки программирования на C и перейти от простеньких программок к большим
системам, в которых ошибки обрабатываются должным образом и которые обладают достаточной гибкостью, чтобы быть готовыми к будущим изменениям
требований и проекта. В этой книге используется концепция паттернов проектирования, чтобы познакомить вас со всеми шагами принятия решений и
оценкой их достоинств и недостатков. Эти паттерны применяются к сквозным
примерам когда, чтобы показать, как код эволюционирует и почему принимает именно такую, а не иную конечную форму.

Основы паттернов
Рекомендации по проектированию в этой книге приводятся в форме паттернов. Идея представлять знания и передовые практики в виде паттернов исходит от архитектора Кристофера Александра, который высказал ее в книге
«The Timeless Way of Building» (Oxford University Press, 1979). Он использует
небольшие проверенные временем фрагменты для решения важнейшей проблемы в своей области: как проектировать и возводить города. Подход на основе паттернов переняли разработчики программного обеспечения, и теперь
проводятся конференции типа Pattern Languages of Programs (PLoP), имеющие
целью расширить наши знания о паттернах. В особенности книга «банды четы-

Основы паттернов  11
рех» «Design Patterns: Elements of Reusable Object-Oriented Software» (Prentice
Hall, 1997) оказала значительное влияние и познакомила разработчиков ПО с
концепцией паттернов проектирования.
Но что же такое паттерн? Определений много, и, если эта тема вас сильно
интересует, почитайте книгу Frank Buschmann et al. «Pattern-Oriented Software
Architecture: On Patterns and Pattern Languages» (Wiley, 2007), где приведены
точные описания и детали. А для наших целей достаточно считать, что паттерн
дает проверенное временем решение какой-то практической задачи. Представленные в этой книге паттерны имеют структуру, описанную в табл. P.1.
Таблица P.1. Структура паттернов, представленных в этой книге
Часть паттерна

Описание

Название

Это легко запоминаемое имя паттерна. Предполагается,
что программисты будут использовать его в повседневном
общении (как в случае паттернов из книги «банды четырех»,
когда можно услышать, например, такую фразу: «И абстрактная
фабрика создает объект»). Названия паттернов в этой книге
начинаются с заглавной буквы

Контекст

Определяет обстановку, в которой действует паттерн. Говорит,
при каких условиях паттерн можно применить

Проблема

Сообщает информацию о задаче, которую мы хотим решить.
Эта часть начинается с основной постановки, выделенной
полужирным шрифтом, после чего детально описывается,
почему данную проблему трудно решить. (В других форматах
паттернов детали вынесены в отдельную часть, называемую
«Движущие силы».)

Решение

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

Последствия

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

Известные примеры
использования

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

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

12

 Предисловие

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

Как читать эту книгу
Вы должны быть знакомы с основами программирования на C. Вы должны
знать синтаксис и семантику C – например, эта книга не расскажет вам о том,
что такое указатель и как им пользоваться. Приводятся рекомендации только
по вопросам более высокого порядка.
Главы книги независимы. Вы можете читать их в произвольном порядке
или выбирать только те, которые вас интересуют. В следующем разделе
приведен краткий обзор всех паттернов, что позволит перейти сразу к тем,
что вам интересны. Так что если вы точно знаете, что ищете, то можете начать
отсюда.
Если вы не ищете какой-то конкретный паттерн, а хотите получить общее
представление о проектировании программ на C, то прочитайте часть I от
начала до конца. Каждая глава этой части посвящена одной теме, начиная с
таких простых, как обработка ошибок и управление памятью, и кончая такими
более продвинутыми и специальными, как проектирование интерфейсов
или платформенно независимого кода. В каждой главе описываются
относящиеся к ее теме паттерны и сквозной пример кода, демонстрирующий
их применение.
Во второй части книги приведено два больших примера, иллюстрирующих
применение многих паттернов из первой части. Здесь вы увидите, как большая
программа строится с использованием паттернов.

Краткий обзор паттернов
В табл. P.2–P.10 перечислены все паттерны. Каждая строка содержит краткое
описание проблемы, после которого идет ключевое слово «Поэтому» и краткое
описание решения.
Таблица P.2. Паттерны для обработки ошибок
Название паттерна

Краткое описание

Разбиение функции

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

Проверка условий

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

Краткий обзор паттернов  13
Название паттерна

Краткое описание

Принцип самурая

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

Переход к обработке
ошибки

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

Запись об очистке

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

Объектная обработка ошибок

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

Таблица P.3. Паттерны для возврата информации об ошибке
Название паттерна

Краткое описание

Возврат кода
состояния

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

Возврат
существенной
информации об
ошибке

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

14

 Предисловие

Название паттерна

Краткое описание

Специальное
возвращаемое
значение

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

Протоколирование
ошибок

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

Таблица P.4. Паттерны для управления памятью
Название паттерна

Краткое описание

Сначала стек

Любому программисту часто приходится принимать решение
о классе хранения и области памяти (стек, куча…) для размещения переменных. Если для каждой переменной по новой
взвешивать все плюсы и минусы различных вариантов, то ни
на что другое не останется времени. Поэтому по умолчанию
размещайте переменные в стеке, это даст возможность воспользоваться механизмом автоматической очистки памяти

Вечная память

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

Отложенная очистка

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

Единоличное
владение

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

Краткий обзор паттернов  15
Название паттерна

Краткое описание

Обертка выделения

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

Проверка указателя

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

Пул памяти

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

Таблица P.5. Паттерны для возврата данных из C-функций
Название паттерна

Краткое описание

Возвращаемое
значение

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

Выходные
параметры

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

Агрегат

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

16

 Предисловие

Название паттерна

Краткое описание

Неизменяемый
экземпляр

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

Буфер,
принадлежащий
вызывающей
стороне

Вызываемая сторона
выделяет память

Таблица P.6. Паттерны, относящиеся ко времени жизни данных и владению ими
Название паттерна

Краткое описание

Программный
модуль без
состояния

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

Программный
модуль с глобальным
состоянием

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

Экземпляр,
принадлежащий
вызывающей
стороне

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

Краткий обзор паттернов  17
Название паттерна

Краткое описание

Разделяемый
экземпляр

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

Таблица P.7. Паттерны для создания гибких API
Название паттерна

Краткое описание

Заголовочные
файлы

Вы хотите, чтобы реализованная вами функциональность была
доступна коду, реализованному другими лицами, но при этом
хотите скрыть детали реализации от вызывающей стороны. Поэтому включите в свой API объявления функций, реализующих
функциональность, предоставляемую пользователям. Скройте
все внутренние функции, внутренние данные и определения
(реализации) функций в файлах реализации и не передавайте
эти файлы пользователям

Описатель

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

Динамический
интерфейс

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

18

 Предисловие

Название паттерна

Краткое описание

Управление
функцией

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

Таблица P.8. Паттерны для создания гибких интерфейсов итератора
Название паттерна

Краткое описание

Доступ по индексу

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

Курсор

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

Итератор обратного
вызова

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

Краткий обзор паттернов  19
Таблица P.9. Паттерны для организации файлов в модульных программах
Название паттерна

Краткое описание

Охрана включения

Включить один и тот же заголовочный файл легко, но это приведет к ошибкам компиляции, если в файле имеются определения типов или макросы определенного вида, поскольку в
процессе компиляции они будут определены повторно. Поэтому защищайте содержимое заголовочных файлов от повторного включения, чтобы разработчику, пользующемуся ими, не
нужно было думать, сколько раз включен файл. Для этого можно использовать директиву #ifdef или #pragma once

Каталоги
программных
модулей

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

Глобальный каталог
include

Чтобы включить файлы из других программных модулей, приходится использовать относительные пути вида
../othersoftwaremodule/file.h. Вы должны знать точное местоположение других заголовочных файлов. Поэтому заведите в кодовой базе один глобальный каталог, содержащий API
всех программных модулей. Добавьте этот каталог в глобальный список путей к включаемым файлам, поддерживаемый
вашим инструментарием

Автономные
компоненты

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

Копия API

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

20

 Предисловие

Таблица P.10. Паттерны, позволяющие избежать ада #ifdef
Название паттерна

Краткое описание

Избегание
вариантов

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

Изолированные
примитивы

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

Атомарные
примитивы

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

Уровень абстракции

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

Разделение
реализаций
вариантов

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

О примерах кода  21

Графические выделения
В книге применяются следующие графические выделения.
Курсив
Новые термины, URL-адреса, адреса электронной почты, имена и расширения файлов.
Полужирный
Выделение формулировки проблемы и ее решения для каждого паттерна.
Моноширинный
Листинги программ, а также элементы кода в основном тексте: имена переменных и функций, базы данных, типы данных, переменные окружения,
предложения и ключевые слова языка.

Так обозначается замечание общего характера.

Так обозначается предупреждение или предостережение.

О примерах кода
Примеры кода в этой книге представляют собой короткие фрагменты, акцентированные на какой-то одной идее, с целью продемонстрировать паттерны и
их применение. Сами по себе эти фрагменты не компилируются, потому что
некоторые вещи в них опущены (в частности, заголовочные файлы). Полный
код, который компилируется, можно скачать с GitHub по адресу https://github.com/
christopher-preschern/fluent-c.
Вопросы технического характера, а также замечания по примерам кода следует отправлять по адресу bookquestions@oreilly.com.
Эта книга призвана помогать вам в работе. Поэтому вы можете использовать
приведенный в ней код в собственных программах и в документации. Спрашивать у нас разрешения необязательно, если только вы не собираетесь воспроизводить значительную часть кода. Например, никто не возбраняет включить
в свою программу несколько фрагментов кода из книги. Однако для продажи
или распространения примеров из книг издательства O'Reilly разрешение требуется. Цитировать книгу и примеры в ответах на вопросы можно без ограничений. Но для включения значительных объемов кода в документацию по
собственному продукту нужно получить разрешение.

22

 Предисловие

Мы высоко ценим, хотя и не требуем ссылки на наши издания. В ссылке
обычно указывается название книги, имя автора, издательство и ISBN, например: «Fluent C by Christopher Preschern (O'Reilly). Copyright 2023 Christopher
Preschern, 978-1-492-09733-4».
Если вы полагаете, что планируемое использование кода выходит за рамки изложенной выше лицензии, пожалуйста, обратитесь к нам по адресу
permissions@oreilly.com.
Все паттерны в этой книги взяты из существующего кода, в котором они
применяются. В списке ниже приведены ссылки на эти примеры кода:




























игра NetHack (https://oreil.ly/nxO5w);
проект OpenWrt (https://oreil.ly/qeppo);
библиотека OpenSSL (https://oreil.ly/zzsMO);
сетевой анализатор Wireshark (https://oreil.ly/M55B5);
портлендский репозиторий паттернов (https://oreil.ly/wkZzb);
система управления версиями Git (https://oreil.ly/7F9Oz);
переносимая среда исполнения Apache (https://oreil.ly/ysaM6);
веб-сервер Apache (https://oreil.ly/W6SMn);
операционная система B&R Automation Runtime (проприетарный закрытый код компании B&R Industrial Automation GmbH);
визуальный редактор системы автоматизации B&R Visual Components,
проприетарный закрытый код компании B&R Industrial Automation
GmbH);
система управления данными NetDRMS (https://oreil.ly/eR0EV);
платформа программирования и численных расчетов MATLAB (https://
oreil.ly/UpvJK);
библиотека GLib (https://oreil.ly/QoUwT);
веб-анализатор реального времени GoAccess (https://oreil.ly/L1Eij);
программа физических расчетов Cloudy (https://oreil.ly/phLBb);
собрание компиляторов GNU (GCC) (https://oreil.ly/KK4jY);
система баз данных MySQL (https://oreil.ly/YKXxs);
диспетчер памяти ION для Android (https://oreil.ly/2JV7h);
Windows API (https://oreil.ly/nnzyX);
Apple Cocoa API (https://oreil.ly/sQual);
операционная система реального времени VxWorks (https://oreil.ly/UMUaj);
текстовый редактор sam (https://oreil.ly/k3SQI);
функции из стандартной библиотеки C: реализация glibc (https://oreil.ly/9Qr95);
проект Subversion (https://oreil.ly/8Yz5R);
инструмент исследования сети Nmap (https://oreil.ly/sg9sz);
монитор производительности в реальном времени и система визуализации Netdata (https://oreil.ly/1sDZz);

Благодарности  23












файловая система OpenZFS (https://oreil.ly/VWeQL);
каркас обратной разработки Radare (https://oreil.ly/TUYfh);
цифровые обучающие программы Education First (https://www.ef.com);
текстовый редактор VIM (https:/gitub.com/vim/vim);
программа построения графиков GNUplot (https://oreil.ly/PIQPj);
движок базы данных SQLite (https://oreil.ly/5Knfz);
программа сжатия данных gzip (https://oreil.ly/it40Z);
веб-сервер lighttpd (https://github.com/lighttpd);
начальный загрузчик U-Boot (https://oreil.ly/IKVYV);
система моделирования дискретных событий (https://oreil.ly/NJnCH);
платформа Nokia Maemo (https://oreil.ly/RwDtt).

Как с нами связаться
Вопросы и замечания по поводу этой книги отправляйте в издательство:
O'Reilly Media, Inc.
1005 Gravenstein Highway North Sebastopol, CA 95472
800-998-9938 (в США и Канаде)
707-829-0515 (международный или местный)
707-829-0104 (факс).
Для этой книги имеется веб-страница, на которой публикуются списки замеченных ошибок, примеры и прочая дополнительная информация. Адрес
страницы: https://www.oreil.ly/fluent-c.
Замечания и вопросы технического характера следует отправлять по адресу
bookquestions@oreilly.com.
Дополнительную информацию о наших книгах, конференциях и новостях
вы можете найти на нашем сайте по адресу http://www.oreilly.com.
Ищите нас в LinkedIn: https://linkedin.com/company/oreilly-media.
Следите за нашей лентой в Twitter: http://twitter.com/oreillymedia.
Смотрите нас на YouTube: http://www.youtube.com/oreillymedia.

Благодарности
Я хочу поблагодарить свою жену Силке, которая теперь даже знает, что такое
паттерны :-), и свою дочь Илви. Обе они делают меня счастливее и обе следят
за тем, чтобы я не сидел за компьютером все время, а наслаждался жизнью.
Эта книга не увидела бы свет без помощи многих энтузиастов паттернов.
Я благодарен всем участникам семинара Writers' Workshops на Европейской
конференции по языкам паттернов в программах за отзывы о паттернах.
В частности, я хочу выразить благодарность следующим лицам, которые дали
очень полезные отзывы во время так называемого пастырского процесса на
этой конференции, среди них Яри Раухамяки, Тобиас Раутер, Андреа Холлер,

24

 Предисловие

Джеймс Коплиен, Уве Здун, Томас Разер, Иден Бэртон, Клаудиус Линк, Валентино Вранич и Сумит Калра. Отдельное спасибо моим коллегам по работе, в
особенности Томасу Гавловецу, который проследил за тем, чтобы все детали
программирования на С в моих паттернах были правильны. Роберт Ханмер,
Майкл Вейсс, Дэвид Гриффитс и Томас Круг потратили немало времени на рецензирование этой книги и поделились со мной мыслями о том, как сделать ее
лучше, – большое вам спасибо! Также я признателен всему коллективу издательства O'Reilly, помогавшему мне в работе над этой книгой. Особенно хочу
поблагодарить редактора-консультанта Корбина Коллинза и выпускающего
редактора Джонатона Оуэна.
Текст этой книги основан на следующих статьях, которые были приняты на
Европейской конференции по языкам паттернов в программах и опубликованы в изданиях ACM. Эти статьи можно скачать бесплатно на сайте http://www.
preschern.com.
• «A Pattern Story About C Programming», EuroPLoP '21: 26th European
Conference on Pattern Languages of Programs, July 2015, article no. 53,
1–10, https://dl.acm.org/doi/10.1145/3489449.3489978.
• «Patterns for Organizing Files in Modular C Programs», EuroPLoP '20:
Proceedings of the European Conference on Pattern Languages of Programs,
July 2020, article no. 1, 1–15, https://dl.acm.org/doi/10.1145/3424771.3424772.
• «Patterns to Escape the #ifdef Hell», EuroPLop '19: Proceedings of the 24th
European Conference on Pattern Languages of Programs, July 2019, article
no. 2, 1–12, https://dl.acm.org/doi/10.1145/3361149.3361151.
• «Patterns for Returning Error Information in C», EuroPLop '19: Proceedings
of the 24th European Conference on Pattern Languages of Programs, July
2019, article no. 3, 1–14, https://dl.acm.org/doi/10.1145/3361149.3361152.
• «Patterns for Returning Data from C Functions», EuroPLop '19: Proceedings
of the 24th European Conference on Pattern Languages of Programs, July
2019, article no. 37, 1–13, https://dl.acm.org/doi/10.1145/3361149.3361188.
• «C Patterns on Data Lifetime and Ownership», EuroPLop '19: Proceedings of
the 24th European Conference on Pattern Languages of Programs, July 2019,
article no. 36, 1–13, https://dl.acm.org/doi/10.1145/3361149.3361187.
• «Patterns for C Iterator Interfaces», EuroPLoP '17: Proceedings of the 22nd
European Conference on Pattern Languages of Programs, July 2017, article
no. 8, 1–14, https://dl.acm.org/doi/10.1145/3147704.3147714.
• «API Patterns in C», EuroPlop '16: Proceedings of the 21st European
Conference on Pattern Languages of Programs, July 2016, article no. 7, 1–11,
https://dl.acm.org/doi/10.1145/3011784.3011791.
• «Idioms for Error Handling in C», EuroPLoP '15: Proceedings of the 20th
European Conference on Pattern Languages of Programs, July 2015, article
no. 53, 1–10, https://dl.acm.org/doi/10.1145/2855321.2855377.

Часть

I
Паттерны на C

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

Глава

1
Обработка ошибок

Обработка ошибок – важная часть написания программ; если это сделано неправильно, то программу становится трудно развивать и сопровождать. В таких языках программирования, как C++ и Java, имеются «исключения» и «деструкторы», упрощающие обработку ошибок. В C подобных механизмов нет, а
сведения о хороших способах обработки ошибок в программах на C разбросаны по всему интернету.
В этой главе представлены коллективные знания о правильной обработке
ошибок в форме паттернов и сквозного примера применения этих паттернов.
Паттерны предлагают проверенные практикой проектные решения вместе с
указаниями на то, когда их применять и к каким последствиям это приводит.
С точки зрения программиста, паттерны избавляют от бремени принятия многих детальных решений и позволяют положиться на знания, заключенные в
паттерне, и взять его за основу для написания хорошего кода.
На рис. 1.1 приведен обзор паттернов, рассматриваемых в этой главе, и связи
между ними, а в табл. 1.1 дано краткое описание паттернов.
Заголовочные
файлы

Возврат существенной
информации
об ошибке

Разбиение
функции

Возврат кода
состояния

Проверка
условий

Протоколиро­
вание ошибок

Переход
к обработке
ошибки

Агрегат

Объектная
обработка ошибок

Принцип
самурая

Запись
об очистке

Пояснения
Паттерн, представленный
в этой главе
Паттерн, представленный
в другой главе

Описатель

B можно использовать для
реализации и дополнения A

Рис. 1.1. Обзор паттернов для обработки ошибок

Сквозной пример  27
Таблица 1.1. Паттерны для обработки ошибок
Название паттерна

Краткое описание

Разбиение функции

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

Проверка условий

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

Принцип самурая

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

Переход к обработке
ошибки

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

Запись об очистке

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

Объектная обработка
ошибок

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

Сквозной пример
Вы хотите написать функцию, которая ищет в файле определенные ключевые
слова и возвращает информацию о том, какие слова были найдены.
Стандартный способ сообщить об ошибке в C – использовать возвращаемое
функцией значение. Для передачи дополнительной информации в унаследованных функциях часто записывают конкретный код ошибки в переменную
errno (см. файл errno.h). Вызывающая сторона может затем проверить errno,
чтобы узнать, какая произошла ошибка.

 Глава 1. Обработка ошибок

28

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

int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
if(file_name!=NULL)
{
if(file_pointer=fopen(file_name, "r"))
{
if(buffer=malloc(BUFFER_SIZE))
{
/* разобрать содержимое файла */
return_value = NO_KEYWORD_FOUND;
while(fgets(buffer, BUFFER_SIZE, file_pointer)!=NULL)
{
if(strcmp("KEYWORD_ONE\n", buffer)==0)
{
return_value = KEYWORD_ONE_FOUND_FIRST;
break;
}
if(strcmp("KEYWORD_TWO\n", buffer)==0)
{
return_value = KEYWORD_TWO_FOUND_FIRST;
break;
}
}
free(buffer);
}
fclose(file_pointer);
}
}
return return_value;
}
В коде мы должны проверять возвращаемые значения после вызовов функций, чтобы узнать, была ли ошибка. Поэтому возникают глубоко вложенные
предложения if. Это создает следующие проблемы:
• функция оказывается длинной, в ее коде перемешаны обработка ошибок, инициализация, очистка и собственно логика функции. Поэтому
код трудно сопровождать;
• основной код, который читает и интерпретирует данные файла, глубоко
вложен, поэтому трудно следить за логикой программы;

Сквозной пример  29
• функции очистки далеко отстоят от функций инициализации, поэтому
можно легко забыть о необходимости какой-то очистки. В особенности это относится к функциям, содержащим несколько предложений
return.
Чтобы улучшить ситуацию, произведем для начала «Разбиение функции».

Разбиение функции
Контекст
Имеется функция, выполняющая несколько действий. Например, она выделяет ресурс (скажем, динамическую память или описатель файла), использует
этот ресурс и освобождает его.

Проблема
У функции несколько обязанностей, что затрудняет ее чтение и сопровождение.
Такая функция могла бы нести ответственность за выделение ресурсов, действия с этими ресурсами и их очистку. Быть может, очистка даже разбросана
по всей функции и дублируется в нескольких местах. Особенно трудно функцию становится читать из-за обработки ошибок выделения ресурса, потому
что очень часто при этом появляются вложенные предложения if.
Если выделять, очищать и использовать несколько ресурсов в одной функции, то легко можно забыть об очистке какого-то ресурса, особенно если код
впоследствии изменяется. Например, если в середину кода будет добавлено
предложение return, то вполне можно забыть об очистке уже выделенных к
этому моменту ресурсов.

Решение
Разбейте функцию на части. Выделите часть функции, которая представляется полезной сама по себе, создайте из нее новую функцию и придумайте для
нее имя.
Чтобы понять, какую часть функции выделять, просто подумайте, можно
ли дать ей осмысленное имя и действительно ли такое разбиение изолирует
какую-то обязанность. Например, таким образом можно было бы выделить
функцию, содержащую только функциональный код, и функцию, содержащую
только код обработки ошибок.
Хороший признак необходимости разбиения функции на части – наличие
кода очистки в нескольких местах. В таком случае было бы гораздо лучше поместить в одну функцию код, который выделяет и освобождает ресурсы, а в
другую – код, который использует эти ресурсы. Тогда функция, использующая
ресурсы, вполне может содержать несколько предложений return без необходимости очищать ресурсы перед каждым из них, потому что очистка производится в другой функции. Это показано в следующем коде:

void someFunction()
{

 Глава 1. Обработка ошибок

30

char* buffer = malloc(LARGE_SIZE);
if(buffer)
{
mainFunctionality(buffer);
}
free(buffer);
}
void mainFunctionality()
{
// здесь должна быть реализация
}
Теперь у нас две функции вместо одной. Конечно, это означает, что вызывающая функция больше не автономна, а зависит от другой функции. И вам
придется решить, куда эту функцию поместить. Первая мысль – поместить ее
в один файл с вызывающей, но если две функции не являются тесно связанными, то можно поместить вызываемую функцию в отдельный файл и завести
заголовочный файл с объявлением этой функции.

Последствия
Код стал лучше, потому что две короткие функции проще читать и сопровождать, чем одну длинную. В частности, код проще читать, потому что функции
очистки расположены ближе к функциям, нуждающимся в очистке, и потому
что выделение и очистка ресурсов не смешиваются с основной логикой программы. Поэтому в будущем основную логику будет легче сопровождать и расширять.
Теперь вызываемая функция может содержать несколько предложений return, так как ей не нужно заботиться об очистке ресурсов перед каждым return.
Очистка производится в одном месте вызывающей функцией.
Если в вызываемой функции используется несколько ресурсов, то все они
должны быть ей переданы. При наличии большого числа параметров код
функции становится трудно читать, а если по неосторожности передать параметры не в том порядке, то произойдет ошибка. Во избежание такого развития
события можно использовать паттерн «Агрегат».

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Практически в любом коде на C есть части, в которых этот паттерн
применяется, и части, где он не применяется; последние сопровождать
труднее. Согласно книге Robert C. Martin «Clean Code: A Handbook of Agile
Software» Craftsmanship»1 (Prentice Hall, 2008), у каждой функции должна
быть ровно одна обязанность (принцип единственной обязанности),
поэтому обработка ресурса и прочая программная логика всегда должны
разноситься по разным функциям.
1

Роберт Мартин. Чистый код. Создание анализ и рефакторинг. Питер, 2022.

Сквозной пример  31
• В портлендском репозитории паттернов этот паттерн называется
«Оберткой функции» (Function Wrapper).
• В объектно ориентированном программировании паттерн «Шаблонный
метод» также описывает способ структурирования кода путем его
разбиения на части.
• Критерии того, когда и в каких местах разбивать функцию, описаны в
книге Martin Fowler «Refactoring: Improving the Design of Existing Code»1
(Addison-Wesley, 1999) в виде паттерна «Извлечение метода».
• В игре NetHack этот паттерн применяется в функции read_config_file: там
обрабатываются ресурсы и вызывается функция parse_conf_file, которая
работает с этими ресурсами.
• В коде OpenWrt этот паттерн используется в нескольких местах для
работы с буферами. Например, код, отвечающий за вычисление хеша
MD5, выделяет буфер, передает его другой функции, которая работает с
этим буфером, а затем освобождает этот буфер.

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

int searchFileForKeywords(char* buffer, FILE* file_pointer)
{
while(fgets(buffer, BUFFER_SIZE, file_pointer)!=NULL)
{
if(strcmp("KEYWORD_ONE\n", buffer)==0)
{
return KEYWORD_ONE_FOUND_FIRST;
}
if(strcmp("KEYWORD_TWO\n", buffer)==0)
{
return KEYWORD_TWO_FOUND_FIRST;
}
}
return NO_KEYWORD_FOUND;
}
int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
1

Мартин Фаулер. Рефакторинг. Улучшение проекта существующего кода. Диалектика-Вильмс,
2019.

 Глава 1. Обработка ошибок

32

if(file_name!=NULL)
{
if(file_pointer=fopen(file_name, "r"))
{
if(buffer=malloc(BUFFER_SIZE))
{
return_value = searchFileForKeywords(buffer, file_pointer);
free(buffer);
}
fclose(file_pointer);
}
}
return return_value;
}
Глубина вложенности if уменьшилась, но в функции parseFile все еще
остались три предложения if для проверки ошибок выделения ресурсов –
это слишком много. Эту функцию можно сделать чище, воспользовавшись
паттерном «Проверка условий».

Проверка условий
Контекст
Имеется функция, выполняющая задачу, которая может быть успешно завершена только при определенных условиях (например, правильных входных
параметрах).

Проблема
Функцию трудно читать и сопровождать, потому что проверка предусловий перемешана с основной логикой.
Выделение ресурсов всегда должно сопровождаться их освобождением. Если
вы выделили ресурс, а позже поняли, что какое-то предусловие функции не
удовлетворяется, то этот ресурс придется освободить.
Трудно понять логику программы, если проверки нескольких предусловий
разбросаны по функции, особенно если они реализованы во вложенных
предложениях if. Когда таких проверок много, функция становится очень
длинной, что само по себе признак кода с душком.
Код с душком
Говорят, что код «дурно пахнет», если он плохо структурирован или написан так, что его трудно сопровождать. Примерами кода с душком являются очень длинные функции или
дублирование кода. Другие примеры и контрмеры описаны
в книге Martin Fowler «Refactoring: Improving the Design of
Existing Code» (Addison-Wesley, 1999).

Сквозной пример  33

Решение
Проверяйте все обязательные предусловия и немедленно возвращайте управление,
если они не выполнены.
Например, следует проверять допустимость входных параметров и находится ли программа в состоянии, допускающем выполнение остальной части
функции. Тщательно обдумывайте, какие нужно установить предусловия вызова функции. С одной стороны, вы облегчите себе жизнь, если будете очень
строго относиться к входным параметрам функции, но, с другой стороны, вызывающей стороне будет проще, если вы станете относиться к параметрам
более снисходительно (закон Постеля формулирует это так: «Будь консервативен в собственных действиях и либерален к тому, что принимаешь от других»).
Если имеется много предусловий, то можно завести отдельную функцию для
их проверки. В любом случае выполняйте проверки до выделения ресурсов,
потому что вернуть управление из функции гораздо проще, когда не нужно
производить никакой очистки.
Четко описывайте предусловия своей функции в ее интерфейсе. Лучшее
мес­то для документирования этого поведения – заголовочный файл, в котором функция объявлена.
Если вызывающей стороне важно знать, какое предусловие не выполнено,
то можно предоставить ей информацию об ошибке. Например, можно вернуть
код состояния, следя за тем, чтобы возвращать только существенную информацию об ошибке. В коде ниже приведен пример без возврата информации об
ошибке.
someFile.h

/* эта функция работает с параметром 'user_input', который должен быть отличен
от NULL */
void someFunction(char* user_input);
someFile.c

void someFunction(char* user_input)
{
if(user_input == NULL)
{
return;
}
operateOnData(user_input);
}

Последствия
Код функции, которая немедленно возвращает управление, если предусловия не выполнены, проще читать, чем вложенные предложения if. Сразу видно, что в этом случае выполнение функции прерывается. Поэтому предусловия
очень четко отделены от остального кода.

34

 Глава 1. Обработка ошибок

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

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Паттерн «Проверка условий» описан в портлендском репозитории паттернов.
• В статье Klaus Renzel «Error Detection» (Proceedings of the 2nd EuroPLoP
conference, 1997) описан очень похожий паттерн «Обнаружение ошибок», который предлагает включать проверки пред- и постусловий.
• В игре NetHack этот паттерн используется в нескольких местах, например в функции placebc. Эта функция в качестве наказания набрасывает
цепь на героя NetHack, что уменьшает скорость его передвижения. Она
возвращает управление немедленно, если нет доступных объектов цепи.
• Этот паттерн используется в коде OpenSSL. Например, функция SSL_new
немедленно возвращает управление, если входные параметры недопус­
тимы.
• Функция capture_stats в Wireshark, отвечающая за сбор статистики при
анализе сетевых пакетов, сначала проверяет, допустимы ли входные параметры, и сразу же возвращает управление, если это не так.

Применение к сквозному примеру
В коде ниже показано, как функция parseFile применяет описанный паттерн для проверки предусловий:

int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
if(file_name==NULL) 
{
return ERROR;
}
if(file_pointer=fopen(file_name, "r"))
{
if(buffer=malloc(BUFFER_SIZE))
{
return_value = searchFileForKeywords(buffer, file_pointer);
free(buffer);

Сквозной пример  35
}
fclose(file_pointer);
}
return return_value;
}
 Если переданы недопустимые параметры, то мы сразу же возвращаем управление, и никакая очистка не нужна, потому что ресурсы еще не выделялись.

Для реализации паттерна «Проверка условий» здесь используется возврат
кода состояния. Если параметр равен NULL, то возвращается константа ERROR.
Вызывающая сторона может проверить возвращенное значение и узнать, что
функции был передан недопустимый параметр NULL. Но обычно это признак
программной ошибки, а проверять программные ошибки и распространять
эту информацию по всему коду – неудачная идея. В таком случае проще применить «Принцип самурая».

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

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

36

 Глава 1. Обработка ошибок

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

Решение
Возвращаться из функции только с победой или не возвращаться вовсе («Принцип
самурая»). Если вы точно знаете, что обработать ошибку невозможно, то завершайте
программу.
Не используйте для возврата информации об ошибке выходные параметры или возвращаемое значение. Вся информация уже в наличии, так что
обработайте ошибку немедленно. Если ошибка произошла, дайте программе
возможность «грохнуться». Но делайте это упорядоченным образом, воспользовавшись макросом assert. Кстати, assert позволяет включить отладочную
информацию, как показано ниже:

void someFunction()
{
assert(checkPreconditions() && "Предусловия не выполнены");
mainFunctionality();
}
Здесь в assert проверяется условие, и если оно не выполнено, то на stderr
выводится сообщение, включающее строку справа от оператора &&, и программа завершается. Можно завершить программу и менее структурированным
способом, если не проверять указатели на NULL и обращаться к памяти по таким
указателям. Просто сделайте так, чтобы программа гарантированно завершалась в месте возникновения ошибки.
Очень часто проверки условий – отличное место для завершения программы в случае ошибок. Например, если вы точно знаете, что имеет место программная ошибка (вызывающая сторона передала вам указатель NULL), то завершите программу и запишите в журнал отладочную информацию, вместо
того чтобы возвращать информацию об ошибке вызывающей стороне. Но не
надо завершать программу из-за любой ошибки. Например, такие ошибки, как
неправильный ввод данных пользователем, очевидно, не должны приводит к
аварийному завершению.
Вызывающая сторона должна быть хорошо осведомлена о поведении вашей
функции, поэтому в описании ее API следует документировать случаи, когда
функция завершает программу. Например, в документации может быть написано, что программа «грохается», если функции передан нулевой указатель в
качестве параметра.
Разумеется, «Принцип самурая» применим не ко всем ошибкам и не ко
всем видам программ. Нельзя аварийно завершать программу при получе-

Сквозной пример  37
нии неожиданных данных от пользователя. Но в случае программной ошибки
«быстрый отказ» с немедленным завершением программы, возможно, имеет
смысл. Тогда программисту будет совсем просто отыскать ошибку.
Тем не менее такой отказ необязательно показывать пользователю. Если
ваша программа – всего лишь некритическая часть большего приложения, то
можно позволить ей завершиться. Но в контексте всего приложения ваша программа может завершиться «по-тихому», не тревожа ни остальное приложение, ни пользователя.
Макросы assert в выпускных версиях
При использовании assert часто возникает вопрос, следует ли
активировать их только в отладочных версиях исполняемых
файлов или также в выпускных. Деактивировать assert можно, определив в своем коде макрос NDEBUG перед включением assert.h или прямо в цепочке инструментов. Основной
аргумент в пользу деактивации assert в выпускных версиях – то, что ошибки, для которых используется assert при
тестировании отладочных версий, и так обрабатываются,
поэтому не нужно оставлять возможность случайного аварийного завершения из-за присутствия assert в выпускных
версиях. Основной аргумент в пользу того, чтобы оставлять
assert активными и в выпускных версиях, заключается в том,
что их в любом случае следует использовать для критических
ошибок, которые невозможно обработать корректно, и что
такие ошибки никогда не должны оставаться незамеченными, даже в исполняемых файлах, поставляемых заказчикам.

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

38

 Глава 1. Обработка ошибок

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Похожий паттерн, предлагающий добавлять отладочную строку в макрос assert, называется Контекстом утверждения и описан в книге Adam
Tornhill «Patterns in C» (Leanpub, 2014).
• В сетевом анализаторе Wireshark этот паттерн применяется повсеместно. Например, в функции register_capture_dissector макрос assert используется для проверки единственности регистрации диссектора.
• В исходном коде проекта Git используются макросы assert. Например,
в функциях для хранения SHA1-хешей assert проверяет правильность
пути к файлу, в котором должен храниться хеш.
• В коде OpenWrt, отвечающем за обработку больших чисел, assert используется для проверки предусловий в функциях.
• Похожий паттерн под названием «Дай ему упасть» представлен Пекка
Алхо и Яри Раухамяки в статье «Patterns for Light-Weight Fault Tolerance
and Decoupled Design in Distributed Control Systems» (https://oreil.ly/x0tQW).
Паттерн ориентирован на распределенные системы управления и
предлагает позволить аварийное завершение с последующим быстрым
перезапуском одиночным отказоустойчивым процессам.
• В стандартной библиотеке C функция strcpy не проверяет корректность
входных данных. Если вы передадите ей нулевой указатель, то функция
«грохнется».

Применение к сквозному примеру
Теперь функция parseFile выглядит гораздо лучше. Вместо того чтобы возвращать код ошибки, мы включили простой макрос assert. В результате код
оказался короче, а вызывающей стороне не приходится нести бремя проверки
возвращенного значения:

int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
assert(file_name!=NULL && "Недопустимое имя файла");
if(file_pointer=fopen(file_name, "r"))
{
if(buffer=malloc(BUFFER_SIZE))
{
return_value = searchFileForKeywords(buffer, file_pointer);
free(buffer);
}
fclose(file_pointer);
}

Сквозной пример  39
return return_value;
}
Предложения if, не требующие очистки ресурсов, устранены, но код все равно содержит вложенные if для всего, что требует очистки. Кроме того, не обрабатывается ситуация, когда вызов malloc завершается ошибкой. Все это можно
исправить, воспользовавшись паттерном «Переход к обработке ошибки».

Переход к обработке ошибки
Контекст
Имеется функция, которая захватывает и освобождает несколько ресурсов.
Быть может, вы уже пытались уменьшить ее сложность, применяя паттерны
«Проверка условий», «Разбиение функции» и «Принцип самурая», но все же
остались глубоко вложенные if, особенно в связи с выделением ресурсов. Возможно даже, что код очистки ресурсов дублируется.

Проблема
Код функции становится трудно читать и сопровождать, если он захватывает и освобождает несколько ресурсов в разных местах.
Затруднения вызваны тем, что выделение любого ресурса может завершиться неудачно, а освобождать ресурс нужно, только если он был выделен успешно. Чтобы обеспечить это, необходимо много предложений if, но при плохой
реализации наличие вложенных if в одной функции затрудняет чтение и сопровождение кода.
Поскольку ресурсы необходимо освобождать, возврат в середине функции,
если что-то пошло не так, – не самая лучшая мысль. Ведь все уже захваченные
ресурсы нужно будет освобождать перед каждым предложением return. Поэтому в коде образуется несколько мест, где освобождается один и тот же ресурс,
но мы не хотим дублировать код обработки ошибок и очистки.

Решение
Поместите всю очистку ресурсов и обработку ошибок в конец функции. Если ресурс не
удалось захватить, перейдите на код очистки с помощью предложения goto.
Захватывайте ресурсы в том порядке, в каком считаете нужным, а в конце
функции освобождайте их в обратном порядке. Для каждого ресурса заведите
отдельную метку, на которую можно будет перейти, чтобы очистить его. В случае ошибки или при невозможности захватить ресурс просто перейдите на эту
метку, но не делайте несколько переходов и всегда переходите только вперед,
как показано в следующем коде1:

void someFunction()
{
if(!allocateResource1())
1

Код неправилен. Если Resource1 не удалось захватить, то его не нужно и очищать. И точно так
же для Resource 2. Метки расставлены неверно. – Прим. перев.

 Глава 1. Обработка ошибок

40

{
goto cleanup1;
}
if(!allocateResource2())
{
goto cleanup2;
}
mainFunctionality();
cleanup2:
cleanupResource2();
cleanup1:
cleanupResource1();
}
Если принятый в вашей организации стандарт кодирования запрещает использование goto, то его можно эмулировать, поместив код внутрь цикла do{
... }while(0);. В случае ошибки выполните break, чтобы выйти из цикла туда,
где находится код обработки ошибок. Однако обычно в таком обходном маневре нет ничего хорошего, потому что если стандарт кодирования не разрешает использовать goto, то не нужно эмулировать его, только чтобы настоять
на своем собственном стиле программирования. В качестве альтернативы goto
можно использовать паттерн «Запись об очистке».
В любом случае использование goto может указывать на то, что ваша функция уже слишком сложна, и лучше бы разбить ее на части, воспользовавшись,
например, паттерном «Объектная обработка ошибок».
goto: добро или зло?
Много спорят о том, хорошо или плохо использовать предложение goto. Самая знаменитая статья о вреде использования goto
принадлежит перу Эдсгера В. Дейкстры, который аргументирует
свою точку зрения тем, что goto мешает следить за потоком выполнения программы. Это правда, если goto используется для
переходов вперед и назад, но в C goto невозможно использовать так же безответственно, как в тех языках, о которых писал
Дейкстра (в C goto не может выводить за пределы функции).

Последствия
Функция имеет единственную точку возврата, а основной поток программы
отделен от обработки ошибок и очистки ресурсов. Чтобы достичь этого результата, не понадобилось вложенных предложений if, но не все привыкли и готовы читать код с предложениями goto.
Допуская предложения goto, контролируйте себя, потому что может возникнуть соблазн использовать их не только для обработки ошибок и очистки, а
вот тогда код точно станет нечитаемым. Кроме того, внимательно следите за

Сквозной пример  41
соответствием между метками и функциями очистки. Типичная ошибка – помещать функцию очистки не под той меткой1.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В ядре Linux в основном используется обработка ошибок с применением goto. Например, в книге Alessandro Rubini and Jonathan Corbet «Linux
Device Drivers» (O'Reilly, 2001) описывается такой подход для программирования драйверов устройств в Linux.
• В книге C. Seacord «The CERT C Coding Standard by Robert» (AddisonWesley Professional, 2014) рекомендуется использовать goto для обработки ошибок.
• Эмуляция goto с помощью цикла do-while описана в портлендском репозитории паттернов под названием Тривиальный цикл do-while.
• В коде OpenSSL используется goto. Например, в функциях для обработки
сертификатов X509 goto применяется для перехода вперед на центральный обработчик ошибок.
• В коде Wireshark goto используется в функции main для перехода на
центральный обработчик ошибок, расположенный в конце функции.

Применение к сквозному примеру
Хотя довольно много людей категорически не одобряют использование goto,
обработка ошибок стала лучше по сравнению с предыдущим примером. В следующем коде нет вложенных предложений if, и код очистки четко отделен от
основного потока программы:

int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
assert(file_name!=NULL && "Недопустимое имя файла");
if(!(file_pointer=fopen(file_name, "r")))
{
goto error_fileopen;
}
if(!(buffer=malloc(BUFFER_SIZE)))
{
goto error_malloc;
}
return_value = searchFileForKeywords(buffer, file_pointer);
free(buffer);
1

Ну в точности так, как в примере, приведенном самим автором. Врачу, исцелися сам. – Прим.
перев.

42

 Глава 1. Обработка ошибок

error_malloc:
fclose(file_pointer);
error_fileopen:
return return_value;
}
А теперь допустим, что вы сами не любите goto или принятые в организации
стандарты кодирование запрещают их использование, но очищать ресурс все
равно надо. Что ж, есть альтернативы. Например, можно использовать паттерн
«Запись об очистке».

Запись об очистке
Контекст
Имеется функция, которая захватывает и освобождает несколько ресурсов.
Быть может, вы уже пытались уменьшить ее сложность, применяя паттерны
«Проверка условий», «Разбиение функции» или «Принцип самурая», но все же
остались глубоко вложенные if, связанные с выделением ресурсов. Возможно
даже, что код очистки ресурсов дублируется. Принятые в организации стандарты кодирования запрещают использовать паттерн «Переход к обработке
ошибки», или вы сами не хотите использовать goto.

Проблема
Код функции становится трудно читать и сопровождать, если он захватывает и освобождает несколько ресурсов, особенно когда эти ресурсы зависят друг от друга.
Затруднения вызваны тем, что выделение любого ресурса может завершиться неудачно, а освобождать ресурс нужно, только если он был выделен успешно. Чтобы обеспечить это, необходимо много предложений if, но при плохой
реализации наличие вложенных if в одной функции затрудняет чтение и сопровождение кода.
Поскольку ресурсы необходимоосвобождать, возврат в середине функции,
если что-то пошло не так, – не самая лучшая мысль. Ведь все уже захваченные
ресурсы нужно будет освобождать перед каждым предложением return. Поэтому в коде образуется несколько мест, где освобождается один и тот же ресурс,
но мы не хотим дублировать код обработки ошибок и очистки.

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

Сквозной пример  43
с ресурсами, поместите в тело предложения if, а все освобождение ресурсов –
после предложения if. При этом освобождать нужно только те ресурсы, которые были успешно захвачены. Пример приведен ниже:

void someFunction()
{
if((r1=allocateResource1()) && (r2=allocateResource2()))
{
mainFunctionality();
}
if(r1) 
{
cleanupResource1();
}
if(r2) 
{
cleanupResource2();
}
}
 Чтобы код было проще читать, эти проверки можно поместить внутрь функций очистки. Это

разумно, если функции очистке все равно нужно передавать указатель на ресурс.

Последствия
Теперь не осталось ни одного вложенного if, а в конце функции имеется
центральная точка для очистки ресурсов. Код стало значительно проще читать,
потому что основному потоку программы больше не мешает обработка ошибок.
Кроме того, функцию проще читать, потому что в ней всего одна точка выхода. Однако из-за того, что появилось много переменных для учета того, какие ресурсы были успешно захвачены, код усложнился. Быть может, паттерн
«Агрегат» поможет структурировать эти переменные.
Если захватывается много ресурсов, то в одном предложении if вызывается
много функций. Такое предложение очень трудно читать и еще труднее отлаживать. Поэтому когда захватывается много ресурсов, гораздо лучше применить «Объектную обработку ошибок».
Еще одна причина применить «Объектную обработку ошибок» вместо
«Записи об очистке» заключается в том, что показанный выше код все еще сложен, так как состоит из единственной функции, которая содержит как основную функциональность, так и логику захвата и освобождения ресурсов. То есть
у одной функции несколько обязанностей.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В портлендском репозитории паттернов представлено похожее решение, в котором каждая вызванная функция регистрирует обработчик
очистки в списке обратных вызовов. Для очистки вызываются все функ-

44

 Глава 1. Обработка ошибок
ции из этого списка.
• В функции dh_key2buf из библиотеки OpenSSL ленивое вычисление if
применяется для отслеживания выделенных байтов, которые освобождаются впоследствии.
• В функции cap_open_socket из сетевого анализатора Wireshark ленивое
вычисление if используется для сохранения выделенных ресурсов в переменных. На этапе очистки эти переменные проверяются, и если ресурс был выделен успешно, то он освобождается.
• В исходном коде OpenWrt функция nvram_commit выделяет ресурсы
внут­ри предложения if и сохраняет их в переменных прямо в том же if.

Применение к сквозному примеру
Теперь вместо goto и вложенных if мы имеем единственное предложение if. Преимущество отказа от goto в показанном ниже коде заключается
в том, что обработка ошибок четко отделена от основного потока программы:

int parseFile(char* file_name)
{
int return_value = ERROR;
FILE* file_pointer = 0;
char* buffer = 0;
assert(file_name!=NULL && "Недопустимое имя файла");
if((file_pointer=fopen(file_name, "r")) &&
(buffer=malloc(BUFFER_SIZE)))
{
return_value = searchFileForKeywords(buffer, file_pointer);
}
if(file_pointer)
{
fclose(file_pointer);
}
if(buffer)
{
free(buffer);
}
return return_value;
}
И все же код выглядит коряво. У этой функции слишком много обязанностей: выделение ресурсов, освобождение ресурсов, работа с файлом и обработка ошибок. Эти обязанности следует разнести по разным функциям, воспользовавшись паттерном «Объектная обработка ошибок».

Сквозной пример  45

Объектная обработка ошибок
Контекст
Имеется функция, которая захватывает и освобождает несколько ресурсов.
Быть может, вы уже пытались уменьшить ее сложность, применяя паттерны
«Проверка условий», «Разбиение функции» или «Принцип самурая», но все же
остались глубоко вложенные if, связанные с выделением ресурсов. Возможно
даже, что код очистки ресурсов дублируется. Но, быть может, вы уже избавились от вложенных предложений if, применив паттерн «Переход к обработке
ошибки» или «Запись об очистке».

Проблема
Наличие нескольких обязанностей у одной функции, например выделение, освобождение и использование ресурса, затрудняет реализацию, чтение, сопровождение и
тестирование.
Затруднения вызваны тем, что выделение любого ресурса может завершиться неудачно, а освобождать ресурс нужно, только если он был выделен успешно. Чтобы обеспечить это, необходимо много предложений if, но при плохой
реализации наличие вложенных if в одной функции затрудняет чтение и сопровождение кода.
Поскольку ресурсы необходимо освобождать, возврат в середине функции,
если что-то пошло не так, – не самая лучшая мысль. Ведь все уже захваченные
ресурсы нужно будет освобождать перед каждым предложением return. Поэтому в коде образуется несколько мест, где освобождается один и тот же ресурс,
но мы не хотим дублировать код обработки ошибок и очистки.
Даже после применения паттернов «Запись об очистке» или «Переход к обработке ошибки» функцию все еще трудно читать, так как в ней смешаны разные обязанности. Функция отвечает за захват нескольких ресурсов, обработку
ошибок и освобождение ресурсов. Однако каждая функция должна отвечать за
что-то одно.

Решение
Поместите инициализацию и очистку в разные функции, как это делается в конструкторах и деструкторах в объектно ориентированном программировании.
В главной функции просто вызовите одну функцию, которая захватит все
ресурсы, другую функцию, которая будет работать с этими ресурсами, и третью
функцию, которая освободит все ресурсы.
Если захваченные ресурсы не глобальны, то их нужно передавать между функциями. Когда ресурсов несколько, можно передавать содержащий их
«Агрегат». Если вы хотите скрыть сами ресурсы от вызывающей стороны, то
можете использовать «Описатель» для передачи функциям информации о ресурсах.
Если выделение ресурса завершилось неудачно, сохраните информацию об
этом в переменной (например, указатель NULL свидетельствует об ошибке вы-

46

 Глава 1. Обработка ошибок

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

void someFunction()
{
allocateResources();
mainFunctionality();
cleanupResources();
}

Последствия
Теперь код функции легко читать. Хотя выделение, освобождение и использование нескольких ресурсов никуда не делись, эти разные по характеру задачи разнесены по нескольким функциям.
Заведение похожих на объекты экземпляров, которые передаются между
функциями, называется «объектным» стилем программирования. При таком
подходе процедурное программирование больше напоминает объектно ориентированное, поэтому код, написанный в этом стиле, выглядит более знакомым программистам, привыкшим к объектной ориентированности.
В главной функции больше нет причин для появления нескольких предложений return, потому что не осталось вложенных if, реализующих логику выделения и освобождения ресурсов. Но, конечно, эта логика не исчезла совсем,
а просто перенесена в отдельные функции, чтобы избежать смешения с операциями над ресурсами.
Вместо одной функции мы теперь имеем несколько. Это может снизить производительность, но обычно снижение настолько незначительно, что в большинстве приложений им можно пренебречь.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Эта форма очистки применяется в объектно ориентированном программировании, где неявно вызываются конструкторы и деструкторы.
• Этот паттерн используется в коде OpenSSL. Например, выделение и
освобождение буферов реализовано функциями BUF_MEM_new и BUF_MEM_
free, которые вызываются повсеместно для работы с буферами.
• Функция show_help в исходном коде OpenWrt отображает справочную
информацию в контекстном меню. Она вызывает функцию инициализации, чтобы создать структуру struct, затем производит действия
с этой структурой и напоследок вызывает функцию для ее освобож­
дения.
• Функция cmd__windows_named_pipe из проекта Git использует паттерн
«Описатель» для создания канала, затем работает с этим каналом и вызывает отдельную функцию для его очистки.

Сквозной пример  47

Применение к сквозному примеру
Наконец-то мы пришли к следующему коду, в котором функция parseFile
вызывает другие функции для создания и очистки экземпляра анализатора:

typedef struct
{
FILE* file_pointer;
char* buffer;
} FileParser;
int parseFile(char* file_name)
{
int return_value;
FileParser* parser = createParser(file_name);
return_value = searchFileForKeywords(parser);
cleanupParser(parser);
return return_value;
}
int searchFileForKeywords(FileParser* parser)
{
if(parser == NULL)
{
return ERROR;
}
while(fgets(parser->buffer, BUFFER_SIZE, parser->file_pointer)!=NULL)
{
if(strcmp("KEYWORD_ONE\n", parser->buffer)==0)
{
return KEYWORD_ONE_FOUND_FIRST;
}
if(strcmp("KEYWORD_TWO\n", parser->buffer)==0)
{
return KEYWORD_TWO_FOUND_FIRST;
}
}
return NO_KEYWORD_FOUND;
}
FileParser* createParser(char* file_name)
{
assert(file_name!=NULL && "Недопустимое имя файла");
FileParser* parser = malloc(sizeof(FileParser));
if(parser)
{
parser->file_pointer=fopen(file_name, "r");

 Глава 1. Обработка ошибок

48

parser->buffer = malloc(BUFFER_SIZE);
if(!parser->file_pointer || !parser->buffer)
{
cleanupParser(parser);
return NULL;
}
}
return parser;
}
void cleanupParser(FileParser* parser)
{
if(parser)
{
if(parser->buffer)
{
free(parser->buffer);
}
if(parser->file_pointer)
{
fclose(parser->file_pointer);
}
free(parser);
}
}
В этом коде больше нет каскада if в основном потоке программы. Поэтому
функцию parseFile гораздо проще читать, отлаживать и сопровождать. Главная
функция не занимается выделением ресурсов, освобождением ресурсов и обработкой ошибок. Все эти детали перенесены в отдельные функции, каждая из
которых отвечает за что-то одно.
Оцените, насколько элегантнее стал код по сравнению с первоначальной
версией. Шаг за шагом применение паттернов делало код проще для чтения и
сопровождения. На каждом шаге мы удаляли каскад if и улучшали методику
обработки ошибок.

Резюме
В этой главе было показано, как выполнять обработку ошибок в программах на C. Паттерн «Разбиение функции» рекомендует разделять функцию на
меньшие части, чтобы упростить обработку ошибок в этих частях. Паттерн
«Проверка условий» рекомендует проверять предусловия функции и возвращать управление немедленно, если они не выполнены. Это оставляет на долю
остального кода меньше забот об обработке ошибок. Вместо того чтобы возвращать управление из функции, можно сразу завершить программу, следуя
«Принципу самурая». Что до более сложной обработки ошибок – особенно в сочетании с захватом и освобождением нескольких ресурсов, – в вашем распоря-

Для дополнительного чтения  49
жении имеется несколько паттернов на выбор. «Переход к обработке ошибки»
рекомендует совершать прямой переход в точку обработки ошибки. Паттерн
«Запись об очистке» рекомендует вместо перехода сохранять информацию
о том, какие ресурсы нуждаются в очистке, и выполнять эту очистку в конце
функции. Метод захвата ресурсов, более близкий к объектно ориентированному программированию, предлагает паттерн «Объектная обработка ошибок», в
котором используются отдельные функции инициализации и очистки по аналогии с идеей конструкторов и деструкторов.
Имея в своем арсенале эти паттерны обработки ошибок, вы сможете писать
небольшие программы, которые обрабатывают ошибки, не жертвуя удобством
сопровождения кода.

Для дополнительного чтения
Если у вас проснулся аппетит, то вот еще несколько ресурсов, которые расширят ваши знания об обработке ошибок.
• В портлендском репозитории паттернов (https://oreil.ly/qFLDa) предлагается
много паттернов с обсуждениями на тему обработки ошибок и не только. В большинстве паттернов обработки ошибок речь идет об обработке
исключений и использовании утверждений, но есть и несколько паттернов для C.
• Исчерпывающий обзор обработки ошибок вообще имеется в магистерской диссертации Томаса Аглассингера «Error Handling in Structured and
Object-Oriented Programming Languages» (Университет Оулу, 1999). Описываются различные типы ошибок, обсуждаются механизмы обработки
ошибок в языках программирования C, Basic, Java и Eiffel, и даются рекомендации, в частности, выполнять очистку ресурсов в порядке, противоположном их выделению. В диссертации упоминаются также сторонние решения в форме библиотеки, предлагающей улучшенные средства
обработки ошибок для C, например обработку исключений с помощью
команд setjmp и longjmp.
• Пятнадцать объектно ориентированных паттернов обработки ошибок, адаптированных для деловых систем, представлены в статье Klaus
Renzel «Error Handling for Business Information Systems» (https://oreil.ly/
bQnfx), и большая их часть применима не только к объектно ориентированным программам. Представленные паттерны охватывают обнаружение, протоколирование и обработку ошибок.
• Реализации некоторых паттернов проектирования из книги «банды
четырех», включающие фрагменты кода на C, приведены в книге Adam
Tornhill «Patterns in C» (Leanpub, 2014). Там приводится описание передовых практик в форме паттернов C, некоторые из которых относятся к
обработке ошибок.
• Собрание паттернов для обработки и протоколирования ошибок представлено в статьях Andy Longshaw and Eoin Woods (https://oreil.ly/7Yj8h)
«Patterns for Generation, Handling and Management of Errors» и «More

50

 Глава 1. Обработка ошибок
Patterns for the Generation, Handling and Management of Errors». Большинство из них ориентировано на обработку ошибок в форме исключений.

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

Глава

2
Возврат информации
об ошибке

Предыдущая глава была посвящена обработке ошибок. Здесь мы продолжим
это обсуждение, но сместим акцент на то, как информировать пользователей
об обнаруженных ошибках.
В любой большой программе программист должен решить, как реагировать
на ошибки, возникающие в его собственном коде, как реагировать на ошибки
в чужом коде, как передавать информацию об ошибке и как представлять ее
пользователям.
В большинстве объектно ориентированных языков имеется удобный механизм исключений, дающий программисту дополнительный канал возврата
информации об ошибке, но в C такого встроенного механизма нет. Существуют
способы эмулировать обработку исключений и даже наследование исключений в C, например описанные в книге Axel-Tobias Schreiner «Object-Oriented
Programming with ANSI-C». Но для программистов, работающих с унаследованным кодом на C или желающих придерживаться свойственного C стиля, такие
механизмы исключений неприемлемы. Взамен им нужны рекомендации по
применению механизмов обработки ошибок, которые уже имеются в C.
В этой главе как раз и даются рекомендации о передаче информации об
ошибках между функциями и интерфейсами. На рис. 2.1 приведен обзор паттернов, рассматриваемых в этой главе и связей между ними, а в табл. 2.1 –
краткое описание этих паттернов.
Выходные
параметры

Возвращае­
мое значение

Специальное
возвращаемое
значение

Принцип
самурая

Протоколирова­
ние ошибок

Возврат кода
состояния

Возврат
существенной
информации об
ошибке

Пояснения
Паттерн, представленный
в этой главе
Паттерн, представленный
в другой главе
B можно использовать для
реализации и дополнения A

Рис. 2.1. Обзор паттернов для возврата информации об ошибках

52

 Глава 2. Возврат информации об ошибке

Таблица 2.1. Паттерны для возврата информации об ошибках
Название паттерна

Краткое описание

Возврат кода
состояния

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

Возврат
существенной
информации об
ошибке

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

Специальное
возвращаемое
значение

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

Протоколирование
ошибок

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

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

Сквозной пример  53
API реестра

/* Описатель раздела реестра */
typedef struct Key* RegKey;
/* Создать новый раздел реестра с именем 'key_name' */
RegKey createKey(char* key_name);
/* Сохранить значение 'value' в разделе с именем ''key' */
void storeValue(RegKey key, char* value);
/* Сделать раздел доступным для чтения (другими функциями,
которые в этом примере не рассматриваются) */
void publishKey(RegKey key);
Реализация реестра

#define STRING_SIZE 100
#define MAX_KEYS 40
struct Key
{
char key_name[STRING_SIZE];
char key_value[STRING_SIZE];
};
/* глобальный на уровне файла массив, содержащий все разделы реестра */
static struct Key* key_list[MAX_KEYS];
RegKey createKey(char* key_name)
{
RegKey newKey = calloc(1, sizeof(struct Key));
strcpy(newKey->key_name, key_name);
return newKey;
}
void storeValue(RegKey key, char* value)
{
strcpy(key->key_value, value);
}
void publishKey(RegKey key)
{
int i;
for(i=0; ikey_name, key_name);
*key = newKey;
return OK;
}
 Вместо того чтобы возвращать INVALID_PARAMETER или STRING_TOO_LONG, мы теперь
завершаем программу, если какой-либо из переданных параметров некорректен.

Завершение программы в случае слишком длинных строк на первый взгляд
кажется чрезмерно суровой мерой. Но, как и нулевые указатели, слишком
длинная строка является недопустимым параметром. Если реестр принимает строку не от пользователя посредством графического интерфейса, а от вызывающей программы, то программа завершается только в случае очевидной
ошибки, и такое поведение вполне оправдано.
Далее мы видим, что функция createKey возвращает всего два разных кода:
OUT_OF_MEMORY и OK. Код станет немного чище, если передавать эту информацию
с помощью возврата специальных значений.

Сквозной пример  67

Специальное возвращаемое значение
Контекст
Имеется функция, которая вычисляет некоторый результат, и вы хотите передать вызывающей стороне информацию об ошибке в случае возникновения
таковой. Но желательно возвращать только существенные ошибки.

Задача
Вы хотите возвращать информацию об ошибке, но не хотите явно возвращать коды
состояний, потому что тогда функции труднее возвращать другие данные. Можно
было бы добавить выходные параметры, но это усложнило бы вызов функции.
Полный отказ от возврата информации об ошибке – тоже не вариант. Какую-то информацию вызывающей стороне предоставить нужно, и вы хотите,
чтобы та могла отреагировать на нее. Но вы хотите передавать вызывающей
стороне не слишком много информации. Например, это может быть просто
двоичный признак: успешно завершилась функция или неудачно. Возвращать
код состояния для такой простой информации – пожалуй, перебор. Применить
«Принцип самурая» и завершить программу нельзя, потому что возникающие
в функции ошибки не настолько серьезны. Или, быть может, вы хотите, чтобы
вызывающая сторона решала, как обрабатывать ошибки, потому что она может сделать это разумно.

Решение
Используйте возвращаемое функцией значение для возврата вычисленных ей данных, но зарезервируйте одно или несколько специальных значений для возврата информации об ошибках.
Например, если функция возвращает указатель, то в качестве специального
значения, показывающего, что произошла ошибка, можно использовать NULL.
Нулевой указатель по определению недопустим, поэтому есть уверенность,
что это специальное значение не перепутаешь с правильным указателем, вычисленным функцией. Ниже показано, как возвращать информацию об ошибке при работе с указателями:
Реализация вызываемой функции

void* func()
{
if(somethingGoesWrong())
{
return NULL;
}
else
{

 Глава 2. Возврат информации об ошибке

68

return some_pointer;
}
}
Код вызывающей стороны

pointer = func();
if(pointer != NULL)
{
/* работа с указателем */
}
else
{
/* обработка ошибки */
}
Вы должны обязательно документировать в API все специальные возвращаемые значения. Иногда имеется общепринятое соглашение о том, какие
специальные значения соответствуют ошибкам. Например, очень часто для
обозначения ошибок используются отрицательные целые числа. Но даже в таком случае семантику значений необходимо документировать.
Необходимо, чтобы специальное значение, обозначающее ошибку, не могло
возникнуть в ситуации, когда ошибки нет. Например, если функция возвращает температуру по Цельсию в виде целого числа, то было бы неправильно
следовать принятому в UNIX соглашению о том, что любое отрицательное число означает ошибку. Вместо этого можно было бы выбрать в качестве индикатора ошибки число –300, потому что физически невозможны температуры
ниже –273 °С.

Последствия
Теперь функция может сообщать информацию об ошибке с помощью возвращаемого значения, даже если оно используется также для возврата результата функции. При этом нет нужды в дополнительных выходных параметрах.
Иногда специальных значений для кодирования информации об ошибке не так много. Например, если возвращается указатель, то только значение
NULL можно использовать для обозначения ошибки. В результате мы можем
сообщить вызывающей стороне только одно: была ошибка или нет. А значит,
никакой детальной информации об ошибке передать не получится. Но можно
найти и хорошую сторону в этой ситуации: нет соблазна вернуть ненужную
информацию об ошибке. Во многих случаях достаточно сообщить, что ошибка имела место, а на более подробную информацию вызывающая сторона все
равно не смогла бы отреагировать.
Если впоследствии вы поймете, что нужна более подробная информация об
ошибке, то может оказаться, что предоставить ее невозможно, потому что не
осталось неиспользованных специальных значений. Тогда придется изменять
всю сигнатуру функции и использовать возврат кодов состояния для предо-

Сквозной пример  69
ставления дополнительной информации. Изменение сигнатуры неприемлемо,
если API должен оставаться совместимым с существующими клиентами. Если
вы предвидите изменения в будущем, то не используйте специальные возвращаемые значения, а сразу выбирайте возврат кодов состояния.
Иногда программисты предполагают, что нет нужды пояснять, какие возвращаемые значения означают ошибку, – мол, и так ясно. Например, для кого-то очевидно, что нулевой указатель – признак ошибки. А еще для кого-то –
что –1 обозначает ошибку. Это создает опасную ситуацию, когда программист
предполагает, что всем ясно, какие значения соответствуют ошибкам. Однако
это не более чем предположение. В любом случае в API должно быть четко документировано, какие значения обозначают ошибки, – и не следует пренебрегать этим под предлогом, что и так все понятно.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Функция getobj в игре NetHack возвращает указатель на некоторый
объект или NULL в случае ошибки. Чтобы сообщить о специальном случае – отсутствии объекта, который можно было бы вернуть, – функция
возвращает указатель на глобальный объект zeroobj; он принадлежит
тому же типу, что указан в сигнатуре функции, и известен вызывающей стороне. Вызывающая сторона может проверить, совпадает ли возвращенный указатель с указателем на этот глобальный объект, и таким
образом отличить допустимый объект от объекта zeroobj, имеющего
специальную семантику.
• Функция getchar из стандартной библиотеки C читает символ из stdin.
Она возвращает значение типа int, позволяющее вернуть гораздо больше информации, чем просто символ. Если символов больше нет, функция возвращает константу EOF, обычно определенную как –1. Поскольку
символы не могут принимать отрицательные значения, EOF легко отличима от нормальных результатов и потому может обозначать специальную ситуацию – отсутствие доступных символов.
• В большинстве функций UNIX или POSIX отрицательные числа используются для обозначения ошибки. Например, POSIX-функция write возвращает число записанных байтов или –1 в случае ошибки.

Применение к сквозному примеру
При использовании паттерна «Специальное возвращаемое значение» код
выглядит, как показано ниже. Для простоты представлена только функция
createKey:
Объявление функции createKey

/* Создать новый раздел реестра с именем 'key_name' (должно быть
отлично от NULL, макс. длина - STRING_SIZE символов). Возвращает
описатель раздела или NULL в случае ошибки. */
RegKey createKey(char* key_name);

 Глава 2. Возврат информации об ошибке

70

Реализация функции createKey

RegKey createKey(char* key_name)
{
assert(key_name != NULL);
assert(STRING_SIZE > strlen(key_name));
RegKey newKey = calloc(1, sizeof(struct Key));
if(newKey == NULL)
{
return NULL;
}
strcpy(newKey->key_name, key_name);
return newKey;
}
Функция createKey стала гораздо проще. Теперь она возвращает не коды
состояний, а сразу описатель, и выходной параметр больше не нужен. Документация API функции тоже сократилась, поскольку не нужно описывать дополнительный параметр и включать длинное описание того, как результат
функции будет возвращен вызывающей стороне.
Для вызывающей стороны все стало намного проще. Ей не нужно передавать описатель в выходном параметре, она получит его непосредственно в
виде возвращаемого значения – поэтому код стал удобнее для чтения и сопровождения.
Однако теперь у нас появилась проблема, которой не было при возврате
детальной информации об ошибке в виде кода состояния – мы знаем только,
успешно или нет завершилась функция. Все внутренние сведения об ошибке
отброшены, и если впоследствии они понадобятся, например для отладки, то
получить их нет никакой возможности. Для решения этой проблемы можно
использовать паттерн «Протоколирование ошибок».

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

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

Сквозной пример  71
может обратиться к вам (например, по горячей линии) и спросить, что значит
код и как исправить ошибку. А вы, имея детальную информацию, используемую для отладки программы, можете определить, что случилось.
Однако у такого подхода есть крупный недостаток – вызывающая сторона,
которой вообще нет дела до ваших ошибок, должна предъявлять информацию
о них пользователю только для того, чтобы тот мог сообщить ее вам. А пользователю тоже такая подробная информация неинтересна.
Кроме того, возврат кодов состояний плох тем, что возвращаемое значение
используется для возврата информации об ошибке, а собственно результаты
функции приходится возвращать в выходных параметрах. Иногда, правда,
можно воспользоваться специальными возвращаемыми значениями, но не
всегда. А вы не хотите заводить дополнительные параметры только для того,
чтобы предоставить информацию об ошибке, так как это усложняет код вызывающей стороны.

Решение
Используйте разные каналы для передачи информации об ошибке, существенной
для вызывающего кода, и информации, существенной для разработчика. Например,
записывайте отладочную информацию в файл журнала и не возвращайте детальные
сведения об ошибке вызывающей стороне.
В случае ошибки пользователь программы должен будет предоставить вам
отладочную информацию из журнала, чтобы вы могли легко определить причину ошибки. Например, ее можно было бы передать по электронной почте.
Альтернативно можно было бы записывать сведения об ошибке в журнал на
стыке между вами и вызывающей стороной и возвращать вызывающей стороне только существенную информацию об ошибке. Например, вызывающую
сторону можно было бы проинформировать о том, что произошла внутренняя
ошибка, но не сообщать детали. Таким образом, вызывающая сторона могла
бы обработать общую ошибку, ничего не зная о том, что делать с ее конкретными разновидностями, но вы при этом не потеряете ценную отладочную информацию.
Чтобы не потерять отладочную информацию, вы должны протоколировать
сведения о программных и неожиданных ошибках. Очень полезно сохранять
информацию о степени серьезности и месте ошибки, например имя исходного
файла и номер строки в нем или трассу вызова. В языке C есть специальные
макросы для получения номера текущей строки (__LINE__), имени текущей
функции (__func__) и имени текущего файла (__FILE__). В следующем коде для
протоколирования используется макрос __func__.

void someFunction()
{
if(something_goes_wrong)
{
logInFile("что-то пошло не так", ERROR_CODE, __func__);
}
}

72

 Глава 2. Возврат информации об ошибке

Многострочные макросы
Погрузив предложения макроса в цикл do/while, вы сможете избежать
проблем, демонстрируемых в следующем коде:

#define MACRO(x) \
x=1;
\
x=2;
\
if(x==0)
MACRO(x)
Здесь нет фигурных скобок вокруг тела if, поэтому, читая код, можно подумать, что код макроса выполняется только при x==0. Но на самом деле после
расширения макроса получается такой код:

if(x==0)
x=1;
x=2;
Последняя строка не принадлежит телу if, а это совсем не то, что мы хотели. Чтобы избежать таких проблем, лучше всего помещать предложения
макроса внутрь цикла do/while..
Для получения более детальной информации можно сохранить трассу вызовов функций вместе с возвращенными ими значениями. Это поможет воспроизвести ситуацию, в которой произошла ошибка, но, разумеется, влечет за
собой дополнительные накладные расходы. Для трассировки значений, возвращенных функциями, можно использовать такой код:

#define RETURN(x)
\
do {
\
logInFile(__func__, x); \
return x;
\
while (0)
int soneFunction()
{
RETURN(-1);
}
Журнал ошибок можно хранить в файле, как показано выше. Но надо предусмотреть особые случаи, например отсутствие места для файла на диске или
крах программы во время записи в файл. Обработать такие ситуации нелегко,
но очень важно, если вы хотите иметь надежный механизм протоколирования,
на который можно будет положиться для последующей отладки. Если данные
в журналах некорректны, то в своей охоте за ошибками вы можете пойти по
ложному следу.

Сквозной пример  73

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

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В веб-сервере Apache функция ap_log_error записывает информацию
об ошибках, связанных с запросами и подключениями, в журнал. В каж­
дой записи журнала хранится имя файла и строка кода, в которой произошла ошибка, а также строка, переданная функцией вызывающей стороне. Журнал хранится в файле error_log на сервере.
• В операционной системе B&R Automation Runtime используется система протоколирования, позволяющая программистам сообщать информацию пользователям путем вызова функции eventLogWrite из любого
места программы. Это позволяет предоставлять информацию, не передавая ее по всей цепочке вызовов в какой-то центральный компонент
протоколирования.
• Паттерн «Контекст утверждения» из книги Adam Tornhill «Patterns in C»
(Leanpub, 2014) рекомендует завершать программу в случае ошибок и
протоколировать причину и место ошибки, добавляя строку в вызов
assert. Если условие в assert не выполнено, то будет напечатана строка
кода, содержащая assert, которая включает и добавленную строку.

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

74

 Глава 2. Возврат информации об ошибке

API реестра

/* макс. размер строковых параметров (включая завершающий NULL) */
#define STRING_SIZE 100
/* Коды ошибок, возвращаемые реестром */
typedef enum
{
OK,
CANNOT_ADD_KEY
} RegError;
/* Описатель раздела реестра */
typedef struct Key* RegKey;
/* Создать новый раздел реестра с именем 'key_name' (должно быть
отлично от NULL, макс. размер STRING_SIZE символов). Возвращает
описатель раздела или NULL в случае ошибки. */
RegKey createKey(char* key_name);
/* Сохранить переданное значение 'value' (должно быть отлично
от NULL, макс. размер STRING_SIZE символов) в разделе с
именем 'key' (должно быть отлично от NULL) */
void storeValue(RegKey key, char* value);
/* Сделать раздел с именем 'key' (должно быть отлично от NULL)
доступным для чтения. Возвращает OK, если все хорошо, или
CANNOT_ADD_KEY, если реестр заполнен и новые разделы
опубликовать невозможно. */
RegError publishKey(RegKey key);
Реализация реестра

#define MAX_KEYS 40
struct Key
{
char key_name[STRING_SIZE];
char key_value[STRING_SIZE];
};
/* макрос для протоколирования отладочной информации и утверждения */
#define logAssert(X)
\
if(!(X))
\
{
\
printf("Error at line %i", __LINE__); \
assert(false); \

Сквозной пример  75
}
/* глобальный на уровне файла массив, содержащий все разделы реестра */
static struct Key* key_list[MAX_KEYS];
RegKey createKey(char* key_name)
{
logAssert(key_name != NULL)
logAssert(STRING_SIZE > strlen(key_name))
RegKey newKey = calloc(1, sizeof(struct Key));
if(newKey == NULL)
{
return NULL;
}
strcpy(newKey->key_name, key_name);
return newKey;
}
void storeValue(RegKey key, char* value)
{
logAssert(key != NULL && value != NULL)
logAssert(STRING_SIZE > strlen(value))
strcpy(key->key_value, value);
}
RegError publishKey(RegKey key)
{
logAssert(key != NULL)
int i;
for(i=0; i0)
{
text = malloc(size);
readFileContent("my-file.txt", text, size);
caesar(text, strnlen(text, size));
printf(«Зашифрованный текст: %s\n», text);
/* здесь память не освобождается */
}
}
Мы выделяем память, но не вызываем free для ее освобождения. Вместо
этого мы позволяем указателям на память покинуть область видимости, создавая тем самым утечку памяти. Но это не проблема, потому что программа
все равно завершается сразу после этого, и операционная система освобождает
память.
Такой подход кажется небрежным, но в некоторых случаях вполне приемлем. Если память нужна на протяжении всего времени работы программы или
если программа работает недолго и вы уверены, что ее код не будет развиваться или использоваться где-то еще, то возможность наплевать на освобождение
памяти может стать решением, котором сильно облегчит вам жизнь. Но надо
иметь уверенность в том, что программа не начнет эволюционировать и не
превратится в долго работающую. Если такой уверенности нет, то нужно поискать другой подход.
Именно этим мы и займемся далее. Вы хотите зашифровать несколько
файлов, а именно все файлы, находящиеся в текущем каталоге. Вы понимаете,
что придется выделять память чаще и отказ от освобождения памяти больше
не годится, потому что приведет к потреблению слишком большого объема

94

 Глава 3. Управление памятью

памяти. Это может создать проблемы для других программ или для вашей
собственной.
Возникает вопрос, в каком месте кода следует освобождать память. Кто за
это отвечает? Необходим паттерн «Единоличное владение».

Единоличное владение
Контекст
В программе имеется много данных заранее неизвестно размера, и для их
хранения используется динамическая память. Память не нужна на протяжении
всего времени работы программы, и вы часто вынуждены выделять память
разного размера, поэтому прибегнуть к «Отложенной очистке» не получится.

Проблема
Использование динамической памяти дает большую власть, но вместе с ней приходит и большая ответственность – память нужно освобождать. В больших программах
трудно гарантировать, что вся динамическая память будет надлежащим образом
освобождена.
Освобождение динамической памяти сулит немало трудностей. Можно освободить ее слишком рано, когда кому-то еще нужен к ней доступ (висячий
указатель). Или по ошибке освободить одну и ту же память несколько раз. Обе
программных ошибки ведут к неопределенному поведению, например к краху
программы в каком-то другом месте. К тому же они создают угрозу безопасности, которая может быть эксплуатирована противником. И ко всему прочему
такие ошибки очень трудно отлаживать.
Но память все равно освобождать нужно, потому что иначе со временем
программа захватит слишком много памяти, после чего она сама или другие
процессы не смогут получить дополнительную память.

Решение
В тот момент, когда выделяете память, четко определите и документируйте, где она
должна быть освобождена и кто это должен сделать.
В коде должно быть ясно документировано, кто владеет памятью и как долго
она должна оставаться действительной. Лучше всего еще перед написанием
первого вызова malloc спросить себя, где эта память будет освобождена. В комментариях к объявлению функции нужно указать, передает ли эта функция
какие-нибудь буферы памяти и, если да, кто отвечает за их освобождение.
В других языках программирования, например в C++, для такой документации можно использовать программные конструкции. В таких конструкциях,
как unique_ptr или shared_ptr, уже на уровне объявления функции видно, кто
отвечает за освобождение памяти. Поскольку в C подобных конструкций нет,
приходится тщательно документировать обязанности в форме комментариев.
Если возможно, возлагайте ответственность за выделение и освобождение
на одну функцию, как в паттерне «Объектная обработка ошибок», где в коде

Хранение данных и проблемы с динамической памятью  95
имеется ровно одна точка для вызова функций, подобных конструктору и деструктору:

#define DATA_SIZE 1024
void function()
{
char* memory = malloc(DATA_SIZE);
/* работа с памятью */
free(memory);
}
Если ответственность за выделение и освобождение разнесена и владение
памятью передается, то ситуация осложняется. В некоторых случаях без этого не обойтись, например если одна лишь выделяющая память функция знает
размер данных, а эти данные необходимы другим функциям:

/* Выделяет и возвращает буфер, который должен быть освобожден вызывающей стороной */
char* functionA()
{
char* memory = malloc(data_size); 
/* заполнить память */
return memory;
}
void functionB()
{
char* memory = functionA();
/* поработать с памятью */
free(memory); 
}
 Вызываемая сторона выделяет память.
 Вызывающая сторона отвечает за освобождение памяти.

По возможности старайтесь избегать разнесения ответственности за выделение и освобождение памяти по разным функциям. Но в любом случае документируйте, кто отвечает за освобождение, чтобы тут не оставалось места для
сомнений.
Есть и другой паттерн, описывающий более специальные ситуации, относящиеся к владению памятью: «Буфер» (или «Экземпляр»), принадлежащий
вызывающей стороне, когда вызывающая сторона отвечает за выделение и
освобождение памяти.

Последствия
Наконец, можно выделить память и корректно освободить ее. Это повышает
гибкость. Можно временно заимствовать большие объемы памяти из кучи, а
впоследствии вернуть ее, сделав доступной другим.

96

 Глава 3. Управление памятью

Но, конечно, за это удобство приходится расплачиваться. Вы должны заниматься освобождением памяти, что усложняетпрограммирование. Даже при
использовании «Единоличного владения» ошибки при работе с памятью случаются и приводят к ситуациям, которые трудно отлаживать. Кроме того, на
освобождение памяти уходит время. Явное документирование того, где должна освобождаться память, помогает предотвратить некоторые ошибки и в целом делает код проще для понимания и сопровождения. Чтобы еще снизить
количество ошибок, можете использовать паттерны «Обертка выделения» и
«Проверка указателя».
При использовании выделения и освобождения динамической памяти нарастают проблемы фрагментации кучи и увеличивается время выделения и
доступа к памяти. В некоторых приложениях это вообще не проблема, но есть
и такие, где эти вопросы стоят очень остро. В таком случае может помочь параметр «Пул памяти».

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В книге Kamran Amini «Extreme C» (Packt, 2019) рекомендуется возлагать ответственность за освобождение памяти на функцию, которая ее
выделила, и документировать в комментариях, какая функция или объект владеет памятью. Конечно, эта идея остается в силе и при использовании обертывающих функций. Тогда функция, вызвавшая обертку
выделения, должна вызывать и обертку освобождения.
• В реализации функции mexFunction из среды численных расчетов
MATLAB четко документировано, кто владеет памятью и должен ее освободить.
• В игре NetHack явно документировано, должны ли освобождать ка­
кую-то память стороны, вызывающие функции. Например, функция
nh_compose_ascii_screenshot выделяет и возвращает строку, которая
должна быть освобождена вызывающей стороной.
• Диссектор Wireshark для хешей «идентификаторов сообщества» в документации по входящим в него функциям ясно указывает, кто отвечает
за освобождение памяти. Например, функция communityid_calc выделяет память и требует, чтобы ее освобождала вызывающая сторона.

Применение к сквозному примеру
Функциональность encryptCaesarFile осталась прежней. Изменилось только
то, что теперь вы вызываете free для освобождения памяти и четко документируете, кто отвечает за ее освобождение. Кроме того, реализована функция
encryptDirectoryContent, которая шифрует все файлы в текущем каталоге.

/* Получив имя файла 'filename', эта функция читает текст из указанного файла
и печатает текст, зашифрованный шифром Цезаря. Эта функция отвечает за выделение
и освобождение буферов, необходимых для хранения содержимого файла. */
void encryptCaesarFile(char* filename)

Хранение данных и проблемы с динамической памятью  97
{
char* text;
int size = getFileLength(filename);
if(size>0)
{
text = malloc(size);
readFileContent(filename, text, size);
caesar(text, strnlen(text, size));
printf("Зашифрованный текст: %s\n", text);
free(text);
}
}
/* Для каждого файла в текущем каталоге эта функция читает текст из файла
и печатает текст, зашифрованный шифром Цезаря. */
void encryptDirectoryContent()
{
struct dirent *directory_entry;
DIR *directory = opendir(".");
while ((directory_entry = readdir(directory)) != NULL)
{
encryptCaesarFile(directory_entry->d_name);
}
closedir(directory);
}
Этот код печатает результаты шифрования всех файлов в текущем каталоге
шифром Цезаря. Отметим, что он работает только в UNIX-системах, и для простоты не проверяется, что содержимое находящихся в каталоге файлов удовлетворяет предъявляемым требованиям.
Теперь память освобождается, когда в ней отпадает необходимость. Отметим, что не вся необходимая программе память выделяется одновременно. В
каждый момент времени занята только память для хранения одного файла.
Поэтому потребление памяти программой значительно сокращается, что особенно заметно, когда каталог содержит много файлов.
В показанном выше коде ошибки не обрабатываются. Например, что будет,
если памяти не хватает? Программа просто «грохнется». В таких ситуациях какая-то обработка ошибок необходима, но каждый раз проверять указатели, возвращенные malloc, слишком утомительно. Нам нужна «Обертка выделения».

Обертка выделения
Контекст
Вы выделяете динамическую память в нескольких местах и хотите реагировать на такие ошибки, как нехватка памяти.

98

 Глава 3. Управление памятью

Проблема
Любое выделение динамической памяти может завершиться неудачно, поэтому нужно проверять результат и реагировать соответственно. Это утомительно, потому что в
коде может быть много мест, где такие проверки необходимы.
Функция malloc возвращает NULL, если запрошенной памяти нет. С одной
стороны, пренебрежение проверкой значения, возвращенного malloc, чревато
крахом программы в случае, если возвращен указатель NULL. С другой стороны,
если проверять возвращенное значение везде, где выделяется память, то код
станет более сложным, а значит, его будет труднее читать и сопровождать.
Если такие проверки разбросаны по всей кодовой базе, а впоследствии вы
захотите изменить поведение программы в случае ошибок выделения памяти, то придется просматривать много мест. Кроме того, простое добавление
проверки ошибок в существующие функции нарушает принцип единственной
обязанности, который гласит, что каждая функция должна отвечать только за
что-то одно (а не за такие разные вещи, как логика программы и выделение
памяти).
Кроме того, если вы впоследствии захотите изменить способ выделения памяти, например явно инициализировать всю выделенную память, то наличие
многочисленных обращений к функциям выделения, разбросанных по всему
коду, затруднит работу.

Решение
Оберните вызовы функций выделения и освобождения и реализуйте в этих обертках
обработку ошибок или дополнительное управление памятью.
Реализуйте функции-обертки для вызовов malloc и free и, когда нужно выделить или освободить память, вызывайте только эти функции. Обертка будет
служить единым центральным местом для обработки ошибок. Например, можно проверить возвращенный указатель на выделенную память (см. паттерн
«Проверка указателя») и в случае ошибки завершить программу, как показано
ниже.

void* checkedMalloc(size_t size)
{
void* pointer = malloc(size);
assert(pointer);
return pointer;
}
#define DATA_SIZE 1024
void someFunction()
{
char* memory = checkedMalloc(DATA_SIZE);
/* поработать с памятью */
free(memory);
}

Хранение данных и проблемы с динамической памятью  99
Вместо завершения программы можно запротоколировать ошибки. Для протоколирования отладочной информации проще даже использовать макрос, а
не функцию. Тогда можно, не утруждая вызывающую сторону, записать в журнал имя файла, имя функции и номер строки, в которой произошла ошибка.
При наличии такой информации программисту будет очень легко найти место,
где возникла ошибка. К тому же использование макроса вместо функции в качестве обертки позволяет сэкономить на одном вызове функции (но в большинстве случаев это неважно, потому что компилятор в любом случае встроит
вызов). Применяя макросы для выделения и освобождения памяти, мы даже
можем реализовать синтаксический аналог конструктора:

#define NEW(object, type)
do {
object = malloc(sizeof(type));
if(!object)
{
printf("Malloc Error: %s\n", __func__);
assert(false);
}
} while (0)

\
\
\
\
\
\
\
\

#define DELETE(object) free(object)
typedef struct{
int x;
int y;
}MyStruct;
void someFunction()
{
MyStruct* myObject;
NEW(myObject, MyStruct);
/* поработать с объектом */
DELETE(myObject);
}
Помимо обработки ошибок, в функциях-обертках можно делать и другие
вещи. Например, можно следить, какую память выделяла программа, и сохранять эту информацию в списке вместе с именем файла и номером строки (для
этого понадобилась бы также обертка для free, как в примере выше). Так вы
сможете легко напечатать отладочную информацию, если захотите узнать, какая память выделена в данный момент (и какую вы, возможно, забыли освободить). Но если вас интересует такого рода информация, то проще воспользоваться инструментом для отладки работы с памятью типа valgrind. Кроме того,
если вы будете следить за тем, какую память выделили, то сможете написать
функцию, которая освобождает всю память разом, – это один из вариантов сделать программу чище в случае использования паттерна «Отложенная очистка».

100

 Глава 3. Управление памятью

Помещать все в одно место не всегда удобно. Быть может, в приложении есть
некритические части, где возникновение ошибки необязательно должно приводить к завершению всего приложения. В таком случае имеет смысл завести
несколько «Оберток выделения». Одна по-прежнему будет содержать assert и
может использоваться в критических частях, ошибка в которых не позволяет
приложению работать дальше, а другая – в некритических, где можно вернуть
код состояния и корректно обработать ошибку.

Последствия
Обработка ошибок и другие операции управления памятью теперь сосредоточены в одном месте. В тех местах программы, где нужно выделить память,
вы просто вызываете обертку, так что явно обрабатывать ошибки в этом месте
уже не нужно. Но эта идея работает только для некоторых видов обработки
ошибок. Если в случае ошибки нужно завершить программу, то это как раз то,
что нужно, но если можно продолжить работу, сократив функциональность, то
все равно придется вернуть из обертки какую-то информацию об ошибке и
отреагировать на нее. Поэтому «Обертка выделения» не облегчает жизнь. Однако и при таком сценарии в обертке можно реализовать протоколирование,
немного уменьшив объем работы.
Функция-обертка полезна при тестировании, потому что является центральным местом, где можно изменить поведение функции выделения памяти. Кроме того, в обертке можно реализовать имитацию (подменить обернутые вызовы какой-то другой тестовой функцией), не затрагивая другие вызовы malloc
(скажем, в стороннем коде).
Отделение обработки ошибок от вызывающего кода с помощью обертки –
рекомендуемая практика, потому что у вызывающей стороны не возникает
искушения реализовать обработку ошибок прямо в коде, занимающемся реализацией другой логики. Выполнение нескольких вещей в одной функции
(логики программы и развернутой обработки ошибок) нарушает принцип
единст­венной обязанности.
Обертка выделения позволяет единообразно обрабатывать ошибки выделения памяти и в случае необходимости изменить способ обработки ошибок.
Если вы решите протоколировать информацию об ошибке, то внести изменения надо будет только в одном месте. Если впоследствии вы захотите не вызывать malloc напрямую, а воспользоваться «Пулом памяти», то наличие обертки
существенно упростит задачу.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В книге David R. Hanson «C Interfaces and Implementations» (AddisonWesley, 1996) функция-обертка используется для выделения памяти в
реализации пула памяти. Обертки просто вызывают assert для завершения программы в случае ошибки.
• Библиотека GLib предоставляет функции g_malloc и g_free среди прочих функций работы с памятью. Функция g_malloc хороша тем, что в
случае ошибки останавливает программу («Принцип самурая»). Поэто-

Хранение данных и проблемы с динамической памятью  101
му вызывающей стороне не нужно проверять значение, возвращенное
при каждом выделении памяти.
• В анализаторе веб-журналов реального времени GoAccess реализована
функция xmalloc, обертывающая вызовы malloc с целью обработки ошибок.
• «Обертка выделения» – частный случай паттерна «Декоратор», описанного в книгах Erich Gamma, Richard Helm, Ralph Johnson и John Vlissides
«Design Patterns: Elements of Reusable Object-Oriented Software» (Prentice
Hall, 1997).

Применение к сквозному примеру
Теперь вместо вызова самих функций malloc и free мы всюду в коде используем функции-обертки:

/* Выделяет память и вызывает assert, если памяти недостаточно */
void* safeMalloc(size_t size)
{
void* pointer = malloc(size);
assert(pointer); 
return pointer;
}
/* Освобождает память, на которую указывает 'pointer' */
void safeFree(void *pointer)
{
free(pointer);
}
/* Получив имя файла 'filename', эта функция читает текст из указанного файла
и печатает текст, зашифрованный шифром Цезаря. Эта функция отвечает за выделение
и освобождение буферов, необходимых для хранения содержимого файла. */
void encryptCaesarFile(char* filename)
{
char* text;
int size = getFileLength(filename);
if(size>0)
{
text = safeMalloc(size);
readFileContent(filename, text, size);
caesar(text, strnlen(text, size));
printf("Зашифрованный текст: %s\n", text);
safeFree(text);
}
}
 В случае ошибки выделения мы следуем «Принципу самурая» и завершаем программу. Для такого приложения это допустимая стратегия. Если невозможно обработать ошибку корректно, то
завершение программы – правильный выбор.

102

 Глава 3. Управление памятью

Паттерн «Обертка выделения» хорош тем, что предлагает центральное место
для обработки ошибок выделения памяти. Не нужно писать код для проверки
указателя после каждого выделения. Имеется также обертка для освобождения
памяти, которая может пригодиться в будущем, например, если вы решите
вести учет памяти, выделенной приложением в каждый момент времени.
Сразу после выделения полученный указатель проверяется. И больше нигде
это не делается, считается, что указатели, передаваемые между функциями,
действительны. Это хорошо при условии, что в код не закрались ошибки, но
если непреднамеренно производится доступ по недействительному указателю,
то отладить ситуацию будет трудно. Чтобы улучшить код и подстраховаться, вы
решаете воспользоваться паттерном «Проверка указателя».

Проверка указателя
Контекст
В вашей программе есть много мест, где память выделяется и освобождается, и много мест, где производится доступ к памяти или другим ресурсам по
указателю.

Проблема
Программные ошибки, заключающиеся в доступе по недействительному указателю,
приводят к неуправляемому поведению программы, их очень трудно отлаживать. Но
поскольку работа с указателями встречается сплошь и рядом, велики шансы внести
такие ошибки.
Программирование на C требует повсеместных операций с указателями,
и чем больше в коде таких мест, тем больше шансов внести ошибки. Использование уже освобожденного или неинициализированного указателя ведет к
ошибкам, которые трудно отлаживать. Любая ситуация такого рода очень серь­
езна. Результатом становится неуправляемое поведение программы и (если
повезет) ее крах. Если же не повезет, то ошибка проявится в каком-то другом
месте, возможно, далеко отстоящем от места возникновения, и вы потратите
неделю, чтобы найти и исправить ее. Вы хотите, чтобы программа была более
надежно защищена от таких ошибок, чтобы они были менее серьезными и чтобы, если ошибка все-таки произойдет, найти ее причину было проще.

Решение
Явно делайте недействительными неинициализированные или освобожденные указатели и всегда проверяйте действительность указателя, прежде чем производить
доступ по нему.
В объявлении указательной переменной присвойте ей значение NULL. Также
сразу после вызова free явно присваивайте указателю значение NULL. Если используете паттерн «Обертка» выделения и определили макрос, обертывающий
функцию free, то можете сбросить указатель в NULL внутри макроса, чтобы не
писать код обнуления указателя после каждого освобождения памяти.

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

void someFunction()
{
char* pointer = NULL; /* явно сделать недействительным неинициализированный
указатель */
pointer = malloc(1024);
if (pointer != NULL) /* проверить указатель перед доступом по нему */
{
/* поработать с указателем */
}
free(pointer);
pointer = NULL; /* явно сделать недействительным указатель на освобожденную память */
}

Последствия
Ваш код стал немного более защищенным от ошибок, связанных с указателями. Любая такая ошибка может быть идентифицирована и не приводит к неопределенному поведению программы, которое могло бы стоить вам многих
часов или даже дней отладки.
Однако за все приходится платить. Код становится длиннее и сложнее. Примененная стратегия аналогична строительному страховочному поясу. Мы проделываем дополнительную работу ради большей безопасности. Для каждого
доступа по указателю имеется дополнительная проверка. Такой код труднее
читать. Проверка указателя перед доступом по нему требует по меньшей мере
одной лишней строки кода. Если вы не завершаете программу, а продолжаете
работу с сокращенной функциональностью, то программу становится гораздо
труднее читать, сопровождать и тестировать.
Если вы случайно вызовете free несколько раз для одного и того же указателя, то второй вызов не приведет к ошибке, потому что после первого указатель
уже недействителен, а вызов free для нулевого указателя безвреден. Тем не
менее имеет смысл запротоколировать такую ситуацию, чтобы впоследствии
отыскать первопричину ошибки.
Но даже после всего этого вы не застрахованы на 100 % от всех типов ошибок,
связанных с указателями. Например, можно забыть об освобождении памяти – и вот вам утечка. Или обратиться по указателю, который не был должным
образом инициализирован, но эту ошибку вы, по крайней мере, обнаружите
и сможете отреагировать соответственно. Потенциальный недостаток заключается в том, что если вы решите сократить функциональность программы,
но продолжить работу, то можете не узнать об ошибке, которую потом будет
труднее найти.

104

 Глава 3. Управление памятью

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В реализации умных указателей на C++ обернутый указатель делается
недействительным после освобождения умного указателя.
• Cloudy – программа для физических расчетов (спектрального синтеза).
Она содержит код интерполяции данных (фактор Гаунта). Программа
проверяет указатели перед доступом по ним и явно сбрасывает указатели в NULL после вызова free.
• Программа libcpp из коллекции компиляторов GNU (GCC) делает освобожденные указатели недействительными. Например, так делается для
указателей в файле macro.c.
• Функция HB_GARBAGE_FUNC из СУБД MySQL присваивает указателю ph
значение NULL, чтобы предотвратить случайный доступ по нему или повторное освобождение.

Применение к сквозному примеру
Теперь код принял такой вид:

/* Получив имя файла 'filename', эта функция читает текст из указанного файла
и печатает текст, зашифрованный шифром Цезаря. Эта функция отвечает за выделение
и освобождение буферов, необходимых для хранения содержимого файла. */
void encryptCaesarFile(char* filename)
{
char* text = NULL; 
int size = getFileLength(filename);
if(size>0)
{
text = safeMalloc(size);
if(text != NULL) 
{
readFileContent(filename, text, size);
caesar(text, strnlen(text, size));
printf("Зашифрованнй текст: %s\n", text);
}
safeFree(text);
text = NULL; 
}
}
 В тех местах, где указатель недействителен, мы явно сбрасываем его в NULL – для страховки.
 Перед доступом по указателю text проверяется, что он действителен. Если нет, то указатель не
используется (не разыменовывается).

Хранение данных и проблемы с динамической памятью  105
Избыточное выделение памяти в Linux
Имейте в виду, что действительный указатель на память не
всегда гарантирует, что к этой памяти можно безопасно обращаться. В современных версиях Linux применяется принцип
избыточного выделения памяти (overcommit). В результате
программе, запрашивающей память, предоставляется виртуальная память, но между ней и физической памятью нет
прямого соответствия. Доступна ли требуемая физическая
память, проверяется только в момент доступа к ней. Если
физической памяти не хватает, то ядро Linux останавливает
приложения, потребляющие слишком много памяти (среди
них может оказаться и ваше). Избыточное выделение хорошо тем, что становится не так важно проверять, успешно ли
выделена память (потому что обычно с этим все хорошо), и
вы можете выделить много памяти, чтобы подстраховаться,
даже если на самом деле нужно меньше. Но у этого подхода есть и большой недостаток — даже имея действительный
указатель, вы не можете быть уверены, что доступ к памяти
окажется успешным и программа не «грохнется». Есть и еще
один недостаток – вы начинаете лениться, не проверяете
возвращенные значения и не вычисляете, сколько памяти
вам действительно нужно.
Следующая задача – показать имя файла вместе с зашифрованным текстом. Но вы не хотите выделять память прямо из кучи, поскольку опасаетесь
фрагментации памяти в результате многократного выделения небольших
блоков (для имен файлов) и больших блоков (для содержимого файлов). Вместо непосредственного выделения динамической памяти вы реализуете «Пул
памяти».

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

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

106

 Глава 3. Управление памятью

операциями других работающих программ, так что в итоге размеры блоков
будут различаться, и возникнет фрагментация.
Функция malloc завершается успешно, только если имеется достаточно
непрерывной памяти. Это значит, что, даже когда общего объема свободной
памяти хватает, malloc все равно может вернуть ошибку, если память фрагментирована и непрерывного участка требуемого размера нет. Фрагментация означает, что память используется не очень хорошо.
Фрагментация – серьезная проблема для долго работающих систем, в том
числе встраиваемых. Если система работает годами, выделяя и освобождая
много мелких блоков, то в конце концов она не сможет выделить блок большого размера. Поэтому обязательно нужно решать проблему фрагментации,
если вы не готовы смириться с тем, что систему придется время от времени
перезагружать.
Еще одна проблема при использовании динамической памяти, особенно в
сочетании со встраиваемыми системами, – то, что выделение памяти из кучи
требует времени. Другие процессы могут использовать ту же самую кучу, поэтому операции выделения чередуются, и время выполнения одной операции
становится очень трудно предсказать.

Решение
Выделите большой кусок памяти, который будет существовать на всем протяжении
работы программы. Во время выполнения выделяйте блоки фиксированного размера из этого пула памяти, а не напрямую из кучи.
Пул памяти можно разместить в статической памяти или выделить из кучи
на этапе инициализации программы и освободить в самом конце. Выделение
из кучи хорошо тем, что в случае необходимости можно будет выделить дополнительную память и увеличить размер пула.
Напишите функции для выделения и освобождения блоков памяти заранее заданного фиксированного размера из пула. Всюду, где программе нужна память
такого размера, она может использовать эти функции (вместо malloc и free).

#define MAX_ELEMENTS 20;
#define ELEMENT_SIZE 255;
typedef struct
{
bool occupied;
char memory[ELEMENT_SIZE];
} PoolElement;
static PoolElement memory_pool[MAX_ELEMENTS];
/* Возвращает память размера не меньше 'size' или NULL, если
в пуле не осталось свободных блоков. */
void* poolTake(size_t size)
{

Хранение данных и проблемы с динамической памятью  107
if(size x,
использовать my_instance->y, ... */
 Вызывающая сторона получает ссылку, но не становится владельцем памяти.

API вызываемой стороны

struct ImmutableInstance
{
int x;
int y;
};
Реализация вызываемой стороны

static struct ImmutableInstance inst = {12, 42};
const struct ImmutableInstance* getData()
{
return &inst;
}

130

 Глава 4. Возврат данных из C-функций

Последствия
Вызывающей стороне достаточно вызвать одну простую функцию, чтобы
получить доступ даже к сложным и большим данным, и при этом ей не нужно
думать, где эти данные хранятся. Вызывающей стороне не нужно предоставлять буферы, в которые будут помещены данные, не нужно очищать память и
нет нужды думать о времени жизни данных – они просто существуют.
Вызывающая сторона может читать все данные по возвращенному указателю. Простая функция, получающая этот указатель, реентерабельна, ее безопасно использовать в многопоточном окружении. Сами данные тоже безопасно
использовать в многопоточном окружении, потому что они не изменяются во
время выполнения, и чтение данных из нескольких потоков не создает проблем.
Однако данные нельзя изменять во время выполнения без принятия дополнительных мер. Если необходимо, чтобы вызывающая сторона могла изменять
данные, то можно реализовать что-то вроде копирования при записи. Если
данные в принципе могут изменяться во время выполнения, то «Неизменяемый экземпляр» не выход, а для хранения сложных и больших данных следует
использовать один из паттернов «Буфер, принадлежащий вызывающей стороне» или «Вызываемая сторона выделяет память».

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В статье Kevlin Henney «Patterns in Java: Patterns of Value» (https://oriel.ly/
cVY9N) подробно описан похожий паттерн «Неизменяемый объект» и
приведены примеры кода на C++.
• В игре NetHack атрибуты монстров хранятся в «Неизменяемом экземп­
ляре», и для доступа к этой информации предоставляется функция.

Применение к сквозному примеру
Обычно возврат указателя на данные, хранящиеся внутри компонента, –
дело непростое. Проблема в том, что если к этим данным обращаются несколько вызывающих сторон (и, быть может, записывают их), то простой указатель
не выход, потому что неизвестно, действителен ли этот указатель до сих пор и
согласованы ли хранящиеся по нему данные. Но в данном случае все нормально, потому что экземпляр неизменяемый. Имя и описание драйвера – информация, которая задается на этапе компиляции и в дальнейшем не изменяется.
Поэтому мы можем просто запросить константный указатель на эти данные.
API драйвера Ethernet

struct EthernetDriverInfo{
char name[64];
char description[1024];
};

Сквозной пример  131
/* Возвращает имя и описание драйвера */
const struct EthernetDriverInfo* ethernetDriverGetInfo();
Код вызывающей стороны

void ethShow()
{
struct EthernetDriverStat eth_stat = ethernetDriverGetStatistics();
printf("получено пакетов: %i\n", eth_stat.received_packets);
printf("отправлено пакетов: %i\n", eth_stat.total_sent_packets);
printf("успешно отправлено пакетов: %i\n", eth_stat.successfully_sent_packets);
printf("не удалось отправить пакетов: %i\n", eth_stat.failed_sent_packets);
const struct EthernetDriverInfo* eth_info = ethernetDriverGetInfo();
printf("Имя драйвера: %s\n", eth_info->name);
printf("Описание драйвера: %s\n", eth_info->description);
}
Следующим шагом, помимо имени и описания Ethernet-интерфейса, мы хотели бы показать пользователю текущий IP-адрес и маску подсети. Они хранятся в виде строки внутри драйвера Ethernet. Оба значения могут изменяться
во время выполнения, поэтому не получится вернуть указатель на «Неизменяемый экземпляр».
Можно было бы заставить драйвер упаковать эти строки в «Агрегат» и просто вернуть его (при возврате структуры хранящиеся в ней массивы копируются), но для больших объемов данных такое решение непопулярно, потому что
потребляет много памяти в стеке. Обычно вместо этого применяются указатели. Такое решение, которое нас вполне устроило бы, описывается паттерном
«Буфер, принадлежащий вызывающей стороне».

Буфер, принадлежащий вызывающей стороне
Контекст
Имеется много данных, которые вы хотите разделить между различными
компонентами.

Проблема
Вы хотите предоставить вызывающей стороне сложные или большие данные известного размера, но эти данные не являются неизменяемыми (могут изменяться во
время выполнения).
Поскольку данные изменяются во время выполнения (быть может, потому
что вы предоставляете вызывающей стороне функции для записи данных),
нельзя просто передать вызывающей стороне указатель на статические данные, как в случае «Неизменяемого экземпляра». В этом случае данные, прочитанные одной вызывающей стороной, могли бы оказаться несогласованными
(частично перезаписанными), потому что другой поток одновременно записывает эти данные.

132

 Глава 4. Возврат данных из C-функций

Просто скопировать все данные в «Агрегат» и передать его вызывающей стороне в виде возвращенного значения – не выход, потому что большие данные
нельзя передать в ограниченном стеке.
Если вместо этого передавать указатель на «Агрегат», то проблемы с ограничением памяти в стеке не возникнет, но надо помнить, что C не занимается
глубоким копированием, а возвращает только указатель. Вы должны гарантировать, что данные (хранящиеся в «Агрегате» или в массиве), на которые ведет указатель, остаются действительными после вызова функции. Например,
нельзя хранить данные в автоматических переменных внутри функции и передавать на них указатель, потому что после возврата из функции эти переменные выйдут из области видимости.
Возникает вопрос, где же хранить данные. Следует уточнить, кто должен выделить необходимую память – вызывающая или вызываемая сторона – и кто
будет отвечать за ее освобождение.

Решение
Потребуйте, чтобы вызывающая сторона предоставила буфер и его размер функции,
возвращающей большие или сложные данные. Внутри функции скопируйте данные
в буфер, если он достаточно велик.
Обеспечьте неизменность данные во время копирования. Для этого можно
применить Мьютекс или Семафор. Тогда у вызывающей стороны будет моментальный снимок данных в буфере, причем она является единственным владельцем этого снимка и, следовательно, может обращаться к нему, не опасаясь
несогласованности, даже если исходные данные в это время изменяются.
Вызывающая сторона может предоставить буфер и его размер в отдельных
параметрах или упаковать их в «Агрегат» и передать указатель на него.
Поскольку буфер и размер предоставляет вызывающая сторона, она должна
знать размер заранее. Чтобы вызывающая сторона могла узнать размер буфера, требуемый размер должен быть частью API. Это можно реализовать, определив размер в виде макроса или определив структуру struct, содержащую буфер требуемого размера, в API.
На рис. 4.6 и в последующем коде иллюстрируется идея «Буфера, принадлежащего вызывающей стороне».
Код вызывающей стороны

struct Buffer buffer;
getData(&buffer);
/* использовать buffer.data */
API вызываемой стороны

#define BUFFER_SIZE 256
struct Buffer
{
char data[BUFFER_SIZE];

Сквозной пример  133
};
void getData(struct Buffer* buffer);
Реализация вызываемой стороны

void getData(struct Buffer* buffer)
{
memcpy(buffer->data, some_data, BUFFER_SIZE);
}
Вызывающая Вызываемая
сторона сторона
Выделить буфер
Вызвать функцию,
передав ей указатель
на буфер и размер
буфера

Буфер

Размер

Параметры
Буфер

Скопировать в буфер
(если размер
достаточен)

Требуемые
данные

Размер

Обратиться к данным,
скопированным в буфер

Рис. 4.6. Буфер, принадлежащий вызывающей стороне

Последствия
Большие и сложные данные можно согласованным образом предоставить
вызывающей стороне с помощью одного вызова функции. Функция реентерабельна и может безопасно использоваться в многопоточном окружении. Кроме
того, вызывающая сторона может безопасно обращаться к полученным данным, потому что является единственным владельцем буфера.
Вызывающая сторона предоставляет буфер ожидаемого размера и даже
может решить, в какой памяти этот буфер разместить. Она может разместить
буфер в стеке (см. паттерн «Сначала стек») и воспользоваться тем, что стек автоматически очищается после выхода переменной из области видимости. Или
же она может выделить буфер в куче, чтобы самостоятельно определять время
жизни переменной или не расходовать память в стеке. Кроме того, вызывающая функция могла бы иметь только ссылку на буфер, полученную от функции,
которая ее вызвала. В таком случае этот буфер можно просто передать дальше
и не выделять несколько буферов.

134

 Глава 4. Возврат данных из C-функций

Занимающая много времени операция выделения и освобождения памяти
не выполняется в момент вызова функции. Вызывающая сторона сама решает, когда выполнять эти операции, поэтому вызов функции оказывается более
быстрым и детерминированным.
Из API абсолютно ясно, что буфером владеет вызывающая сторона. Именно она должна предоставить буфер, а затем очистить его. Если вызывающая
сторона выделяла буфер, то она и отвечает за его последующее освобождение.
Вызывающая сторона должна заранее знать размер буфера, а поскольку этот
размер известен, то функция может безопасно работать с буфером. Но в некоторых случаях точный размер может быть неизвестен вызывающей стороне,
и тогда лучше воспользоваться паттерном «Вызываемая сторона выделяет память».

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В игре NetHack этот паттерн используется для предоставления информации о сохраняемой игре компоненту, который затем фактически записывает текущее состояние игры на диск.
• В операционной системе B&R Automation Runtime этот паттерн используется в функции для получения IP-адреса.
• Функция fgets из стандартной библиотеки C читает данные из потока и
сохраняет их в предоставленном вызывающей стороной буфере.

Применение к сквозному примеру
Теперь вы предоставляете буфер, принадлежащий вызывающей стороне,
функции драйвера Ethernet, а функция копирует в этот буфер свои данные.
Вы должны заранее знать размер буфера. Если требуется получить строку
IP-адреса, то это не проблема, потому что размер такой строк фиксирован.
Поэтому можно просто выделить буфер для IP-адреса в стеке и передать соответствующую автоматическую переменную драйверу. Альтернативно можно
было бы выделить буфер в куче, но в данном случае этого не требуется, потому что размер IP-адреса известен и настолько мал, что спокойно поместится
в стеке.
API драйвера Ethernet

struct IpAddress{
char address[16];
char subnet[16];
};
/* Сохраняет IP-адрес и маску подсети в параметре 'ip',
который должен быть предоставлен вызывающей стороной */
void ethernetDriverGetIp(struct IpAddress* ip);

Сквозной пример  135
Код вызывающей стороны

void ethShow()
{
struct EthernetDriverStat eth_stat = ethernetDriverGetStatistics();
printf("получено пакетов: %i\n", eth_stat.received_packets);
printf("отправлено пакетов: %i\n", eth_stat.total_sent_packets);
printf("успешно отправлено пакетов: %i\n", eth_stat.successfully_sent_packets);
printf("не удалось отправить пакетов: %i\n", eth_stat.failed_sent_packets);
const struct EthernetDriverInfo* eth_info = ethernetDriverGetInfo();
printf("Имя драйвера: %s\n", eth_info->name);
printf("Описание драйвера: %s\n", eth_info->description);
struct IpAddress ip;
ethernetDriverGetIp(&ip);
printf("IP-адрес: %s\n", ip.address);
}
Далее вы хотите развить диагностический компонент, так чтобы он еще печатал дамп последнего полученного пакета. Этот блок информации слишком
велик для размещения в стеке, а поскольку размер Ethernet-пакетов переменный, мы не можем заранее знать, сколько места выделить для буфера. Поэтому
«Буфер, принадлежащий вызывающей стороне» не годится.
Конечно, можно было бы просто завести функции EthernetDriverGetPacket­
Size() и EthernetDriverGetPacket(buffer), но тут возникает уже знакомая проблема – нужно вызывать две функции, а между этими вызовами драйвер мог
бы получить новый пакет, и данные стали бы несогласованными. К тому же это
решение не элегантно, потому что нужно вызывать две функции для достижения одной цели. Гораздо проще воспользоваться паттерном «Вызываемая
сторона выделяет память».

Вызываемая сторона выделяет память
Контекст
Имеется много данных, которые вы хотите разделить между различными
компонентами.

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

136

 Глава 4. Возврат данных из C-функций

танные одной вызывающей стороной, могли бы оказаться несогласованными
(частично перезаписанными), потому что другой поток одновременно записывает эти данные.
Просто скопировать все данные в «Агрегат» и передать его вызывающей стороне в виде возвращенного значения – не выход. С помощью возвращенного
значения можно передать только данные известного размера, а поскольку данные велики, их нельзя передать в ограниченном стеке.
Если вместо этого передавать указатель на «Агрегат», то проблемы с ограничением памяти в стеке не возникнет, но надо помнить, что C не занимается
глубоким копированием, а возвращает только указатель. Вы должны гарантировать, что данные (хранящиеся в «Агрегате» или в массиве), на которые ведет указатель, остаются действительными после вызова функции. Например,
нельзя хранить данные в автоматических переменных внутри функции и передавать на них указатель, потому что после возврата из функции эти переменные выйдут из области видимости и будут стерты.
Возникает вопрос, где же хранить данные. Следует уточнить, кто должен выделить необходимую память – вызывающая или вызываемая сторона – и кто
будет отвечать за ее освобождение.
Объем возвращаемых данных не фиксирован на этапе компиляции. Например, может понадобиться вернуть строку заранее неизвестного размера. Поэтому паттерн «Буфер, принадлежащий вызывающий стороне» не подходит,
так как вызывающая сторона не знает размер буфера заранее. Вызывающая
сторона могла бы предварительно спросить, какой размер буфера необходим
(например, с помощью функции getRequiredBufferSize()), но это тоже непрактично, потому что для получения одного элемента данных пришлось бы делать
несколько вызовов. Кроме того, данные, которые вы собираетесь предоставить, могут измениться между вызовами функций, а тогда вызывающая сторона предоставит буфер неправильного размера.

Решение
Выделите буфер нужного размера внутри функции, предоставляющей большие и
сложные данные. Скопируйте данные в буфер и верните указатель на него.
Передайте указатели на буфер и его размер в выходных параметрах. После
возврата из функции вызывающая сторона сможет работать с буфером, зная
его размер, и будет его единственным владельцем. Вызывающая сторона определяет время жизни буфера и несет ответственность за его освобождение, как
показано на рис. 4.7 и в последующем коде.
Код вызывающей стороны

char* buffer;
int size;
getData(&buffer, &size);
/* использовать буфер */
free(buffer);

Сквозной пример  137
Код вызываемой стороны

void getData(char** buffer, int* size)
{
*size = data_size;
*buffer = malloc(data_size);
/* записать данные в буфер */ 
}
 Обеспечьте неизменность данных во время копирования в буфер. Это можно сделать, воспользовавшись Мьютексом или Семафором.

Вызывающая Вызываемая
сторона сторона
Вызвать функцию,
предоставив
указатели

Буфер

Буфер

Размер

Обратиться к данным
по предоставленным
указателям

Установить
указатель на буфер
и указатель
на размер

Размер

Выделить буфер
требуемого размера

Параметры

Требуемые
данные
Скопировать данные
в буфер

Рис. 4.7. Вызываемая сторона выделяет память
Альтернативно указатели на буфер и его размер можно поместить в «Агрегат» и вернуть его. Чтобы вызывающей стороне было понятно, что в агрегате
хранится указатель, можно включить в API дополнительную функцию для его
освобождения. Когда предоставляется такая функция, API становится очень
похожим на «Описатель», что повышает гибкость, сохраняя при этом совместимость.
Неважно, как именно вызванная функция предоставляет буфер – в агрегате
или в выходных параметрах, – вызывающая сторона должна ясно понимать,
что именно она владеет буфером и отвечает за его освобождение. Владение
должно быть документировано в API.

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

138

 Глава 4. Возврат данных из C-функций

Вызывающая сторона владеет буфером, определяет время его жизни и отвечает за его освобождение (как в случае «Описателя»). Одного взгляда на интерфейс должно быть достаточно, чтобы понять, что это задача именно вызывающей стороны. Один из способов добиться такого понимания – документировать
порядок использования в API. Другой способ – предоставить явную функцию
очистки, чтобы было очевидно, что имеется нечто, нуждающееся в очистке.
У такой функции есть дополнительное преимущество – компонент, выделивший память, и освобождает ее. Это важно, когда два компонента компилируются разными компиляторами или работают на разных платформах – в таком
случае функции освобождения и выделения памяти в разных компонентах
могут различаться, и тогда просто необходимо, чтобы выделял и освобождал
память один и тот же компонент.
Вызывающая сторона не может определить, какого вида память следует
использовать для буфера (это было бы возможно при использовании буфера, принадлежащего вызывающей стороне). Теперь вызывающая сторона
вынуждена пользоваться той памятью, которая выделена вызванной функцией.
Выделение памяти требует времени, поэтому, по сравнению с буфером, принадлежащим вызывающей стороне, функция становится более медленной и
менее детерминированной.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Именно так ведет себя функция malloc. Она выделяет память и предоставляет ее вызывающей стороне.
• Функция strdup принимает на входе строку выделяет память для ее
копии и возвращает копию.
• В Linux функция getifaddrs предоставляет информацию о сконфигурированных IP-адресах. Данные помещаются в буфер, выделенный
этой функцией.
• В игре NetHack этот паттерн применяется для получения буферов.

Применение к сквозному примеру
Финальный код нашего диагностического компонента копирует данные пакета в буфер, выделенный вызываемой стороной.
API драйвера Ethernet

structPacket
{
char data[1500]; /* не более 1500 байтов на пакет */
int size;
/* фактический размер данных в пакета */
};

Резюме  139
/* Возвращается указатель на пакет. Эта память должна быть освобождена вызывающей
стороной. */
struct Packet* ethernetDriverGetPacket();
Код вызывающей стороны

void ethShow()
{
struct EthernetDriverStat eth_stat = ethernetDriverGetStatistics();
printf("получено пакетов: %i\n", eth_stat.received_packets);
printf("отправлено пакетов: %i\n", eth_stat.total_sent_packets);
printf("успешно отправлено пакетов: %i\n", eth_stat.successfully_sent_packets);
printf("не удалось отправить пакетов: %i\n", eth_stat.failed_sent_packets);
const struct EthernetDriverInfo* eth_info = ethernetDriverGetInfo();
printf("Имя драйвера: %s\n", eth_info->name);
printf("Описание драйвера: %s\n", eth_info->description);
struct IpAddress ip;
ethernetDriverGetIp(&ip);
printf("IP-адрес: %s\n", ip.address);
struct Packet* packet = ethernetDriverGetPacket();
printf("Дамп пакета:");
fwrite(packet->data, 1, packet->size, stdout);
free(packet);
}
В этой окончательной версии диагностического компонента мы видим все
описанные способы получить информацию от другой функции. Смешение их
всех в одном куске кода – не лучшая идея, потому что немного странно и сбивает с толку, когда один блок данных находится в стеке, а другой – в куче. Не стоит
смешивать разные подходы к выделению буферов в одной функции. Лучше
выберите какой-нибудь один подход, отвечающий всем вашим потребностям,
и придерживайтесь его в рамках одной функции или компонента. Тогда ваш
код будет единообразным, и понять его станет проще.
Однако если вам необходимо получить один блок данных от другого компонента и у вас есть выбор (из рассмотренных в этой главе паттернов), то всегда
выбирайте самый простой вариант, чтобы сделать код проще. Например, если
можно разместить буферы в стеке, так и поступайте, это позволит не освобождать буфер впоследствии.

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

140

 Глава 4. Возврат данных из C-функций

следует выбрать паттерн «Выходные параметры» или, еще лучше, «Агрегат».
Если подлежащие возврату данные не изменяются во время выполнения, то
можно воспользоваться паттерном «Неизменяемый экземпляр». Ели данные
возвращаются в буфере, то можно использовать паттерн «Буфер, принадлежащий вызывающей стороне», если размер буфера известен заранее, или паттерн
«Вызываемая сторона выделяет память», если размер неизвестен.
Овладев паттернами из этой главы, программист на C будет иметь в своем
арсенале средства и рекомендации по передаче данных между функциями и
будет знать, какие существуют варианты возврата данных, выделения и освобождения буферов.

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

Глава

5
Время жизни
и владение данными

В процедурных языках программирования, к числу которых относится и C, нет
встроенных объектно ориентированных механизмов. Это несколько усложняет жизнь, потому что рекомендации по проектированию рассчитаны прежде
всего на объектно ориентированные программы (как, например, паттерны из
книги «банды четырех»).
В этой главе обсуждаются паттерны, описывающие, как структурировать
C-программу, включив в нее похожие на объекты элементы. Для таких объектоподобных элементов особое внимание уделяется тому, кто отвечает за их
создание и уничтожение, – иными словами, вопросам времени жизни и владения. Эта тема особенно важна для языка C, потому что в нем нет автоматических деструкторов и механизма сборки мусора, поэтому нужно уделять особое
внимание очистке ресурсов.
Но что такое «объектоподобный элемент» и в чем его значение для C? Термин
объект корректно определен в объектно ориентированных языках программирования, но для других непонятно, что под ним понимается. Для C можно
дать такое простое определение объекта:
Объект – это именованная область памяти.
(Керниган и Ритчи)

Обычно такой объект описывает набор взаимосвязанных данных, имеющий идентификатор и свойства и используемый для хранения представлений вещей, встречающихся в реальном мире. В объектно ориентированном
программировании объект дополнительно обладает способностью к полиморфизму и наследованию. Объектоподобные элементы, рассматриваемые
в этой книге, такими способностями не обладают, поэтому в дальнейшем мы
не будем употреблять слово объект. А будем рассматривать объектоподобный элемент просто как экземпляр структуры данных и называть его экземп­
ляром.
Такие экземпляры существуют не сами по себе, а комплектуются кодом, который дает возможность производить над ними операции. Обычно этот код

142

 Глава 5. Время жизни и владение данными

находится частично в заголовочных файлах, где определяется интерфейс, и
частично в файлах реализации. В этой главе совокупность этого взаимосвязанного кода, аналогичного объектно ориентированному классу в том смысле,
что он определяет, какие операции можно производить над экземпляром, мы
будем называть программным модулем.
При программировании на C описанные выше экземпляры данных обычно реализуются как абстрактные типы данных (например, создают структуру
struct и функции, обращающиеся к членам этой структуры). Примером такого
экземпляра является структура FILE из стандартной библиотеки C, предназначенная для хранения такой информации, как указатель на файл и текущее положение в файле. Соответствующим стандартным модулем будет API в файле
stdio.h и реализации функций типа fopen и fclose, которые предоставляют доступ к экземплярам FILE.
На рис. 5.1 приведен обзор паттернов, рассматриваемых в этой главе и связей между ними, а в табл. 5.1 – краткое описание этих паттернов.

Возвращаемое
значение

Экземпляр,
принадлежащий вызы­
вающей стороне

Программный
модуль без состояния

Каталоги
программных
модулей

Агрегат

Вечная память

Заголовочные
файлы

Программный
модуль с глобальным
состоянием

Единоличное
владение

Разделяемый
экземпляр

Пояснения
Паттерн, представленный в этой главе

Описатель

Паттерн, представленный в другой
главе
B можно использовать для реализации
и дополнения A

Рис. 5.1. Обзор паттернов, относящихся к времени жизни данных
и владению ими
Таблица 5.1. Паттерны, относящиеся к времени жизни данных и владению ими
Название паттерна

Краткое описание

Программный
модуль без
состояния

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

Сквозной пример  143
Название паттерна

Краткое описание

Программный
модуль с
глобальным
состоянием

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

Экземпляр,
принадлежащий
вызывающей
стороне

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

Разделяемый
экземпляр

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

Сквозной пример
В качестве сквозного примера мы в этой главе реализуем драйвер устройства
для сетевой карты Ethernet. Сетевая карта устанавливается на машину с операционной системой, предоставляющей описанные в стандарте POSIX функции сокетов для отправки и получения данных по сети. Мы хотим построить
некую абстракцию пользователя, потому что нам нужен более простой способ
отправлять и получать данные, чем предлагают сокеты, и потому что мы хотим
включить дополнительные средства в свой драйвер. Таким образом, мы хотим
реализовать нечто, инкапсулирующее детали сокетов. Для этого начнем с простого паттерна «Программный модуль без состояния».

144

 Глава 5. Время жизни и владение данными

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

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

Решение
Делайте функции простыми и не храните в реализации информацию о состоянии.
Поместите все взаимосвязанные функции в один заголовочный файл и предоставьте
вызывающей стороне этот интерфейс к своему программному модулю.
Между функциями не должно быть ни обмена информацией, ни разделения
информации о внутреннем или внешнем состоянии. Это значит, что функции
вычисляют результат или выполняют действие, которое не зависит от вызовов
других функций из API (заголовочного файла) или предыдущих вызовов функции. Единственное взаимодействие имеет место между вызывающей и вызываемой функцией (например, в форме возвращаемых значений).
Если функция требует каких-либо ресурсов, например памяти из кучи, то
их следует обрабатывать прозрачно для вызывающей стороны. Их необходимо захватить, неявно инициализировать перед использованием и освободить
внутри вызова функции. Это позволяет вызывать функции абсолютно независимо друг от друга.
Тем не менее функции остаются взаимосвязанными и потому помещаются
в один API. Взаимосвязанность означает, что обычно функции применяются
вызывающей стороной совместно (принцип разделения интерфейсов), и если
они изменяются, то это происходит по одной и той же причине (принцип общего замыкания). Эти принципы изложены в книге Robert C. Martin «Clean Architecture» (Prentice Hall, 2018)1.
1

Мартин Роберт. Чистая архитектура. Питер 2022.

Сквозной пример  145
Поместите объявления взаимосвязанных функций в один заголовочный
файл, а реализации – в один или несколько файлов реализации, находящихся
в одном и том же каталоге программного модуля. Функции взаимосвязаны,
потому что логически нераздельны, но у них нет общего состояния, и они не
влияют на состояние друг друга, поэтому нет необходимости разделять информацию между функциями с помощью глобальных переменных или инкапсулировать эту информацию, передавая экземпляры. Поэтому реализацию каждой
функции можно поместить в отдельный файл.
Ниже приведен пример простого программного модуля без состояния.
Код вызывающей стороны

int result = sum(10, 20);
API (заголовочный файл)

/* Возвращает сумму обоих параметров */
int sum(int summand1, int summand2);
Реализация

int sum(int summand1, int summand2)
{
/* вычислить результат, опираясь только на параметры и
не требуя никакой информации о состоянии */
return summand1 + summand2;
}
Вызывающая сторона вызывает sum и получает копию результата функции.
Если вызвать эту функцию дважды с одними и теми же параметрами, то она
вернет одинаковые результаты, потому что в программном модуле не запоминается никакой информации о состоянии. И никакой другой функции, которая
хранила бы информацию о состоянии, в этом примере тоже не вызывается.
На рис. 5.2 иллюстрируется паттерн «Программный модуль без состояния».
Вызывающая
сторона

Между вызовами
функции не запомина­
ется никакой информа­
ции о состоянии

Рис. 5.2. Программный модуль без состояния

146

 Глава 5. Время жизни и владение данными

Последствия
Интерфейс очень простой, и вызывающей стороне не нужно думать об инициализации или очистке чего-либо в вашем программном модуле. Она просто
вызывает функцию независимо от предшествующих вызовов и других частей
программы, например других потоков, конкурентно обращающихся к модулю.
Благодаря отсутствию информации о состоянии понять, что делает функция,
гораздо проще.
Вызывающей стороне не нужно думать о владении, потому что и владеть-то
нечем – у функции нет никакого состояния. Ресурсы, необходимые функции,
захватываются и освобождаются внутри нее, для вызывающей стороны это
прозрачно.
Но не любую функциональность можно предоставить с помощью такого
простого интерфейса. Если функции, принадлежащие API, разделяют ка­куюлибо информацию о состоянии или данные (например, одна выделяет ресурсы, необходимые другой), то нужен другой подход, скажем «Программный
модуль с глобальным состоянием» или «Экземпляр, принадлежащий вызывающей стороне».

Известные примеры применения
Такой тип взаимосвязанных функций, собранных в один API, встречается
всякий раз, как функция, принадлежащая API, не требует разделяемой информации или информации о состоянии. Следующие примеры демонстрируют
применение этого паттерна.
• Функции sin и cos объявлены в одном и том же заголовочном файле
math.h, и вычисленный результат зависит только от входных данных.
Они не хранят никакой информации о состоянии, и вызов с одним и
тем же входом порождает один и тот же выход.
• Функции strcpy и strcat, объявленные в файле string.h, не зависят друг
от друга. Они не разделяют никакой информации, но взаимосвязаны и
потому являются частью одного API.
• Заголовочный файл VersionHelpers.h в Windows предоставляет информацию о работающей версии Microsoft Windows. Функции типа IsWindows7OrGreater и IsWindowsServer, очевидно, взаимосвязаны, но не разделяют никакой информации и не зависят друг от друга.
• В заголовочном файле Linux parser.h объявлены функции match_int и
match_hex. Они пытаются разобрать подстроку как целое или шестнадцатеричное значение соответственно. Функции не зависят друг от друга, но тем не менее принадлежат одному и тому же API.
• В исходном коде игры NetHack тоже встречается много примеров этого
паттерна. Например, в заголовочный файл vision.h включены функции,
вычисляющие, видит ли игрок определенные предметы на карте игры.
Функции couldsee(x,y) и cansee(x,y) вычисляют, находится ли предмет
на линии прямой видимости игрока и смотрит ли игрок на этот предмет.
Функции не зависят друг от друга и не разделяют никакой информации.

Сквозной пример  147
• Паттерн «Заголовочные файлы» предлагает вариант этого паттерна с
большим упором на гибкость API.
• Паттерн «Экземпляр, связанный с запросом» из книги Markus Voelter
et al. «Remoting Patterns» (Wiley, 2007) объясняет, что сервер в распределенном объекте промежуточного слоя ПО должен активировать нового
работника при каждом обращении и что, после того как работник обработает запрос, нужно вернуть результат и деактивировать работника.
При таких обращениях к серверу не запоминается никакой информации о состоянии, и в этом отношении паттерн напоминает «Программный модуль без состояния» с тем отличием, что последний не имеет
дела с удаленными объектами.

Применение к сквозному примеру
Код вашего первого драйвера устройства выглядит так:
API (заголовочный файл)

void sendByte(char data, char* destination_ip);
char receiveByte();
Реализация

void sendByte(char data, char* destination_ip)
{
/* открыть сокет с ip адресата, отправить данные через этот сокет
и закрыть его */
}
char receiveByte()
{
/* открыть сокет для получения данных, подождать некоторое время
и вернуть полученные данные */
}
Пользователь вашего драйвера Ethernet не должен возиться с деталями реализации, например как обращаться к сокетам, а может просто использовать
предоставленный API. Обе входящие в состав этого API функции можно вызывать в любой момент независимо друг от друга, и вызывающая сторона может
получить от функций данные, не думая о владении и освобождении ресурсов.
API действительно простой, но при этом крайне ограниченный.
Далее вы собираетесь расширить функциональность своего драйвера. Вы
хотите, чтобы пользователь мог видеть, работает ли связь через Ethernet, а
значит, хотите предоставить статистику отправленных и принятых байтов.
Простой «Программный модуль без состояния» не позволит это сделать, потому что не выделена память для хранения информации о состоянии между
вызовами функций. Чтобы решить задачу, потребуется «Программный модуль
с глобальным состоянием».

148

 Глава 5. Время жизни и владение данными

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

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

Решение
Заведите один глобальный экземпляр, чтобы реализации взаимосвязанных функций
могли разделять общие ресурсы. Поместите все функции, работающие с этим экземпляром, в один заголовочный файл и предоставьте вызывающей стороне этот интерфейс к своему программному модулю.
Поместите объявление функций своего программного модуля в заголовочный файл, а реализации – в один файл реализации в каталоге программного
модуля. В этом файле реализации заведите глобальный экземпляр (статическую на уровне файла структуру struct или несколько статических переменных, см. паттерн «Вечная память»). Тогда реализации функций смогут обращаться к этим разделяемым ресурсам; это похоже на механизм работы с
закрытыми переменными-членами в объектно ориентированных языках программирования.
Инициализация ресурсов прозрачно производится программным модулем,
он же управляет временем их жизни, так что оно и не зависит от времени жизни вызывающих сторон. Если ресурсы вообще нуждаются в инициализации, то
это можно сделать как в момент запуска, так и отложенно – непосредственно
перед первым использованием.
Из сигнатуры вызова вызывающая сторона не видит, что функции работают
с общими ресурсами, так что это нужно явно документировать. Внутри программного модуля доступ к глобальным на уровне файла ресурсам, возможно,

Сквозной пример  149
необходимо защищать примитивами синхронизации, например Мьютексом,
чтобы к модулю можно было обращаться из разных потоков. Делайте синхронизацию частью реализации своих функций, чтобы вызывающей стороне не
нужно было думать об этом.
Ниже приведен пример простого «Программного модуля с глобальным состоянием».
Код вызывающей стороны

int result;
result = addNext(10);
result = addNext(20);
API (заголовочный файл)

/* Прибавляет параметр 'value' к сумме, накопленной
при выполнении предыдущих вызовов этой функции . */
int addNext(int value);
Реализация

static int sum = 0;
int addNext(int value)
{
/* вычисление результата, зависящего от параметра и
состояния, образовавшегося после предыдущих вызовов функции */
sum = sum + value;
return sum;
}
Вызывающая сторона вызывает addNext и получает копию результата. При
вызове функции дважды с одними и тем же параметрами результаты могут
различаться, потому что функция хранит информацию о состоянии.
На рис. 5.3 схематически изображена работа «Программного модуля с глобальным состоянием».
Вызывающая
сторона

Программный
модуль с глобальным
состоянием

Информация
о состоянии, сохраня­
емая между вызовами
функции

Рис. 5.3. Программный модуль с глобальным состоянием

150

 Глава 5. Время жизни и владение данными

Последствия
Теперь ваши функции могут разделять информацию или ресурсы, хотя от
вызывающей стороны не требуется передавать параметры, содержащие эту
информацию, и она не несет ответственности за выделение и освобождение
ресурсов. Чтобы добиться такого разделения информации в программном модуле, вы реализовали C-версию паттерна «Одиночка» (Singleton). Но берегитесь этого паттерна – к нему есть много претензий, и иногда его даже называют антипаттерном.
Тем не менее в C такие программные модули с глобальным состоянием широко распространены, поскольку написать ключевое слово static перед переменной очень легко, а стоит это сделать, как вы тут же получаете «Одиночку». В некоторых случаях это нормально. Если файлы реализации короткие,
то переменные, глобальные на уровне файла, аналогичны закрытым переменным-членам в объектно ориентированном программировании. Если функция
не требует информации о состоянии или не работает в многопоточном окружении, то все, возможно, и обойдется. Но если многопоточность или информация о состоянии налицо, а ваш файл реализации становится все длиннее и
длиннее, то вы «попали», и «Программный модуль с глобальным состоянием»
перестает быть хорошим решением.
Если «Программный модуль с глобальным состоянием» нуждается в инициализации, то это можно сделать на этапе общей инициализации, например
при запуске программы, или отложить инициализацию до момента первого
использования ресурсов. Однако у отложенного подхода есть недостаток – продолжительность вызовов оказывается переменной, потому что при первом
вызове исполняется дополнительный код инициализации. В любом случае захват ресурса прозрачен для вызывающей стороны. Ресурсы принадлежат вашему программному модулю, поэтому вызывающая сторона не обременена
владением и не должна явно захватывать и освобождать ресурсы.
Однако не любую функциональность можно предоставить с помощью такого простого интерфейса. Если входящие в состав API функции разделяют информацию, зависящую от вызывающей стороны, то необходим другой подход,
например паттерн «Экземпляр, принадлежащий вызывающей стороне».

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Функция strtok, объявленная в файле string.h, разбивает строку на лексемы. При каждом вызове функции возвращается следующая лексема.
Для хранения информации о состоянии, необходимой для решения о
том, какую лексему возвращать следующей, функция пользуется статическими переменными.
• Доверенный платформенный модуль (Trusted Platform Module – TPM)
позволяет хранить хеш-значения загруженных программ. Соответствующая функция в коде TPM-Emulator v0.7 пользуется статическими переменными для хранения хеш-значений.

Сквозной пример  151
• Библиотека math использует состояние для генерирования случайных
чисел. При каждом вызове функции rand вычисляется новое псевдо­
случайное число, зависящее от предыдущего вызова rand. Предварительно нужно вызвать функцию srand, чтобы задать начальное значение (статическое) для генератора псевдослучайных чисел, вызывае­мого rand.
• Паттерн «Неизменяемый экземпляр» можно рассматривать как частный случай «Программного модуля с глобальным состоянием», в котором экземпляр не изменяется во время выполнения.
• В игре NetHack информация о предметах (мечах, щитах) хранится в статическом списке, определенном на этапе компиляции, и предоставляются функции для доступа к этой разделяемой информации.
• Паттерн «Статический экземпляр» из книги Markus Voelter et al.
«Remoting Patterns» (Wiley, 2007) рекомендует предоставлять удаленные
объекты, время жизни которых не зависит от времени жизни вызывающей стороны. Удаленные объекты можно, например, инициализировать на этапе запуска программы, а затем предоставлять вызывающей
стороне по запросу. Программный модуль с глобальным состоянием
предлагает ту же идею заведения статических данных, но не предполагает наличия нескольких экземпляров для разных вызывающих сторон.

Применение к сквозному примеру
Теперь код нашего драйвера Ethernet принимает описанный ниже вид.
API (заголовочный файл)

void sendByte(char data, char* destination_ip);
char receiveByte();
int getNumberOfSentBytes();
int getNumberOfReceivedBytes();
Реализация

static int number_of_sent_bytes = 0;
static int number_of_received_bytes = 0;
void sendByte(char data, char* destination_ip)
{
number_of_sent_bytes++;
/* работа с сокетами */
}
char receiveByte()
{
number_of_received_bytes++;
/* работа с сокетами */
}
int getNumberOfSentBytes()

152

 Глава 5. Время жизни и владение данными

{
return number_of_sent_bytes;
}
int getNumberOfReceivedBytes()
{
return number_of_received_bytes;
}
API очень похож на API «Программного модуля без состояния», но теперь
за ним кроется возможность сохранять между вызовами функций информацию, необходимую для подсчета отправленных и полученных байтов. Если у
этого API есть только один пользователь (один поток), то все хорошо. Но когда
потоков несколько, использование статических переменных с неизбежностью
ведет к возникновению состояний гонки, если только не реализован какой-то
механизм взаимного исключения при доступе к статическим переменным.
Что ж, теперь вы хотите сделать драйвер Ethernet более эффективным и отправлять больше данных. Для этого можно было бы часто вызывать функцию
sendByte, но сейчас реализация устроена так, что при каждом вызове sendByte
устанавливается соединение через сокет, отправляются данные и соединение
закрывается. На установление и закрытие соединения уходит большая часть
времени.
Это очень неэффективно, и вы предпочли бы открыть соединение через сокет один раз, затем отправить все данные, вызывая sendByte несколько раз, и
в конце закрыть соединение. Однако теперь sendByte нуждается в этапе подготовки и этапе очистки. Это состояние нельзя хранить в «Программном модуле с
глобальным состоянием», потому что, как только вызывающих сторон становится несколько (т. е. больше одного потока), возникает проблема одновременной
отправки данных из нескольких потоков, быть может, даже разным адресатам.
Чтобы решить проблему, нужно предоставить каждой вызывающей стороне
свой «Экземпляр, принадлежащий вызывающей стороне».

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

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

Сквозной пример  153
Быть может, одна функция должна быть вызвана раньше другой, потому
что она влияет на хранимое в программном модуле состояние, необходимое
второй функции. Решить задачу можно с помощью паттерна «Программный
модуль с глобальным состоянием», но только в том случае, когда имеется всего
одна вызывающая сторона. В многопоточном окружении не существует одного
центрального программного модуля, в котором хранилась бы вся зависящая от
вызывающей стороны информация о состоянии.
И тем не менее вы хотите скрыть детали реализации от вызывающей стороны, сделав доступ к своей функциональности максимально простым. Следует
четко определить, отвечает ли вызывающая сторона за выделение и освобождение ресурсов.

Решение
Потребуйте, чтобы вызывающая сторона передавала всем вашим функциям экземпляр, в котором хранятся ресурсы и информация о состоянии. Предоставьте явные
функции для создания и уничтожения таких экземпляров, чтобы вызывающая сторона сама могла определять время их жизни.
Чтобы реализовать такой экземпляр, к которому можно было бы обращаться
из нескольких функций, передавайте указатель на struct всем функциям, которые нуждаются в разделении ресурсов или информации о состоянии. Функции
могут использовать члены struct, аналогичные закрытым переменным-членам в объектно ориентированных языках, для сохранения и чтения ресурсов
или информации о состоянии.
Структуру struct можно объявить в API, чтобы вызывающей стороне было
удобно обращаться к ее членам непосредственно. Или же саму struct можно
объявить в файле реализации, а в API объявить только указатель на нее (как
рекомендует паттерн «Описатель»). Тогда вызывающая сторона не будет знать
членов struct (они аналогичны закрытым переменным-членам) и сможет работать со struct только посредством функций.
Поскольку к экземпляру могут обращаться несколько функций, и вы не знаете, когда вызывающая сторона закончит их вызывать, время жизни экземпляра должно определяться вызывающей стороной. Поэтому документируйте,
что экземпляром владеет вызывающая сторона, и предоставьте явные функции для создания и уничтожения экземпляра. Между вызывающей стороной и
экземпляром существует связь типа агрегирования.

Агрегирование и ассоциация
Если один экземпляр семантически связан с другим, то эти
экземпляры ассоциированы. Более сильной разновидностью
ассоциации является агрегирование, когда один экземпляр
владеет другим.

154

 Глава 5. Время жизни и владение данными

Ниже приведен пример «Экземпляра, принадлежащего вызывающей стороне».
Код вызывающей стороны

struct INSTANCE* inst;
inst = createInstance();
operateOnInstance(inst);
/* доступ к inst->x или inst->y */
destroyInstance(inst);
API (заголовочный файл)

struct INSTANCE
{
int x;
int y;
};
/* Создает экземпляр, необходимый для работы с функцией 'operateOnInstance' */
struct INSTANCE* createInstance();
/* Работает с данными, хранящимися в экземпляре */
void operateOnInstance(struct INSTANCE* inst);
/* Очищает экземпляр, созданный функцией 'createInstance' */
void destroyInstance(struct INSTANCE* inst);
Реализация

struct INSTANCE* createInstance()
{
struct INSTANCE* inst;
inst = malloc(sizeof(struct INSTANCE));
return inst;
}
void operateOnInstance(struct INSTANCE* inst)
{
/* работа с inst->x и inst->y */
}
void destroyInstance(struct INSTANCE* inst)
{
free(inst);
}
Функция operateOnInstance работает с ресурсами, созданными ранее функцией createInstance. Ресурс или информация о состоянии передается между
функциями вызывающей стороной, которая должна передавать параметр

Сквозной пример  155
INSTANCE при каждом вызове функции, а затем освободить ресурсы, вызвав
destroyInstance.
На рис. 5.4 схематически изображена работа «Экземпляра, принадлежащего
вызывающей стороне».
Вызывающая
сторона

Экземпляр,
принадлежащий вы­
зывающей стороне

Рис. 5.4. Экземпляр, принадлежащий вызывающей стороне

Последствия
Функции, являющиеся частью вашего API, стали мощнее, потому что теперь
могут разделять информацию о состоянии и работать с разделяемыми данными, оставаясь доступными нескольким вызывающим сторонам (т. е. нескольким потокам). У каждого экземпляра, принадлежащего вызывающей стороне,
имеются собственные закрытые переменные, и, даже если создается несколько
таких экземпляров (например, в нескольких потоках), проблемы не возникает.
Но для достижения такого результата пришлось усложнить API. Понадобилось ввести явные функции create() и destroy() для управления временем
жизни экземпляра, потому что в C нет конструкторов и деструкторов. Это
заметно затрудняет работу с экземплярами, потому что вызывающая сторона получает их во владение и отвечает за очистку. Поскольку это необходимо
делать вручную путем вызова destroy(), а не автоматически с помощью деструктора, как в объектно ориентированных языках программирования, мы
получаем распространенный источник утечек памяти. Эта проблема решается
паттерном «Объектная обработка ошибок», который рекомендует заводить на
вызывающей стороне специальную функцию очистки, чтобы сделать эту задачу более явной.
Кроме того, по сравнению с «Программным модулем без состояния», вызов
каждой функции становится чуть более сложным. Каждая функция принимает дополнительный параметр – ссылку на экземпляр, и функции уже нельзя
вызывать в произвольном порядке – вызывающая сторона должна знать, какую функцию следует вызывать первой. Это проясняется с помощью сигнатур
функций.

156

 Глава 5. Время жизни и владение данными

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• Примером использования «Экземпляра, принадлежащего вызывающей стороне», является дважды связанный список в библиотеке glibc.
Вызывающая сторона создает список, вызывая функцию g_list_alloc,
после чего может вставлять в него элементы с помощью функции
g_list_insert. Закончив работу со списком, вызывающая сторона должна очистить его, вызвав g_list_free.
• Этот паттерн упоминается в статье Robert Strandh «Modular C» (https://
oreil.ly/UVodl), где описано, как писать модульные программы на C. В статье подчеркивается важность выявления абстрактных типов данных,
операции над которыми производятся с помощью функций, в приложении.
• Windows API для создания отдельных меню в полосе меню включает
функцию создания экземпляра меню (CreateMenu), функции для работы
с меню (например, InsertMenuItem) и функцию уничтожения экземпляра
меню (DestroyMenu). Все эти функции принимают параметр, в котором
передается «Описатель» экземпляра меню.
• В программном модуле Apache для обработки HTTP-запросов имеются функции для создания всей необходимой информации о запросе
(ap_sub_req_lookup_uri), ее обработки (ap_run_sub_req) и уничтожения
(ap_destroy_sub_req). Эти функции принимают указатель на структуру с
экземпляром запроса.
• В игре NetHack экземпляр struct используется для представления монстров, и предоставляются функции для создания и уничтожения монстра. NetHack также предоставляет функцию для получения информации о монстрах (is_starting_pet, is_vampshifter).
• Паттерн «Зависящий от клиента экземпляр» из книги Markus Voelter et
al. «Remoting Patterns» (Wiley, 2007) рекомендует в ПО промежуточного уровня для управления распределенными объектами предоставлять
удаленные объекты, время жизни которых контролируется клиентами.
Сервер создает новые экземпляры для клиентов, а клиент работает с экземпляром, передает его между функциями и уничтожает.

Применение к сквозному примеру
Теперь код драйвера Ethernet принимает следующий вид.
API (заголовочный файл)

struct Sender
{
char destination_ip[16];
int socket;
};

Сквозной пример  157
struct Sender* createSender(char* destination_ip);
void sendByte(struct Sender* s, char data);
void destroySender(struct Sender* s);
Реализация

struct Sender* createSender(char* destination_ip)
{
struct Sender* s = malloc(sizeof(struct Sender));
/* создать сокет для destination_ip и сохранить его в Sender s */
return s;
}
void sendByte(struct Sender* s, char data)
{
number_of_sent_bytes++;
/* отправить через сокет данные, хранящиеся в Sender s */
}
void destroySender(struct Sender* s)
{
/* закрыть сокет, хранящийся в Sender s */
free(s);
}
Вызывающая сторона сначала может создать отправителя, затем отправить
сразу все данные, после чего уничтожить отправителя. Таким образом, вызывающая сторона может гарантировать, что соединение не придется устанавливать заново в каждом вызове sendByte(). Вызывающая сторона владеет созданным отправителем, полностью контролирует время его жизни и отвечает
за его очистку.
Код вызывающей стороны

struct Sender* s = createSender("192.168.0.1");
char* dataToSend = "Hello World!";
char* pointer = dataToSend;
while(*pointer != '\0')
{
sendByte(s, *pointer);
pointer++;
}
destroySender(s);
Далее предположим, что вы не единственный пользователь этого API. Его
могут использовать несколько потоков. При условии, что один поток создает
отправителя для отправки IP-адресу X, а другой – отправителя для отправки Y,
все будет хорошо, и драйвер Ethernet создаст два независимых сокета для двух
потоков.

158

 Глава 5. Время жизни и владение данными

Но допустим, что оба потока хотят отправлять данные одному и тому же получателю. Теперь драйвер Ethernet в затруднении, потому что на одном порту
можно открыть только один сокет для каждого конечного IP-адреса. Решение
проблемы – не позволять двум разным потоком отправлять данные одному
и тому же получателю, – второй поток просто получит ошибку при попытке
создать отправителя. Однако же можно и разрешить двум потокам отправлять
данные одному адресату.
Для этого нужно просто создать «Разделяемый экземпляр».

Разделяемый экземпляр
Контекст
Вы хотите предоставить вызывающей стороне набор взаимосвязанных
функций. Эти функции работают с общими разделяемыми между ними данными и могут требовать предварительной подготовки ресурсов, например памяти, которая должна быть инициализирована до вызова функций. Существует несколько контекстов, в которых будет вызываться эта функциональность,
и эти контексты разделяются между вызывающими сторонами.

Проблема
Вы хотите предоставить нескольким вызывающим сторонам или потокам доступ
к функциональности, реализованной взаимозависимыми функциями, и при взаимодействии функций с вызывающей стороной образуется информация о состоянии, которая должна разделяться между вызывающим сторонами.
Хранить информацию о состоянии в «Программном модуле с глобальным
состоянием» – не выход из-за наличия нескольких вызывающих сторон, которым нужна разная информация о состоянии. Хранить информацию о состоянии
в «Экземпляре, принадлежащем вызывающей стороне», тоже не годится, потому что некоторые вызывающие стороны могут оперировать с одним и тем же
экземпляром или потому что вы не хотите создавать новые экземпляры для
каждой вызывающей стороны, стремясь сэкономить ресурсы.
И тем не менее вы хотите скрыть детали реализации от вызывающей стороны, сделав доступ к своей функциональности максимально простым. Следует
четко определить, отвечает ли вызывающая сторона за выделение и освобож­
дение ресурсов.

Решение
Потребуйте, чтобы вызывающая сторона передавала всем вашим функциям экземпляр, в котором хранятся ресурсы и информация о состоянии. Используйте один и тот
же экземпляр для нескольких вызывающих сторон и оставьте владение этим экземпляром за своим программным модулем.
Как и в случае «Экземпляра, принадлежащего вызывающей стороне», предоставьте вызывающей стороне указатель на struct или «Описатель», который

Сквозной пример  159
та сможет передавать вашим функциям. Теперь при создании экземпляра вызывающая сторона должна будет также передать идентификатор (например,
уникальное имя), определяющий вид создаваемого экземпляра. Зная идентификатор, вы можете проверить, существует ли уже такой экземпляр. Если да, то
новый экземпляр не создается, а вместо него возвращается указатель на struct
или «Описатель» уже созданного экземпляра, который был возвращен другим
вызывающим сторонам.
Чтобы узнать, существует ли уже экземпляр, необходимо хранить список
уже созданных экземпляров в программном модуле. Для хранения списка
можно, например, реализовать паттерн «Программный модуль с глобальным
состоянием». Кроме того, вне зависимости от того, был создан экземпляр или
нет, можно хранить информацию о том, кто в настоящее время к каким экземплярам обращается или, по крайней мере, сколько вызывающих сторон в
настоящее время обращаются к экземпляру. Эта дополнительная информация
необходима, потому что в тот момент, когда у экземпляра не останется пользователей, вы должны будете уничтожить его, так как являетесь его единоличным владельцем.
Нужно также проверить, могут ли ваши функции одновременно вызываться разными сторонами для одного и того же экземпляра. В некоторых, более
простых, случаях доступ к данным из разных потоков, возможно, и нет нужды синхронизировать, потому что данные только читаются. Тогда можно было
бы реализовать паттерн «Неизменяемый экземпляр», который не позволяет
вызывающей стороне модифицировать экземпляр. Но в остальных случаях
придется реализовать в ваших функциях взаимное исключение при доступе к
разделяемым ресурсам.
Ниже приведен пример простого «Разделяемого экземпляра».
Код вызывающей стороны 1

struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B);
/* работать с тем же экземпляром, что и вызывающая сторона 2 */
operateOnInstance(inst);
closeInstance(inst);
Код вызывающей стороны 2

struct INSTANCE* inst = openInstance(INSTANCE_TYPE_B);
/* работать с тем же экземпляром, что и вызывающая сторона 1*/
operateOnInstance(inst);
closeInstance(inst);
API (заголовочный файл)

struct INSTANCE
{
int x;
int y;
};

160

 Глава 5. Время жизни и владение данными

/* будут использоваться в роли идентификаторов для функции openInstance */
#define INSTANCE_TYPE_A 1
#define INSTANCE_TYPE_B 2
#define INSTANCE_TYPE_C 3
/* Получить экземпляр с идентификатором 'id'. Создается новый экземпляр, если никакая
другая вызывающая сторона еще не получала экземпляр с таким же идентификатором. */
struct INSTANCE* openInstance(int id);
/* Работает с данными, хранящимися в экземпляре. */
void operateOnInstance(struct INSTANCE* inst);
/* Освобождает экземпляр, полученный от 'openInstance'.
Если все вызывающие экземпляры освободили экземпляр, то он уничтожается.
void closeInstance(struct INSTANCE* inst);
Реализация

#define MAX_INSTANCES 4
struct INSTANCELIST
{
struct INSTANCE* inst;
int count;
};
static struct INSTANCELIST list[MAX_INSTANCES];
struct INSTANCE* openInstance(int id)
{
if(list[id].count == 0)
{
list[id].inst = malloc(sizeof(struct INSTANCE));
}
list[id].count++;
return list[id].inst;
}
void operateOnInstance(struct INSTANCE* inst)
{
/* работа с inst->x и inst->y */
}
static int getInstanceId(struct INSTANCE* inst)
{
int i;
for(i=0; iarray = array;
context->length = length;
/* заполнить context данными или информацией о состоянии */

174

 Глава 6. Гибкие API

return context;
}
void sort(SORT_HANDLE context)
{
/* работает с данными из контекста */
}
Заведите в своем API одну функцию для создания «Описателя». Эта функция
возвращает «Описатель» вызывающей стороне. Затем вызывающая сторона
может вызывать другие функции API, нуждающиеся в «Описателе». В большинстве случаев нужна также функция удаления «Описателя» для очистки выделенных ресурсов.

Последствия
Теперь вы можете разделять информацию о состоянии и ресурсы между
функциями, не утруждая этой проблемой вызывающую сторону и не давая ей
возможности внести в код зависимость от этих внутренних деталей.
Поддерживается несколько экземпляров данных. Функцию создания «Описателя» можно вызывать несколько раз для получения нескольких контекстов,
а затем работать с этими контекстами независимо друг от друга.
Если функции, работающие с «Описателем», в будущем изменятся и понадобится разделять другие или дополнительные данные, то члены struct можно
будет просто изменить, не требуя вносить изменений в вызывающий код.
Из объявлений функций явно видно, что они тесно связаны, потому что все
требуют «Описателя». Поэтому, с одной стороны, легко понять, какие функции должны быть помещены в один заголовочный файл, а с другой стороны,
вызывающей стороне очень просто найти, какие функции следует применять
совместно.
Паттерн «Описатель» требует, чтобы вызывающая сторона передавала один
дополнительный параметр каждой функции, а каждый дополнительный параметр делает код сложнее для восприятия.

Известные примеры применения
Следующие примеры демонстрируют применение этого паттерна.
• В стандартной библиотеке C имеется определение типа FILE в файле
stdio.h. В большинстве реализаций этот тип определен как указатель на
struct, а сама структура не является частью заголовочного файла. Описатель FILE создается функцией fopen, после чего для открытого файла
можно вызывать другие функции (fwrite, fread и т. д.).
• struct AES_KEY в коде OpenSSL используется для передачи контекста
между несколькими функциями, относящимися к AES-шифрованию
(AES_set_decrypt_key, AES_set_encrypt_key). Структура и ее члены не
скрыты в недрах реализации, а являются частью заголовочного файла,
потому что в разных местах кода OpenSSL необходимо знать размер
структуры.

Сквозной пример  175
• Код протоколирования в проекте Subversion работает с «Описателем».
Тип struct logger_t определен в файле реализации, а указатель на
структуру – в соответствующем заголовочном файле.
• Этот паттерн описан в книге David R. Hanson «C Interfaces and
Implementations» (Addison-Wesley, 1996) под названием «Непрозрачный
указатель», а в книге Adam Tornhill «Patterns in C» как «Паттерн полноценного абстрактного типа данных».

Применение к сквозному примеру
Теперь вы можете поддержать столько сетевых карт Ethernet, сколько нужно. Каждый созданный экземпляр драйвера порождает собственный контекст
данных, который затем передается функциям в виде «Описателя». API драйвера устройства принимает следующий вид.

/* INTERNAL_DRIVER_STRUCT содержит данные, разделяемые функциями (например,
как выбрать сетевую карту, за которую отвечает драйвер) */
typedef struct INTERNAL_DRIVER_STRUCT* DRIVER_HANDLE;
/* 'initArg' содержит информацию, идентифицирующую конкретную
сетевую карту для данного экземпляра драйвера */
DRIVER_HANDLE driverCreate(void* initArg);
void driverDestroy(DRIVER_HANDLE h);
void sendByte(DRIVER_HANDLE h, char byte);
char receiveByte(DRIVER_HANDLE h);
void setIpAddress(DRIVER_HANDLE h, char* ip);
void setMacAddress(DRIVER_HANDLE h, char* mac);
Требования в очередной раз изменились. Теперь вы должны поддерживать
несколько разных сетевых карт Ethernet, например от разных производителей.
Функциональность всех карт схожа, но детали доступа к регистрам различаются, а следовательно, необходимы разные реализации драйверов. Ниже описаны два прямолинейных способа поддержать это требование.
• Завести два отдельных API драйвера. Недостаток этого подхода в том,
что пользователям придется создавать громоздкие механизмы выбора
драйвера во время выполнения. Кроме того, наличие двух разных API
ведет к дублированию кода, потому что у драйверов как минимум общий поток управления (например, создание и уничтожение драйвера).
• Добавить в API функции типа sendByteDriverA и sendByteDriverB. Однако
обычно вы хотите, чтобы API был минимальным, а включение всех функций драйвера в один API может привести пользователя в замешательство. Кроме того, код пользователя зависит от сигнатур всех функций,
включенных в API, а если код от чего-то зависит, то это что-то должно
быть как можно меньше (см. принцип разделения интерфейсов).
Гораздо лучше поддержать разные сетевые карты Ethernet с помощью «Динамического интерфейса».

176

 Глава 6. Гибкие API

Динамический интерфейс
Контекст
Вы или вызывающая вас сторона хотите реализовать несколько видов функциональности, имеющих схожую логику, но отличающееся в деталях поведение.

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

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

/* Функция compare должна возвращать true, если x меньше y,
и false в противном случае */
typedef bool (*COMPARE_FP)(int x, int y);
void sort(COMPARE_FP compare, int* array, int length);
Реализация

void sort(COMPARE_FP compare, int* array, int length)
{
int i, j;for(i=0; icurrentElement;
}
void destroyIterator(ITERATOR iterator)
{
free(iterator);
}
Как в этом коде передать вызывающей стороне имя пользователя? Следует
ли просто предоставить ей указатель на данные? А если вы копируете данные
в буфер, то кто должен его выделить?
В этой ситуации буфер для строки выделяет вызываемая сторона. Тогда вызывающая сторона имеет полный доступ к строке, но не имеет возможности
изменить ее в userList. Кроме того, она может не опасаться, что другой поток
изменит данные, пока она будет их читать.
Название паттерна

Краткое описание

Вызываемая
сторона выделяет
память

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

Применение системы управления пользователями
Итак, вы написали код управления пользователями. Ниже показано, как им
можно воспользоваться.

char* element;
addUser("A", "pass");
addUser("B", "pass");
addUser("C", "pass");
ITERATOR it = createIterator();
while(true)
{
element = getNextElement(it);
if(strcmp(element, "") == 0)
{
break;
}

Резюме  291
printf("Пользователь: %s ", element);
printf("Аутентификация успешна? %d\n", authenticateUser(element, "pass"));
}
destroyIterator(it);
В этой главе паттерны помогли вам спроектировать окончательный код.
Теперь вы можете сказать начальнику, что справились с задачей реализации
требуемой системы хранения имен и паролей пользователей. Применив основанный на паттернах подход к проектированию, вы воспользовались документированными решениями, проверенными на практике.

Резюме
В этой главе вы писали код шаг за шагом, применяя описанные в части I паттерны для решения проблем по мере возникновения. В начале было много
вопросов о том, как организовать файлы и обрабатывать ошибки. Паттерны
показали путь. Они упростили пошаговое построение кода. Благодаря им
вы стали лучше понимать, почему код выглядит и ведет себя именно так. На
рис. 11.2 показано, какие паттерны вы применяли. Вы видите, сколько решений пришлось принять и сколько из них направлялось паттернами.
Построенная система управления пользователями содержит базовые средства для добавления, поиска и аутентификации пользователей. Конечно, в нее
можно добавить много других функций, например: изменение пароля, хранение паролей в зашифрованном виде или проверка того, что пароль отвечает
определенным критериям безопасности. Эта дополнительная функциональность не рассматривалась, чтобы было проще понять ту сторону разработки,
которая связана с паттернами.

 Глава 11. Построение системы управления пользователями

Организация
файлов

Организация
данных

292

Решение о том, где хранить и как обращаться к данным
Программный модуль Решение о времени жизни памяти
с глобальным состо­
янием

Вечная память

Решение об интерфейсе с вызывающей стороной

Заголовочные
файлы

Охрана

Каталоги про­

граммных модулей
включения
Решение о
Решение
том, где хранить
о том, как
интерфейс и реализацию
защититься от повторного
включения

Добавление
пользователей:
обработка ошибок

Аутентифика­ Аутентификация:
ция: протоколи­ обработка ошибок
рование ошибок

Решение о том, как предоставлять информацию вызывающей стороне
Решение о том, какую информацию
предоставлять вызывающей стороне

Возврат существенной
информации об
ошибке

Возвращаемое
значение

Решение о том, какие ошибки обрабатывать самостоятельно

Принцип самурая
Решение о том, как
информировать
разработчика об ошибках

Решение о том, что делать
Решение о том, в какой
с кросс-платфор­
памяти хра­
Протоколирова­
менностью
нить данные
Избегание
Сначала стек

ние ошибок

вариантов

Решение о том, какие ошибки обрабатывать самостоятельно

Разбиение
функции
Решение о том, где
реализовать обязанность

Проверка
условий

Решение о том, как проверять
предусловия
Решение о том, как предоставлять информацию
вызывающей стороне

Принцип
самурая

Возврат кодов
состояния

Итерирование

Решение о том, как предоставить набор данных вызывающей стороне
Решение о том, как разделять информацию
между функциями

Описатель
Решение о том, кто отвечает
за очистку

Единоличное
владение

Решение о
времени
жизни
данных

Курсор

Решение о том, как
передать информацию об
ошибке вызыва­
Буфер, принадлежа­ ющей стороне
Специальное воз­
щий вызывающей
вращаемое значение
стороне

Решение о том, как реализовать инициализацию и очистку
Вызываемая
сторона выделяет
память

Решение о том, кто отвечает за
предоставление ресурсов

Рис. 11.2. Паттерны, примененные в этой истории

Объектная
обработка ошибок

Глава

12
Заключение

Чему вы научились
Прочитав эту книгу, вы познакомились с некоторыми продвинутыми идеями
программирования на C. Глядя на примеры больших программ, вы теперь будете понимать, почему код выглядит так, а не иначе. Вы знаете, какие рассуждения стоят за выбором проектных решений. Например, вы теперь понимаете, почему в примере драйвера Ethernet, приведенном в предисловии к этой
книге, имеется явная функция driverCreate и структура данных DRIVER_HANDLE
для хранения информации о состоянии. Паттерны из первой части направляли
решения, принятые в этом примере, равно как и во многих других, встречающихся в книге.
Истории о паттернах во второй части продемонстрировали применение
паттернов и постепенное построение кода. Когда вы столкнетесь с очередной
проблемой при программировании на C, просмотрите разделы «Проблема» в
описаниях паттернов и решите, какой из них отвечает вашей проблеме. Если
такой обнаружится, то вам повезло, потому что вы сможете следовать наставлениям паттерна.

Для дополнительного чтения
Эта книга поможет вам вырасти от начинающего до опытного программиста
на C. Вот еще несколько книг, которые помогли лично мне отточить навыки
программирования на C.
• В книге Robert C. Martin «Clean Code: A Handbook of Agile Software
Craftsmanship» (Prentice Hall, 2008) обсуждаются базовые принципы
реализации высококачественного кода, который должен работать длительное время. Это полезное чтение для любого программиста; рассматриваются такие темы, как тестирование, документация, стиль кодирования и многое другое.
• В книге James W. Grenning «Test-Driven Development for Embedded C»
(Pragmatic Bookshelf, 2011) используется сквозной пример для объяснения того, как писать автономные тесты для программ на C, работающих
на уровне, близком к оборудованию.

294

 Глава 12. Заключение
• Книга Peter van der Linden «Expert C Programming» (Prentice Hall, 1994) –
одна из первых, посвященных продвинутому программированию на C.
В ней подробно описывается синтаксис C и типичные подводные камни. Обсуждаются также вопросы управления памятью в C и принципы
работы компоновщика.
• К моей книге близко примыкает книга Adam Tornhill «Patterns in C»
(Leanpub, 2014). В ней также описываются паттерны с упором на том,
как реализовать на С паттерны из книги «банды четырех».

Заключительные замечания
По сравнению с новичком, только приступающим к изучению C, вы теперь обладаете знаниями о методах, применяемых при построении крупномасштабных производственных программ на C. Вы знаете, как:
• обрабатывать ошибки, даже в отсутствие механизма исключений;
• управлять памятью, хотя в вашем распоряжении нет сборщика мусора и
деструкторов, автоматически освобождающих память;
• реализовывать гибкие интерфейсы, даже когда нет встроенных механизмов абстрагирования;
• организовывать файлы и код в отсутствие классов и пакетов.
Теперь вы можете писать на C, несмотря на отсутствие в нем некоторых
удобств, присущих современным языкам программирования.

Об авторе
Кристофер Прешерн организует конференции по паттернам проектирования и другие мероприятия с целью улучшить владение паттернами. Работая
программистом в компании ABB, он собрал и документировал практические
знания о том, как писать код производственного качества. Он читал лекции
по кодированию и контролю качества в Технологическом университете Граца.
Имеет степень доктора философии по компьютерным наукам.

Об иллюстрации на обложке
На обложке этой книги изображен какаду инка (Lophochroa leadbeateri), или какаду Лидбитера. Этот какаду среднего размера был отрыт майором Томасом
Митчеллом, исследователем и описателем Юго-Восточной Австралии. Обитает в засушливых и полузасушливых частях Австралии, предпочитает лесистые
местности, где питается семенами растений. Оперение в основном белое и
бледно-розовое, более насыщенного розового цвета под крыльями. Хохолок
окрашен в ярко-красный, желтый и белый цвета. Самцы и самки отличаются
мало; самцы немного крупнее, с коричневыми глазами, тогда как у самок красноватые глаза и более широкая желтая полоса на хохолке.
Какаду инка популярны в качестве домашних питомцев, хотя это очень общительные птицы, требующие от хозяина немалого внимания. В природе гнездятся парами и нуждаются в большой территории, поэтому их среда обитания
уязвима к фрагментации. Хотя этот вид не считается таксоном минимального
риска, их количество уменьшилось в связи с вырубкой лесов. Им также угрожает браконьерское отлавливание для содержания в неволе. Многие животные,
изображенные на обложках книг издательства O'Reilly, находятся под угрозой
исчезновения, все они важны для нашего мира.

Предметный указатель

Символы

А

#ifdef директивы
защита заголовочных файлов 210
избегание плохо реализованных
дополнительное чтение 261
обзор паттернов 237
паттерн Атомарные
примитивы 246, 274
паттерн Избегание
вариантов 240, 273, 284
паттерн Изолированные
примитивы 243, 274
паттерн Разделение реализаций
вариантов 255, 275
паттерн Уровень абстракции 250, 273
сквозной пример 238
слабости 236
#include директивы 217
#pragma once директивы 210

абстрактные типы данных 142
абстрактные указатели 173
автоматические переменные 84
агрегирование и ассоциация 153
аргументы, передаваемые по ссылке 120
аутентификация
обработка ошибок 282
протоколирование ошибок 284

A
assert макрос 36
aрагментация памяти 106

I
if предложения 42, 45

M
Makefile 214

S
SOLID принципы 167

V
valgrind 91, 99

Б
буферы 132

В
варианты кода 244
возврат информации об ошибке
для дополнительного чтения 77
незамеченные ошибки 35
обзор паттернов 13, 52, 77
паттерн Вечная память 86, 268, 280
паттерн Возврат кода состояния 54, 286
паттерн Возврат существенной информации об
ошибке 61, 269, 282
паттерн Протоколирование ошибок 70, 284
паттерн Специальное возвращаемое
значение 67, 289
проблемы 51
сквозной пример 52
время жизни и владение данными
для дополнительного чтения 165
обзор паттернов 142, 165
объектоподобные элементов 141
паттерн Программный модуль без состояния 143
паттерн Программный модуль с глобальным
состоянием 147, 268, 280
паттерн Разделяемый экземпляр 158
паттерн Экземпляр, принадлежащий
вызывающей стороне 152, 288

Предметный указатель  297

Г

М

гибкие API
дополнительное чтение 183
обзор паттернов 168, 183
паттерн Динамический интерфейс 176, 271
паттерн Заголовочные файлы 169, 265, 281
паттерн Описатель 172, 288
паттерн Совместимость интерфейсов 228
паттерн Управление функцией 179
трудности определения 167
глобальные переменные 115, 119, 124, 148

массивы переменной длины 85
многопоточное окружение 121, 153, 197
модульные программы
организация файлов 19
удобство сопровождения 169

Н
неоднократное освобождение памяти 81

О

обработка ошибок
для дополнительного чтения 49
обзор паттернов 12, 27, 48
двоичный интерфейс приложения 228
паттерн Запись об очистке 42
детали реализации, сокрытие 169
паттерн Объектная обработка ошибок 45, 289
динамическая память 80, 90, 105
паттерн Переход к обработке ошибки 39
паттерн Принцип самурая 35, 266, 283
З
паттерн Проверка условий 32, 287
заголовочные файлы
паттерн Разбиение функции 29, 286
защита от повторного включения 210
проблемы 26
избегание зависимостей 218
сквозной пример 27
помещение в подкаталоги 221
объектоподобные
элементы 141
помещение только платформенно независимых
Одиночка
паттерн/антипаттерн
150
функций 250
организация файлов в модульных программах
размещение вместе с файлами реализации 212
обзор паттернов 19, 206, 235
паттерн Автономный компонент 221
И
паттерн Глобальный каталог include 217, 265
избыточное выделение памяти (Linux) 105
паттерн Каталоги программных
интерфейсы итераторов
модулей 212, 265, 282
гибкие 185
паттерн Копия API 226
для дополнительного чтения 203
паттерн Охрана включения 209, 266, 281
обзор паттернов 18, 186, 202
проблемы 205
паттерн Доступ по индексу 188
сквозной пример 207
паттерн Итератор обратного вызова 197
отладка
паттерн Курсор 192, 288
valgrind средство отладки 99
сквозной пример 187
возврат информации об ошибке 70
информация о состоянии, разделение 172
и Единоличное владение 96
и Отложенная очитска 90
К
обнаружение утечек памяти 91
код с душком 32
проблемы с динамической памятью 81
компоненты 221
протоколирование отладочной информации 99
кросс-платформенная обработка файлов 273
удаленная 264
указатель NULL 103

Д

Л

ленивое вычисление 42

П
пакеты 205

298

 Предметный указатель

параметры сборки 214
пароли 280
паттерны
Автономный компонент 221
Агрегат 123
Атомарные примитивы 246, 274
Буфер, принадлежащий вызывающей
стороне 131, 267
Вечная память 86, 268, 272, 280
Возврат кода состояния 54, 286
Возврат существенной информации
об ошибке 61, 286
Возвращаемое значение 116, 269, 282
Вызываемая сторона выделяет память 135, 290
Выходные параметры 119
Глобальный каталог include 217, 265
Динамический интерфейс 176, 271
Доступ по индексу 188, 270
Единоличное владение 94, 289
Заголовочные файлы 169, 265, 281
Запись об очистке 42
Избегание вариантов 240, 273, 284
Изолированные примитивы 243, 274
Итератор обратного вызова 197
Каталоги программных модулей 212, 265, 282
Копия API 226
Курсор 192, 288
Неизменяемый экземпляр 128
Обертка выделения 97
Объектная обработка ошибок 45, 289
Описатель 172, 288
Отложенная очистка 90
Отложенный захват 272
Охрана включения 209, 266, 281
Переход к обработке ошибки 39
Принцип самурая 35, 267, 283
Проверка указателя 102
Проверка условий 32, 287
Программный модуль без состояния 143, 266
Программный модуль с глобальным
состоянием 147, 268, 280
Протоколирование ошибок 70, 284
пример реализации 264
Пул памяти 105
Разбиение функции 29, 286
Разделение реализаций вариантов 255, 276
Разделяемый экземпляр 158

Сначала стек 83, 285
Специальное возвращаемое значение 67, 289
Управление функцией 179
Уровень абстракции 250, 273
Экземпляр, принадлежащий вызывающей
стороне 152, 289
паттерны проектирования
краткий обзор 12
определение термина 11
преимущества 277, 294
ссылки на известные примеры применения 22
ссылки на опубликованные статьи 24
структура 11
цели 8
подсоленный хеш 280
принцип единственной обязанности 167
принцип инверсии зависимости 167
принцип открытости-закрытости 167
принцип подстановки Лисков 167
принцип разделения интерфейсов 167
проверка предусловий 32
программные модули 142

Р
ресурсы
время жизни и владение 141
захват и очистка нескольких 39, 42, 45

С
сборка мусора
и утечки памяти 92
отсутствие 141
семантическое версионирование 229
синхронизации проблемы 121
совместимость интерфейсов 228
статическая память 80, 87

У
умные указатели 82
управление памятью
для дополнительного чтения 111
обзор паттернов 14, 79, 111
паттерн Вечная память 86, 268, 272, 280
паттерн Единоличное владение 94, 289
паттерн Обертка выделения 97
паттерн Отложенная очистка 90
паттерн Проверка указателя 102

Предметный указатель  299
паттерн Пул памяти 105
паттерн Сначала стек 83, 285
проблемы 78
сквозной пример 83
хранение данных и проблемы с динамической
памятью 80
утечки памяти
и сборка мусора 92
обнаружение 91
сознательные 91
устранение риска 85

Ф
фрагментация памяти 82
функции
возврат информации о состоянии 54
возврат нескольких элементов
информации 119, 124
выбор элементов по одному 193
доступ из нескольких потоков 152
завершение программы в случае ошибки 35
и глобальные экземпляры 148
использование стандартизованных 240
обработка только одного варианта 247
обход элементов 188
отделение инициализации от очистки 45
очистка нескольких ресурсов 39, 42, 45
передача метаинформации 180
передача экземпляров 158
помещение только платформенно независимых
в заголовочные файлы 250
разделение информации о состоянии или
ресурсов 172
разделение обязанностей 29
сокрытие деталей реализации 170
сохранение детальной информации
об ошибке 70
улучшение удобочитаемости 32

Х
хранение данных
выбор паттернов для 279
динамическая память 90
запоминание данных на длительное время 86
определение и документирование очистки 94
паттерн Сначала стек 83
предоставление большого блока неизменяемых
данных 128

проблемы с динамической памятью 80, 105
разделение данных 115, 131, 135
статическая память 87

Ц
центральная функцию протоколирования 266

Э
экземпляры
и программные модули 142
определение термина 141
разделение 158

Книги издательства «ДМК ПРЕСС» можно купить оптом и в розницу
в книготорговой компании «Галактика»
(представляет интересы издательств
«ДМК ПРЕСС», «СОЛОН ПРЕСС», «КТК Галактика»).
Адрес: г. Москва, пр. Андропова, 38;
Тел.: +7(499) 782-38-89. Электронная почта: books@alians-kniga.ru.
При оформлении заказа следует указать адрес (полностью),
по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине:
www.galaktika-dmk.com.

Прешерн К.

Язык С. Мастерство программирования
Принципы, практики и паттерны

Главный редактор

Мовчан Д. А.

dmkpress@gmail.com

Перевод с английского
Корректор
Верстка
Дизайн обложки

Слинкин А. Н.
Абросимова Л. А.
Паранская Н. В.
Мовчан А. Г.

Формат 70×1001/16.
Печать цифровая. Усл. печ. л. 24,29.
Тираж 100 экз.
Веб-сайт издательства: www.dmkpress.com