Эффективный C. Профессиональное программирование
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Эффективный C. Профессиональное программирование

 

Роберт С. Сикорд
Эффективный C. Профессиональное программирование
2021

Научный редактор Р. Хазанский

Литературный редактор Н. Хлебина

Художник В. Мостипан

Корректоры Е. Павлович, Н. Смолич


 

Роберт С. Сикорд

Эффективный C. Профессиональное программирование. — СПб.: Питер, 2021.

 

ISBN 978-5-4461-1851-9

© ООО Издательство "Питер", 2021

 

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

 

Посвящается моим внучкам, Оливии и Изабелле, а также другим молодым женщинам, которые станут учеными и инженерами, когда вырастут.

 

Предисловие Паскаля Куока

Впервые о Роберте Сикорде я услышал в 2008 году. В то время он уже был широко известен среди программистов на C своей работой над стандартом программированияCERT C и приложением K к стандарту C. Однако на тот момент прошло лишь пара лет с тех пор, как я, молодой и глупый, присоединился к проекту Frama-C, призванному гарантировать отсутствие нежелательного поведения в программах на C. Однажды мой интерес привлекла заметка об уязвимостях от CERT, согласно которой в кое-каких компиляторах C (в частности, в GCC) были убраны определенные проверки переполнения операций с плавающей запятой. Это решение имело причину: проверки были слишком примитивными и в случае переполнения приводили к неопределенному поведению.

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

Эта книга научит вас программировать на C в современных условиях. Она поможет выработать хорошие привычки, которые не позволят вам сознательно или по небрежности использовать нежелательное поведение. Читатель должен понимать, что в объемных программах на языке C такое поведение может быть вызвано произвольным вводом, поэтому устранения обычных ошибок программистов может быть недостаточно!

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

Паскаль Куок, главный научный сотрудник, TrustInSoft

Предисловие Олли Уайтхауса

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

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

Мнение Роберта о том, как профессионально и безопасно программировать на C, пользуется большим уважением. Уже больше десяти лет я советую его материал как клиентам, так и разработчикам внутри компании. Никто лучше него не научит вас писать код на C профессионально и среди прочего безопасно.

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

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

Олли Уайтхаус, технический директор, NCC Group

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

Хочу отметить всех тех, кто помогал в создании этой книги. Начну с Билла Поллока из No Starch Press, который неустанно убеждал меня написать книгу о C.

Благодарю Олли Уайтхауса и Паскаля Куока за отличные предисловия.

Аарон Баллман был прекрасным партнером в этом начинании. Помимо двух написанных им глав, он вычитывал весь текст (часто по несколько раз) и помогал мне решать как сложные, так и тривиальные проблемы.

Дуглас Гвин, заслуженный сотрудник научно-исследовательской лаборатории сухопутных войск США и почетный член комитета во главе стандарта C, помог вычитать все главы. Когда мои писательские навыки не соответствовали его ожиданиям, он подталкивал меня в правильном направлении.

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

Помимо Аарона, Дага и Мартина отдельные главы вычитывали и другие уважаемые члены комитетов во главе стандартов C и C++, включая Джима Томаса, Томаса Кёппе, Нилла Дугласа, Тома Хонерманна и Жана Хейда Менеида. В число научных редакторов вошли мои коллеги из NCC Group: Ник Данн, Джонатан Линдси, Томаш Крамковски, Алекс Донисторп, Джошуа Доу, Каталин Висинеску, Аарон Адамс и Саймон Харраги. Техническими редакторами, не имеющими отношения к этим организациям, были Дэвид Леблан, Николас Уинтер, Джон Макфарлейн и Скотт Алоизио.

Кроме того, я хотел бы поблагодарить следующих профессионалов из No Starch, которые обеспечили выпуск качественного продукта: Элизабет Чедвик, Фрэнсис Со, Зака Лебовски, Энни Чой, Барбару Йен, Катрину Тейлор, Натали Глисон, Дерека Йи, Лорен Чун, Джину Редман, Шерон Уилки, Эмили Баттаглиа и Дапиндера Досанжа.

Введение

Язык C был разработан для системного программирования еще в 1970-х, но до сих пор остается невероятно популярным. Системные языки рассчитаны на производительность и простоту доступа к внутреннему аппаратному обеспечению, но при этом обладают высокоуровневыми возможностями. Существуют более современные языки, но их компиляторы и библиотеки обычно написаны на C. Как однажды сказал Карл Саган, «если вы хотите приготовить яблочный пирог с нуля, то вам сначала нужно создать вселенную».

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

Согласно рейтингу TIOBE, с 2001 года C остается либо самым популяр­ным, либо вторым по популярности языком программирования1. В 2019 году C стал языком года по версии TIOBE. Своей популярностью он, скорее всего, обязан нескольким основополагающим принципам, известным как дух C.

• Доверие к программисту. В целом язык C подразумевает, что вы знаете, что делаете, и позволяет вам делать это. Иногда такие действия имеют плохие последствия (например, когда вы не знаете, что делаете).

• Не следует мешать программисту делать то, что нужно. Язык C предназначен для системного программирования, поэтому должен уметь справляться с разнообразными низкоуровневыми задачами.

• Язык должен быть компактным и простым. Язык C достаточно сильно связан с оборудованием и является легковесным.

• Для выполнения любой операции должен быть лишь один способ.

• Язык должен быть быстрым, даже если его переносимость не гарантируется. Главный приоритет — возможность написания оптимально эффективного кода. А сделать его переносимым, безопасным и защищенным должны вы, программист.

Краткая история C

Язык программирования C был создан в 1972 году Деннисом Ритчи и Кеном Томпсоном в лабораториях Bell Labs. Позже Деннис Ритчи вместе с Брайаном Керниганом написал книгу The C Programming Language2 (K&R, 1988). В 1983 году Американский национальный институт стандартов (American National Standards Institute, ANSI) сформировал комитет X3J11 для разработки стандартной спецификации C, а в 1989 году стандарт C был ратифицирован как ANSI X3.159-1989, «Язык программирования C». Эта версия языка называется ANSI C или C89.

В 1990 году стандарт ANSI C был принят (без изменений) совместным комитетом Международной организации по стандартизации (International Organization for Standardization, ISO) и Международной электротехнической комиссией (International Electrotechnical Commission, IEC) и опубликован в качестве первой редакции стандарта C, C90 (ISO/IEC 9899:1990). Вторая редакция, C99, была опубликована в 1999 году (ISO/IEC 9899:1999), а третья, C11, — в 2011-м (ISO/IEC 9899:2011). Последняя (на момент написания этих строк), четвертая версия стандарта C вышла в 2018 году под названием C17 (ISO/IEC 9899:2018). В настоящий момент ISO/IEC работают над новейшей редакцией языка C, известной как C2x. Согласно опросу, проведенному компанией JetBrains в 2018 году, 52 % программистов на C используют C99, 36 % — C11, а 23 % работают с языком С для встраиваемых систем3.

Стандарт C

Стандарт C (ISO/IEC 9899:2018) описывает язык и является главным его авторитетом. Он может казаться неясным или даже непостижимым, но если вы хотите писать переносимый, безопасный и защищенный код, то вам необходимо понимать этот стандарт. Он предоставляет реализациям существенную степень свободы, позволяя им оставаться оптимально эффективными на разных аппаратных платформах. Реализация в терминологии стандарта C обозначает компилятор и имеет следующее определение:

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

Из этого определения следует, что каждый компилятор с неким набором флагов командной строки и стандартной библиотекой C считается отдельной реализацией и что поведение разных реализаций может существенно различаться. Это видно на примере компилятора GNU GCC, который использует флаг -std= для определения стандарта языка; для данного параметра можно указывать такие значения, как c89, c90, c99, c11, c17, c18 и c2x. Значение по умолчанию зависит от версии компилятора. Если диалект C не был указан, то GCC 10 выбирает -std=gnu17, предоставляя тем самым расширения языка C. Если вам нужна переносимость, указывайте используемый вами стандарт. Для доступа к новым возможностям языка выбирайте последнюю спецификацию. Так, в 2019 году хорошим выбором был -std=c17.

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

Стандарт программирования CERT C

Руководя командой, отвечающей за безопасность кода в Институте программной инженерии при Университете Карнеги — Меллона, я написал справочное пособие The CERT® C Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems (Сикорд, 2014). В нем представлены примеры распространенных ошибок программирования на C и рекомендации по их исправлению. В данной книге некоторые из этих рекомендаций упоминаются в качестве источника подробной информации о тех или иных аспектах программирования на языке C.

Для кого эта книга

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

Книга содержит краткое введение в основы программирования на языке C, и после ее прочтения вы сможете писать программы, решать задачи и создавать рабочие системы. Здесь вы найдете понятные идиоматические примеры кода.

Вы познакомитесь с ключевыми концепциями программирования на C и научитесь писать высококачественный код, выполняя упражнения для каждой рассматриваемой темы. Вы узнаете, какие методики рекомендуются для разработки правильного и безопасного кода на C. Обновления и дополнительный материал можно найти на веб-странице этой книги https://www.nostarch.com/effective_c/ и на сайте http://www.robertseacord.com/. Если после прочтения вам захочется узнать больше о безопасном программировании на C, C++ или других языках, то, пожалуйста, ознакомьтесь с учебными курсами, которые предлагает NCC Group, пройдя по адресу https://www.nccgroup.trust/us/our-services/cyber-security/security-training/secure-coding/.

Структура книги

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

• В главе 1 «Знакомство с C» вы напишете простую программу на C, чтобы научиться использовать функцию main. Мы также рассмотрим несколько разных редакторов и компиляторов.

• В главе 2 «Объекты, функции и типы» изложены основы языка, такие как объявление переменных и функций. Вы также познакомитесь с принципами использования базовых типов.

• В главе 3 «Арифметические типы» рассказано о двух видах арифметических типов данных: целочисленных и с плавающей запятой.

• В главе 4 «Выражения и операции» вы узнаете, что такое операции5, и научитесь писать простые выражения для выполнения операций с объектами различных типов.

• В главе 5 «Управляющая логика» вы узнаете, как управлять порядком вычисления отдельных операторов. Для начала мы пройдемся по операторам-выражениям и составным операторам, которые описывают, какую работу нужно проделать. Затем рассмотрим три вида операторов, которые определяют, какие блоки кода выполняются и в каком порядке это происходит: операторы выбора, итерирования и перехода.

• В главе 6 «Динамически выделяемая память» вы узнаете, как память динамически выделяется в куче во время работы программы. Она нужна в ситуациях, когда до запуска кода неизвестно, сколько памяти ему понадобится.

• В главе 7 «Символы и строки» вы изучите различные кодировки, включая ASCII и Unicode, которые можно использовать для составления строк. Вы научитесь применять устаревшие функции из стандартной библиотеки C, интерфейсы с проверкой ограничений, а также API POSIX и Windows для представления и изменения строк.

• В главе 8 «Ввод/вывод» вы научитесь выполнять операции ввода/вывода для чтения и записи данных в терминалы и файловые системы. Ввод/вывод охватывает все пути, которыми информация попадает в программу и покидает ее. Без этого ваш код был бы бесполезным. Мы рассмотрим методики на основе стандартных потоков C и файловых дескрипторов POSIX.

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

• В главе 10 «Структура программы» вы научитесь разбивать свои программы на разные единицы трансляции, состоящие как из исходных, так и заголовочных файлов. Кроме того, вы узнаете, как скомпоновать несколько объектных файлов в единое целое, чтобы создать библиотеку или исполняемую программу.

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

Вам предстоит путешествие, по завершении которого вы станете новоиспеченным, но профессиональным программистом на C.

От издательства

Ваши замечания, предложения, вопросы отправляйте по электронному адресу comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1 Рейтинг TIOBE оценивает популярность языков программирования и находится по адресу https://www.tiobe.com/tiobe-index/. При его составлении учитываются количество квалифицированных программистов, учебных курсов и сторонних поставщиков для каждого языка. Этот рейтинг может пригодиться, если вы выбираете язык для изучения или разработки новой программной системы.

2Керниган Б., Ритчи Д. Язык программирования C. — М.: Вильямс, 2015.

3 Подробности исследования ищите на сайте JetBrains: https://www.jetbrains.com/lp/devecosystem-2019/c/.

4 Для этого есть замечательный инструмент Compiler Explorer: https://godbolt.org/.

5 Английское слово operator, соответствующее термину «операция», иногда ошибочно переводят как «оператор». На самом деле (по историческим причинам) русский термин «оператор» соответствует английскому statement. Разговаривая с коллегами, скорее всего, вы будете использовать термин «оператор» как аналог англоязычного operator. — Примеч. пер.

Для этого есть замечательный инструмент Compiler Explorer: https://godbolt.org/.

Язык программирования C был создан в 1972 году Деннисом Ритчи и Кеном Томпсоном в лабораториях Bell Labs. Позже Деннис Ритчи вместе с Брайаном Керниганом написал книгу The C Programming Language2 (K&R, 1988). В 1983 году Американский национальный институт стандартов (American National Standards Institute, ANSI) сформировал комитет X3J11 для разработки стандартной спецификации C, а в 1989 году стандарт C был ратифицирован как ANSI X3.159-1989, «Язык программирования C». Эта версия языка называется ANSI C или C89.

Английское слово operator, соответствующее термину «операция», иногда ошибочно переводят как «оператор». На самом деле (по историческим причинам) русский термин «оператор» соответствует английскому statement. Разговаривая с коллегами, скорее всего, вы будете использовать термин «оператор» как аналог англоязычного operator. — Примеч. пер.

Рейтинг TIOBE оценивает популярность языков программирования и находится по адресу https://www.tiobe.com/tiobe-index/. При его составлении учитываются количество квалифицированных программистов, учебных курсов и сторонних поставщиков для каждого языка. Этот рейтинг может пригодиться, если вы выбираете язык для изучения или разработки новой программной системы.

Керниган Б., Ритчи Д. Язык программирования C. — М.: Вильямс, 2015.

Подробности исследования ищите на сайте JetBrains: https://www.jetbrains.com/lp/devecosystem-2019/c/.

• В главе 4 «Выражения и операции» вы узнаете, что такое операции5, и научитесь писать простые выражения для выполнения операций с объектами различных типов.

Согласно рейтингу TIOBE, с 2001 года C остается либо самым популяр­ным, либо вторым по популярности языком программирования1. В 2019 году C стал языком года по версии TIOBE. Своей популярностью он, скорее всего, обязан нескольким основополагающим принципам, известным как дух C.

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

В 1990 году стандарт ANSI C был принят (без изменений) совместным комитетом Международной организации по стандартизации (International Organization for Standardization, ISO) и Международной электротехнической комиссией (International Electrotechnical Commission, IEC) и опубликован в качестве первой редакции стандарта C, C90 (ISO/IEC 9899:1990). Вторая редакция, C99, была опубликована в 1999 году (ISO/IEC 9899:1999), а третья, C11, — в 2011-м (ISO/IEC 9899:2011). Последняя (на момент написания этих строк), четвертая версия стандарта C вышла в 2018 году под названием C17 (ISO/IEC 9899:2018). В настоящий момент ISO/IEC работают над новейшей редакцией языка C, известной как C2x. Согласно опросу, проведенному компанией JetBrains в 2018 году, 52 % программистов на C используют C99, 36 % — C11, а 23 % работают с языком С для встраиваемых систем3.

1. Знакомство с C

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

Разработка вашей первой программы на C

Изучение программирования на C лучше всего начать с написания программ, и традиционной программой, с которой принято начинать, является Hello, world!.

Для написания данной программы нам понадобится текстовый редактор или интегрированная среда разработки (integrated development environment, IDE). Выбрать есть из чего, но пока можете открыть свой любимый редактор, а другие варианты мы рассмотрим позже в этой главе.

Введите в своем текстовом редакторе программу из листинга 1.1.

Листинг 1.1. Программа hello.c

#include <stdio.h>

#include <stdlib.h>

int main(void) {

  

puts("Hello, world!");

  

return EXIT_SUCCESS;

}

Чуть позже мы пройдемся по каждой строчке этого кода. Но пока сохраните его в файл hello.c. Расширение .c означает, что данный файл содержит исходный код на языке C.

ПРИМЕЧАНИЕ

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

Компиляция и запуск вашей программы

Дальше нам нужно скомпилировать и запустить эту программу. Данный процесс состоит из двух отдельных этапов. Вы можете выбрать один из множества компиляторов C, и от этого выбора будет зависеть команда для компиляции программы. В Linux и других Unix-подобных операционных системах можно воспользоваться системным компилятором с помощью команды cc. Чтобы скомпилировать свою программу, введите в командной строке cc и укажите имя соответствующего файла:

% cc hello.c

ПРИМЕЧАНИЕ

Эти команды относятся к Linux (и другим Unix-подобным операционным системам). Другие компиляторы в других операционных системах нужно запускать по-другому. Сверьтесь с документацией вашего конкретного компилятора.

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

% ls

a.out hello.c

Файл a.out — это исполняемая программа, которую можно запустить в командной строке:

% ./a.out

Hello, world!

Если все было сделано правильно, то программа должна вывести в окно терминала Hello,world!. В противном случае сравните исходный текст в листинге 1.1 со своим кодом и убедитесь, что они совпадают.

У команды cc есть множество флагов и параметров компиляции. Флаг -ofile, к примеру, позволяет назначить исполняемому файлу более запоминающееся имя вместо a.out. Следующая команда компиляции называет исполняемый файл hello:

% cc -o hello hello.c

% ./hello

Hello, world!

Теперь проанализируем программу hello.c строчка за строчкой.

Директивы препроцессора

В первых двух строчках программы hello.c используется директива препроцессора #include, которая ведет себя так, будто вы подставили вместо нее содержимое указанного файла. Мы подключаем заголовочные файлы <stdio.h> и <stdlib.h>, чтобы получить доступ к объявленным в них функциям, которые затем сможем вызывать в своем коде. Функция puts объявлена в <stdio.h>, а макрос EXIT_SUCCESS — в <stdlib.h>. Как можно догадаться по названиям этих файлов, <stdio.h> содержит объявления функций ввода/вывода (I/O), стандартных для C, а в <stdlib.h> находятся объявления служебных функций общего назначения. Вам нужно подключить объявления всех библиотечных функций, которые используются в вашей программе.

Функция main

Главная часть программы, показанной в листинге 1.1, начинается со строчки :

int main(void) {

Данная строчка определяет функцию main, которая вызывается при запуске программы. Это главная точка входа программы, которая выполняется в серверной среде, когда программа запускается из командной строки или другой программы. Язык C имеет две потенциальные среды выполнения: минимальную (freestanding) и полноценную (hosted). Минимальная может существовать вне ОС и обычно используется в программировании встраиваемых систем. Такие среды предоставляют минимальный набор библиотечных функций, а название и тип точки входа, которая вызывается при запуске программы, зависит от реализации. Эта книга в основном ориентирована на полноценные среды.

Мы определили функцию main так, чтобы она возвращала значение типа int, и указали void в скобках; это значит, она не принимает никаких аргументов. Тип int — это знаковый целочисленный тип, который можно использовать для представления как положительных, так и отрицательных целых значений (а также 0). По аналогии с другими процедурными языками программы на C состоят из процедур (называемых функциями), которые могут принимать аргументы и возвращать значения. Каждую функцию можно вызывать столько раз, сколько потребуется. В данном случае значение, возвращаемое функцией main, говорит о том, завершилась ли программа успешно. Работа, выполняемая этой функцией , состоит в выводе строки Hello,world!:

puts("Hello, world!");

Функция puts входит в стандартную библиотеку C и записывает строковый аргумент в поток stdout, который обычно представляет консоль или окно терминала, и добавляет к выводу символ перевода строки. "Hello,world!" — это строковый литерал, который ведет себя как строка, доступная только для чтения. Вызов этой функции выводит Hello,world! в терминал.

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

return EXIT_SUCCESS;

EXIT_SUCCESS — это объектоподобный макрос, который обычно разворачивается в 0 и, как правило, имеет следующее определение:

#define EXIT_SUCCESS 0

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

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

int main(void) {

  // ---snip---

}

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

int main(void)

{

  // ---snip---

}

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

Проверка возвращаемого значения функции

Функции зачастую возвращают значения, которые являются результатом вычислений или указывают на то, успешно ли выполнена задача. Например, функция puts, которую мы использовали в нашей программе Hello, world!, принимает строку, которую нужно вывести, и возвращает значение типа int. Если произошла ошибка, то это значение равно макросу EOF (отрицательному целому числу); в противном случае возвращается неотрицательное целое значение.

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

#include <stdio.h>

#include <stdlib.h>

int main(void) {

  if (puts("Hello, world!") == EOF) {

    return EXIT_FAILURE;

    // здесь код никогда не выполняется

  }

  return EXIT_SUCCESS;

  // здесь код никогда не выполняется

}

Эта переработанная версия программы Hello, world! проверяет, возвращает ли вызов puts значение EOF, сигнализирующее об ошибке записи. Если мы получаем EOF, то программа возвращает значение макроса EXIT_FAILURE (который соответствует ненулевому числу). Если нет, то функция завершается успешно, а программа возвращает макрос EXIT_SUCCESS (который должен быть равен 0). Скрипт, вызывающий программу, может проверить ее состояние, чтобы определить, было ли ее выполнение успешным. Код, идущий за оператором return, является мертвым («недостижимым») и не выполняется никогда. Об этом сигнализируют однострочные комментарии в переработанной версии программы. Компилятор игнорирует все, что находится после //.

Форматированный вывод

Функция puts позволяет простым и удобным способом вывести строки в stdout. Но рано или поздно вам нужно будет вывести отформатированный текст с помощью функции printf — например, чтобы отобразить нестроковые аргументы. Функция printf принимает строку, описывающую формат вывода, и произвольное количество аргументов, представляющих собой значения, которые вы хотите вывести. Например, если вы хотите отобразить с помощью функции printf строку Hello,world!, то это можно сделать так:

printf("%s\n", "Hello, world!");

Первый аргумент — это строка форматирования "%s\n". Элемент %s — специ­фикация преобразования, которая заставляет функцию printf прочитать второй аргумент (строковой литерал) и вывести его в stdout. Элемент \n — это алфавитная управляющая последовательность, предназначенная для представления невидимых символов; она сообщает функции о том, что после текста нужно добавить перевод строки. Без этой последовательности следующие символы (скорее всего, приглашение командной строки) отображались бы в той же строчке. Этот вызов имеет следующий вывод:

Hello, world!

Следите за тем, чтобы данные, предоставленные пользователем, не стали первым аргументом функции printf, поскольку это чревато уязвимостью безопасности на основе отформатированного вывода (Сикорд, 2013).

Как уже было показано ранее, самый простой способ вывести строки — использовать функцию puts. Если в переработанной версии программы Hello, world! заменить ее функцией printf, то ваш код перестанет работать, поскольку printf возвращает состояние не так, как puts. В случае успеха функция printf возвращает количество выведенных символов, а при возникновении ошибки — отрицательное значение. Попробуйте в качестве упражнения сделать так, чтобы программа Hello, world! использовала printf.

Редакторы и интегрированные среды разработки

Для разработки программ на C можно использовать всевозможные редакторы и IDE. На рис. 1.1 перечислены самые популярные из них, согласно исследованию JetBrains за 2018 год.

 

Рис. 1.1. Использование IDE/редакторов

То, какие именно инструменты вам доступны, зависит от используемой вами системы. Данная книга ориентирована на Linux, Windows и macOS, поскольку это самые распространенные платформы для разработки.

Для Microsoft Windows очевидным выбором является Visual Studio IDE (https://visualstudio.microsoft.com/). Она предлагается в трех вариантах: Community, Professional и Enterprise. Преимущество версии Community состоит в ее бесплатности, а другие варианты имеют дополнительные, хоть и платные возможности. В этой книге вам будет достаточно версии Community.

В Linux все не так очевидно. Вы можете выбрать Vim, Emacs, Visual Studio Code или Eclipse. Vim — любимый текстовый редактор многих программистов и опытных пользователей. Он основан на проекте vi, написанном Биллом Джоем для одной из разновидностей Unix. Vim унаследовал от vi сочетания клавиш, но при этом предлагает дополнительные функции и расширяемость, которой недостает оригиналу. При желании для Vim можно установить подключаемые модули, такие как YouCompleteMe (https://github.com/Valloric/YouCompleteMe/) или deoplete (https://github.com/Shougo/deoplete.nvim/), которые предоставляют встроенное семантическое дополнение кода для программирования на C.

GNU Emacs — расширяемый, гибкий и бесплатный текстовый редактор. По своей сути это интерпретатор для Emacs Lisp, диалекта языка программирования Lisp с поддержкой расширений для редактирования текста — хотя для меня это никогда не было проблемой. Признаюсь: почти весь код на C, который я когда-либо разработал, был написан в Emacs.

Visual Studio Code (VS Code) — это удобный в использовании редактор кода с поддержкой таких аспектов разработки, как отладка, запуск заданий и управление версиями (подробности — в главе 11). Он предоставляет именно те инструменты, которые нужны программисту для быстрого цикла «программирование — сборка — отладка». VS Code работает на macOS, Linux и Windows и бесплатен для личного и коммерческого использования. Инструкции по установке доступны для Linux и других платформ6; в Windows вы, скорее всего, остановите свой выбор на Microsoft Visual Studio. На рис. 1.2 показан редактор Visual Studio Code в Ubuntu, с помощью которого написана программа из листинга 1.1. Как видно в консоли отладки, программа, как и ожидалось, вышла с кодом состояния 0.

 

Рис. 1.2. Visual Studio Code в Ubuntu

Компиляторы

Компиляторов C много, поэтому я не стану обсуждать их все. Каждый из них реализует определенные версии стандарта C. Многие компиляторы для встраиваемых систем поддерживают только C89/C90. Популярные компиляторы для Linux и Windows пытаются поддерживать современные стандарты, включая C2x.

GNU Compiler Collection

GCC (GNU Compiler Collection — набор компиляторов GNU) поддерживает C, C++, Objective-C и другие языки (https://gcc.gnu.org/). Разработка GCC проводится в соответствии с четким планом и под надзором руководящего комитета.

GCC — стандартный компилятор в системах Linux, хотя у него также есть версии для Microsoft Windows, macOS и других платформ. Установить GCC в Linux довольно просто. Например, ниже показана команда для установки GCC 8 в Ubuntu:

% sudo apt-get install gcc-8

Следующая команда позволяет проверить версию GCC, которая у вас установлена:

% gcc --version

gcc (Ubuntu 8.3.0-6ubuntu1~18.04) 8.3.0

This is free software; see the source for copying conditions. There is NO

Warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Если вы собираетесь разрабатывать программное обеспечение для Red Hat Enterprise Linux, то идеальной системой для этого будет Fedora. Ниже показано, как установить GCC в данной системе:

% sudo dnf install gcc

Clang

Еще один популярный компилятор — Clang (https://clang.llvm.org/). Установить Clang в Linux не составляет труда. Например, вот как это делается в Ubuntu:

% sudo apt-get install clang

Проверить, какая версия Clang у вас установлена, можно с помощью следующей команды:

% clang --version

clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)

Target: x86_64-pc-linux-gnu

Thread model: posix

InstalledDir: /usr/bin

Microsoft Visual Studio

Microsoft Visual Studio — самая популярная среда разработки для Windows, которая включает в себя как IDE, так и компилятор. На момент написания этой книги самой последней версией является Visual Studio 2019 (https://visualstudio.microsoft.com/downloads/). Она поставляется вместе с пакетом Visual C++ 2019, в который входят компиляторы для C и C++.

Параметры для Visual Studio можно указывать на экранах Project Property. Откройте вкладку Advanced в разделе C/C++ и убедитесь в том, что ваш код компилируется в режиме C; для этого должен быть выбран пункт Compile as C Code (/TC), а не Compile as C++ Code (/TP). Если файл имеет расширение .c, то по умолчанию компилируется с параметром /TC. Если же используется расширение .cpp, .cxx или одно из нескольких других, то компиляция происходит с /TP.

Переносимость

Все реализации компилятора C имеют некие особенности. Они постоянно развиваются, поэтому, к примеру, такой компилятор, как GCC, может иметь полноценную поддержку C17, а работы над поддержкой С2х еще ведутся; в этом случае одни возможности C2x будут поддерживаться, а другие — нет. Следовательно, компиляторы не поддерживают весь спектр спецификаций стандарта C (в том числе и промежуточных). В целом реа­лизации C развиваются медленно, и многие компиляторы существенно отстают от стандарта языка.

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

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

В дополнении J к стандарту C перечислено пять видов проблем переносимости:

поведение, определяемое реализацией;

• неуточненное поведение;

• неопределенное поведение;

• поведение, зависящее от региональных параметров;

распространенные расширения.

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

Поведение, определяемое реализацией

Поведение, определяемое реализацией (implementation-defined behavior), не оговаривается стандартом C и может иметь разные результаты в разных реализациях. При этом в рамках отдельно взятой реализации это поведение является предсказуемым и задокументированным. Примером может служить количество битов в байте.

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

Неуточненное поведение

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

Неопределенное поведение

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

когда требование вида «должно» или «не должно» нарушается и, по всей видимости, выходит за рамки ограничения, поведение не определено;

• когда поведение явно описано как неопределенное;

когда у поведения нет никакого явного определения.

Первые два случая часто называют явным неопределенным поведением, а третий — неявным неопределенным поведением. Но по своему значению они эквивалентны; каждый из них описывает поведение, которое не определено. Приложение J.2 к стандарту C, «Неопределенное поведение», содержит перечень примеров такого поведения в C.

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

дает разработчику реализации С повод не отлавливать программные ошибки, которые сложно диагностировать;

• избегает определения запутанных, граничных случаев, которые хорошо подходят только для какой-то одной стратегии реализации;

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

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

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

• вести себя задокументированным образом, характерным для окружения (с выводом диагностического сообщения или без него);

прекратить компиляцию или выполнение (с выводом диагностического сообщения).

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

Поведение, зависящее от региональных параметров, и распространенные расширения

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

Резюме

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

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

6 Инструкции по установке в Linux находятся на странице Visual Studio Code on Linux по адресу https://code.visualstudio.com/docs/setup/linux/.

7 У некоторых компиляторов есть «педантичный» режим, который может уведомлять программиста о таких проблемах переносимости.

Visual Studio Code (VS Code) — это удобный в использовании редактор кода с поддержкой таких аспектов разработки, как отладка, запуск заданий и управление версиями (подробности — в главе 11). Он предоставляет именно те инструменты, которые нужны программисту для быстрого цикла «программирование — сборка — отладка». VS Code работает на macOS, Linux и Windows и бесплатен для личного и коммерческого использования. Инструкции по установке доступны для Linux и других платформ6; в Windows вы, скорее всего, остановите свой выбор на Microsoft Visual Studio. На рис. 1.2 показан редактор Visual Studio Code в Ubuntu, с помощью которого написана программа из листинга 1.1. Как видно в консоли отладки, программа, как и ожидалось, вышла с кодом состояния 0.

У некоторых компиляторов есть «педантичный» режим, который может уведомлять программиста о таких проблемах переносимости.

Инструкции по установке в Linux находятся на странице Visual Studio Code on Linux по адресу https://code.visualstudio.com/docs/setup/linux/.

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

2. Объекты, функции и типы

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

Объекты, функции, типы и указатели

Объект — это хранилище, в котором можно представлять значения. Если быть точным, то в стандарте C (ISO/IEC 9899:2018) объектом называется «область хранилища данных в среде выполнения, содержимое которого может представлять значения» с примечанием: «при обращении к объекту можно считать, что он обладает определенным типом». Один из примеров объекта — переменная.

Переменные имеют объявленный тип, который говорит о том, какого рода объект представляет его значение. Например, объект типа int содержит целочисленное значение. Важность типа объясняется тем, что набор битов, представляющий объект одного типа, скорее всего, будет иметь другое значение, если его интерпретировать как объект другого типа. Например, в IEEE 754 (стандарт IEEE для арифметических операций с плавающей запятой) число 1 представлено как 0x3f800000 (IEEE 754–2008). Но если интерпретировать тот же набор битов как целое число, то вместо 1 получится значение 1 065 353 216.

Функции не являются объектами, но тоже имеют тип. Тип функции характеризуется как ее возвращаемым значением, так и числом и типами ее параметров.

Кроме того, в C есть указатели, которые можно считать адресами — областями памяти, в которых хранятся объекты или функции. Тип указателя, основанный на типе функции или объекта, называется ссылочным типом. Указатель, имеющий ссылочный тип T, называют указателем на T.

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

Объявление переменных

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

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

Листинг 2.1. Программа, которая должна менять местами два целых числа

#include <stdio.h>

 

void swap(int, int); // определена в листинге 2.2

 

int main(void) {

  int a = 21;

  int b = 17;

 

  

swap(a, b);

  printf("main: a = %d, b = %d\n", a, b);

  return 0;

}

Эта демонстрационная программа состоит из функции main с единственным блоком кода между фигурными скобками. Такого рода блоки называют составными операторами. Внутри функции main мы определяем две переменные, a и b. Мы объявляем их как переменные типа int и присваиваем им значения 21 и 17 соответственно. У всякой переменной должно быть объявление. Затем внутри main происходит вызов функции swap, чтобы поменять местами значения этих двух целочисленных переменных. В данной программе функция swap объявлена , но не определена. Позже в этом разделе мы рассмотрим некоторые потенциальные ее реализации.

Объявление нескольких переменных

Вы можете объявлять сразу несколько переменных, но это может сделать код запутанным, если они имеют разные типы или являются указателями или массивами. Например, все следующие объявления являются корректными:

char *src, c;

int x, y[5];

int m[12], n[15][3], o[21];

В первой строчке объявляются две переменные, src и c, которые имеют разные типы. Переменная src имеет тип char *, а c — тип char. Во второй строчке тоже происходит объявление двух переменных разных типов, x и y; первая имеет тип int, а вторая является массивом из пяти элементов типа int. В третьей строчке объявлено три массива, m, n и o, с разной размерностью и количеством элементов.

Эти объявления будет легче понять, если разделить их по отдельным строчкам:

char *src;    // src имеет тип char *

char c;       // c имеет тип char

int x;        // x имеет тип

...