C와 C++의 주요 차이점: 사실 기반 안내서
C++는 종종 “클래스가 있는 C”로 묘사되며 C에서 파생되었지만, 현재는 독자적이고 강력한 멀티 패러다임 언어로 발전했습니다. 시스템 프로그래밍 분야의 개발자에게 두 언어의 차이점을 이해하는 것은 매우 중요합니다. 본 안내서는 사실에 입각하여 두 언어를 비교 설명합니다.
1. 프로그래밍 패러다임
- C: 엄격한 절차적(procedural) 언어입니다. 코드를 함수 단위로 구성하며, 주어진 작업을 단계별로 처리하는 데 중점을 둡니다.
- C++: 멀티 패러다임(multi-paradigm) 언어입니다. 절차적, 객체 지향, 일반화, 함수형 프로그래밍 스타일을 모두 지원합니다. 가장 중요한 추가 사항은 객체 지향 프로그래밍(OOP)입니다.
2. 객체 지향 프로그래밍 (OOP)
이것이 아마도 가장 큰 차이점일 것입니다. C++에는 C에 없는 다음과 같은 기능이 있습니다.
- 클래스와 객체: 데이터(속성)와 그 데이터에 대한 연산을 수행하는 메서드(함수)를 하나로 묶는 객체를 생성하기 위한 청사진입니다.
- 캡슐화: 접근 지정자(
public
,private
,protected
)를 사용하여 클래스 내부의 구현 세부 정보를 외부로부터 숨깁니다. C++의static
키워드는 클래스 수준의 변수나 메서드를 위해 사용되며, 접근 제어와는 관련이 없습니다. - 상속: 기존 클래스의 동작을 재사용, 확장, 수정하는 새로운 클래스를 생성하는 기능입니다.
- 다형성: 주로
virtual
함수를 통해, 서로 다른 기본 형태(데이터 타입)에 대해 단일 인터페이스를 가질 수 있는 기능입니다.
3. 메모리 관리
핵심 철학이 크게 다릅니다.
- C:
malloc()
,calloc()
,realloc()
,free()
를 사용한 수동 메모리 관리를 합니다. 개발자가 메모리 할당과 해제를 전적으로 책임져야 합니다. - C++:
new
와delete
를 통한 수동 관리를 여전히 지원하지만, 현대 C++는 RAII(Resource Acquisition Is Initialization) 기법을 강조합니다.- RAII: 자원의 생명주기를 객체의 생명주기에 바인딩하는 디자인 패턴입니다. 생성자에서 자원(예: 메모리, 파일 핸들)을 획득하고, 소멸자에서 해제합니다. 이를 통해 결정론적이고 자동적인 자원 정리가 가능해집니다.
- 스마트 포인터: RAII를 실제로 구현한 것들입니다 (예:
std::unique_ptr
,std::shared_ptr
,std::weak_ptr
). 메모리를 자동으로 관리하여 누수를 방지합니다. 여기서 중요한 점은 C++에는 Java나 C#과 같은 내장 가비지 컬렉터(GC)가 없다는 것입니다. 스마트 포인터는 결정론적 자동 메모리 관리를 제공하며, 이는 비결정론적 GC와는 다릅니다.
더 명확한 구분을 위해 C++ 스마트 포인터와 전통적인 가비지 컬렉션을 직접 비교하면 다음과 같습니다.
핵심 차이점: C++ 스마트 포인터와 가비지 컬렉션(GC)
특징 | C++ 스마트 포인터 | 가비지 컬렉션 (GC) |
---|---|---|
메모리 해제 시점 | 결정적 (Deterministic): 스마트 포인터 객체가 범위를 벗어나거나 명시적으로 해제될 때 소멸자가 호출되어 메모리가 즉시 해제됩니다. (RAII 패턴) | 비결정적 (Non-deterministic): GC가 특정 시점에 ‘더 이상 사용되지 않는’ 메모리(garbage)를 식별하고 한꺼번에 해제합니다. 정확한 해제 시점을 예측하기 어렵습니다. |
주요 작동 방식 | 소유권(Ownership) 기반의 객체 생명주기 관리: unique_ptr , shared_ptr , weak_ptr 를 통해 소유권을 명확히 하고, 소유자가 사라지면 자원을 해제합니다. shared_ptr 는 참조 횟수 기법을 사용합니다. |
도달 가능성(Reachability) 기반의 메모리 관리: ‘루트(root)’에서 시작하여 참조 체인을 따라가며 도달 가능한 객체를 식별하고, 도달할 수 없는 객체를 ‘쓰레기’로 간주하여 수거합니다. (예: Mark-and-Sweep) |
순환 참조 문제 | shared_ptr 끼리 서로를 가리키는 순환 참조가 발생하면 참조 횟수가 0이 되지 않아 메모리 누수가 발생합니다. 이를 해결하기 위해 weak_ptr 를 사용해야 합니다. |
대부분의 현대적인 GC는 순환 참조를 자동으로 감지하고 해결할 수 있습니다. |
성능 오버헤드 | 참조 횟수를 증가시키고 감소시키는 데 약간의 오버헤드가 발생하지만, 메모리 해제 시점이 예측 가능합니다. | GC가 실행되는 동안에는 프로그램의 실행이 잠시 멈출 수 있으며(stop-the-world), 언제 실행될지 몰라 실시간 시스템에는 부적합할 수 있습니다. |
4. 일반화 프로그래밍: 템플릿
- C: 일반화 프로그래밍을 위한 네이티브 지원이 부족합니다. 비슷한 기능을 구현하기 위해 개발자들은 종종 타입에 안전하지 않은
void*
포인터나 매크로에 의존합니다. - C++: 템플릿 기능을 제공하여, 타입 안전성을 희생하지 않으면서 모든 데이터 타입에 대해 동작하는 함수와 클래스를 작성할 수 있게 합니다. 이는 표준 템플릿 라이브러리(STL)의 기반이 됩니다.
5. 표준 라이브러리
- C: 입출력, 문자열 조작, 수학 등을 위한 기본적인 함수를 제공하는 간결한 표준 라이브러리를 가집니다.
- C++: C 표준 라이브러리를 포함하며, 여기에 방대한 표준 템플릿 라이브러리(STL)를 추가합니다. STL은 미리 만들어진 효율적인 자료 구조(예:
std::vector
,std::map
,std::set
), 반복자, 알고리즘을 제공합니다.
6. 기타 주요 언어 기능
- 참조 (
&
): C++는 다른 변수의 별칭으로 작동하는 참조 타입을 도입했습니다. 참조는 null이 될 수 없으며 초기화 후 다른 객체를 가리키도록 변경할 수 없어, 포인터보다 더 안전하고 편리한 경우가 많습니다. 참조는 본질적으로 스레드에 안전하지 않습니다. 다중 스레드 환경에서 참조를 통해 데이터에 접근할 때는 여전히 동기화(예: 뮤텍스, 아토믹)가 필요합니다. - 네임스페이스: 코드를 논리적 그룹으로 구성하고, 특히 대규모 프로젝트에서 이름 충돌을 방지하기 위해 사용되는 C++ 기능입니다. C와 C++ 코드를 올바르게 연결하기 위해서는
extern "C"
링크 명세를 사용합니다. - 컴파일 타임 평가 (
constexpr
): 표현식과 함수가 컴파일 시점에 평가될 수 있도록 하는 C++11 키워드입니다. 이는 상당한 성능 향상으로 이어질 수 있습니다.constexpr
변수는 암묵적으로const
이며, 반드시 상수 표현식으로 초기화되어야 합니다. - 인라인 어셈블리: 대부분의 플랫폼을 위한 C와 C++ 컴파일러는 모두 인라인 어셈블리(예:
asm
,__asm__
사용)를 지원하여 어셈블리 코드를 직접 통합할 수 있게 합니다. 별도의 어셈블리 파일을 링크하는 방법 또한 두 언어 모두에서 사용 가능합니다.