C# y C++ comparten una letra y una sintaxis de llaves, pero están en niveles distintos. C++ (1985) compila directamente a código máquina y te permite controlar exactamente dónde vive cada objeto y cuándo muere. C# (2002) compila a un lenguaje intermedio que el runtime de .NET convierte en código máquina en tiempo de ejecución, y un recolector de basura recupera la memoria por ti. Casi todas las diferencias que siguen se derivan de esa decisión.
De un vistazo
| C# | C++ | |
|---|---|---|
| Ejecución | Gestionada: IL, compilado con JIT por el CLR | Nativa: compilado de antemano a código máquina |
| Memoria | Recolector de basura | Manual, RAII, punteros inteligentes |
| Punteros | Referencias; punteros en bruto solo en código unsafe | Punteros en bruto y referencias en todas partes |
| Seguridad | Arrays con comprobación de límites, sin referencias colgantes | Comportamiento indefinido fuera de límites o tras liberar |
| Modelo de compilación | Proyectos y ensamblados, sin cabeceras | Cabeceras, preprocesador, unidades de traducción, enlazador |
| Genéricos | Genéricos, comprobados una vez, resueltos en ejecución | Plantillas, instanciadas en tiempo de compilación |
| Biblioteca estándar | Grande: colecciones, HTTP, JSON, archivos, hilos | Más pequeña: contenedores, algoritmos, hilos |
| Velocidad de compilación | Rápida | Lenta en bases de código grandes |
| Juegos | Unity, Godot | Unreal, la mayoría de los motores AAA propios |
| Otros usos principales | Backends web, escritorio, nube, herramientas | Motores, navegadores, SO, embebidos, trading |
Memoria: recolector de basura frente a RAII
En C++, un objeto con almacenamiento automático se destruye cuando sale de ámbito, y su destructor se ejecuta en ese momento exacto. Los objetos del heap tienen como dueños punteros inteligentes (std::unique_ptr, std::shared_ptr) o se gestionan a mano con new y delete. Este patrón, Resource Acquisition Is Initialization (RAII), da una limpieza determinista de la memoria y de cualquier otro recurso.
// 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
En C#, toda instancia de clase vive en el heap gestionado y el recolector de basura la libera en algún momento posterior, cuando nada hace referencia a ella. Nunca escribes delete y nunca puedes liberar algo que todavía está en uso. La memoria está resuelta; lo que el recolector no gestiona con rapidez son los otros recursos: archivos, sockets, conexiones a bases de datos. Para ellos, C# tiene IDisposable y la sentencia using, que llama a Dispose al final de un bloque, lo más parecido a un destructor:
Salida:
open db
open cache
db <- SELECT 1
cache <- PING
close cache
close db
done
La diferencia es que en C++ la limpieza está ligada al ámbito para cada objeto local y cada objeto con un puntero inteligente como dueño, mientras que en C# es automática para la memoria y opcional (mediante using) para todo lo demás. Más sobre esto en la página de la sentencia using.
Seguridad: excepciones en lugar de comportamiento indefinido
Leer más allá del final de un array en C++ es comportamiento indefinido: el programa puede imprimir basura, fallar o seguir funcionando con la memoria corrompida, y el resultado puede cambiar entre compilaciones. El mismo error en C# lanza una excepción en la línea exacta.
// C++: compiles, and the behavior is undefined
int scores[3] = {90, 85, 77};
int x = scores[5]; // reads whatever is in memory there
Salida:
Index 5 is outside an array of length 3
name was null
Toda la clase de bugs de corrupción de memoria (desbordamientos de buffer, uso después de liberar, doble liberación, punteros colgantes) no existe en el C# seguro. Esa es una gran parte de la razón por la que el código C# se escribe y se revisa más rápido.
Punteros y código unsafe
C# sí tiene punteros, pero solo dentro de bloques unsafe, y el proyecto debe activarlos con <AllowUnsafeBlocks>true</AllowUnsafeBlocks>. Los objetos del heap gestionado pueden moverse durante la recolección de basura, así que los fijas con fixed antes de tomar su dirección:
// C#, requires AllowUnsafeBlocks
unsafe
{
int[] data = { 1, 2, 3 };
fixed (int* p = data)
{
*(p + 1) = 20; // data is now { 1, 20, 3 }
}
}
El código unsafe se usa para la interoperabilidad con bibliotecas nativas y para unos pocos bucles críticos. La mayor parte del C# de bajo nivel actual usa en su lugar Span<T>, variables locales ref y stackalloc, que dan un rendimiento parecido al de los punteros sin perder las comprobaciones de límites.
Rendimiento
C++ entrega al compilador todo el programa de antemano y no añade nada en tiempo de ejecución: ni recolector de basura, ni JIT, ni comprobaciones de límites salvo que las pidas. Eso lo convierte en la opción cuando cuenta cada microsegundo o cada byte, y cuando las pausas son inaceptables.
C# paga su seguridad con un runtime, el calentamiento del JIT al arrancar y alguna pausa ocasional del recolector. En rendimiento bruto suele estar a un factor pequeño de C++, y la distancia se ha reducido con cada versión de .NET: JIT por niveles con optimización guiada por perfiles, structs y Span<T> para evitar reservas de memoria, intrínsecos de hardware para SIMD y Native AOT para compilar de antemano en un único ejecutable nativo. En las API web, las herramientas y la lógica de negocio, dominan la base de datos y la red, y la diferencia de lenguaje rara vez se nota.
Desarrollo de videojuegos: Unity frente a Unreal
Aquí es donde la mayoría de la gente se encuentra con la pregunta. Unity se programa en C#: el código de juego, la interfaz y las herramientas son clases C# asociadas a objetos del juego, mientras que el núcleo del motor es C++. Unreal Engine está escrito en C++, y el gameplay es C++ más el sistema de scripting visual Blueprints. Godot admite tanto GDScript como C#.
C# con Unity es más rápido de aprender y de iterar, por eso es tan habitual en juegos independientes y para móviles. Unreal y C++ son el estándar en los estudios AAA, y los programadores de motores de todas partes trabajan en C++. Un camino habitual es empezar en Unity y después aprender C++ si pasas a Unreal o a trabajar en motores.
Modelo de compilación
Un programa en C++ se divide en cabeceras (.h, declaraciones) y archivos fuente (.cpp, definiciones). El preprocesador pega las cabeceras en cada archivo fuente, cada archivo se compila por separado y el enlazador une los resultados. Las plantillas se instancian en cada archivo que las usa, lo que es una de las razones por las que las compilaciones grandes de C++ son lentas.
C# no tiene nada de eso. Un proyecto es un conjunto de archivos .cs compilados juntos en un ensamblado (.dll); el orden de las declaraciones y de los archivos no importa, y un tipo de un archivo puede usar un tipo de otro sin ningún include. Las bibliotecas llegan como paquetes NuGet. La directiva using importa un namespace, no un archivo.
Diferencias de sintaxis que notarás
- Objetos.
auto p = std::make_unique<Player>();yp->Jump();en C++;var p = new Player();yp.Jump();en C#. C# usa.para todo. - Strings.
std::stringes un valor mutable; elstringde C# es un tipo de referencia inmutable. - Herencia múltiple. C++ permite que una clase herede de varias clases; C# permite una clase base más cualquier número de interfaces.
- Plantillas frente a genéricos. Las plantillas de C++ son generación de código en tiempo de compilación y permiten metaprogramación; los genéricos de C# se comprueban una sola vez con restricciones explícitas (
where T : IComparable<T>). - Biblioteca estándar. La de C# incluye HTTP, JSON, expresiones regulares, E/S de archivos, compresión y criptografía; en C++ buena parte de eso viene de bibliotecas de terceros.
Cuál aprender
Aprende C# si quieres crear aplicaciones, backends web, herramientas o juegos en Unity y ver resultados rápido. Aprende C++ si apuntas a motores de juego, gráficos, sistemas embebidos, sistemas operativos, navegadores o cualquier cosa en la que el control a nivel de hardware y una latencia predecible sean el objetivo. C# es el primer lenguaje más amable; C++ merece la pena como segundo, porque enseña lo que el runtime de C# hace por ti.
Preguntas frecuentes
¿Cuál es la principal diferencia entre C# y C++?
C# es gestionado: compila a un lenguaje intermedio que el runtime de .NET compila con JIT, y un recolector de basura libera la memoria. C++ compila directamente a código máquina, y tú controlas cuándo se crean y se destruyen los objetos. Ese intercambio da a C++ más control y previsibilidad, y a C# más seguridad y un desarrollo más rápido.
¿Es C# más fácil que C++?
Sí, para la mayoría de la gente. C# no tiene gestión manual de memoria, ni archivos de cabecera, ni comportamiento indefinido en el código seguro, y sus errores de compilación son más claros. C++ es un lenguaje más grande con más formas de cometer errores sutiles, como punteros colgantes, desbordamientos de buffer y uso después de liberar, que el compilador no detecta.
¿Es C++ más rápido que C#?
Un C++ bien escrito suele ser más rápido y, sobre todo, más predecible, porque no hay pausas del recolector de basura ni calentamiento del JIT. El C# moderno reduce la distancia con structs, Span<T>, SIMD y compilación anticipada, y en aplicaciones de negocio la diferencia rara vez es el cuello de botella. Para motores, drivers y trading de alta frecuencia, C++ sigue siendo el estándar.
¿Debo aprender C# o C++ para desarrollar videojuegos?
Para tus primeros juegos, C# con Unity (o Godot) te permite construir y publicar más rápido. Unreal Engine usa C++ (más Blueprints), y los estudios AAA que escriben código de motor contratan programadores de C++. Muchos desarrolladores empiezan con Unity y C#, y aprenden C++ cuando necesitan trabajar a nivel de motor.
¿Tiene C# punteros?
Sí, dentro de bloques de código unsafe, que deben activarse con la opción de proyecto AllowUnsafeBlocks. El C# normal usa referencias, que el recolector de basura sigue y que no pueden apuntar a memoria liberada. ref, Span<T> y stackalloc cubren la mayoría de los casos en los que usarías un puntero.