Почему не стоит использовать C в C++
Друзья! В данной статье мы бы хотели порассуждать на тему использования инструментария языка C в C++, и как это может повлиять на исходную программу.
Ссылки на полезные ресурсы вы сможете увидеть в конце статьи, и обязательно делитесь своим мнением в комментариях, нам будет очень интересно с ним ознакомиться!
История C++
Чтобы понять, почему C и C++ часто используют в одном коде и к чему это может привести, начнём с истории создания языка C++.
В 70-е годы язык C стал революцией в мире программирования, предоставив разработчикам гибкость и мощь для низкоуровневого управления системой. Однако с увеличением сложности программ возникла необходимость в языках, поддерживающих абстракции. Это понимание и привело к созданию языка C++.
В конце 70-х годов, будучи аспирантом Кембриджского университета, Бьёрн Страуструп задался целью создать язык, который бы сочетал производительность C с поддержкой высокоуровневых абстракций. Этот язык он изначально назвал «C with Classes» — 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
void print_message(const char* message) {
std::cout << message << std::endl;
}
void print_message(int number) {
std::cout << number << 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