Почему не стоит использовать C в C++

от автора

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

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

История C++

Чтобы понять, почему C и C++ часто используют в одном коде и к чему это может привести, начнём с истории создания языка C++.

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

В конце 70-х годов, будучи аспирантом Кембриджского университета, Бьёрн Страуструп задался целью создать язык, который бы сочетал производительность C с поддержкой высокоуровневых абстракций. Этот язык он изначально назвал «C with Classes»C с классами. На основе C он добавил концепцию классов и поддержал инкапсуляцию, что позволяло создавать более сложные структуры.

Создатель языка программирования C++ — Бьёрн Страуструп

Создатель языка программирования C++ — Бьёрн Страуструп

В 1982 году Страуструп развил язык, добавив новые возможности для решения проблем распределённых вычислений. Он назвал обновлённый язык C++. Ключевые новшества включали классы, функции-члены и основную поддержку объектно-ориентированного программирования (ООП). C++ всё ещё требовал компиляции в C-код, поэтому до появления специализированных компиляторов работал как препроцессор к C.

Развитие компиляторов C++

Первая версия компилятора C++, известная как Cfront, появилась в 1983 году. Это был инструмент, который переводил код на C++ в код на C. Первый публичный релиз — Cfront 1.0 — появился в 1985 году и уже был способен компилировать достаточно сложные программы, однако для работы с ним нужно было досконально знать язык C, поскольку ошибки или неполадки легко могли возникнуть из-за сочетания C и C++ конструкций.

С ростом популярности C++ начали разрабатываться независимые компиляторы. В 1987 году GCC (GNU Compiler Collection) добавил поддержку C++, а в 1989 году вышел Cfront 2.0, с более устойчивой поддержкой нового синтаксиса и улучшениями компиляции.

К 1990 году начал работу комитет по стандартизации ANSI C++, а в 1991 году — международный комитет ISO C++, что привело к появлению стандарта C++98, а позднее и его более новых версий: C++03, C++11, C++14, C++17, C++20, каждая из которых вносила дополнительные возможности и улучшения. Современный C++ далеко ушёл от своего предшественника, включив в себя поддержку шаблонов, многопоточности, систем обработки ошибок, стандартной библиотеки STL и множество других возможностей.

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


Почему использование C в C++ может быть вредным?

1. Управление памятью: char[] и malloc вместо std::string и new

В C++ предусмотрено множество средств для безопасного управления памятью, таких как умные указатели (std::unique_ptr, std::shared_ptr), класс std::string, контейнеры из библиотеки STL, и ключевое слово new. Однако, иногда разработчики, знакомые с C, продолжают использовать низкоуровневые конструкции C:

  • Необработанные массивы char[] вместо std::string

    Использование char[] требует ручного управления памятью, что повышает вероятность ошибок, особенно при динамическом выделении и освобождении памяти.

  • Функции malloc и free вместо new и delete

    В C++ new и delete интегрированы в систему типов языка, что делает их безопаснее. При использовании malloc и free в C++ отсутствует автоматическая инициализация и проверка типов, что может привести к неопределённому поведению.

Пример

// Небезопасно: низкоуровневый массив и malloc char* text = (char*) malloc(100); // необходимо вручную освобождать память strcpy(text, "Hello, world");  // Безопаснее: использование std::string std::string text = "Hello, world"; // автоматическое управление памятью

2. Ввод и вывод: scanf/printf вместо std::cin/std::cout

C++ предоставляет удобные и безопасные потоки для ввода и вывода, но иногда можно встретить scanf и printf, что несёт следующие риски:

  • Типобезопасность

    std::cin и std::cout проверяют типы при компиляции, тогда как scanf и printf полагаются на форматные строки, что может привести к ошибкам на этапе выполнения.

  • Читаемость и удобство.

    Потоки ввода-вывода C++ интуитивнее и легче читаются благодаря синтаксису << и >>.

  • Управление форматированием.

    С помощью манипуляторов (std::fixed, std::setprecision) легко управлять выводом, чего трудно достичь в printf.

Пример

// В стиле C++ int number; std::cout << "Enter a number: "; std::cin >> number; // безопасно и проверяет типы  // В стиле C printf("Enter a number: "); scanf("%d", &number); // типобезопасности нет, возможны ошибки

3. Заголовочные файлы: .h и .hpp

Смешивание заголовочных файлов C и C++ может привести к путанице:

  • Форматирование кода.

    В IDE часто настраивают форматирование под .h и .hpp файлы по-разному. Если использовать .h для C++-заголовков, можно случайно применить стилизацию C.

  • Путаница в имёнованиях.

    Заголовки C и C++ с похожими именами (например, MyClass.h и MyClass.hpp) помогают быстро различать файлы для C и C++, что особенно важно при использовании обёрток для библиотек на C.

  • Ошибки при подключении C-заголовков в C++.

    При подключении заголовков на C нужно оборачивать их в extern "C", чтобы избежать конфликтов вызовов из-за различного манглинга имён. Если не делать этого, могут возникнуть ошибки компиляции.

Пример

// C-заголовок, например, library.h #ifdef __cplusplus extern "C" { #endif  void someFunction();  #ifdef __cplusplus } #endif  // C++ заголовок library.hpp можно подключать без дополнительных настроек class MyClass { public:     void someMethod(); }; 

Манглинг в языках программирования C и C++

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

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

Пример манглинга в C и C++

Рассмотрим простую функцию на C:

// example.c void print_message(const char* message) {     printf("%s\n", message); }

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

Скомпилированный ассемблерный код будет выглядеть примерно так:

print_message:     push    rbp     mov     rbp, rsp     sub     rsp, 16     mov     rdi, rsi       ; аргумент для printf копируется в rdi     call    printf         ; вызов функции printf     leave     ret 

Здесь имя функции print_message остается неизменным в ассемблерном коде, и это имя будет использоваться при линковке.

Теперь рассмотрим аналогичную функцию на C++, но добавим к ней перегрузку:

// example.cpp #include <iostream>  void print_message(const char* message) {     std::cout &lt;&lt; message &lt;&lt; std::endl; }  void print_message(int number) {     std::cout &lt;&lt; number &lt;&lt; std::endl; }

Компилятор C++ создаст уникальные имена для каждой версии print_message, учитывая тип их параметров. Например, в ассемблере имена функций могут выглядеть так:

_Z13print_messagePKc:   ; "print_message" для const char* (строка)     push    rbp     mov     rbp, rsp     sub     rsp, 16     ; код вывода строки     leave     ret  _Z13print_messagei:     ; "print_message" для int (число)     push    rbp     mov     rbp, rsp     sub     rsp, 16     ; код вывода числа     leave     ret 

Эти имена _Z13print_messagePKc и _Z13print_messagei — сманглированные. Здесь содержатся:

  • _Z — префикс, указывающий, что это сманглированное имя.

  • 13 — длина имени функции print_message.

  • PKc — код для указателя на const char.

  • i — код для int.

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

Использование extern «C» для интеграции кода C и C++

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

Рассмотрим, как extern "C" поможет избежать ошибок:

// Подключение C-кода в C++ extern "C" {     #include "some_c_library.h" // библиотека на C } 

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

Пример ошибки линковки без extern «C»

Допустим, у нас есть функция на C:

// example.c void print_message(const char* message) {     printf("%s\n", message); }

При попытке вызвать эту функцию из C++ без extern "C" могут возникнуть проблемы:

// main.cpp #include "example.h" // файл с объявлением print_message  int main() {     print_message("Hello, World!");     return 0; } 

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

undefined reference to `print_message`

Чтобы избежать этой ошибки, добавим extern "C" к объявлению функции:

// example.h #ifdef __cplusplus extern "C" { #endif  void print_message(const char* message);  #ifdef __cplusplus } #endif

Теперь при компиляции C++ код будет воспринимать print_message как неманглированное имя, как в C, и ошибка исчезнет.

Полезные ресурсы

История C++ — https://www.geeksforgeeks.org/history-of-c/

Наше руководство по стилизации кода на C++ — https://case-technologies.ru/guides.php

Манглирование — https://en.wikipedia.org/wiki/Name_mangling

Наши ссылки

Официальный сайт — https://case-technologies.ru/

Наш GitHub — https://github.com/case-tech


ссылка на оригинал статьи https://habr.com/ru/articles/858366/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *