구조체와 클래스의 차이점
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(복사 생성자)를 따로 정의하는 경우가 있는데, 이때는 단순히 메모리 카피가 일어나는 것보다 더 큰 오버헤드가 발생할 수 있기는 하다.
근데 사실 성능의 대한 이유보다는 그냥 코딩 스타일을 위한 가이드라고 생각하면 될 것 같다. 반드시 이 규칙을 따르지 않더라도, 클래스와 구조체를 일관되게 사용하는 것 자체는 중요하다. 그래야 적어도 이 구조체 안에는 멤버 함수는 없겠구나, 단순한 데이터 컨테이너겠구나 정도는 알 수 있다.
보통 보편적으로 따르는 코딩 스타일이기도 하다.