У C# и C++ общая буква и синтаксис с фигурными скобками, но находятся они на разных уровнях. C++ (1985) компилируется прямо в машинный код и позволяет точно управлять тем, где живёт каждый объект и когда он уничтожается. C# (2002) компилируется в промежуточный язык, который среда выполнения .NET превращает в машинный код во время работы, а память за вас освобождает сборщик мусора. Почти все различия ниже вытекают из этого одного решения.
Коротко
| C# | C++ | |
|---|---|---|
| Выполнение | Управляемое: IL, JIT-компиляция в CLR | Нативное: заранее скомпилировано в машинный код |
| Память | Сборщик мусора | Вручную, RAII, умные указатели |
| Указатели | Ссылки; сырые указатели только в коде unsafe | Сырые указатели и ссылки повсюду |
| Безопасность | Проверка границ массивов, нет висячих ссылок | Неопределённое поведение при выходе за границы, использовании после освобождения |
| Модель сборки | Проекты и сборки, без заголовков | Заголовки, препроцессор, единицы трансляции, компоновщик |
| Обобщения | Обобщения, проверяются один раз, разрешаются во время выполнения | Шаблоны, инстанцируются при компиляции |
| Стандартная библиотека | Большая: коллекции, HTTP, JSON, файлы, потоки | Меньше: контейнеры, алгоритмы, потоки |
| Скорость компиляции | Быстрая | Медленная на больших кодовых базах |
| Игры | Unity, Godot | Unreal, большинство собственных 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 покрывают большинство случаев, где вы потянулись бы за указателем.