Друзья! В данной статье мы бы хотели порассуждать на тему использования инструментария языка C в C++, и как это может повлиять на исходную программу.
Ссылки на полезные ресурсы вы сможете увидеть в конце статьи, и обязательно делитесь своим мнением в комментариях, нам будет очень интересно с ним ознакомиться!
История C++
Чтобы понять, почему C и C++ часто используют в одном коде и к чему это может привести, начнём с истории создания языка C++.
В 70-е годы язык C стал революцией в мире программирования, предоставив разработчикам гибкость и мощь для низкоуровневого управления системой. Однако с увеличением сложности программ возникла необходимость в языках, поддерживающих абстракции. Это понимание и привело к созданию языка C++.
В конце 70-х годов, будучи аспирантом Кембриджского университета, Бьёрн Страуструп задался целью создать язык, который бы сочетал производительность C с поддержкой высокоуровневых абстракций. Этот язык он изначально назвал "C with Classes" — 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 << 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
Комментарии (41)
kenomimi
13.11.2024 18:25А как их не использовать совместно? Распространенный lvgl написан на C, gtk - снова C, и так куда не плюнь - везде пересечения...
MonkeyWatchingYou
13.11.2024 18:25Прошу не серчать, я не програмист, я конструктор, но сообщество ИТ специфично и интересно (да, это как отдушина).
Суть вопроса (ситуации):
Поскольку согласен, что это разные языки и мешать их не самая лучшая практика, но!
Столкнулся с тем, что при написании библиотеки бинарного хранения конфиг файлов используя C++ уперся в стену:
- В С++ решение выделить память С ВЫРАВНИВАЕМ и последующим удалением мягко скажем многословней чем в СИ.
- Само выделение очень вариативно и по синтаксису и по способам, это сбивает с толку новичка
- Даже выделив её покапаться изрядно в ней (перемещения, наложения и пр.) средствами C++ используя только его и std:: порождает ощущение танцев очень похожих на Си, но с прелюдией.
Да, есть обёртки над СИшными операциями с блоками памяти, но я не увидел какого то нового уровня обеспечения безопасности при большей многословности кода.Буду признателен:
Лаконичному примеру в C++ который алоцирует с выравниванием память и совершает например запись люб. значения по люб. по адресу и в добавок выполняет аналог memmove с наложением. Но сугубо в рамках C++ и std::
Спасибо!Janycz
13.11.2024 18:25А почему в C++ выделить память сложнее с выравниванием? Есть же
T *PTR = new (std::align_val_t(ALIGN)) T[ARRAY_SIZE]
(для удаленияdelete[] (std::align_val_t(ALIGN), PTR)
, также есть версия без[]
) илиstd::aligned_alloc
(для удаленияstd::free
). Более того, вы просили пример, вот он: https://pastebin.com/TPHftGiA (да, я использовалrand()
, который типа C, -- но это просто для заполнения массива, то, что вы просили, сделано средствами C++).StillNEntity
13.11.2024 18:25Действительно, похоже на код в стиле C++, однако я не очень понимаю, чем тот же оператор new так сильно отличается от вызова malloc из std библиотеки C. Хотя отличия несомненно есть, это не вносит существенной разницы. В частности, в вашем примере оператор new - это функция malloc, сказанная другими словами. Да и в случае перевыделения памяти в C++ всё равно неизбежно приходится использовать realloc. А, простите, из-за того, что всю стандартную библиотеку языка C поставили под неймспейс std, считать код подогнанным под идеологию C++, ну, лично для меня, не очень корректно.
Jijiki
13.11.2024 18:25Можете уточнить что значит бинарное хранение конфиг файлов и почему нужно именно выравнивание?
kenomimi
13.11.2024 18:25что значит бинарное хранение конфиг файлов
берем структуру, и пишем ее как есть в файл/eeprom. Обычно актуально для маленьких микроконтроллеров, где сериализация будет дорогой штукой по ресурсам. Ну а чтобы это занимало минимум места, выравниваем побайтово.
Вопрос только в том, причем тут плюсы... Обычно в таких штуках чистый С.
redfox0
13.11.2024 18:25Непонятно причём здесь выравнивание, достаточно добавить атрибут, чтобы лишних паддингов не было:
struct __attribute__((packed)) Foo { char a; int c; char b; };
Jijiki
13.11.2024 18:25спасибо, просто мне надо было уточнить о чем речь + я попутно посмотрел хотябы на своём коде cache-misses в моём коде (без контроллеров с задержкой 60 мс программа даёт 39процентов, если выключить задержку вообще 1 процент, ) .
насчет примера тоже спасибо. приложу свой пример (пример к тому что файл это набор char) там не возьмусь прям советовать как лучше
unordered_map<string,vector<pair<int,vector<uint8_t>>>> worker; ... void serialize() { ofstream file; file.open("assets.bin",ios::binary); int s=worker.size(); //cout << s <<" 1"<< endl; file.seekp(0); file.write(reinterpret_cast<const char*>(&s),sizeof(s)); for(auto a:worker) { const char* b=a.first.data(); int slen=a.first.size(); int n1=a.second.size(); file.write(reinterpret_cast<const char*>(b),sizeof(char)*40); file.write(reinterpret_cast<char*>(&n1),sizeof(n1)); //cout << b <<" 3"<< endl; std::vector<std::pair<int,vector<uint8_t>>> l=a.second; for(auto& c:l) { int k=c.first; vector<uint8_t>t=c.second; //cout << k <<" 2"<< endl; file.write(reinterpret_cast<const char*>(&k),sizeof(k)); writeVector(file,t); } } file.close(); } //сразу файлами тоесть на входе факторка дающая указатели //или загрузил все файлы и через факторку дал указатели, //где в программе удалил и по новой единственный нюанс не знаю //как на контроллерах на андроид как я понял надо кинуть в JNI ifstream //я заюзал SDL3 уже перегруженое чтение
mOlind
13.11.2024 18:25Язык C достаточно простой и эффективный. На нем можно писать простые функции и эффективные решения. На нем много библиотек. Если вы делаете ошибки в C - рано увеличивать сложность и переходить к C++. И совет не используйте выделение памяти в стиле С в C++ потому что можно выделить что-то в стеке... Это очень странно.
9241304
13.11.2024 18:25Каша какая-то. Нет ничего плохого в использовании готового C кода в плюсах. А вот за что надо руки отбивать - за программирование в сишном стиле на плюсах. Ну и как-то по детски написано. Что ж будет, когда автор откроет для себя stdcall и cdecl)
ImagineTables
13.11.2024 18:25Здравое зерно, безусловно, есть. Я думаю, всем будет лучше, если люди, которым не нравятся сишные корни, отселятся уже в какой-нибудь отдельный пузырь. Жаль, что этого не случилось сразу после появления «Си с классами».
Serpentine
13.11.2024 18:25Тема манглинга немного не соответствует названию статьи. Есть мнение, что у каждой хорошей С++ библиотеки должен быть С-интерфейс (см. в конце этой статьи), как раз потому что в C нет манлирования.
На мой взгляд, уместнее было бы рассказать о разнице в приведении типов (как например здесь) или о поведении C-cast в C++ коде.
На правах оффтопа: вы же игровой движок разрабатываете, было бы гораздо интереснее прочитать от вас кейсы по этой теме причем любой. Например, как 3D редактор в него интегрировали. Гайджины и Playrix в своих блогах раньше хорошие статьи размещали по теме.
На правах шутки:
Тут один известный в геймдеве мужик в очках рассказывает, как он пишет на C++ сейчас (см. с отметки 1:55)
Melirius
13.11.2024 18:25cin и cout весьма медленные по сравнению с scanf и printf. Так что если нужно выжимать производительность, то совет из статьи так себе.
Janycz
13.11.2024 18:25Если правильно использовать (
std::ios::sync_with_stdio(false); std::cin.tie(nullptr);
и не пользоватьсяstd::endl
(он сбрасывает буффер вывода, вместо него лучше использовать'\n'
-- это не сбрасывает буффер вывода), то std::cin и std::cout будут быстрее. Другое дело, что в Microsoft Visual C++ не поддерживаетсяstd::ios::sync_with_stdio(false);
(ну как бы такая функция есть, но она ничего не делает, т. е. std::cin и std::cout не ускоряются от нее)... Но даже так в C++ стандартный ввод-вывод все еще остается достаточно медленным.Melirius
13.11.2024 18:25Хмм, у меня и на Clang со всеми этими ухищрениями было медленнее. Надо перепроверить на 20-м.
VoodooCat
13.11.2024 18:25Проблема с cout, имхо, не сколько в скорости, а в том что этим просто невозможно пользоваться: IO-манипуляторы это какое-то нежелательное состояние да еще и очень многословное. То ли запоминать состояние и восстанавливать, то ли портить его. Тот же printf этим не страдает.
А в итоге - если вам нужно вывалить из многопроцессной среды лог - в один stdout/stderr (файл или пайп) - всего то и надо что sprintf + запись в файл, и на относительно коротких сообщениях это будет атомарно и не будут рваться строки. Разумеется лучше когда это в обернуто в приличную библиотку. Но сам смысл не меняется - cout в стандартной реализации как-то никамильфо.
Это же подтверждают мириады языков где форматирование похоже на printf с "местечковыми" различиями.
dombran
13.11.2024 18:25Автор статьи скорее расписался в недостаточной квалификации нежели привел доводы.
С и С++ хоть и похожи, но это инструменты для разных задач. Это как раньше писали код на С и делали ассемблерных вставки для ускорения задач. Так и сейчас в С++ вставки на С для ускорения кода и возможности работы напрямую с памятью без API прокладок. Я уже молчу что код на С без проблем переносится от версии к версии компилятора.
Dooez
13.11.2024 18:25Какие вставки на C вы делаете для ускорения задач?
На текущий момент язык C - это в первую очередь ABI склеивающий разные языки. Если смотреть на него с такой точки зрения то это действительно разные задачи с C++.
dmitrysbor
13.11.2024 18:25В своё время именно незабвенное творение Дохлогострауса и стандартной библиотеки С достало меня настолько, что я поменял карьеру и ушёл из разработки насовсем в QA автоматизацию на Python. Я пришёл к выводу, что за все годы кодинга на С/С++ я не написал ничего значительного, а какие то отдельные куски кода в каких то подсистемах, чаще всего которые даже протестировать то нормально было невозможно. Ну юнит тест, да: слёзы одни. Просто фрагментарные куски в огромной системе. Да, я фиговый разработчик и мне не особенно нравится писать код по требованиям, но даже с учётом этого С/С++ никогда не давал мне ощущения законченности и не приносил удовлетворения (не говоря о куче ошибок, которые находили потом QA). Да, на СС++ написаны драйвера и операционные системы и прочий рокет сайенс, но это просто не мой уровень.
AndreyFr
13.11.2024 18:25Ну видимо тебе плевать, когда прога, которая должна жрать 300 КБ жрет 10 МБ и т.д.
Я вот от такого не имею никакой удовлетворенности...
dmitrysbor
13.11.2024 18:25Рынок диктует скорость при разработке и да, вообще плевать сколько занимает памяти. Вот на время прогона нет. Я же не сетевые драйвера пишу.
AndreyFr
13.11.2024 18:25Угу. Рынок - хрынок. Кто на что уповает...
dmitrysbor
13.11.2024 18:25Тесты на С/С++? Наверное кто то всё ещё пишет для себя всякие юнит-тесты. Но уж точно с такой специализацией вы работу не найдёте никогда ;)
AndreyFr
13.11.2024 18:25Как вообще то что ты написал, ко мне относится ?
dmitrysbor
13.11.2024 18:25То что к тебе относится это твоё дело. Мой ответ касается моего комментария
brownfox
13.11.2024 18:25Каждому овощу - свой фрукт :)
Я охотно использую python для быстрого прототипирования сервисов и систем, но когда речь заходит о продакшен-версиях, многое переписывается на плюсы именно из-за выигрыша в быстродействии и памяти. В итоге, получаем продукт, где верхнеуровневые грабли, в целом, пойманы и ликвидированы, а производительность в разы выше, чем в прототипе.
Что до С в С++, то "портабельный ассемблер" часто удобен, просто требует, как и любой инструмент, грамотного обращения. Например, аккуратных оберток для вызова из плюсов.
VoodooCat
13.11.2024 18:25"Необработанные массивы" - звучит очень, ну очень странно.
GidraVydra
13.11.2024 18:25А чего тут странного? Необработанный массив сосны, например, или дуба, ходовой товар, кстати. Если, повезет, то и ясеня, но чаще все-таки дуба.
VoodooCat
13.11.2024 18:25То, что необработанный массив - подразумевает состояние программы, массива, в частности его какой-то возможной обработки, что не мудрено - специфично для программы.
В данном случае же речь только о заворачивании его во враппер, однако враппер так же не имеет никакого свойства обработанности.
Пример с массивом сосны - мне нравится, если его обернуть стяжками для транспортировки - то состояние его обработанности никак не изменится. :)
AndreyFr
13.11.2024 18:25вредит чистоте и безопасности кода.
Как обычно, "старая песня" для криворуких, на какую я как-то ложил.
Продолжу использовать c в c++ и дальше, там где мне нравится.
Bear_Head_Studio
13.11.2024 18:25Можете пожалуйста уточнить как использование низкоуровневых конструкций C в коде на C++ может влиять на безопасность и производительность программы?
BorisU
про управление форматированием в C странный заход. Там все есть, что надо.