C# et C++ partagent une lettre et une syntaxe à accolades, mais ils se situent à des niveaux différents. C++ (1985) est compilé directement en code machine et vous laisse contrôler exactement où vit chaque objet et quand il meurt. C# (2002) est compilé en un langage intermédiaire que le runtime .NET transforme en code machine à l'exécution, et un ramasse-miettes récupère la mémoire pour vous. Presque toutes les différences ci-dessous découlent de ce seul choix.
En un coup d'œil
| C# | C++ | |
|---|---|---|
| Exécution | Managée : IL, compilé par le JIT du CLR | Native : compilée à l'avance en code machine |
| Mémoire | Ramasse-miettes | Manuelle, RAII, pointeurs intelligents |
| Pointeurs | Références ; pointeurs bruts seulement en code unsafe | Pointeurs bruts et références partout |
| Sûreté | Tableaux avec vérification des bornes, pas de références pendantes | Comportement indéfini en cas de dépassement, d'utilisation après libération |
| Modèle de compilation | Projets et assemblies, pas d'en-têtes | En-têtes, préprocesseur, unités de traduction, éditeur de liens |
| Génériques | Génériques, vérifiés une fois, résolus à l'exécution | Templates, instanciés à la compilation |
| Bibliothèque standard | Vaste : collections, HTTP, JSON, fichiers, threads | Plus réduite : conteneurs, algorithmes, threads |
| Vitesse de compilation | Rapide | Lente sur les grosses bases de code |
| Jeux | Unity, Godot | Unreal, la plupart des moteurs AAA maison |
| Autres usages principaux | Backends web, bureau, cloud, outils | Moteurs, navigateurs, OS, embarqué, trading |
Mémoire : ramasse-miettes ou RAII
En C++, un objet à durée de stockage automatique est détruit quand il sort de sa portée, et son destructeur s'exécute à ce moment précis. Les objets sur le tas appartiennent à des pointeurs intelligents (std::unique_ptr, std::shared_ptr) ou sont gérés à la main avec new et delete. Ce modèle, Resource Acquisition Is Initialization (RAII), assure un nettoyage déterministe de la mémoire et de toutes les autres ressources.
// 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#, chaque instance de classe vit sur le tas managé, et le ramasse-miettes la libère plus tard, une fois que plus rien n'y fait référence. Vous n'écrivez jamais delete et ne pouvez jamais libérer quelque chose qui est encore utilisé. La mémoire est prise en charge ; ce que le GC ne libère pas rapidement, ce sont les autres ressources : fichiers, sockets, connexions à une base de données. Pour celles-ci, C# a IDisposable et l'instruction using, qui appelle Dispose à la fin d'un bloc, ce qui se rapproche le plus d'un destructeur :
Sortie :
open db
open cache
db <- SELECT 1
cache <- PING
close cache
close db
done
La différence est qu'en C++ le nettoyage est lié à la portée pour chaque objet local et chaque objet détenu par un pointeur intelligent, alors qu'en C# il est automatique pour la mémoire et à activer (via using) pour tout le reste. Plus de détails sur la page instruction using.
Sûreté : des exceptions au lieu du comportement indéfini
Lire au-delà de la fin d'un tableau en C++ est un comportement indéfini : le programme peut afficher n'importe quoi, planter ou continuer avec une mémoire corrompue, et le résultat peut changer d'une compilation à l'autre. La même erreur en C# lève une exception à la ligne exacte.
// C++: compiles, and the behavior is undefined
int scores[3] = {90, 85, 77};
int x = scores[5]; // reads whatever is in memory there
Sortie :
Index 5 is outside an array of length 3
name was null
Toute la catégorie des bugs de corruption mémoire (dépassements de tampon, utilisation après libération, double libération, pointeurs pendants) n'existe pas en C# sûr. C'est en grande partie pour cela que le code C# est plus rapide à écrire et à relire.
Pointeurs et code unsafe
C# a bien des pointeurs, mais seulement dans des blocs unsafe, et le projet doit les autoriser avec <AllowUnsafeBlocks>true</AllowUnsafeBlocks>. Les objets du tas managé peuvent être déplacés pendant le ramasse-miettes, il faut donc les épingler avec fixed avant de prendre leur adresse :
// C#, requires AllowUnsafeBlocks
unsafe
{
int[] data = { 1, 2, 3 };
fixed (int* p = data)
{
*(p + 1) = 20; // data is now { 1, 20, 3 }
}
}
Le code unsafe sert à l'interopérabilité avec des bibliothèques natives et à quelques boucles critiques. La plupart du C# bas niveau actuel utilise plutôt Span<T>, les variables locales ref et stackalloc, qui offrent des performances proches des pointeurs tout en conservant la vérification des bornes.
Performances
C++ donne au compilateur le programme entier à l'avance et n'ajoute rien à l'exécution : ni ramasse-miettes, ni JIT, ni vérification des bornes sauf si vous la demandez. C'est donc le choix quand chaque microseconde ou chaque octet compte, et quand les pauses sont inacceptables.
C# paie sa sûreté par un runtime, un temps de chauffe du JIT au démarrage et des pauses occasionnelles du GC. En débit, il reste en général à un petit facteur de C++, et l'écart s'est réduit à chaque version de .NET : JIT à plusieurs niveaux avec optimisation guidée par profil, structs et Span<T> pour éviter les allocations, intrinsèques matériels pour le SIMD, et Native AOT pour compiler à l'avance en un seul exécutable natif. Pour les API web, les outils et la logique métier, la base de données et le réseau dominent, et la différence de langage se voit rarement.
Jeux vidéo : Unity ou Unreal
C'est là que la plupart des gens rencontrent la question. Unity se programme en C# : le code de gameplay, l'interface et les outils sont des classes C# attachées aux objets du jeu, tandis que le cœur du moteur est en C++. Unreal Engine est écrit en C++, et le gameplay se fait en C++ plus le système de script visuel Blueprints. Godot prend en charge GDScript et C#.
C# avec Unity s'apprend et s'itère plus vite, c'est pourquoi il est si répandu dans les jeux indépendants et mobiles. Unreal et C++ sont la norme dans les studios AAA, et les programmeurs moteur travaillent en C++ partout. Un parcours courant consiste à commencer avec Unity, puis à apprendre C++ si vous passez à Unreal ou au travail sur les moteurs.
Modèle de compilation
Un programme C++ est découpé en en-têtes (.h, les déclarations) et en fichiers source (.cpp, les définitions). Le préprocesseur colle les en-têtes dans chaque fichier source, chaque fichier est compilé séparément et l'éditeur de liens assemble les résultats. Les templates sont instanciés dans chaque fichier qui les utilise, ce qui explique en partie la lenteur des grosses compilations C++.
C# n'a rien de tout cela. Un projet est un ensemble de fichiers .cs compilés ensemble en un assembly (.dll) ; l'ordre des déclarations et des fichiers n'a pas d'importance, et un type d'un fichier peut utiliser un type d'un autre sans include. Les bibliothèques sont distribuées sous forme de packages NuGet. La directive using importe un namespace, pas un fichier.
Les différences de syntaxe que vous remarquerez
- Objets.
auto p = std::make_unique<Player>();etp->Jump();en C++ ;var p = new Player();etp.Jump();en C#. C# utilise.partout. - Chaînes.
std::stringest une valeur modifiable ; lestringde C# est un type référence immuable. - Héritage multiple. C++ permet à une classe d'hériter de plusieurs classes ; C# autorise une classe de base plus un nombre quelconque d'interfaces.
- Templates ou génériques. Les templates C++ génèrent du code à la compilation et permettent la métaprogrammation ; les génériques C# sont vérifiés une fois, avec des contraintes explicites (
where T : IComparable<T>). - Bibliothèque standard. Celle de C# inclut HTTP, JSON, les expressions régulières, les E/S de fichiers, la compression et la cryptographie ; en C++, une grande partie vient de bibliothèques tierces.
Lequel apprendre
Apprenez C# si vous voulez construire des applications, des backends web, des outils ou des jeux Unity et voir des résultats rapidement. Apprenez C++ si vous visez les moteurs de jeu, le graphisme, les systèmes embarqués, les systèmes d'exploitation, les navigateurs ou tout ce où le contrôle au niveau du matériel et une latence prévisible sont l'enjeu. C# est le premier langage le plus accessible ; C++ vaut la peine d'être appris ensuite, car il montre ce que le runtime C# fait pour vous.
Questions fréquentes
Quelle est la principale différence entre C# et C++ ?
C# est managé : il est compilé en langage intermédiaire que le runtime .NET compile à la volée (JIT), et un ramasse-miettes libère la mémoire. C++ est compilé directement en code machine, et vous décidez quand les objets sont créés et détruits. Ce compromis donne à C++ plus de contrôle et de prévisibilité, et à C# plus de sûreté et un développement plus rapide.
C# est-il plus facile que C++ ?
Oui, pour la plupart des gens. C# n'a pas de gestion manuelle de la mémoire, pas de fichiers d'en-tête, pas de comportement indéfini dans le code sûr, et des erreurs de compilation plus claires. C++ est un langage plus vaste, avec davantage de façons de commettre des erreurs subtiles que le compilateur ne détecte pas, comme les pointeurs pendants, les dépassements de tampon et l'utilisation après libération.
C++ est-il plus rapide que C# ?
Du C++ bien écrit est en général plus rapide et surtout plus prévisible, car il n'y a ni pause du ramasse-miettes ni temps de chauffe du JIT. Le C# moderne réduit l'écart avec les structs, Span<T>, le SIMD et la compilation anticipée, et pour les applications métier la différence est rarement le goulot d'étranglement. Pour les moteurs, les pilotes et le trading haute fréquence, C++ reste la norme.
Faut-il apprendre C# ou C++ pour créer des jeux vidéo ?
Pour vos premiers jeux, C# avec Unity (ou Godot) vous permet de construire et de publier plus vite. Unreal Engine utilise C++ (plus les Blueprints), et les studios AAA qui écrivent du code moteur recrutent des programmeurs C++. Beaucoup de développeurs commencent avec Unity et C#, puis apprennent C++ quand ils ont besoin de travailler au niveau du moteur.
C# a-t-il des pointeurs ?
Oui, dans des blocs de code unsafe, qu'il faut activer avec le paramètre de projet AllowUnsafeBlocks. Le C# ordinaire utilise des références, que le ramasse-miettes suit et qui ne peuvent pas pointer vers de la mémoire libérée. ref, Span<T> et stackalloc couvrent la plupart des cas où vous auriez recours à un pointeur.