[C++] struct와 class의 차이점

구조체와 클래스의 차이점


C++에는 여러 데이터 집합을 담을 수 있는 대표적인 방법이 두 가지가 있는데 구조체(struct)와 클래스가 바로 그것이다. 둘이 역할은 비슷해보이는데, 둘의 차이점은 무엇일까?

접근 제한자 - struct는 public, class는 private

일단 기본 접근 제한자가 다르다. 기본 접근 제한자란 해당 구조체 혹은 클래스를 선언하고, 명시적으로 접근 제한자를 설정하지 않았을 때 기본적으로 주어지는 멤버의 접근 제한자를 의미한다.

그래서 struct를 생성했을 때, 접근 제한자를 아무것도 쓰지 않는다면 자동으로 모든 멤버가 public으로 처리된다.

반대로 class를 생성했을 때는 기본적으로 private하게 주어진다.

struct UserData {
    int ID; // public
    std::string name; // public
};

class DataManager {
    UserData user; // private
    std::vector<int> buy_list; // private
};

물론 권장되는 코딩 스타일 가이드는 클래스에서 접근 제한자를 명시적으로 모두 기술하는 것이다. 즉 private라고 기본으로 주어진다 하더라도 일단 명시적으로 private라 쓰는 게 권장 스타일이긴 하다(struct의 경우는 이후 설명할 이유때문에 좀 다르다. 그 얘기는 밑에서)

struct DataManager {
private: // recommended style
    UserData user;
    std::vector<int> buy_list;
};

사실 그 외에 차이점은 없다.

재밌게도, 접근 제한자 외에 클래스와 구조체는 C++에서 차이점이 없다. 둘 다 명시적으로 접근 제한자를 기술한다면 private, protected, public 멤버를 가질 수 있고, 재밌는 점은 구조체 역시 C++에선 상속이 가능하다. C++의 struct는 C의 struct와 사뭇 다르다.

사실 어셈블리 관점에서도, struct와 class는 똑같다. 둘 다 동일한 레이아웃을 가진 연속적인 데이터 블록으로 메모리에 표현된다.

사실 C++의 클래스는 본질적으로 C언어에서 구조체에다가 함수 포인터를 더한 것일 뿐이다. C 언어에서도 클래스를 만들 수야 있는데, 그게 바로 struct 에다가 함수 포인터를 집어넣은 것이다. 본질적으로 클래스와 구조체는 다르지 않다.

실제로 클래스와 구조체는 컴파일되는 어셈블리를 놓고 비교해본다면 둘이 완전히 동일하다.
믿지 못 할까봐 실제로 보여드림

#include <string>

struct UserData1 {
    int ID; // public
    std::string name; // public
};

class UserData2 {
    int ID; // public
    std::string name; // public
};

int main()
{
    UserData1 user1;
    UserData2 user2;
    return 0;
}

위 코드를 어셈블리로 변환해보면 다음과 같다.

UserData1::UserData1() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        add     rax, 8
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string() [complete object constructor]
        nop
        leave
        ret

; destructor는 생략

UserData2::UserData2() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        add     rax, 8
        mov     rdi, rax
        call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string() [complete object constructor]
        nop
        leave
        ret

;마찬가지로 destructor 생략

위에 어셈블리 코드를 보면 알겠지만, UserData1과 UserData2는 코드가 완전히 동일하다. 즉 클래스와 구조체의 구분은 단순히 컴파일러 단에서 구분되는 표시일 뿐 실제 동작 원리는 완벽하게 똑같음을 알 수 있다.

사실 여담으로 이 사실이 당연하단 것을 기억해야 한다. 왜냐면 **C언어에 없는 것들은 다른 언어에서도 무조건 없기 때문이다.** 애초에 C언어 자체가 기계어와 많이 가깝고, C언어 코드가 대부분 그대로 기계어로 번역되기 때문이다. 이 말이 무엇을 의미하냐면, C에서 없는 기능은 다른 언어에서도 존재하지 않는다. 기계어로 구현할 수 없다. 단지 컴파일러가 잡아낼 수 있도록 인터페이스를 구성한 것 뿐이다. 클래스 역시 C언어에 존재하지 않는다. 이 말은 클래스 역시 사실은 구조체를 변형한 인터페이스일 뿐임을 의미한다.

다만 클래스는 구조체보다 약간의 오버헤드가 발생할 수는 있는데, 그 이유는 클래스는 데이터와 메소드 함수를 그룹화하여 캡슐화추상화하겠다는데 초점을 맞추고 있고, 구조체는 데이터를 표현하는데 초점을 맞추고 있어서, 그에 따라 구현하게 된다면 어느 정도의 오버헤드가 발생할 수도 있다.

구조체 안에서도 함수 선언 가능합니다.

이미 앞에서 언급했지만, C++에서는 구조체 안에서 함수 선언이 가능하다. 물론 뒤에서 설명하겠지만 구조체에 함수를 만들 이유는 하나도 없다.

struct UserData {
    int ID; // public
    std::string name; // public
    auto ResquestUserData(const std::string& uri); // 함수 선언 가능
};

// 클래스처럼 함수 정의도 가능하다
auto UserData::ResquestUserData(const std::string& uri) 
{
    return;
}

C의 구조체와는 다르다고 할 수는 있는데 이 말은 반은 맞고 반은 틀린 말이다.
사실 C에서도 함수 포인터를 이용해 구조체의 멤버 함수로 집어넣을 수 있기는 하다. 다만 C에서는 접근 제한자가 없어서 그렇게 할 이유가 없긴 하다. 기억해야 할 점은 C++의 클래스와 구조체 역시 C의 구조체 + 함수 포인터 + 컴파일러의 규칙을 합쳐서 만든 결과물이다.

무엇을 써야 하나


이 부분은 기술적인 내용보다는 코딩 스타일에 관한 내용이다.

사실 이 부분의 답은 간단하다. 단순히 데이터 집합만을 나타내는 것이 아니라면, 클래스를 쓰면 된다.

사실 거의 대부분의 경우 그냥 클래스를 쓰면 된다. 메소드 함수, 연산자 오버로딩, constructor/destructor, private 멤버가 하나라도 들어가는 순간 class를 쓰면 된다.

다만 예외적으로 모든 멤버가 전부 public이고, 멤버 함수가 하나도 없고, 간단한 데이터의 집합만을 표현할 때는 클래스보다 구조체가 더 유용할 수 있다.

사실 거의 모든 곳에서 클래스를 쓰면 일관되게끔 할 수 있는데, 정말 단순한 데이터의 집합이 필요한 경우가 있는데 그때 구조체가 유용하긴 하다. 오버헤드가 없다는 게 장점인데 예를 들어서 데이터를 복사할 때, 대입 연산자를 하면 구조체에서는 이 복사 연산이 마치 memcpy를 돌린 것처럼 일어난다. 즉 예를 들어 다음과 같이

struct Asset {
    int id;
    double price;
}

int + double 해서 12byte가 있을 경우, 이를 복사생성하면 memcpy처럼 그냥 12바이트가 복사된다(정확히 말하면 16바이트다. 이유는 이 글에서 살짝 언급하는데, 추후에 더 자세히 다루겠다).

그런데 클래스의 경우 보통 copy constructor(복사 생성자)를 따로 정의하는 경우가 있는데, 이때는 단순히 메모리 카피가 일어나는 것보다 더 큰 오버헤드가 발생할 수 있기는 하다.

근데 사실 성능의 대한 이유보다는 그냥 코딩 스타일을 위한 가이드라고 생각하면 될 것 같다. 반드시 이 규칙을 따르지 않더라도, 클래스와 구조체를 일관되게 사용하는 것 자체는 중요하다. 그래야 적어도 이 구조체 안에는 멤버 함수는 없겠구나, 단순한 데이터 컨테이너겠구나 정도는 알 수 있다.

보통 보편적으로 따르는 코딩 스타일이기도 하다.