Друзья! В данной статье мы бы хотели порассуждать на тему использования инструментария языка 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

Комментарии (41)


  1. BorisU
    13.11.2024 18:25

    про управление форматированием в C странный заход. Там все есть, что надо.


  1. anonymous
    13.11.2024 18:25

    НЛО прилетело и опубликовало эту надпись здесь


  1. kenomimi
    13.11.2024 18:25

    А как их не использовать совместно? Распространенный lvgl написан на C, gtk - снова C, и так куда не плюнь - везде пересечения...


  1. MonkeyWatchingYou
    13.11.2024 18:25

    Прошу не серчать, я не програмист, я конструктор, но сообщество ИТ специфично и интересно (да, это как отдушина).

    Суть вопроса (ситуации):
    Поскольку согласен, что это разные языки и мешать их не самая лучшая практика, но!
    Столкнулся с тем, что при написании библиотеки бинарного хранения конфиг файлов используя C++ уперся в стену:

    - В С++ решение выделить память С ВЫРАВНИВАЕМ и последующим удалением мягко скажем многословней чем в СИ.
    - Само выделение очень вариативно и по синтаксису и по способам, это сбивает с толку новичка
    - Даже выделив её покапаться изрядно в ней (перемещения, наложения и пр.) средствами C++ используя только его и std:: порождает ощущение танцев очень похожих на Си, но с прелюдией.


    Да, есть обёртки над СИшными операциями с блоками памяти, но я не увидел какого то нового уровня обеспечения безопасности при большей многословности кода.

    Буду признателен:
    Лаконичному примеру в C++ который алоцирует с выравниванием память и совершает например запись люб. значения по люб. по адресу и в добавок выполняет аналог memmove с наложением. Но сугубо в рамках C++ и std::

    Спасибо!


    1. 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++).


      1. StillNEntity
        13.11.2024 18:25

        Действительно, похоже на код в стиле C++, однако я не очень понимаю, чем тот же оператор new так сильно отличается от вызова malloc из std библиотеки C. Хотя отличия несомненно есть, это не вносит существенной разницы. В частности, в вашем примере оператор new - это функция malloc, сказанная другими словами. Да и в случае перевыделения памяти в C++ всё равно неизбежно приходится использовать realloc. А, простите, из-за того, что всю стандартную библиотеку языка C поставили под неймспейс std, считать код подогнанным под идеологию C++, ну, лично для меня, не очень корректно.


    1. Jijiki
      13.11.2024 18:25

      Можете уточнить что значит бинарное хранение конфиг файлов и почему нужно именно выравнивание?


      1. kenomimi
        13.11.2024 18:25

        что значит бинарное хранение конфиг файлов

        берем структуру, и пишем ее как есть в файл/eeprom. Обычно актуально для маленьких микроконтроллеров, где сериализация будет дорогой штукой по ресурсам. Ну а чтобы это занимало минимум места, выравниваем побайтово.

        Вопрос только в том, причем тут плюсы... Обычно в таких штуках чистый С.


        1. redfox0
          13.11.2024 18:25

          Непонятно причём здесь выравнивание, достаточно добавить атрибут, чтобы лишних паддингов не было:

          struct __attribute__((packed)) Foo {
              char a;
              int c;
              char b;
          };
          


          1. 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 уже перегруженое чтение


    1. Kelbon
      13.11.2024 18:25

      вряд ли выравнивание вам нужно больше чем 16, поэтому

      new char[N];


  1. mOlind
    13.11.2024 18:25

    Язык C достаточно простой и эффективный. На нем можно писать простые функции и эффективные решения. На нем много библиотек. Если вы делаете ошибки в C - рано увеличивать сложность и переходить к C++. И совет не используйте выделение памяти в стиле С в C++ потому что можно выделить что-то в стеке... Это очень странно.


  1. 9241304
    13.11.2024 18:25

    Каша какая-то. Нет ничего плохого в использовании готового C кода в плюсах. А вот за что надо руки отбивать - за программирование в сишном стиле на плюсах. Ну и как-то по детски написано. Что ж будет, когда автор откроет для себя stdcall и cdecl)


  1. ImagineTables
    13.11.2024 18:25

    Здравое зерно, безусловно, есть. Я думаю, всем будет лучше, если люди, которым не нравятся сишные корни, отселятся уже в какой-нибудь отдельный пузырь. Жаль, что этого не случилось сразу после появления «Си с классами».


  1. sergio_nsk
    13.11.2024 18:25

    Есть намного лучшее изложение этого тезиса.
    https://youtu.be/YnWhqhNdYyk



  1. redfox0
    13.11.2024 18:25

    А знаете, что ещё безопаснее, чем Си? Rust /s


  1. Serpentine
    13.11.2024 18:25

    Тема манглинга немного не соответствует названию статьи. Есть мнение, что у каждой хорошей С++ библиотеки должен быть С-интерфейс (см. в конце этой статьи), как раз потому что в C нет манлирования.

    На мой взгляд, уместнее было бы рассказать о разнице в приведении типов (как например здесь) или о поведении C-cast в C++ коде.

    На правах оффтопа: вы же игровой движок разрабатываете, было бы гораздо интереснее прочитать от вас кейсы по этой теме причем любой. Например, как 3D редактор в него интегрировали. Гайджины и Playrix в своих блогах раньше хорошие статьи размещали по теме.

    На правах шутки:

    Тут один известный в геймдеве мужик в очках рассказывает, как он пишет на C++ сейчас (см. с отметки 1:55)


  1. Melirius
    13.11.2024 18:25

    cin и cout весьма медленные по сравнению с scanf и printf. Так что если нужно выжимать производительность, то совет из статьи так себе.


    1. 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++ стандартный ввод-вывод все еще остается достаточно медленным.


      1. Melirius
        13.11.2024 18:25

        Хмм, у меня и на Clang со всеми этими ухищрениями было медленнее. Надо перепроверить на 20-м.


        1. Janycz
          13.11.2024 18:25

          По крайней мере, то, что я сказал, для g++ будет верно. В clang походу также, как и в Microsoft Visual C++ не поддерживается std::ios::sync_with_stdio(false); (ну как бы такая функция есть, но она ничего не делает, т. е. std::cin и std::cout не ускоряются от нее): 1, 2.


    1. Dooez
      13.11.2024 18:25

      Нужно просто использовать std::print ;)


    1. VoodooCat
      13.11.2024 18:25

      Проблема с cout, имхо, не сколько в скорости, а в том что этим просто невозможно пользоваться: IO-манипуляторы это какое-то нежелательное состояние да еще и очень многословное. То ли запоминать состояние и восстанавливать, то ли портить его. Тот же printf этим не страдает.

      А в итоге - если вам нужно вывалить из многопроцессной среды лог - в один stdout/stderr (файл или пайп) - всего то и надо что sprintf + запись в файл, и на относительно коротких сообщениях это будет атомарно и не будут рваться строки. Разумеется лучше когда это в обернуто в приличную библиотку. Но сам смысл не меняется - cout в стандартной реализации как-то никамильфо.

      Это же подтверждают мириады языков где форматирование похоже на printf с "местечковыми" различиями.


  1. dombran
    13.11.2024 18:25

    Автор статьи скорее расписался в недостаточной квалификации нежели привел доводы.

    С и С++ хоть и похожи, но это инструменты для разных задач. Это как раньше писали код на С и делали ассемблерных вставки для ускорения задач. Так и сейчас в С++ вставки на С для ускорения кода и возможности работы напрямую с памятью без API прокладок. Я уже молчу что код на С без проблем переносится от версии к версии компилятора.


    1. Dooez
      13.11.2024 18:25

      Какие вставки на C вы делаете для ускорения задач?

      На текущий момент язык C - это в первую очередь ABI склеивающий разные языки. Если смотреть на него с такой точки зрения то это действительно разные задачи с C++.


  1. dmitrysbor
    13.11.2024 18:25

    В своё время именно незабвенное творение Дохлогострауса и стандартной библиотеки С достало меня настолько, что я поменял карьеру и ушёл из разработки насовсем в QA автоматизацию на Python. Я пришёл к выводу, что за все годы кодинга на С/С++ я не написал ничего значительного, а какие то отдельные куски кода в каких то подсистемах, чаще всего которые даже протестировать то нормально было невозможно. Ну юнит тест, да: слёзы одни. Просто фрагментарные куски в огромной системе. Да, я фиговый разработчик и мне не особенно нравится писать код по требованиям, но даже с учётом этого С/С++ никогда не давал мне ощущения законченности и не приносил удовлетворения (не говоря о куче ошибок, которые находили потом QA). Да, на СС++ написаны драйвера и операционные системы и прочий рокет сайенс, но это просто не мой уровень.


    1. AndreyFr
      13.11.2024 18:25

      Ну видимо тебе плевать, когда прога, которая должна жрать 300 КБ жрет 10 МБ и т.д.

      Я вот от такого не имею никакой удовлетворенности...


      1. dmitrysbor
        13.11.2024 18:25

        Рынок диктует скорость при разработке и да, вообще плевать сколько занимает памяти. Вот на время прогона нет. Я же не сетевые драйвера пишу.


        1. AndreyFr
          13.11.2024 18:25

          Угу. Рынок - хрынок. Кто на что уповает...


          1. dmitrysbor
            13.11.2024 18:25

            Тесты на С/С++? Наверное кто то всё ещё пишет для себя всякие юнит-тесты. Но уж точно с такой специализацией вы работу не найдёте никогда ;)


            1. AndreyFr
              13.11.2024 18:25

              Как вообще то что ты написал, ко мне относится ?


              1. dmitrysbor
                13.11.2024 18:25

                То что к тебе относится это твоё дело. Мой ответ касается моего комментария


                1. AndreyFr
                  13.11.2024 18:25

                  Клинический пустозвон...


                  1. dmitrysbor
                    13.11.2024 18:25

                    GFYS


    1. brownfox
      13.11.2024 18:25

      Каждому овощу - свой фрукт :)

      Я охотно использую python для быстрого прототипирования сервисов и систем, но когда речь заходит о продакшен-версиях, многое переписывается на плюсы именно из-за выигрыша в быстродействии и памяти. В итоге, получаем продукт, где верхнеуровневые грабли, в целом, пойманы и ликвидированы, а производительность в разы выше, чем в прототипе.

      Что до С в С++, то "портабельный ассемблер" часто удобен, просто требует, как и любой инструмент, грамотного обращения. Например, аккуратных оберток для вызова из плюсов.


  1. VoodooCat
    13.11.2024 18:25

    "Необработанные массивы" - звучит очень, ну очень странно.


    1. GidraVydra
      13.11.2024 18:25

      А чего тут странного? Необработанный массив сосны, например, или дуба, ходовой товар, кстати. Если, повезет, то и ясеня, но чаще все-таки дуба.


      1. VoodooCat
        13.11.2024 18:25

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

        В данном случае же речь только о заворачивании его во враппер, однако враппер так же не имеет никакого свойства обработанности.

        Пример с массивом сосны - мне нравится, если его обернуть стяжками для транспортировки - то состояние его обработанности никак не изменится. :)


  1. AndreyFr
    13.11.2024 18:25

    вредит чистоте и безопасности кода.

    Как обычно, "старая песня" для криворуких, на какую я как-то ложил.

    Продолжу использовать c в c++ и дальше, там где мне нравится.


  1. Bear_Head_Studio
    13.11.2024 18:25

    Можете пожалуйста уточнить как использование низкоуровневых конструкций C в коде на C++ может влиять на безопасность и производительность программы?