3 분 소요

이 질문은 내가 면접을 보러다니던 당시 받았던 질문들이며 내가 제대로 답하지 못했다고 생각하는 것이다. 이 문제에 대해 더 공부하고 잊지 않기 위해 여기에 써둔다.

2026년 수정 안내 이 글은 2012년에 처음 썼는데, 당시 검색으로 모아 정리한 내용 대부분이 사실 C#의 값 타입/참조 타입 설명이었다. C++에는 그대로 적용되지 않는 내용이라 본문을 C++ 기준으로 다시 썼다. 원래 어떻게 잘못 알고 있었는지는 오답노트의 성격상 그대로 남겨둔다.

당시의 문답

  • Q. 클래스가 무엇인가?
  • A. 같은 성질을 가진 데이터와 함수들의 집합체이다.
  • Q. 그럼 구조체 역시도 데이터와 함수를 가질 수 있다. 클래스와 동일하게 구조체로도 만들 수 있는데 왜 클래스를 사용하는가?
  • A. 클래스는 상속이 가능하지만 구조체는 불가능하다. → 틀린 답이다.
  • Q. 구조체 안에 구조체를 넣을 수 있지 않는가?
  • A. 그렇다.

두 번째 답이 틀렸다. C++에서 구조체는 상속을 해줄 수도 있고 받을 수도 있다. 면접관이 파고든 지점도 아마 여기였을 것이다.

C++에서의 진짜 차이

C++ 표준에서 classstruct의 차이는 딱 두 가지다.

1. 멤버의 기본 접근 지정자

struct S { int a; };  // a는 public
class  C { int a; };  // a는 private

2. 기본 상속 접근 지정자

struct D1 : Base { };  // public 상속
class  D2 : Base { };  // private 상속

그게 전부다. class C { public: ... };struct C { ... };는 완전히 동일한 타입이며, 컴파일 결과도 같다. 심지어 한쪽에서 struct Foo;로 전방 선언하고 다른 쪽에서 class Foo { ... };로 정의해도 동작한다(MSVC는 경고 C4099를 낸다).

따라서 다음은 모두 구조체에서도 된다.

struct Shape {
    int id = 0;                       // 멤버 기본 초기화
    explicit Shape(int i) : id(i) {}  // 생성자
    virtual ~Shape() = default;       // 가상 소멸자
    virtual double Area() const = 0;  // 순수 가상 함수
private:                              // 접근 지정자
    int internal_ = 0;
};

struct Circle : Shape {               // 상속
    double r;
    Circle(int i, double radius) : Shape(i), r(radius) {}
    double Area() const override { return 3.141592 * r * r; }  // 재정의
};

2012년에 잘못 정리했던 것들

당시 적어둔 목록을 그대로 옮기고, 어디가 틀렸는지 표시한다.

  1. 클래스와 구조체는 데이터타입을 생성한다는 점에서 유사하다. → 맞다.

  2. 구조체는 기본접근자가 public인데 클래스의 기본접근자는 private이다. → 맞다. 여기에 기본 상속 접근 지정자 차이까지 더하면 C++에서의 차이는 끝난다.

  3. 구조체는 데이터의 초기화가 불가능하지만 클래스는 데이터의 초기화가 가능하다.틀렸다. 구조체도 생성자, 멤버 기본 초기화(int x = 0;), 집합체 초기화(S s{1, 2};)를 모두 쓸 수 있다.

  4. 구조체는 value가 복사되지만 클래스는 reference가 복사된다. 따라서 클래스는 얕은 복사, 구조체는 깊은 복사가 일어난다.틀렸고, 용어까지 반대로 썼다. C++에서는 class든 struct든 대입/복사 시 똑같이 값이 복사된다. 참조가 복사되는 건 C#의 참조 타입 이야기다. 그리고 참조를 복사하는 것은 애초에 “얕은 복사”가 아니라 그냥 같은 객체를 가리키는 별칭이다. 오히려 멤버별 값 복사(memberwise copy)가 얕은 복사다. 아래처럼 포인터 멤버가 있으면 포인터 값만 복사되어 두 객체가 같은 대상을 가리킨다.

    struct Buf {
        char* p;  // 기본 복사 생성자는 포인터 "값"만 복사한다 → 얕은 복사
    };
    

    깊은 복사가 필요하면 복사 생성자와 복사 대입 연산자를 직접 구현해야 한다. 이건 class냐 struct냐와 아무 상관이 없다.

  5. 구조체는 상속해줄 수도 상속 받을 수도 없다.틀렸다.Circle : Shape 예제처럼 잘 된다. C#의 struct 제약을 C++에 그대로 옮긴 것이었다.

  6. 구조체는 참조의 낭비를 막을 수 있으며 데이터에 직접 접근하기 때문에 속도가 더 빠르다.틀렸다. 두 키워드 사이에 성능 차이는 없다. 컴파일러 입장에서 같은 타입이기 때문이다. 성능을 가르는 건 가상 함수 유무(vptr과 간접 호출), 힙 할당 여부, 메모리 레이아웃과 캐시 지역성이지 키워드 선택이 아니다.

  7. 구조체는 함수의 재정의가 안된다.틀렸다. 구조체도 virtual 선언과 override가 된다. 오버로딩도 물론 된다.

값 복사 예제도 다시

당시에는 “클래스는 A를 바꾸면 B도 바뀌고, 구조체는 안 바뀐다”고 적었다. C++에서는 둘 다 B가 바뀌지 않는다.

struct SPoint { int x; };
class  CPoint { public: int x; };

SPoint a{1}, b = a;   // 값 복사
CPoint c;  c.x = 1;
CPoint d = c;         // 역시 값 복사

a.x = 99;             // b.x는 그대로 1
c.x = 99;             // d.x도 그대로 1

참조가 공유되게 하려면 포인터나 참조를 명시적으로 써야 한다.

CPoint* e = &c;       // e를 통해 바꾸면 c도 바뀐다
CPoint& f = c;        // 마찬가지

이 역시 class든 struct든 동일하다.

그럼 실무에서는 어떻게 나누는가

문법적으로 같으니 관례로 나눈다. 널리 쓰이는 기준은 이렇다.

  • struct: 불변식(invariant) 없이 데이터를 그냥 묶어놓은 것. 모든 멤버가 public이고 서로 독립적으로 바뀌어도 되는 경우. 패킷 구조체, 설정 값 묶음, 좌표, 타입 트레잇 등.
  • class: 불변식을 유지해야 하는 것. 내부 상태를 감추고 멤버 함수를 통해서만 다루게 하는 경우.

C++ Core Guidelines의 C.2가 정확히 이 기준을 권한다.

참고자료

댓글 남기기