C++ struct vs class | 접근 제어·POD·C 호환 완벽 비교
이 글의 핵심
C++ struct vs class 차이점. 기본 접근 제어만 다르고 (struct: public, class: private), 기능은 완전히 동일합니다. 데이터 묶음은 struct, 캡슐화 객체는 class를 사용하세요.
들어가며
C++에서 struct와 class는 기본 접근 제어만 다를 뿐, 기능은 완전히 동일합니다.
비유로 말씀드리면, 문법상 차이는 회의실 문 앞에 ‘공개’ 스티커를 붙이느냐, ‘비공개’가 기본이냐 정도이고, 방 안에서 할 수 있는 일(메서드, 상속 등)은 같습니다. 관례적으로는 데이터 묶음에는 struct, 불변식을 지키는 객체에는 class를 쓰는 경우가 많습니다.
이 글을 읽으면
- struct와 class의 유일한 차이를 이해합니다
- 사용 규칙과 선택 기준을 파악합니다
- POD 타입과 C 호환성을 확인합니다
- 실무 시나리오별 선택 전략을 익힙니다
목차
struct vs class 차이
유일한 차이: 기본 접근 제어
| 항목 | struct | class |
|---|---|---|
| 기본 접근 제어 | public | private |
| 기본 상속 | public | private |
| 생성자 | ✅ | ✅ |
| 소멸자 | ✅ | ✅ |
| 멤버 함수 | ✅ | ✅ |
| 가상 함수 | ✅ | ✅ |
| 상속 | ✅ | ✅ |
| 템플릿 | ✅ | ✅ |
실전 구현
1) 기본 접근 제어
#include <iostream>
// struct: 기본 public
struct Point {
int x, y; // public (기본)
};
// class: 기본 private
class Point2 {
int x, y; // private (기본)
public:
Point2(int x, int y) : x(x), y(y) {}
int getX() const { return x; }
int getY() const { return y; }
};
int main() {
Point p;
p.x = 10; // ✅ OK
p.y = 20;
Point2 p2(10, 20);
// p2.x = 10; // ❌ 컴파일 에러: private 멤버
std::cout << p2.getX() << std::endl;
return 0;
}
2) 상속 기본 접근 제어
#include <iostream>
class Base {
public:
void foo() {
std::cout << "Base::foo" << std::endl;
}
};
// struct: 기본 public 상속
struct DerivedStruct : Base { // public 상속
};
// class: 기본 private 상속
class DerivedClass : Base { // private 상속
};
int main() {
DerivedStruct ds;
ds.foo(); // ✅ OK (public 상속)
DerivedClass dc;
// dc.foo(); // ❌ 컴파일 에러 (private 상속)
return 0;
}
3) struct도 class처럼 사용 가능
#include <iostream>
struct MyStruct {
private: // private 명시 가능
int x_;
public:
MyStruct(int x) : x_(x) {
std::cout << "생성자: " << x_ << std::endl;
}
virtual void foo() { // 가상 함수
std::cout << "MyStruct::foo: " << x_ << std::endl;
}
virtual ~MyStruct() {
std::cout << "소멸자: " << x_ << std::endl;
}
};
struct Derived : MyStruct {
Derived(int x) : MyStruct(x) {}
void foo() override {
std::cout << "Derived::foo" << std::endl;
}
};
int main() {
MyStruct* p = new Derived(42);
p->foo(); // Derived::foo
delete p;
return 0;
}
4) 사용 규칙 (Google C++ Style Guide)
struct: 데이터만 담는 수동적 객체
// ✅ struct 사용
struct Point {
int x, y;
};
struct Color {
uint8_t r, g, b, a;
};
struct Config {
std::string host;
int port;
bool useSSL;
};
class: 캡슐화와 메서드가 있는 능동적 객체
// ✅ class 사용
class BankAccount {
private:
double balance_;
public:
BankAccount(double initial) : balance_(initial) {}
void deposit(double amount) {
if (amount > 0) {
balance_ += amount;
}
}
void withdraw(double amount) {
if (amount > 0 && balance_ >= amount) {
balance_ -= amount;
}
}
double getBalance() const {
return balance_;
}
};
고급 활용
1) POD 타입
POD(Plain Old Data)는 C와 호환되는 단순 타입입니다.
POD 조건 (C++11):
- Trivial 생성자
- Trivial 소멸자
- Trivial 복사/이동 연산자
- Standard layout (private/protected 멤버 없음, 가상 함수 없음)
#include <iostream>
#include <type_traits>
// ✅ POD
struct Point {
int x, y;
};
static_assert(std::is_pod_v<Point>); // true
// ❌ 비POD (생성자 있음)
struct Point2 {
int x, y;
Point2(int x, int y) : x(x), y(y) {}
};
static_assert(!std::is_pod_v<Point2>); // false
int main() {
std::cout << "Point is POD: " << std::is_pod_v<Point> << std::endl;
std::cout << "Point2 is POD: " << std::is_pod_v<Point2> << std::endl;
return 0;
}
2) C 호환성
// common.h
#ifdef __cplusplus
extern "C" {
#endif
struct Point {
int x, y;
};
void processPoint(struct Point* p);
#ifdef __cplusplus
}
#endif
// common.cpp
#include "common.h"
#include <iostream>
void processPoint(struct Point* p) {
std::cout << "Point: (" << p->x << ", " << p->y << ")" << std::endl;
}
// main.cpp
#include "common.h"
int main() {
Point p = {10, 20}; // C++에서는 struct 키워드 생략 가능
processPoint(&p);
return 0;
}
3) memcpy 가능 (POD)
#include <cstring>
#include <iostream>
struct Point {
int x, y;
};
int main() {
Point p1 = {10, 20};
Point p2;
std::memcpy(&p2, &p1, sizeof(Point)); // ✅ POD는 memcpy 가능
std::cout << "p2: (" << p2.x << ", " << p2.y << ")" << std::endl;
return 0;
}
성능 비교
struct vs class
#include <chrono>
#include <iostream>
struct PointStruct {
int x, y;
};
class PointClass {
public:
int x, y;
};
int main() {
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000000; ++i) {
PointStruct p = {i, i};
int sum = p.x + p.y;
}
auto end1 = std::chrono::high_resolution_clock::now();
auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000000; ++i) {
PointClass p = {i, i};
int sum = p.x + p.y;
}
auto end2 = std::chrono::high_resolution_clock::now();
auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
std::cout << "struct: " << time1 << "ms" << std::endl;
std::cout << "class: " << time2 << "ms" << std::endl;
return 0;
}
결과 (GCC 13, -O3):
| 타입 | 시간 | 상대 속도 |
|---|---|---|
| struct | 5ms | 1.0x |
| class | 5ms | 1.0x |
결론: 성능 차이 없음
실무 사례
사례 1: 게임 엔진 - 데이터 구조
#include <iostream>
#include <vector>
// struct: 데이터만
struct Transform {
float x, y, z;
float rotX, rotY, rotZ;
float scaleX, scaleY, scaleZ;
};
// class: 캡슐화
class Entity {
private:
int id_;
Transform transform_;
bool active_;
public:
Entity(int id) : id_(id), transform_{0, 0, 0, 0, 0, 0, 1, 1, 1}, active_(true) {}
void setPosition(float x, float y, float z) {
transform_.x = x;
transform_.y = y;
transform_.z = z;
}
Transform getTransform() const {
return transform_;
}
bool isActive() const {
return active_;
}
};
int main() {
Entity entity(1);
entity.setPosition(10.0f, 20.0f, 30.0f);
Transform t = entity.getTransform();
std::cout << "Position: (" << t.x << ", " << t.y << ", " << t.z << ")" << std::endl;
return 0;
}
사례 2: 네트워크 - 프로토콜 메시지
#include <cstring>
#include <iostream>
// struct: 네트워크 메시지 (POD)
struct Message {
uint32_t type;
uint32_t length;
char data[256];
};
// class: 메시지 핸들러
class MessageHandler {
public:
void handleMessage(const Message& msg) {
std::cout << "Type: " << msg.type << ", Length: " << msg.length << std::endl;
std::cout << "Data: " << msg.data << std::endl;
}
};
int main() {
Message msg;
msg.type = 1;
msg.length = 5;
std::strncpy(msg.data, "Hello", 255);
msg.data[255] = '\0';
MessageHandler handler;
handler.handleMessage(msg);
return 0;
}
사례 3: 데이터베이스 - DTO
#include <iostream>
#include <string>
#include <vector>
// struct: DTO (Data Transfer Object)
struct UserDTO {
int id;
std::string name;
std::string email;
int age;
};
// class: 서비스
class UserService {
public:
UserDTO getUserById(int id) {
// 데이터베이스 조회
return {id, "홍길동", "[email protected]", 30};
}
void saveUser(const UserDTO& user) {
std::cout << "저장: " << user.name << std::endl;
}
};
int main() {
UserService service;
UserDTO user = service.getUserById(1);
std::cout << "이름: " << user.name << std::endl;
user.age = 31;
service.saveUser(user);
return 0;
}
사례 4: 설정 관리
#include <iostream>
#include <string>
// struct: 설정 데이터
struct ServerConfig {
std::string host;
int port;
int maxConnections;
bool useSSL;
};
// class: 설정 관리자
class ConfigManager {
private:
ServerConfig config_;
public:
ConfigManager(const ServerConfig& config) : config_(config) {}
void validate() {
if (config_.port < 1 || config_.port > 65535) {
throw std::invalid_argument("잘못된 포트 번호");
}
if (config_.maxConnections < 1) {
throw std::invalid_argument("잘못된 최대 연결 수");
}
}
ServerConfig getConfig() const {
return config_;
}
};
int main() {
ServerConfig config = {"localhost", 8080, 100, false};
ConfigManager manager(config);
try {
manager.validate();
std::cout << "설정 유효" << std::endl;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
트러블슈팅
문제 1: struct에 private 멤버
증상: 의도와 다른 접근 제어
// ❌ struct에 private (혼란스러움)
struct Point {
private: // struct인데 private?
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
int getX() const { return x; }
};
// ✅ class 사용
class Point {
private:
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
int getX() const { return x; }
};
문제 2: class에 모두 public
증상: 의도와 다른 접근 제어
// ❌ class에 모두 public (혼란스러움)
class Point {
public:
int x, y; // class인데 public?
};
// ✅ struct 사용
struct Point {
int x, y;
};
문제 3: POD 조건 위반
증상: C 호환 불가
// ❌ 비POD (생성자 있음)
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
static_assert(!std::is_pod_v<Point>); // false
// ✅ POD
struct Point2 {
int x, y;
};
static_assert(std::is_pod_v<Point2>); // true
// C 호환
extern "C" {
void processPoint(Point2* p);
}
문제 4: 상속 접근 제어 혼란
증상: 의도와 다른 상속
class Base {
public:
void foo() {}
};
// ❌ struct인데 private 상속 (명시 필요)
struct Derived : private Base { // private 명시
};
Derived d;
// d.foo(); // ❌ 컴파일 에러 (private 상속)
// ✅ struct는 기본 public 상속
struct Derived2 : Base { // public 상속 (기본)
};
Derived2 d2;
d2.foo(); // ✅ OK
마무리
struct와 class의 차이는 기본 접근 제어뿐입니다.
핵심 요약
-
struct vs class
- struct: 기본 public
- class: 기본 private
- 기능은 완전히 동일
-
사용 규칙
- 데이터만: struct
- 메서드 + 캡슐화: class
- POD 필요: struct
-
POD 타입
- C 호환
- memcpy 가능
- 바이너리 직렬화 가능
-
성능
- struct와 class는 성능 차이 없음
- 캡슐화가 성능에 영향 없음
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| 데이터만 | struct | 의도 명확 |
| 캡슐화 필요 | class | 불변식 보호 |
| C 호환 | struct (POD) | 바이너리 호환 |
| 메서드 많음 | class | 관례 |
| 간단한 값 타입 | struct | 간결 |
코드 예제 치트시트
// struct: 데이터만
struct Point {
int x, y;
};
// class: 캡슐화
class BankAccount {
private:
double balance_;
public:
BankAccount(double initial) : balance_(initial) {}
void deposit(double amount) { balance_ += amount; }
double getBalance() const { return balance_; }
};
// POD 확인
static_assert(std::is_pod_v<Point>);
// C 호환
extern "C" {
void processPoint(Point* p);
}
다음 단계
- 클래스 기초: C++ 클래스 기초
- 접근 제어: C++ 접근 제어
- POD 타입: C++ POD 타입
참고 자료
- “Effective C++” - Scott Meyers
- “C++ Primer” - Stanley Lippman
- Google C++ Style Guide: https://google.github.io/styleguide/cppguide.html
한 줄 정리: struct는 데이터 컨테이너, class는 캡슐화된 객체로 사용하고, 기능은 완전히 동일하지만 의도를 명확히 표현하는 것이 중요하다.