Поскольку директива #include лишь подставляет текст другого файла на этапе препроцессора, многократное подключение одного и того же файла может приводить к ошибкам этапа компиляции. Поэтому в таких файлах используется защита от повторного включения с помощью макрокоманд https://deveducation.com/blog/luchshie-ide-dlya-razrabotki-na-c/ #define и #ifndef[42]. На практике перечисления часто используются для обозначения состояний конечных автоматов, для задания вариантов режимов работы или значений параметров[31], для создания целочисленных констант, а также для перечисления каких-либо уникальных объектов или свойств[32].
C++ и функциональные и скриптовые языки
- Динамическая память лишена этих недостатков, но имеет большие накладные расходы при её использовании и более сложна в использовании.
- Язык Си не предусматривает какого-либо контроля выхода за пределы массива, поэтому программист сам должен следить за работой с массивами.
- Поэтому стандарты языка, поддерживаемые компилятором и библиотекой, могут различаться.
- Для поддержки многобайтовых строк в программах на языке Си, такие строки должны поддерживаться на уровне текущей локали.
- Конструкторы в C++ не могут быть объявлены виртуальными, а деструкторы — могут, и обычно объявляются для всех полиморфных типов, чтобы гарантировать правильное уничтожение доступного по ссылке или указателю объекта независимо от того, какого типа ссылка или указатель.
- Для автоматического управления памятью в C++ традиционно используются так называемые «умные указатели», ручное же управление памятью снижает эффективность самих программистов[⇨].
Преобразования целочисленных типов могут происходить как явно, с помощью оператора приведения типов, так и неявно. Значения типов, меньших по размеру, чем int, при участии в каких-либо операциях или при передаче в вызов функции автоматически приводятся к типу int, а в случае невозможности преобразования — к типу unsigned int. Зачастую подобные неявные приведения необходимы, чтобы результат вычисления оказался правильным, но иногда приводят к интуитивно-непонятным ошибкам в вычислениях. Например, если в операции участвуют числа типа int и unsigned int, а знаковое значение отрицательно, то преобразование отрицательного числа к беззнаковому типу приведёт к переполнению и возникновению очень большого положительного значения, что может привести к неверному результату операций сравнения[19].
Избыточные и опасные возможности
В языке Си активно используется специальная переменная errno из заголовочного файла errno.h, в которую функции заносят код ошибки, возвращая при этом значение, являющееся маркером ошибки. Для проверки результата на ошибки результат сравнивают с маркером ошибки, и, если они совпадают, то можно проанализировать код ошибки, сохранённый в errno, для корректировки работы программы или вывода отладочного сообщения. В стандартной библиотеке стандарт зачастую лишь определяет возвращаемые маркеры ошибок, а выставление errno зависит от конкретной реализации[56]. Текст файла исходного кода на языке Си состоит из набора глобальных определений данных, типов и функций. Глобальные переменные и функции, объявленные со спецификаторами static и inline, доступны только в пределах того файла, в котором они объявлены, либо при включении одного файла в другой через директиву #include.
Развитие и стандартизация языка
В то же время использование сборщика мусора и виртуальной машины создают труднопреодолимые ограничения. Программы на Java, как правило, медленнее, требуют значительно больше памяти, к тому же виртуальная машина изолирует программу от операционной системы, делая невозможным низкоуровневое программирование. Некоторые особенности C++ позднее были перенесены в C, например, ключевые слова const и inline, объявления в циклах for и комментарии в стиле C++ (//). В более поздних реализациях C также были представлены возможности, которых нет в C++, например макросы va_arg и улучшенная работа с массивами-параметрами. Проверка доступа происходит во время компиляции, попытка обращения к недоступному члену класса вызовет ошибку компиляции.
Недостатки отдельных элементов языка
Стандартизируется установка errno многими функциями, позволяя обрабатывать ошибки, возникающие, например, в функциях работы с файлами, а также вводятся потокобезопасные аналоги некоторых функций стандартной библиотеки, безопасные варианты которых в стандарте языка присутствуют лишь в расширении K[114]. Язык программирования C++ был создан из Си и унаследовал его синтаксис, дополнив его новыми конструкциями в духе языков Simula-67, Smalltalk, Modula-2, Ada, Mesa и Clu[93]. Основными дополнениями стали поддержка ООП (описание классов, множественное наследование, полиморфизм, основанный на виртуальных функциях) и обобщённого программирования (механизм шаблонов).
Операторы работы с указателями и членами класса
По мнению Алана Кэя, объектная модель «Алгол с классами», использованная в C++, уступает модели «всё — объект»[37], используемой в Objective-C, по общем объёму возможностей, показателям повторного использования кода, понимаемости, модифицируемости и тестируемости. В результате наследования класс-потомок получает все поля классов-предков и все их методы; можно сказать, что каждый экземпляр класса-потомка содержит подэкземпляр каждого из классов-предков. Если один класс-предок наследуется несколько раз (это возможно, если он является предком нескольких базовых классов создаваемого класса), то экземпляры класса-потомка будет включать столько же подэкземпляров данного класса-предка. Чтобы избежать такого эффекта, если он нежелателен, C++ поддерживает концепцию виртуального наследования. При наследовании базовый класс может объявляться виртуальным; на все виртуальные вхождения класса-предка в дерево наследования класса-потомка в потомке создаётся только один подэкземпляр.
Например, программистами часто игнорируется проверка результата типа ssize_t, а сам результат используется дальше в вычислениях, что приводит к трудно уловимым ошибкам, если возвращается -1[59]. Препроцессор работает до компиляции и преобразует текст файла программы согласно встреченным в нём или переданным в препроцессор директивам. Технически препроцессор может быть реализован по-разному, но логически его удобно представлять именно как отдельный модуль, целиком обрабатывающий каждый предназначенный для компиляции файл и формирующий текст, попадающий затем на вход компилятора.
Отсутствие единой практики обработки ошибок в стандартной библиотеке приводит к появлению собственных способов обработки ошибок и комбинированию часто используемых способов в сторонних проектах. Например, в проекте systemd совместили идеи возвращения кода ошибки и числа -1 в качестве маркера — возвращается отрицательный код ошибки[61]. А в библиотеке GLib ввели в практику возвращение в качестве маркера ошибки значение булева типа, в то время как подробная информация об ошибке помещается в специальную структуру, указатель на которую возвращается через последний аргумент функции[62].
Препроцессор ищет в тексте строки, начинающиеся с символа #, вслед за которым должны следовать директивы препроцессора. Всё, что не относится к директивам препроцессора и не исключено из компиляции согласно директивам, передаётся на вход компилятора в неизменном виде. Объединения необходимы в тех случаях, когда требуется обращаться к одной и той же переменной как к разным типам данных; обозначаются ключевым словом union.
Со стандарта C99 последним элементом структур допускается объявлять массив произвольной длины, что широко используется на практике и поддерживается различными компиляторами. При этом нельзя объявлять массив таких структур и нельзя их помещать в другие структуры. В операциях над такой структурой массив произвольной длины обычно игнорируется, в том числе и при вычислении размера структуры, а выход за пределы массива влечёт за собой неопределённое поведение[37]. Для написания портируемого кода на C++ требуется огромное мастерство и опыт, и «небрежные» коды на C++ с высокой вероятностью могут оказаться непортируемыми[43]. По мнению Линуса Торвальдса, для обеспечения на C++ портируемости, аналогичной Си, программист должен ограничиться возможностями C++, унаследованными от Си[33]. Стандарт содержит множество элементов, определённых как «implementation-defined» (например, размер указателей на методы классов в различных компиляторах варьируется в диапазоне от 4 до 20 байт[44]), что ухудшает портируемость программ с их использованием.
Область инициализированных данных — сегмент данных — тоже содержит глобальные переменные, но в эту область попадают те переменные, которым было задано начальное значение. Неизменяемые данные, включающие в себя переменные, объявленные с модификатором const, строковые литералы и другие составные литералы, помещаются в сегмент текста программы. Сегмент текста программы содержит также исполняемый код и доступен только на чтение, поэтому попытка изменения данных из этого сегмента приведёт к неопределённому поведению в виде ошибки сегментации. Глобальные переменные и функции, кроме static и inline, могут быть доступны из других файлов при условии их надлежащего объявления там со спецификатором extern. Переменные и функции, объявленные с модификатором static, также могут быть доступны в других файлах, но лишь при передаче их адреса по указателю. При необходимости использования в других файлах они должны быть там продублированы либо вынесены в отдельный заголовочный файл.
Новый язык неожиданно для автора приобрёл большую популярность среди коллег и вскоре Страуструп уже не мог лично поддерживать его, отвечая на тысячи вопросов. Поддерживает такие парадигмы программирования, как процедурное программирование, объектно-ориентированное программирование, обобщённое программирование. Язык имеет богатую стандартную библиотеку, которая включает в себя распространённые контейнеры и алгоритмы, ввод-вывод, регулярные выражения, поддержку многопоточности и другие возможности. В сравнении с его предшественником — языком C — наибольшее внимание уделено поддержке объектно-ориентированного и обобщённого программирования[5].
Часть языков-потомков надстраивает Си дополнительными средствами и механизмами, добавляющими поддержку новых парадигм программирования (ООП, функциональное программирование, обобщённое программирование и пр.). К таким языкам относятся, прежде всего, C++ и Objective-C, а опосредованно — их потомки Swift и D. Также известны попытки улучшить Си, исправив его наиболее существенные недостатки, но сохранив его привлекательные черты. Иногда оба направления развития объединяются в одном языке, примером может служить Go. Арифметические операции с вещественными числами также являются неточными и зачастую имеют некоторую плавающую погрешность[20].
Например, на большинстве платформ следующая программа печатает «С», если компилируется компилятором C, и «C++» — если компилятором C++. Так происходит из-за того, что символьные константы в C (например, ‘a’) имеют тип int, а в C++ — тип char, а размеры этих типов обычно различаются. Контейнеры, строки, алгоритмы, итераторы и основные утилиты, за исключением заимствований из библиотеки C, собирательно называются STL (Standard Template Library — стандартная шаблонная библиотека).
IT курсы онлайн от лучших специалистов в своей отросли https://deveducation.com/ here.