В частности, стандарт POSIX.1 предоставляет набор функций и возможностей для создания приложений реального времени[87][88][89], однако поддержка жёсткого реального времени должна быть также реализована и со стороны операционной системы[90]. При этом способ обработки ошибок определяется конкретной реализацией стандартной библиотеки и может отсутствовать совсем. Поэтому в платформонезависимом коде может потребоваться проверка результата сразу двумя способами, в зависимости от значения math_errhandling[64].
На платформах, не соответствующих POSIX, поведение данной программы будет неопределённым в случае ошибки[⇨]. Освобождение ресурсов по ошибкам находится за основным алгоритмом для повышения читабельности, а переход осуществляется с помощью goto[65]. Область неинициализированных данных содержит глобальные переменные (в том числе, объявленные как static), которые не были проинициализированы в программном коде. Область инициализированных данных — сегмент данных — тоже содержит глобальные переменные, но в эту область попадают те переменные, которым было задано начальное значение. Неизменяемые данные, включающие в себя переменные, объявленные с модификатором const, строковые литералы и другие составные литералы, помещаются в сегмент текста программы. Сегмент текста программы содержит также исполняемый код и доступен только на чтение, поэтому попытка изменения данных из этого сегмента приведёт к неопределённому поведению в виде ошибки сегментации.
Также поддерживаются опции встраивания в код проверок выхода за пределы массива, разрушения стека, выхода за пределы динамической памяти, чтения неинициализированных переменных, возможностей неопределённого поведения и т. Однако дополнительные проверки могут сказаться на производительности итогового приложения, поэтому чаще всего их применяют только на этапе отладки. При этом стандартным средством форматированного ввода-вывода являются именно функции с переменным числом параметров (printf(), scanf() и другие), не способные проверить соответствие списка аргументов строке формата. Невозможно статически проконтролировать даже все вызовы функции printf(), поскольку строка формата может создаваться в программе динамически. Системные функции для работы с динамически выделяемой памятью не обеспечивают контроля за правильностью и своевременностью её выделения и освобождения, соблюдение правильного порядка работы с динамической памятью полностью возлагается на программиста. Его ошибки, соответственно, могут приводить к обращению по некорректным адресам, к преждевременному освобождению либо к утечке памяти (последнее возможно, например, если разработчик забыл вызвать free() или вызывающую free() функцию, когда это требовалось)[101].
Стандарт языка даёт программисту большую свободу действий и тем самым — высокие шансы на допущение ошибок. Многое из того, что чаще всего нельзя делать, дозволено языком, и компилятор в лучшем случае выдаёт предупреждения. Хотя современные компиляторы позволяют переводить все предупреждения в класс ошибок, эта возможность используется редко, гораздо чаще предупреждения игнорируются, если программа работает удовлетворительно. Некоторые компиляторы идут в комплекте с компиляторами других языков программирования (включая C++) или являются составной частью среды разработки программного обеспечения. Ошибки требуется проверять и правильно на них реагировать, в том числе часто требуется пробрасывать ошибку из функции на уровень выше для анализа. При этом функцию, в которой произошла ошибка, можно делать реентерабельной, в таком случае по ошибке функция не должна изменять входные или выходные данные, что позволяет безопасно перезапускать её после исправления ошибочной ситуации.
Проблемы И Критика[править Править Код]
Если константе не сопоставлено число, то ей автоматически задаётся либо 0 для первой константы в списке, либо число на единицу бо́льшее, чем задано в предыдущей константе. При этом сам тип данных перечисления по факту может соответствовать любому знаковому или беззнаковому примитивному типу, в диапазон которого умещаются все значения перечислений; решение о выборе того или иного типа принимает компилятор. Альтернативой обычным строкам могут служить широкие строки, в которых каждый символ хранится в специальном типе wchar_t. Данный тип по стандарту должен быть способен уместить в себе все символы самой большой из существующих локалей.
Для поддержки многобайтовых строк в программах на языке Си, такие строки должны поддерживаться на уровне текущей локали. Для явного задания кодировки можно менять текущую локаль с помощью функции setlocale() из заголовочного файла locale.h. Однако задание кодировки для локали должно поддерживаться используемой стандартной библиотекой.
Область стека предназначена для размещения данных, связанных с вызовом функций, и локальных переменных. Перед каждым запуском функции стек увеличивается для размещения в нём аргументов, передаваемых в функцию. После завершения работы функции стек уменьшается до того значения, которое было перед вызовом, однако этого может не происходить при некорректной работе со стеком. В качестве результата функция main() может вернуть любое целое число в диапазоне значений типа int, которое будет передано операционной системе или другому окружению в качестве кода возврата программы[42]. Обычно операционная система, где работают программы, имеет те или иные средства, позволяющие получить значение кода возврата и проанализировать его. Общим является соглашение о том, что нулевое значение кода возврата сигнализирует об успешном завершении программы, а ненулевое представляет собой код возникшей ошибки.
Пользовательские Типы[править Править Код]
В стандарте С99 язык получил новые возможности, такие как массивы переменной длины и встраиваемые функции. Однако с тех пор язык развивается медленно, и в стандарт C18 попали лишь исправления ошибок стандарта C11. Существуют специальные программные средства для статического анализа кода на Си для выявления не-синтаксических ошибок. Их применение не гарантирует безошибочности программ, но позволяет выявить значительную часть типичных ошибок и потенциальных уязвимостей.
Из-за различных допущений в языке программы могут компилироваться со множественными ошибками, что часто приводит к непредсказуемому поведению программы. Современные компиляторы предоставляют опции для статического анализа кода[98][99], но даже они не способны выявить все возможные ошибки. Результатом неграмотного программирования на Си могут стать уязвимости программного обеспечения, что может сказаться на безопасности его использования.
Функции для работы с широкими строками описаны в заголовочном файле wchar.h, а функции для работы с широкими символами описаны в заголовочном файле wctype.h. Минимальный диапазон значений целых типов по стандарту определяется с -(2N-1-1) по 2N-1-1 для знаковых типов и с zero https://deveducation.com/ по 2N-1 — для беззнаковых, где N — разрядность типа. Минимальное и максимальное значения каждого типа указывается в файле limits.h в виде макроопределений.
Заголовочный файл stdlib.h определяет два общих макроопределения EXIT_SUCCESS и EXIT_FAILURE, которые соответствуют успешному и неуспешному завершению работы программы[44]. Коды возврата также могут использоваться в рамках приложений, включающих в себя множество процессов, для обеспечения взаимодействия между этими процессами, в случае чего приложение само определяет смысловое значение для каждого кода возврата. Существует много разных кодировок, в которых отдельный символ может быть запрограммирован разным количеством байт. В Си существует набор функций для преобразования строк из многобайтовых в рамках текущей локали в широкие и наоборот. Функции для работы с многобайтовыми символами имеют префикс либо суффикс mb и описаны в заголовочном файле stdlib.h.
Отсутствие единой практики обработки ошибок в стандартной библиотеке приводит к появлению собственных способов обработки ошибок и комбинированию часто используемых способов в сторонних проектах. Например, в проекте systemd совместили идеи возвращения кода ошибки и числа -1 в качестве маркера — возвращается отрицательный код ошибки[60]. А в библиотеке GLib ввели в практику возвращение в качестве маркера ошибки значение булева типа, в то время как подробная информация об ошибке помещается в специальную структуру, указатель на которую возвращается через последний аргумент функции[61].
Тип wchar_t задумывался для того, чтобы в него мог поместиться любой символ, а широкие строки — для хранения строк любой локали, но в результате API оказался неудобным, а реализации — платформозависимыми. В то же время на платформах Linux[24] и macOS данный тип занимает 32 бита, поэтому для реализации кроссплатформенных задач тип wchar_t не подходит. Как правило возникновение ошибки требует завершения работы функции с возвращением индикатора ошибки.
Для исполняемой программы стандартной точкой входа является функция с именем main, которая не может быть статической и должна быть единственной в программе. Исполнение программы начинается с первого оператора функции main() и продолжается до выхода из неё, после чего программа завершается и возвращает операционной системе абстрактный целочисленный код результата своей работы. Глобальные переменные и функции, кроме static и inline, могут быть доступны из других файлов при условии их надлежащего объявления там со спецификатором extern. Переменные и функции, объявленные с модификатором static, также могут быть доступны в других файлах, но лишь при передаче их адреса по указателю. При необходимости использования в других файлах они должны быть там продублированы либо вынесены в отдельный заголовочный файл.
Язык широко применяется при разработке операционных систем, на уровне прикладного интерфейса операционных систем, во встраиваемых системах, а также для создания высокопроизводительного или критического в плане обработки ошибок кода. Одной из причин широкого распространения для программирования на низком уровне является возможность писать кроссплатформенный код, который может по-разному обрабатываться на разном оборудовании и на разных операционных системах. Структуры представляют собой объединение переменных разных типов данных в рамках одной области памяти; обозначаются ключевым словом struct. С точки зрения адресного пространства поля всегда идут друг за другом в том же порядке, в котором указаны, но компиляторы могут выравнивать адреса полей для оптимизации под ту или иную архитектуру. Таким образом, фактически поле может занимать бо́льший размер, чем указано в программе. Перечисления представляют собой набор именованных целочисленных констант и обозначаются с помощью ключевого слова enum.
- Данный тип по стандарту должен быть способен уместить в себе все символы самой большой из существующих локалей.
- Минимальное и максимальное значения каждого типа указывается в файле limits.h в виде макроопределений.
- При этом функции и переменные, объявленные в заголовочном файле со словом static, будут создаваться заново при каждом подключении заголовочного файла к очередному файлу с исходным кодом.
- Например, в проекте systemd совместили идеи возвращения кода ошибки и числа -1 в качестве маркера — возвращается отрицательный код ошибки[60].
Си (от лат. буквы C англ. языка[⇨]) — компилируемый статически типизированный язык программирования общего назначения, разработанный в 1969—1973 годах сотрудником Bell Labs Деннисом Ритчи как развитие языка Би. Первоначально был разработан для реализации операционной системы UNIX, но впоследствии был перенесён на множество других платформ. Язык программирования Си оказал существенное влияние на развитие индустрии программного обеспечения, а его синтаксис стал основой для таких языков программирования, как C++, C#, Java и Objective-C. В примере реализована функция чтения файла на языке Си, однако она требует соответствия функций fopen() и fread() стандарту POSIX, иначе они могут не выставлять переменную errno, что сильно усложняет как отладку, так и написание универсального и безопасного кода.
Тем не менее среди языков, достигших определённого распространения, прямых потомков у Си немного. Также для Си существуют и другие инструменты, облегчающие и дополняющие разработку, включая статические анализаторы и утилиты для форматирования кода. А автоматическое форматирование кода упрощает организацию совместной работы в системах контроля версий, минимизируя конфликты из-за стилевых правок. Существует также много других систем для тестирования кода на Си, таких как AceUnit, GNU Autounit, cUnit и других, но они либо не осуществляют тестирование в изолированных окружениях, либо предоставляют мало возможностей[76], либо перестали развиваться. Недостатком данного подхода является то, что формат назначаемых обработчиков не предусматривает передачу произвольных данных в функцию, что позволяет создавать обработчики только для глобальных переменных.
Глобальные переменные не позволяют писать реентерабельные алгоритмы, а автоматическое выделение памяти не позволяет возвращать произвольную область памяти из вызова функции. Автоматическое выделение также не подходит для выделения больших объёмов памяти, поскольку может привести к порче стека c# среда разработки или кучи[45]. Динамическая память лишена этих недостатков, но имеет большие накладные расходы при её использовании и более сложна в использовании. Также со стандарта C99 добавлены типы intmax_t и uintmax_t, соответствующие самым большим знаковому и беззнаковому типам соответственно.
При этом во многих программах наиболее требовательные к ресурсам части принято писать на языке Си. Ядро программы Mathematica[85] написано на Си, а MATLAB, изначально написанный на Фортране, был переписан на Си в 1984 году[86]. Язык Си унаследовал линейную адресацию памяти при работе со структурами, массивами и выделенными областями памяти. Стандарт языка также допускает выполнение операций сравнения над нулевым указателем и над адресами в рамках массивов, структур и выделенных областей памяти. Также допускается работа с адресом элемента массива, следующим за последним, что сделано для облегчения написания алгоритмов. Однако сравнение указателей адресов, полученных для разных переменных (или областей памяти) не должно осуществляться, так как результат будет зависеть от реализации конкретного компилятора[48].
Для освобождения ресурсов в рамках программы предусмотрен механизм обработчиков выхода из программы. Обработчики назначаются с помощью функции atexit() и исполняются как по завершении функции main() через оператор return, так и по исполнению функции exit(). В Си отсутствуют какие-либо встроенные механизмы контроля ошибок, но существует несколько общепринятых способов их обработки средствами языка. В общем виде практика обработки ошибок языка Си в отказоустойчивом коде вынуждает писать громоздкие, часто повторяющиеся конструкции, в которых алгоритм совмещён с обработкой ошибок[⇨]. Для автоматически выделяемых переменных с помощью модификатора register можно давать подсказку компилятору о необходимости быстрого доступа к ним.
Технически препроцессор может быть реализован по-разному, но логически его удобно представлять именно как отдельный модуль, целиком обрабатывающий каждый предназначенный для компиляции файл и формирующий текст, попадающий затем на вход компилятора. Препроцессор ищет в тексте строки, начинающиеся с символа #, вслед за которым должны следовать директивы препроцессора. Всё, что не относится к директивам препроцессора и не исключено из компиляции согласно директивам, передаётся на вход компилятора в неизменном виде. Константы, указанные в двух разных перечислениях, относятся к двум разным типам данных, независимо от того, являются ли перечисления именованными или анонимными. Для хранения размера предусмотрен беззнаковый тип size_t из заголовочного файла stddef.h. Данный тип способен уместить максимально возможное количество байт, доступное по указателю, и обычно используется для хранения размера в байтах.