Menu

C# или C++: память, производительность, игры и сложность изучения

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

У C# и C++ общая буква и синтаксис с фигурными скобками, но находятся они на разных уровнях. C++ (1985) компилируется прямо в машинный код и позволяет точно управлять тем, где живёт каждый объект и когда он уничтожается. C# (2002) компилируется в промежуточный язык, который среда выполнения .NET превращает в машинный код во время работы, а память за вас освобождает сборщик мусора. Почти все различия ниже вытекают из этого одного решения.

Коротко

C#C++
ВыполнениеУправляемое: IL, JIT-компиляция в CLRНативное: заранее скомпилировано в машинный код
ПамятьСборщик мусораВручную, RAII, умные указатели
УказателиСсылки; сырые указатели только в коде unsafeСырые указатели и ссылки повсюду
БезопасностьПроверка границ массивов, нет висячих ссылокНеопределённое поведение при выходе за границы, использовании после освобождения
Модель сборкиПроекты и сборки, без заголовковЗаголовки, препроцессор, единицы трансляции, компоновщик
ОбобщенияОбобщения, проверяются один раз, разрешаются во время выполненияШаблоны, инстанцируются при компиляции
Стандартная библиотекаБольшая: коллекции, HTTP, JSON, файлы, потокиМеньше: контейнеры, алгоритмы, потоки
Скорость компиляцииБыстраяМедленная на больших кодовых базах
ИгрыUnity, GodotUnreal, большинство собственных AAA-движков
Другие основные областиВеб-бэкенды, десктоп, облако, утилитыДвижки, браузеры, ОС, встраиваемые системы, трейдинг

Память: сборщик мусора и RAII

В C++ объект с автоматическим временем жизни уничтожается при выходе из области видимости, и его деструктор выполняется ровно в этот момент. Объекты в куче принадлежат умным указателям (std::unique_ptr, std::shared_ptr) или управляются вручную через new и delete. Этот приём, Resource Acquisition Is Initialization (RAII), даёт детерминированное освобождение памяти и любых других ресурсов.

// C++
#include <memory>
#include <fstream>

void save() {
    std::ofstream file("log.txt");          // opened here
    auto buffer = std::make_unique<char[]>(4096);
    file << "saved\n";
}   // file closed and buffer freed here, in reverse order

В C# каждый экземпляр класса живёт в управляемой куче, и сборщик мусора освобождает его когда-то позже, когда на него больше никто не ссылается. Вы никогда не пишете delete и никогда не можете освободить то, что ещё используется. Память обрабатывается автоматически; а вот другие ресурсы сборщик мусора своевременно не освобождает: файлы, сокеты, подключения к базе данных. Для них в C# есть IDisposable и оператор using, который вызывает Dispose в конце блока. Это ближайший аналог деструктора:

Вывод:

open db
open cache
db <- SELECT 1
cache <- PING
close cache
close db
done

Разница в том, что в C++ освобождение привязано к области видимости для каждого локального объекта и каждого объекта во владении умного указателя, а в C# оно автоматическое для памяти и добровольное (через using) для всего остального. Подробнее на странице оператор using.

Безопасность: исключения вместо неопределённого поведения

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

// C++: compiles, and the behavior is undefined
int scores[3] = {90, 85, 77};
int x = scores[5];   // reads whatever is in memory there

Вывод:

Index 5 is outside an array of length 3
name was null

Целого класса ошибок повреждения памяти (переполнения буфера, использования после освобождения, двойного освобождения, висячих указателей) в безопасном C# не существует. Во многом поэтому код на C# быстрее писать и проверять.

Указатели и небезопасный код

Указатели в C# есть, но только внутри блоков unsafe, и проект должен явно это разрешить через <AllowUnsafeBlocks>true</AllowUnsafeBlocks>. Объекты в управляемой куче могут перемещаться при сборке мусора, поэтому перед взятием адреса их закрепляют через fixed:

// C#, requires AllowUnsafeBlocks
unsafe
{
    int[] data = { 1, 2, 3 };
    fixed (int* p = data)
    {
        *(p + 1) = 20;   // data is now { 1, 20, 3 }
    }
}

Небезопасный код используют для взаимодействия с нативными библиотеками и в немногих горячих циклах. Большая часть низкоуровневого кода на C# сегодня использует вместо этого Span<T>, локальные ref и stackalloc, которые дают производительность, близкую к указателям, сохраняя проверку границ.

Производительность

C++ отдаёт компилятору всю программу заранее и ничего не добавляет во время выполнения: нет сборщика мусора, нет JIT, нет проверки границ, если вы её не попросите. Поэтому его выбирают там, где важна каждая микросекунда или каждый байт и где паузы недопустимы.

C# платит за безопасность средой выполнения, прогревом JIT при запуске и редкими паузами сборки мусора. По пропускной способности он обычно уступает C++ ненамного, и разрыв сокращается с каждым выпуском .NET: многоуровневый JIT с оптимизацией по профилю, структуры и Span<T> для избежания выделений памяти, аппаратные интринсики для SIMD и Native AOT для компиляции заранее в один нативный исполняемый файл. Для веб-API, утилит и бизнес-логики доминируют база данных и сеть, и разница между языками редко заметна.

Разработка игр: Unity и Unreal

Именно здесь большинство людей сталкивается с этим вопросом. Unity программируется на C#: игровой код, интерфейс и инструменты это классы C#, прикреплённые к игровым объектам, а ядро движка написано на C++. Unreal Engine написан на C++, а игровая логика пишется на C++ плюс в системе визуального программирования Blueprints. Godot поддерживает и GDScript, и C#.

C# с Unity быстрее изучать и на нём быстрее итерировать, поэтому он так распространён в инди- и мобильных играх. Unreal и C++ стандарт в AAA-студиях, а программисты движков везде работают на C++. Обычный путь начать с Unity, а затем выучить C++, если вы переходите на Unreal или к работе над движком.

Модель сборки

Программа на C++ делится на заголовки (.h, объявления) и исходные файлы (.cpp, определения). Препроцессор вставляет заголовки в каждый исходный файл, каждый файл компилируется отдельно, а компоновщик объединяет результаты. Шаблоны инстанцируются в каждом файле, который их использует, и это одна из причин, почему сборка больших проектов на C++ медленная.

В C# ничего этого нет. Проект это набор файлов .cs, которые вместе компилируются в сборку (.dll); порядок объявлений и порядок файлов не важны, и тип из одного файла может использовать тип из другого без всяких include. Библиотеки поставляются как пакеты NuGet. Директива using импортирует пространство имён, а не файл.

Различия в синтаксисе, которые вы заметите

  • Объекты. auto p = std::make_unique<Player>(); и p->Jump(); в C++; var p = new Player(); и p.Jump(); в C#. C# везде использует ..
  • Строки. std::string это изменяемое значение; string в C# это неизменяемый ссылочный тип.
  • Множественное наследование. C++ позволяет классу наследоваться от нескольких классов; C# допускает один базовый класс плюс любое количество интерфейсов.
  • Шаблоны и обобщения. Шаблоны C++ это генерация кода при компиляции, и с ними возможно метапрограммирование; обобщения C# проверяются по типам один раз с явными ограничениями (where T : IComparable<T>).
  • Стандартная библиотека. В C# в неё входят HTTP, JSON, регулярные выражения, файловый ввод-вывод, сжатие и криптография; в C++ многое из этого берётся из сторонних библиотек.

Что учить

Учите C#, если хотите делать приложения, веб-бэкенды, утилиты или игры на Unity и быстро видеть результат. Учите C++, если цель игровые движки, графика, встраиваемые системы, операционные системы, браузеры или всё, где главное контроль на уровне железа и предсказуемая задержка. C# более мягкий первый язык; C++ стоит изучить вторым, потому что он показывает, что среда выполнения C# делает за вас.

Часто задаваемые вопросы

В чём главное отличие C# от C++?

C# управляемый: он компилируется в промежуточный язык, который JIT-компилирует среда выполнения .NET, а память освобождает сборщик мусора. C++ компилируется прямо в машинный код, и вы сами управляете тем, когда объекты создаются и уничтожаются. Такой обмен даёт C++ больше контроля и предсказуемости, а C# больше безопасности и более быструю разработку.

C# проще, чем C++?

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

C++ быстрее C#?

Хорошо написанный C++ обычно быстрее и, что важнее, предсказуемее, потому что нет пауз сборщика мусора и прогрева JIT. Современный C# сокращает разрыв с помощью структур, Span<T>, SIMD и компиляции заранее (AOT), а для бизнес-приложений эта разница редко становится узким местом. Для движков, драйверов и высокочастотной торговли стандартом остаётся C++.

Что учить для разработки игр, C# или C++?

Для первых игр C# с Unity (или Godot) позволяет делать и выпускать быстрее. Unreal Engine использует C++ (плюс Blueprints), а AAA-студии, пишущие код движка, нанимают программистов на C++. Многие начинают с Unity и C#, а затем изучают C++, когда нужна работа на уровне движка.

Есть ли в C# указатели?

Да, внутри блоков кода unsafe, которые нужно разрешить настройкой проекта AllowUnsafeBlocks. Обычный C# использует ссылки, которые отслеживает сборщик мусора и которые не могут указывать на освобождённую память. ref, Span<T> и stackalloc покрывают большинство случаев, где вы потянулись бы за указателем.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ