C++/C++ 문법과 핵심 개념

C++ Struct와 Class의 차이, 서로 상속할 수 있을까?

loooong 2026. 8. 26. 21:10

C++에서 새로운 타입을 정의하다 보면 struct와 class 중 무엇을 사용해야 할지 고민하게 된다.

처음에는 struct는 단순히 데이터를 묶는 용도이고, class는 함수와 객체 지향 기능을 사용하는 타입이라고 생각하기 쉽다.

 

하지만 C++의 struct와 class는 생각보다 훨씬 비슷하다.

생성자, 소멸자, 상속, 가상 함수, 다형성 등 거의 모든 기능을 동일하게 사용할 수 있다.

 

그렇다면 굳이 두 문법을 나누어 사용하는 이유는 무엇일까?

이번 글에서는 struct와 class의 실제 차이가 무엇인지, 그리고 코드에서 어떤 기준으로 구분해서 사용하는지 정리해본다.

 


Struct와 Class는 기능적으로 거의 같다

C에서는 struct가 주로 여러 데이터를 하나로 묶는 용도로 사용되었다.

하지만 C++의 struct는 클래스가 사용할 수 있는 객체 지향 기능을 대부분 동일하게 사용할 수 있다.

예를 들어 struct에도 생성자와 멤버 함수를 정의할 수 있다.

struct Character
{
    Character()
    {
    }

    void Move()
    {
    }
};

상속과 가상 함수도 사용할 수 있다.

struct Character
{
    virtual void Move()
    {
    }
};

struct Monster : public Character
{
    void Move() override
    {
    }
};

 

즉, C++에서 다음과 같은 기능은 struct와 class 모두 사용할 수 있다.

생성자
소멸자
멤버 함수
상속
가상 함수
다형성

 

그렇다면 둘의 차이는 무엇일까?

언어 차원에서 중요한 차이는 다음 두 가지이다.

1. 기본 접근 지정자
2. 기본 상속 지정자

struct는 기본값이 public이고, class는 기본값이 private이다.

 


접근 지정자란?

접근 지정자는 멤버 변수나 멤버 함수에 어디에서 접근할 수 있는지를 제어하는 키워드이다.

C++에서는 다음 세 가지 접근 지정자를 사용한다.

public
private
protected

 

public

public으로 선언된 멤버는 외부에서도 접근할 수 있다. 따라서 보통 객체가 외부에 공개해야 하는 기능을 public으로 둔다.

class Character
{
public:
    int HP;

    void TakeDamage(int Damage)
    {
        HP -= Damage; // 클래스 내부에서 접근 가능
    }
};

class Monster : public Character
{
public:
    void TakeDamage(int Damage)
    {
        HP -= Damage; // 자식 클래스에서도 접근 가능
    }
};

int main()
{
    Monster Monster1;
    Monster1.HP = 100; // 외부에서도 접근 가능
}

 


private

private 멤버는 해당 클래스 내부에서만 직접 접근할 수 있다.

외부뿐만 아니라 자식 클래스에서도 부모의 private 멤버에는 직접 접근할 수 없다.

이처럼 외부에서 객체의 내부 상태를 마음대로 변경하지 못하도록 제한할 수 있기 때문에 private은 캡슐화를 구현하는 중요한 수단이 된다.

class Character
{
private:
    int HP;

public:
    void TakeDamage(int Damage)
    {
        HP -= Damage; // 클래스 내부에서는 접근 가능
    }
};

class Monster : public Character
{
public:
    void TakeDamage(int Damage)
    {
        HP -= Damage; // Error! 부모의 private 멤버
    }
};

int main()
{
    Monster Monster1;
    Monster1.HP = 100; // Error!
}

 


protected

protected는 외부에서는 접근할 수 없지만 자식 클래스에서는 접근할 수 있다.

class Character
{
protected:
    int HP;

public:
    void TakeDamage(int Damage)
    {
        HP -= Damage;
    }
};

class Monster : public Character
{
public:
    void TakeDamage(int Damage)
    {
        HP -= Damage; // 자식 클래스에서는 접근 가능
    }
};

int main()
{
    Monster Monster1;
    Monster1.HP = 100; // Error! 외부에서는 접근 불가
}

 

 

정리하면 다음과 같다.

접근 지정자 클래스 내부 자식 클래스 외부
public O O O
protected O O X
private O X X

 

protected를 사용하면 자식 클래스에서 부모의 데이터를 직접 다룰 수 있어 편리하다.

하지만 자식 클래스가 부모 클래스의 내부 구현에 강하게 의존하게 될 수도 있다.

따라서 무조건 protected로 열어두기보다는 가능한 한 데이터를 private으로 숨기고, 필요한 기능만 함수로 제공하는 편이 유지보수에는 유리하다.

 


Struct와 Class의 첫 번째 차이 - 기본 접근 지정자

struct와 class의 첫 번째 차이는 접근 지정자를 생략했을 때 적용되는 기본값이다.

 

 

struct의 기본 접근 지정자는 public이다.

struct SMyStruct
{
    float X; // 기본적으로 public
    float Y;
};

따라서 외부에서 바로 접근할 수 있다.

SMyStruct V01;

V01.X = 10.0f;
V01.Y = 20.0f;

 

반면 class의 기본 접근 지정자는 private이다.

class CMyClass
{
    float X; // 기본적으로 private
    float Y;
};

따라서 다음과 같이 외부에서 접근할 수 없다.

CMyClass V02;

V02.X = 10.0f; // Error! Member is inaccessible
V02.Y = 20.0f; // Error! Member is inaccessible

 

 

즉, 다음 두 코드는 같은 의미이다.

struct SMyStruct
{
    float X;
};
struct SMyStruct
{
public:
    float X;
};

 

class는 반대이다.

class CMyClass
{
    float X;
};
class CMyClass
{
private:
    float X;
};

 

정리하면 다음과 같다.

타입 기본 접근 지정자
struct public
class private

 


Struct와 Class의 두 번째 차이 - 기본 상속 지정자

struct와 class에는 한 가지 차이가 더 있다. 바로 기본 상속 지정자이다.

부모의 멤버를 자식 입장에서 어떤 접근 수준으로 상속받을 것인지 결정한다.

 

상속 지정자는 다음 코드에서 public이 들어가는 부분을 말한다.

class Monster : public Character
{
};

 

 

Struct의 기본 상속은 public이다

다음 두 코드는 같은 의미이다.

struct CharacterStruct
{
};

struct MonsterStruct : CharacterStruct
{
};
struct CharacterStruct
{
};

struct MonsterStruct : public CharacterStruct
{
};

 

 

Class의 기본 상속은 private이다

다음 두 코드는 같은 의미이다.

class Character
{
};

class Monster : Character
{
};
class Character
{
};

class Monster : private Character
{
};

 

의도하지 않은 private 상속이 발생하면 단순히 멤버 접근 문제에서 끝나는 것이 아니라 업캐스팅이나 다형성에도 영향을 줄 수 있기 때문에 주의해야 한다.

 


public 상속

일반적인 객체 지향 상속에서 주로 사용하는 방식이다.

부모 멤버의 접근 수준은 다음과 같이 이어진다.

부모 자식에서
public public
protected protected
private 직접 접근 불가

 

예를 들어 다음과 같다.

class Character
{
public:
    int PublicHP;

protected:
    int ProtectedHP;

private:
    int PrivateHP;
};

class Monster : public Character
{
    void Test()
    {
        PublicHP = 100;       // 가능
        ProtectedHP = 100;    // 가능
        PrivateHP = 100;      // Error!
    }
};

 

객체 지향에서 말하는 is-a 관계의 상속은 일반적으로 public 상속으로 표현한다.

Monster is a Character

 

따라서 Monster 객체를 Character 타입으로 취급할 수 있다.

Monster M;
Character* Ptr = &M;

 


private 상속

private 상속에서는 부모의 public, protected 멤버가 자식 입장에서 private으로 취급된다.

부모의 실제 멤버가 private으로 변경되는 것은 아니다.

부모 private 상속 후
public private
protected private
private 직접 접근 불가

 

class Character
{
public:
    int PublicHP;

protected:
    int ProtectedHP;

private:
    int PrivateHP;
};

class Monster : private Character
{
    void Test()
    {
        PublicHP = 100;    // Monster 입장에서 private 멤버로 상속됨
        ProtectedHP = 100; // Monster 입장에서 private 멤버로 상속됨
        //PrivateHP = 100;   // 부모의 private 멤버 - 직접 접근 불가
    }
};

class BabyMonster : public Monster
{
    void Test2()
    {
        //PublicHP = 100;    // Monster에서 private으로 상속됨 - 접근 불가
        //ProtectedHP = 100; // Monster에서 private으로 상속됨 - 접근 불가
        //PrivateHP = 100;   // Character의 private 멤버 - 직접 접근 불가
    }
};

int main() {
    
    Monster M;
    //M.PublicHP = 100;   // Monster에서 private으로 상속됨 - 접근 불가
    //M.ProtectedHP= 100; // Monster에서 private으로 상속됨 - 접근 불가
    //M.PrivateHP= 100;   // Character의 private 멤버 - 직접 접근 불가
    
    return 0;
}

 

Monster를 기준으로 접근 수준이 private처럼 취급되는 것이다.

따라서 Monster 내부에서는 사용할 수 있지만, 외부 & 그 아래의 자식 클래스에서는 접근할 수 없다.

 

 


protected 상속

protected 상속에서는 부모의 public, protected 멤버가 자식에서 protected로 취급된다.

따라서 현재 클래스와 그 아래의 자식 클래스에서는 접근할 수 있지만 외부에서는 접근할 수 없다.

부모 protected 상속 후
public protected
protected protected
private 직접 접근 불가

 

class Character
{
public:
    int PublicHP;

protected:
    int ProtectedHP;

private:
    int PrivateHP;
};

class Monster : protected Character
{
    void Test()
    {
        PublicHP = 100;    // Monster 입장에서 protected 멤버로 상속됨
        ProtectedHP = 100; // Monster 입장에서 protected 멤버로 상속됨
        //PrivateHP = 100;   // 부모의 private 멤버 - 직접 접근 불가
    }
};

class BabyMonster : public Monster
{
    void Test2()
    {
        PublicHP = 100;    // Monster에서 protected으로 상속되어 접근 가능
        ProtectedHP = 100; // Monster에서 protected으로 상속되어 접근 가능
        //PrivateHP = 100;   // Character의 private 멤버 - 직접 접근 불가
    }
};

int main() {
    
    Monster M;
    //M.PublicHP = 100;   // Monster에서 protected으로 상속됨 - 접근 불가
    //M.ProtectedHP= 100; // Monster에서 protected으로 상속됨 - 접근 불가
    //M.PrivateHP= 100;   // Character의 private 멤버 - 직접 접근 불가
    
    return 0;
}

 

 


Struct와 Class는 서로 상속할 수 있을까?

가능하다.

struct와 class는 상속, 가상 함수, 다형성 등의 기능을 동일하게 지원하기 때문에 서로 자유롭게 상속할 수 있다.

struct Character
{
};

class Monster : public Character
{
};

반대도 가능하다.

class Character
{
};

struct Monster : public Character
{
};

 

단, 상속 지정자를 생략했을 때 적용되는 기본값이 다르다는 점을 주의해야 한다.

예를 들어 다음 코드를 보자.

struct Character
{
public:
    int HP;

    void Move()
    {
        cout << "Character Move\n";
    }
};

class Monster : Character // private 상속
{
};

기본 상속 지정자는 부모가 아니라 자식 타입의 선언 방식에 따라 결정된다.

 

Monster가 class이므로 다음 코드는 private 상속이다.

따라서 외부에서는 다음과 같이 사용할 수 없다.

Monster M;

M.Move(); // Error! Member is inaccessible

 


기본 상속 지정자를 놓치면 업캐스팅과 다운캐스팅이 막힌다

struct Character
{
public:
    void Move()
    {
        cout << "Character Move\n";
    }
};

class Monster : Character // private 상속
{
};

 

위 코드에서 private 상속은 멤버 접근뿐 아니라 외부에서 부모-자식 타입 관계를 이용하는 것에도 영향을 준다.

 


외부에서는 Monster*를 Character*로 업캐스팅할 수 없다

int main()
{
    Monster M;

    Character* Ptr = &M; // 컴파일 에러

    return 0;
}

컴파일러는 Monster가 Character를 상속하고 있다는 사실 자체는 알고 있다.

즉, 두 타입 사이의 변환 관계가 없는 것은 아니다. 문제는 그 상속 관계가 private이라는 점이다.

 

main()과 같은 외부 코드는 Monster의 private 상속 관계에 접근할 권한이 없기 때문에 다음 변환을 사용할 수 없다.

그래서 컴파일 에러가 발생한다.

 


Monster 내부에서는 업캐스팅할 수 있다

반면 Monster의 멤버 함수 내부에서는 자신의 private 상속 관계에 접근할 수 있다.

class Monster : Character
{
public:
    Character* GetCharacter()
    {
        return this;
    }
};

여기서 Monster* → Character* 암시적 업캐스팅은

컴파일러가 정적 타입과 상속 관계를 알고 있고, 해당 상속 관계에 접근할 권한도 있기 때문에 컴파일 타임에 자동으로 수행된다.

 

전체 코드를 보면 다음과 같다.

#include <iostream>
using namespace std;

struct Character
{
public:
    void Move()
    {
        cout << "Character Move\n";
    }
};

class Monster : Character // private 상속
{
public:
    Character* GetCharacter()
    {
        return this; // 가능
    }

    void TestInside()
    {
        Character* Ptr = this; // 클래스 내부에서는 업캐스팅 가능
        Ptr->Move();
    }
};

int main()
{
    Monster M;

    // 1. Monster 내부에서는 Character*로 변환 가능
    M.TestInside();

    // 2. public 함수가 Character*를 반환하는 것도 가능
    Character* Ptr1 = M.GetCharacter();
    Ptr1->Move();
    
    // 3. 외부에서 직접 업캐스팅은 불가능
    //Character* Ptr2 = &M; // 컴파일 에러

    return 0;
}

 

Ptr1은 Monster 객체 내부에 존재하는 Character 베이스 서브오브젝트를 가리킨다.

즉, Character의 인터페이스를 사용할 수 있다.

 

Character* Ptr1 = M.GetCharacter();

Ptr1->Move(); // 가능
Monster 객체
└── Character 베이스 서브오브젝트

 


외부에서는 다시 Monster*로 다운캐스팅하는 것도 제한된다

Character*를 얻었다고 해서 외부(ex. main 함수)에서 자유롭게 다시 Monster*로 변환할 수 있는 것도 아니다.

Character* Ptr1 = M.GetCharacter();

// Monster* M2 = static_cast<Monster*>(Ptr1); // Error!

 

 

이 변환 역시 private 상속 관계를 사용해야 하므로 외부에서는 허용되지 않는다.

 

즉, 외부에서는 다음 두 방향 모두 막힌다.

Monster*   → Character*   // 업캐스팅 불가
Character* → Monster*     // 다운캐스팅 불가

중요한 점은 단지 현재 코드가 그 private 상속 관계를 사용할 권한이 없는 것이다.

 


Monster 내부에서는 다운캐스팅도 가능하다

반대로 Monster 내부에서는 private 상속 관계에 접근할 수 있으므로 다운캐스팅도 가능하다.

class Monster : Character
{
public:
    void TestInside()
    {
    	// Monster* → Character*
        Character* CharacterPtr = this;
        
        // Character* → Monster*
        Monster* MonsterPtr =
            static_cast<Monster*>(CharacterPtr);

        MonsterPtr->Move();
    }
};

 

여기서는 다음 두 변환이 모두 가능하다.

둘 다 Monster의 멤버 함수 내부에서 수행되기 때문에 private 상속 관계에 접근할 권한이 있다.\

 

따라서 private 상속을 다음과 같이 이해하면 된다.

private 상속
≠ 상속 관계가 사라짐

private 상속
= 외부에서 그 상속 관계를 타입 변환에 사용할 수 없음

 

 


다형성에도 영향을 준다

private 상속 때문에 가상 함수 자체가 동작하지 않는 것은 아니다. 문제는 외부에서 자식 객체를 부모 타입으로 업캐스팅할 수 없다는 것이다. 이 때문에 부모 포인터나 참조를 이용해 여러 자식 객체를 동일한 인터페이스로 관리하는 일반적인 런타임 다형성 구조에 참여할 수 없게 된다.

#include <iostream>
#include <vector>
using namespace std;

struct Character
{
    virtual ~Character() = default;

    virtual void Move()
    {
        cout << "Character Move\n";
    }
};

class Player : public Character
{
public:
    void Move() override
    {
        cout << "Player Move\n";
    }
};

class Monster : Character // 실수: public 생략 → private 상속
{
public:
    void Move() override
    {
        cout << "Monster Move\n";
    }
};

int main()
{
    Player P;
    Monster M;

    vector<Character*> Characters;

    Characters.push_back(&P); // 가능
    Characters.push_back(&M); // Error!

    for (Character* CharacterPtr : Characters)
    {
        CharacterPtr->Move();
    }

    return 0;
}

 

그래서 객체 지향적인 is-a 관계를 표현할 목적이라면 다음처럼 public을 명시하는 편이 좋다.

class Monster : public Character
{
};

 


정리

처음에는 struct와 class를 서로 다른 종류의 타입이라고 생각하기 쉬웠다.

하지만 C++에서는 생성자, 소멸자, 상속, 가상 함수, 다형성 등 거의 동일한 기능을 사용할 수 있다.

실질적인 언어 차이는 다음 두 가지이다.

  struct class
기본 접근 지정자 public private
기본 상속 지정자 public private

 

 

그럼에도 실제 코드에서는 역할을 구분하기 위해 서로 다르게 사용하는 경우가 많다.

Struct
→ 데이터를 표현하는 값 타입

Class
→ 상태를 관리하고 책임을 수행하는 객체

물론 이것은 C++ 문법이 강제하는 규칙은 아니다.

코드를 읽는 사람에게 이 타입을 어떤 의도로 만들었는지 전달하기 위한 관례에 가깝다.

 

 

 

그리고 개인적으로는 기본 접근 지정자의 차이보다 기본 상속 지정자의 차이를 더 주의해야 한다고 느꼈다.

class에서 상속 지정자를 생략하면 의도하지 않은 private 상속이 발생할 수 있고, 이 경우 외부에서 부모 타입으로 업캐스팅할 수 없어 다형성 활용까지 막힐 수 있기 때문이다.

 

일반적인 is-a 관계의 상속을 표현할 때는 기본값에 의존하기보다 public을 명시하자!