C#과 C++는 글자 하나와 중괄호 문법을 공유하지만 서로 다른 수준에 있습니다. C++(1985)는 기계어로 바로 컴파일되고, 모든 객체가 어디에 있고 언제 사라질지 정확히 제어할 수 있습니다. C#(2002)은 중간 언어로 컴파일되고 .NET 런타임이 실행 시점에 이를 기계어로 바꾸며, 가비지 컬렉터가 메모리를 회수해 줍니다. 아래의 거의 모든 차이가 이 하나의 선택에서 나옵니다.
한눈에 보기
| C# | C++ | |
|---|---|---|
| 실행 | 관리형: IL, CLR이 JIT 컴파일 | 네이티브: 미리 기계어로 컴파일 |
| 메모리 | 가비지 컬렉터 | 수동, RAII, 스마트 포인터 |
| 포인터 | 참조, 원시 포인터는 unsafe 코드에서만 | 어디서나 원시 포인터와 참조 |
| 안전성 | 경계 검사 배열, 댕글링 참조 없음 | 범위 밖 접근, 해제 후 사용 시 정의되지 않은 동작 |
| 빌드 모델 | 프로젝트와 어셈블리, 헤더 없음 | 헤더, 전처리기, 번역 단위, 링커 |
| 제네릭 | 제네릭, 한 번 검사되고 실행 시점에 결정 | 템플릿, 컴파일 시점에 인스턴스화 |
| 표준 라이브러리 | 큼: 컬렉션, HTTP, JSON, 파일, 스레드 | 작음: 컨테이너, 알고리즘, 스레드 |
| 컴파일 속도 | 빠름 | 큰 코드베이스에서 느림 |
| 게임 | Unity, Godot | Unreal, 대부분의 사내 AAA 엔진 |
| 그 밖의 주요 용도 | 웹 백엔드, 데스크톱, 클라우드, 도구 | 엔진, 브라우저, OS, 임베디드, 트레이딩 |
메모리: 가비지 컬렉터와 RAII
C++에서 자동 저장 기간을 가진 객체는 범위를 벗어날 때 파괴되고, 소멸자가 정확히 그 순간 실행됩니다. 힙 객체는 스마트 포인터(std::unique_ptr, std::shared_ptr)가 소유하거나 new와 delete로 직접 관리합니다. RAII(Resource Acquisition Is Initialization)라고 부르는 이 패턴은 메모리와 다른 모든 자원을 결정적으로 정리해 줍니다.
// 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를 쓸 일이 없고, 아직 사용 중인 것을 해제할 수도 없습니다. 메모리는 처리됩니다. GC가 제때 처리하지 않는 것은 다른 자원입니다: 파일, 소켓, 데이터베이스 연결. 이런 자원을 위해 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# 코드를 더 빨리 작성하고 리뷰할 수 있는 큰 이유입니다.
포인터와 unsafe 코드
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 }
}
}
unsafe 코드는 네이티브 라이브러리와의 상호 운용과 일부 성능이 중요한 루프에 쓰입니다. 오늘날 대부분의 저수준 C# 코드는 대신 Span<T>, ref 지역 변수, stackalloc을 쓰며, 이들은 경계 검사를 유지하면서 포인터에 가까운 성능을 냅니다.
성능
C++는 컴파일러에 프로그램 전체를 미리 넘기고 실행 시점에는 아무것도 더하지 않습니다. 가비지 컬렉터도, JIT도, 요청하지 않은 경계 검사도 없습니다. 그래서 마이크로초나 바이트 하나하나가 중요하고 일시 정지가 용납되지 않는 곳에서 선택됩니다.
C#은 안전성의 대가로 런타임, 시작 시 JIT 워밍업, 가끔의 GC 일시 정지를 치릅니다. 처리량은 보통 C++와 몇 배 이내 차이이고, .NET 릴리스마다 격차가 줄었습니다. 프로필 기반 최적화를 쓰는 계층형 JIT, 할당을 피하는 구조체와 Span<T>, SIMD용 하드웨어 내장 함수, 미리 단일 네이티브 실행 파일로 컴파일하는 Native AOT가 그 예입니다. 웹 API, 도구, 비즈니스 로직에서는 데이터베이스와 네트워크가 지배적이라 언어 차이가 거의 드러나지 않습니다.
게임 개발: Unity와 Unreal
대부분의 사람이 이 질문을 만나는 곳이 여기입니다. Unity는 C#으로 스크립트를 작성합니다. 게임플레이 코드, UI, 도구는 게임 오브젝트에 붙는 C# 클래스이고, 엔진 코어는 C++입니다. Unreal Engine은 C++로 작성되었고, 게임플레이는 C++와 블루프린트 비주얼 스크립팅으로 만듭니다. Godot는 GDScript와 C#을 모두 지원합니다.
Unity와 C#은 배우고 반복 작업하기가 더 빨라서 인디 게임과 모바일 게임에서 아주 흔합니다. Unreal과 C++는 AAA 스튜디오의 표준이고, 엔진 프로그래머는 어디서나 C++로 일합니다. 흔한 경로는 Unity로 시작한 뒤 Unreal이나 엔진 작업으로 옮길 때 C++를 배우는 것입니다.
빌드 모델
C++ 프로그램은 헤더(.h, 선언)와 소스 파일(.cpp, 정의)로 나뉩니다. 전처리기가 헤더를 각 소스 파일에 붙여 넣고, 각 파일이 따로 컴파일되며, 링커가 결과를 합칩니다. 템플릿은 그것을 쓰는 모든 파일에서 인스턴스화되는데, 큰 C++ 빌드가 느린 이유 중 하나입니다.
C#에는 그런 것이 없습니다. 프로젝트는 함께 컴파일되어 어셈블리(.dll)가 되는 .cs 파일들의 집합입니다. 선언 순서와 파일 순서는 상관없고, 한 파일의 타입이 include 없이 다른 파일의 타입을 쓸 수 있습니다. 라이브러리는 NuGet 패키지로 받습니다. using 지시문은 파일이 아니라 네임스페이스를 가져옵니다.
눈에 띄는 문법 차이
- 객체. C++에서는
auto p = std::make_unique<Player>();와p->Jump();, C#에서는var p = new Player();와p.Jump();입니다. C#은 모든 곳에.을 씁니다. - 문자열.
std::string은 변경 가능한 값이고, C#의string은 변경할 수 없는 참조 형식입니다. - 다중 상속. C++는 클래스가 여러 클래스를 상속할 수 있고, C#은 기본 클래스 하나와 인터페이스 여러 개를 허용합니다.
- 템플릿과 제네릭. C++ 템플릿은 컴파일 시점 코드 생성이며 메타프로그래밍이 가능합니다. C# 제네릭은 명시적 제약 조건(
where T : IComparable<T>)과 함께 한 번만 타입 검사됩니다. - 표준 라이브러리. C#의 표준 라이브러리에는 HTTP, JSON, 정규식, 파일 IO, 압축, 암호화가 들어 있습니다. C++에서는 그중 상당 부분을 서드파티 라이브러리로 해결합니다.
무엇을 배울까
애플리케이션, 웹 백엔드, 도구, Unity 게임을 만들고 결과를 빨리 보고 싶다면 C#을 배우세요. 게임 엔진, 그래픽스, 임베디드 시스템, 운영체제, 브라우저처럼 하드웨어 수준의 제어와 예측 가능한 지연 시간이 핵심인 분야를 목표로 한다면 C++를 배우세요. C#이 첫 언어로 더 무난하고, C++는 두 번째로 배울 가치가 있습니다. C# 런타임이 대신해 주던 일을 가르쳐 주기 때문입니다.
자주 묻는 질문
C#과 C++의 가장 큰 차이는 무엇인가요?
C#은 관리형입니다. 중간 언어로 컴파일되고 .NET 런타임이 이를 JIT 컴파일하며, 가비지 컬렉터가 메모리를 해제합니다. C++는 기계어로 바로 컴파일되고, 객체를 언제 만들고 없앨지 직접 제어합니다. 그 대가로 C++는 더 많은 제어와 예측 가능성을, C#은 더 높은 안전성과 빠른 개발을 얻습니다.
C#이 C++보다 쉬운가요?
대부분의 사람에게는 그렇습니다. C#에는 수동 메모리 관리도, 헤더 파일도, 안전한 코드에서의 정의되지 않은 동작도 없고, 컴파일러 오류가 더 명확합니다. C++는 더 큰 언어이고, 댕글링 포인터, 버퍼 오버플로, 해제 후 사용처럼 컴파일러가 잡지 못하는 미묘한 실수를 할 여지가 더 많습니다.
C++가 C#보다 빠른가요?
잘 작성한 C++가 보통 더 빠르고, 더 중요하게는 더 예측 가능합니다. 가비지 컬렉터 일시 정지도 JIT 워밍업도 없기 때문입니다. 최신 C#은 구조체, Span<T>, SIMD, AOT 컴파일로 격차를 줄였고, 비즈니스 애플리케이션에서는 이 차이가 병목이 되는 일이 드뭅니다. 엔진, 드라이버, 초단타 매매에서는 여전히 C++가 표준입니다.
게임 개발에는 C#과 C++ 중 무엇을 배워야 하나요?
첫 게임이라면 Unity(또는 Godot)와 C#으로 더 빨리 만들고 출시할 수 있습니다. Unreal Engine은 C++(와 블루프린트)를 쓰고, 엔진 코드를 작성하는 AAA 스튜디오는 C++ 프로그래머를 채용합니다. 많은 개발자가 Unity와 C#으로 시작한 뒤 엔진 수준 작업이 필요해지면 C++를 배웁니다.
C#에도 포인터가 있나요?
네, unsafe 코드 블록 안에서 쓸 수 있으며, 프로젝트 설정 AllowUnsafeBlocks로 켜야 합니다. 일반 C#은 참조를 쓰는데, 참조는 가비지 컬렉터가 추적하고 해제된 메모리를 가리킬 수 없습니다. 포인터가 필요할 법한 대부분의 경우는 ref, Span<T>, stackalloc으로 해결됩니다.