1. 이론 (Theory)
1.1. 구조체의 기초 (Basics of Struct)
1.1.1. 구조체란? (What is a struct?)
구조체(struct
)는 C 언어에서 유래한 개념으로, 관련 있는 여러 데이터를 하나의 단위로 묶기 위한 사용자 정의 자료형이다. C언어의 구조체는 순수하게 데이터의 집합만을 다루었지만, C++로 넘어오면서 클래스(class
)의 거의 모든 기능을 갖도록 확장되었다.
C++에서 구조체는 멤버 변수뿐만 아니라 멤버 함수, 생성자, 소멸자, 상속 등 클래스가 할 수 있는 모든 것을 할 수 있다. 그럼에도 불구하고, C++ 프로그래머들은 관례적으로 구조체와 클래스를 다른 용도로 사용한다.
1.1.2. struct
vs. class
C++ 문법상 struct
와 class
의 유일한 차이점은 기본 접근 지정자와 기본 상속 접근 지정자다.
구분 | struct |
class |
---|---|---|
기본 멤버 접근 지정자 | public |
private |
기본 상속 접근 지정자 | public |
private |
예시:
struct MyStruct {
int data; // 기본적으로 public
};
class MyClass {
int data; // 기본적으로 private
};
MyStruct s;
s.data = 10; // 가능
MyClass c;
c.data = 10; // 컴파일 에러! private 멤버에 접근 불가
이 작은 차이점으로부터 두 키워드의 관례적인 사용법이 나뉜다.
struct
: 모든 데이터가 외부에 공개되어도 괜찮은, 수동적인 데이터 묶음(Passive Data Structure) 에 주로 사용됩. 데이터 자체를 담는 것이 주 목적일 때 적합. (e.g.,Vector3
,ItemData
,PlayerInfo
)class
: 데이터와 그것을 다루는 행위를 강력하게 결합하고, 정보 은닉(private)을 통해 데이터의 무결성을 보장해야 하는 능동적인 객체(Active Object) 에 주로 사용. (e.g.,InventoryManager
,CharacterController
,DatabaseConnection
)
1.1.3. POD (Plain Old Data)와 표준 레이아웃 (Standard-Layout)
- POD (Plain Old Data): C언어의
struct
처럼 메모리에 직접 복사(memcpy
)하거나 파일에 쓰고 읽을 수 있는 단순한 데이터 덩어리를 의미하는 개념이다. C++03까지는 엄격한 규칙(가상 함수나 참조 멤버가 없을 것 등)을 가졌다. POD 타입은 C 라이브러리와의 호환성, 네트워크 통신 등에서 중요하게 다뤄졌다. - 표준 레이아웃 (Standard-Layout, C++11 이후): C++11부터 POD 개념은 Trivial과 Standard-Layout이라는 더 세분화된 개념으로 발전했다. Standard-Layout은 멤버들이 메모리에 선언된 순서대로 배치되는 것을 보장하는 속성이다. (특정 조건 하에) 이는 C와 같은 다른 언어와 데이터를 주고받을 때 예측 가능한 메모리 구조를 제공하기 때문에 매우 중요하다.
struct
는 기본적으로public
이므로, 단순 데이터 묶음을 만들 때 자연스럽게 Standard-Layout 규칙을 만족하기 쉽다. 이것이struct
가 데이터 교환용으로 선호되는 이유 중 하나다.
1.1.4. 상속에서의 차이 (Difference in Inheritance)
struct
와 class
의 또 다른 중요한 차이점은 기본 상속 접근 지정자다.
struct
는 기본적으로public
상속.class
는 기본적으로private
상속.
이는 struct
가 개방적인 데이터 구조를, class
가 폐쇄적인 캡슐화를 지향하는 설계 사상을 반영한다.
예시:
class Base {
public:
void public_func() {}
protected:
void protected_func() {}
private:
void private_func() {}
};
// struct는 기본적으로 public 상속
// struct DerivedStruct : public Base 와 동일
struct DerivedStruct : Base {
void test() {
public_func(); // 가능
protected_func(); // 가능
// private_func(); // 접근 불가 (Base의 private)
}
};
// class는 기본적으로 private 상속
// class DerivedClass : private Base 와 동일
class DerivedClass : Base {
void test() {
public_func(); // 가능 (private으로 상속됨)
protected_func(); // 가능 (private으로 상속됨)
// private_func(); // 접근 불가 (Base의 private)
}
};
int main() {
DerivedStruct ds;
ds.public_func(); // 가능. public 상속했으므로 public 멤버
DerivedClass dc;
// dc.public_func(); // 컴파일 에러! private으로 상속받았으므로 외부 접근 불가
}
1.1.5. 메모리 관점에서의 차이 (Difference in Memory)
결론부터 말하면, 메모리 할당 방식이나 내부 구조(레이아웃) 관점에서 struct
와 class
는 아무런 차이가 없습니다.
- 동일한 블루프린트: 컴파일러에게
struct
와class
는 모두 객체를 만들기 위한 청사진(블루프린트)일 뿐입니다. 컴파일러는struct
키워드를 보든class
키워드를 보든, 내부에 정의된 멤버 변수와 가상 함수 유무 등을 바탕으로 동일한 규칙에 따라 메모리 크기와 레이아웃(패딩, 정렬 포함)을 계산합니다. - 동일한 할당 방식: 객체가 메모리에 할당되는 위치(스택 또는 힙)는 키워드가 아닌 선언 방식에 따라 결정됩니다.
MyStruct s;
또는MyClass c;
처럼 함수 내에 선언하면 스택에 생성됩니다.new MyStruct();
또는new MyClass();
처럼new
를 사용하면 힙에 생성됩니다.
따라서 "메모리 등록"이라는 관점에서 둘의 차이는 없으며, 오직 접근 제어와 관련된 문법적, 관례적 차이만 존재합니다.
1.2. 구조체의 활용 (Using Structs)
게임 개발에서 구조체는 다음과 같은 상황에서 매우 유용하게 사용됩니다.
- 게임 데이터 정의: 아이템 정보, 몬스터 스탯, 레벨 구성 데이터 등 순수한 정보의 묶음을 정의할 때 사용됩니다.
struct ItemData { int id; std::string name; float weight; };
- 수학적 표현: 3D 벡터, 위치 좌표, 색상(RGBA) 등 여러 요소를 하나로 묶어 표현할 때 사용됩니다.
struct Vector3 { float x, y, z; };
- 함수의 반환 값: 함수가 여러 개의 값을 반환해야 할 때, 이 값들을 담는 구조체를 정의하여 반환 타입으로 사용할 수 있습니다.
struct QueryResult { bool success; std::string data; std::string errorMessage; }; QueryResult FetchDataFromServer();
1.3. 고급 주제: 비트 필드 (Bit-fields)
구조체는 메모리 사용량을 극한으로 최적화해야 할 때 비트 필드라는 기능을 제공합니다. 이는 멤버 변수가 사용할 비트(bit) 수를 직접 지정하는 기능입니다. 주로 네트워크 패킷이나 하드웨어 제어처럼 데이터 크기에 매우 민감한 경우에 사용됩니다.
예시: 8비트(1바이트) 공간에 여러 상태 값을 저장
struct PlayerState {
// 1비트만 사용하여 bool처럼 활용
bool isJumping : 1;
bool isAttacking : 1;
bool isGrounded : 1;
// 3비트를 사용하여 0~7까지의 값 저장
unsigned int weaponType : 3;
// 나머지 2비트는 사용하지 않음 (패딩)
unsigned int : 2;
};
// sizeof(PlayerState)는 1바이트가 될 가능성이 높음 (컴파일러에 따라 다름)
주의: 비트 필드의 메모리 레이아웃은 컴파일러마다 다를 수 있어 이식성이 떨어지므로, 신중하게 사용해야 합니다.
1.4. 동작 계층 구조 (Operational Hierarchy)
struct
의 동작 계층은 class
와 거의 동일합니다. 컴파일러와 런타임은 struct
를 class
의 특별한 형태로 취급합니다.
- 사용자 코드: 개발자가
struct Item { ... };
와 같이 구조체를 정의하고,Item sword;
처럼 변수를 선언합니다. - 컴파일러:
struct
정의를 파싱하고 멤버들의 메모리 레이아웃을 결정합니다. (패딩, 정렬 규칙 적용)struct
의 멤버 접근이public
규칙에 맞는지 확인합니다.- 멤버 함수가 있다면
class
와 동일하게 코드를 생성합니다.
- 런타임 및 운영체제:
class
와 마찬가지로, 지역 변수는 스택에,new
로 생성 시 힙에 메모리가 할당됩니다.- 생성자가 있다면 호출되어 멤버를 초기화합니다.
결론적으로, 문법적 차이 외에 컴파일 및 실행 단계에서 struct
와 class
는 본질적으로 동일하게 처리됩니다. 차이는 프로그래머의 설계 의도를 표현하는 데 있습니다.
2. 실습 코드 (Practice Code)
다음은 게임 인벤토리 시스템에서 struct
와 class
를 역할에 맞게 사용한 예시다.
Inventory.h
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// Q1. 아래 `ItemData`를 `class`가 아닌 `struct`로 정의한 주된 이유는 무엇일까요?
// 만약 `class`로 정의했다면 코드에 어떤 변화가 필요할까요?
/* A. ItemData는 POD이기 때문에 멤버 변수와 생성자, 함수를 외부에서 자유롭게 접근
* 할 수 있어야 함. 따라서 struct를 사용.
* class로 정의했다면 public: 접근 지정자를 명시해야 함.
* */
// 아이템의 순수한 데이터를 표현하는 구조체
struct ItemData {
int id;
std::string name;
std::string description;
// 구조체도 생성자를 가질 수 있음
ItemData(int id, const std::string& name, const std::string& desc)
: id(id), name(name), description(desc) {}
void Print() const {
std::cout << "- " << name << " (ID: " << id << ")\n " << description << std::endl;
}
};
// 인벤토리를 관리하는 클래스
class InventoryManager {
public:
// Q2. 아래 `std::vector`에 `ItemData` 자체를 저장하지 않고 `std::shared_ptr<ItemData>`를 사용한 이유는 무엇일까요?
// 이렇게 했을 때의 장점과 단점을 설명하세요.
/* A. 장점
* shared_ptr를 사용해 참조 카운트를 기반으로 더 이상 사용되지 않을 때 자동 해제.
* 포인터만 복사하기 때문에 가벼움.
* 동일한 객체를 여러 컨테이너가 동시에 참조할 수 있음.
* 단점
* 참조 카운팅 오버헤드
* shared_ptr끼리 서로 참조하면 순환 참조 위험
* 객체를 new로 만들어서 힙 할당 강제. 스택 생성보다 비용이 큼.
* */
void AddItem(std::shared_ptr<ItemData> item) {
if (item) {
items.push_back(item);
std::cout << "'" << item->name << "'을(를) 인벤토리에 추가했습니다." << std::endl;
}
}
void PrintAllItems() const {
std::cout << "\n[인벤토리 목록]" << std::endl;
if (items.empty()) {
std::cout << "(비어 있음)" << std::endl;
} else {
for (const auto& item : items) {
item->Print();
}
}
}
private:
std::vector<std::shared_ptr<ItemData>> items;
};
main.cpp
#include "Inventory.h"
// 아이템 정보를 생성해서 반환하는 함수
// Q3. 이 함수가 `ItemData`를 포인터나 참조가 아닌 값 자체로 반환(`return`)하고 있습니다.
// 이렇게 해도 괜찮은 이유는 무엇이며, 어떤 과정이 일어날까요? (RVO를 검색해보세요)
/* A. 복사 생성자가 반드시 호출되는 것이 아니기 때문.
* 컴파일러가 자동으로 최적화 적용 -> 반환할 객체를 호출자의 스택 프레임에 직접 생성
* */
ItemData CreateNewItem(int id, const std::string& name, const std::string& desc) {
return ItemData(id, name, desc);
}
int main() {
// 인벤토리 관리자 객체 생성
InventoryManager inventory;
// 아이템 데이터 생성
auto healingPotion = std::make_shared<ItemData>(101, "체력 물약", "체력을 50 회복합니다.");
inventory.AddItem(healingPotion);
// Q4. 아래와 같이 `CreateNewItem` 함수를 호출하여 지역 변수 `swordData`를 생성했습니다.
// 이 `swordData`의 메모리는 언제, 어디서 해제될까요?
/* A. 스택 영역에 생성 -> 메인 함수의 실행이 끝나거나, 범위를 벗어날 때 자동으로 소멸자가 호출되어 정리.
* 복사해서 새로운 객체가 힙에 생성되고 shared_ptr로 관리 -> 더 이상 참조되는 shared_ptr가 없을 때 자동 해제.
* 즉, 참조가 모두 사라질 때 해제됨.
* */
ItemData swordData = CreateNewItem(201, "강철 검", "기본적인 강철 검입니다.");
inventory.AddItem(std::make_shared<ItemData>(swordData));
inventory.PrintAllItems();
return 0;
}
'C++' 카테고리의 다른 글
vtable과 vptr (0) | 2025.09.05 |
---|---|
동적 바인딩 feat. vtable & vptr (0) | 2025.09.03 |
클래스 (0) | 2025.09.02 |
가상 함수와 순수 가상 함수 (0) | 2025.09.02 |
템플릿 (2) | 2025.09.01 |