[03-1] C++에서의 구조체

김민성·2022년 7월 13일
post-thumbnail

구조체의 등장배경은 무엇인가?

구조체가 주는 이점은 다음과 같다.

"연관 있는 데이터를 하나로 묶으면, 프로그램의 구현 및 관리가 용이하다."

위에서 말하듯이, 구조체는 연관있는 데이터를 묶을 수 있는 문법적 장치로 데이터의 표현에 매우 큰 도움을 준다.

예를 들어, 레이싱게임의 캐릭터로 등장하는 '자동차'를 표현해보자. 다음의 유사한 정보들이 모여 게임의 자동차가 표현된다.

  • 소유주
  • 연료량
  • 현재속도

위와 같은 정보를 토대로 구조체를 정의하면 다음과 같이 수월하게 프로그래밍을 할 수 있다.

struct Car
{
	char gamerID[ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도
};

C++에서의 구조체 변수의 선언

C언어에서 구조체 변수를 선언하는 방법은 다음과 같다.

struct Car basicCar;
struct Car simpleCar;

struct는 이어서 선언되는 자료형이 구조체를 기반으로 정의된 자료형임을 나타낸다. 키워드 struct를 생략하려면 별도의 typedef 선언을 추가해야한다.

하지만, C++에서는 기본 자료형 변수의 선언방식이나 구조체를 기반으로 정의된 자료형의 변수 선언방식에 차이가 없다! 즉, C++에서는 별도의 typedef 선언 없이도 다음과 같이 변수를 선언할 수 있다.

Car basicCar;
Car simpleCar;

그럼 이어서 앞서 정의한 구조체를 기반으로 예제를 작성해보자.

RacingCar.cpp

#include <iostream>
using namespace std;

#define ID_LEN  20
#define MAX_SPD 200
#define FUEL_STEP   2
#define ACC_STEP    10
#define BRK_STEP    10

struct Car
{
	char gamerID[ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도
};

void ShowCarState(const Car &car)
	{
		cout<<"소유자ID: "<<car.gamerID<<endl;
		cout<<"연료량: "<<car.fuelGauge<<"%"<<endl;
		cout<<"현재속도: "<<car.curSpeed<<"km/s"<<endl<<endl;
	}
void Accel(Car &car)
{
	if(car.fuelGauge<=0)
		return;
	else
		car.fuelGauge-=FUEL_STEP;

	if(car.curSpeed+ACC_STEP>=MAX_SPD)
	{
		car.curSpeed=MAX_SPD;
		return;
	}
	
	car.curSpeed+=ACC_STEP;
}
void Break(Car &car)
{
	if(car.curSpeed<BRK_STEP)
	{
		car.curSpeed=0;
		return;
	}

	car.curSpeed-=BRK_STEP;
}

int main(void) {
    Car run99 = {"run99", 100, 0};
    Accel(run99);
    Accel(run99);
    ShowCarState(run99);
    Break(run99);
    ShowCarState(run99);

    Car sped77 = {"sped77", 100, 0};
    Accel(sped77);
    Break(sped77);
    ShowCarState(sped77);
    return 0;
}
소유자ID: run99
연료량: 96%
현재속도: 20km/s

소유자ID: run99
연료량: 96%
현재속도: 10km/s

소유자ID: sped77
연료량: 98%
현재속도: 0km/s

코드를 살펴보자.

#define ID_LEN  20
#define MAX_SPD 200
#define FUEL_STEP   2
#define ACC_STEP    10
#define BRK_STEP    10

구조체 Car와 관련된 각종 정보를 상수화 했다.

void ShowCarState(const Car &car)
{
	cout<<"소유자ID: "<<car.gamerID<<endl;
	cout<<"연료량: "<<car.fuelGauge<<"%"<<endl;
	cout<<"현재속도: "<<car.curSpeed<<"km/s"<<endl<<endl;
}

차의 정보를 출력하는 함수를 구현했다. 단순히 정보만 출력하기에 const 참조자를 매개변수로 선언했다.

void Accel(Car &car)
{
	if(car.fuelGauge<=0)
		return;
	else
		car.fuelGauge-=FUEL_STEP;

	if(car.curSpeed+ACC_STEP>=MAX_SPD)
	{
		car.curSpeed=MAX_SPD;
		return;
	}
	
	car.curSpeed+=ACC_STEP;
}

차의 가속을 위해 엑셀을 밟은 상황을 표현한 함수이다. 엑셀을 밟을 때마다 연료가 FUEL_STEP만큼 줄어들고, ACC_STEP만큼 속도가 올라가는데, MAX_SPD까지만 올라가게끔 구현했다.

void Break(Car &car)
{
	if(car.curSpeed<BRK_STEP)
	{
		car.curSpeed=0;
		return;
	}

	car.curSpeed-=BRK_STEP;
}

브레이크를 밟은 상황을 구현한 함수이다. 브레이크를 밟은 상황은 연료를 소모하지 않게끔, 그리고 BRK_STEP만큼 속도가 감소되게 구현했다. 그리고 Car의 curSpeed가 음수가 나오지 않게끔 if문을 활용해 구현했다.

구조체 안에 함수 삽입하기

구조체 Car안에 종속적인 함수들을 구조체 안에 함께 묶어버리면 자동차와 관련된 데이터와 함수를 모두 묶는 셈이 되기에 보다 확실한 구분이 가능해진다. 한번 묶어보자.


struct Car
{
	char gamerID[ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도

    void ShowCarState()
	{
		cout<<"소유자ID: "<<gamerID<<endl;
		cout<<"연료량: "<<fuelGauge<<"%"<<endl;
		cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
	}
    void Accel()
    {
        if(fuelGauge<=0)
            return;
        else
            fuelGauge-=FUEL_STEP;

        if(curSpeed+ACC_STEP>=MAX_SPD)
        {
            curSpeed=MAX_SPD;
            return;
        }
        
        curSpeed+=ACC_STEP;
    }
    void Break()
    {
        if(curSpeed<BRK_STEP)
        {
            curSpeed=0;
            return;
        }

        curSpeed-=BRK_STEP;
    }
};

이렇게 하면 어떠한 변화가 생겼는가? ShowCarState 함수를 예로 확인해보자.

void ShowCarState(const Car &car)
{
	cout<<"소유자ID: "<<car.gamerID<<endl;
	cout<<"연료량: "<<car.fuelGauge<<"%"<<endl;
	cout<<"현재속도: "<<car.curSpeed<<"km/s"<<endl<<endl;
}
void ShowCarState()
{
	cout<<"소유자ID: "<<gamerID<<endl;
	cout<<"연료량: "<<fuelGauge<<"%"<<endl;
	cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
}

첫번째 ShowCarState 함수는 RacingCar.cpp의 것 즉, 함수가 구조체 밖에 있는 것이고,
두번째 ShowCarState 함수는 구조체 안에 함수를 삽입한 것이다.

이 둘의 차이점을 알겠는가? 함수가 구조체 밖에 있을 때에는 'car.'을 앞에 덧붙여 직접참조를 하고 있다.
하지만, 함수를 구조체 안에 삽입하게 되면 구조체 내에 선언된 변수에 "직접접근"이 가능해져 'car.'을 붙힐 필요가 없어진다.

따라서 다음과 같이 구조체 변수를 각각 선언하면,

Car run99 = {"run99", 100, 0};
Car sped77 = {"sped77", 100, 0};

다음의 형태로 구조체 변수가 생성된다.

참고로, 위 그림에서는 모든 구조체 변수 내에 함수가 각각 별도로 존재하는 것처러 묘사를 했는데, 실제로는 구조체 변수마다 함수가 독립적으로 각각 존재하는 구조는 아니다. 모든 Car 구조체 변수가 하나의 함수를 공유하는 형태가 맞다. 그러나 논리적으로는 각각의 변수가 자신의 함수를 별도로 지니는 것과는 같은 효과와 결과를 나타내므로 위 그림대로 이해하자.

위를 바탕으로 코드를 다시 구현해보면,

RacingfCarFuncAdd.cpp

#include <iostream>
using namespace std;

#define ID_LEN  20
#define MAX_SPD 200
#define FUEL_STEP   2
#define ACC_STEP    10
#define BRK_STEP    10

struct Car
{
	char gamerID[ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도

    void ShowCarState()
	{
		cout<<"소유자ID: "<<gamerID<<endl;
		cout<<"연료량: "<<fuelGauge<<"%"<<endl;
		cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
	}
    void Accel()
    {
        if(fuelGauge<=0)
            return;
        else
            fuelGauge-=FUEL_STEP;

        if(curSpeed+ACC_STEP>=MAX_SPD)
        {
            curSpeed=MAX_SPD;
            return;
        }
        
        curSpeed+=ACC_STEP;
    }
    void Break()
    {
        if(curSpeed<BRK_STEP)
        {
            curSpeed=0;
            return;
        }

        curSpeed-=BRK_STEP;
    }
};



int main(void) {
    Car run99 = {"run99", 100, 0};
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();

    Car sped77 = {"sped77", 100, 0};
    sped77.Accel();
    sped77.Break();
    sped77.ShowCarState();
    return 0;
}
소유자ID: run99
연료량: 96%
현재속도: 20km/s

소유자ID: run99
연료량: 96%
현재속도: 10km/s

소유자ID: sped77
연료량: 98%
현재속도: 0km/s

구조체 안에 enum 상수의 선언

예제 RacingfCarFuncAdd.cpp를 보면, 다음의 매크로 상수들이 존재한다.

#define ID_LEN  20
#define MAX_SPD 200
#define FUEL_STEP   2
#define ACC_STEP    10
#define BRK_STEP    10

그런데 이 상수들은 구조체 Car에게만 사용되는 상수들이다. 즉, 다른 영역에서는 사용되지 않으니 이 상수들도 구조체 내에 포함시키는 것이 좋다.

따라서 이러한 경우에는 열거형 enum을 이용해 구조체 내에서만 유효한 상수를 정의하면 된다.

struct Car
{
	enum {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    };
	char gamerID[CAR_CONST::ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도

    void ShowCarState(){ . . . . }
    void Accel(){ . . . . }
    void Break(){ . . . . }
};

참고로 enum 선언을 구조체 내부에 삽입하는 것이 부담스럽다면, 이름공간을 이용하면 된다. 실제로 이름공간을 활용하는 것이 가독성 측면에서도 더 좋다.

RacingCarEnum.cpp

#include <iostream>
using namespace std;

namespace CAR_CONST {
    enum {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    };
}
struct Car
{
	char gamerID[CAR_CONST::ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도

    void ShowCarState()
	{
		cout<<"소유자ID: "<<gamerID<<endl;
		cout<<"연료량: "<<fuelGauge<<"%"<<endl;
		cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
	}
    void Accel()
    {
        if(fuelGauge<=0)
            return;
        else
            fuelGauge-=CAR_CONST::FUEL_STEP;

        if(curSpeed+CAR_CONST::ACC_STEP>=CAR_CONST::MAX_SPD)
        {
            curSpeed=CAR_CONST::MAX_SPD;
            return;
        }
        
        curSpeed+=CAR_CONST::ACC_STEP;
    }
    void Break()
    {
        if(curSpeed<CAR_CONST::BRK_STEP)
        {
            curSpeed=0;
            return;
        }

        curSpeed-=CAR_CONST::BRK_STEP;
    }
};



int main(void) {
    Car run99 = {"run99", 100, 0};
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();

    Car sped77 = {"sped77", 100, 0};
    sped77.Accel();
    sped77.Break();
    sped77.ShowCarState();
    return 0;
}
소유자ID: run99
연료량: 96%
현재속도: 20km/s

소유자ID: run99
연료량: 96%
현재속도: 10km/s

소유자ID: sped77
연료량: 98%
현재속도: 0km/s

코드에서 볼 수 있듯이, 이름공간을 지정해서 상수를 선언했기 때문에 범위 지정 연산자 :: 를 이용해 이 상수가 어느 영역에서 선언되고 사용되는지 쉽게 알 수 있게 되었다.

함수는 외부로 뺄 수 있다.

함수가 포함되어 있는 C++의 구조체를 보는 순간, 다음의 정보들이 쉽게 눈에 들어와야 코드의 분석이 용이하다.

  • 선언되어 있는 변수정보
  • 정의되어 있는 변수정보

구조체를 보는 순간, 정의되어 있는 함수의 종류와 기능이 한눈에 들어오게끔 코드를 작성하려면,
구조체 밖으로 함수를 빼낼 필요가 있다.

struct Car {
	. . . . .
    void ShowCarState();
    void Accel();
    . . . . .
};

void Car::ShowCarState() {
 	. . . . .
}
void Car::Accel() {
	. . . . .
}

위와 같이 함수의 원형선언을 구조체 안에 두고, 함수의 정의를 구조체 밖으로 빼내는 것이다. 다만, 빼낸 다음 해당 함수가 어디에 정의되어 있는지 범위지정연산자(::)를 이용해 추가해주면 된다. 예제를 통해 확인해보자

RacingCarOuterFunc.cpp

#include <iostream>
using namespace std;

namespace CAR_CONST {
    enum {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    };
}
struct Car
{
	char gamerID[CAR_CONST::ID_LEN];	// 소유자 ID
	int fuelGauge;		// 연료량
	int curSpeed;		// 현재속도

    void ShowCarState();
    void Accel();
    void Break();

};
void Car::ShowCarState()
	{
		cout<<"소유자ID: "<<gamerID<<endl;
		cout<<"연료량: "<<fuelGauge<<"%"<<endl;
		cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
	}
void Car::Accel()
{
    if(fuelGauge<=0)
        return;
    else
        fuelGauge-=CAR_CONST::FUEL_STEP;

    if(curSpeed+CAR_CONST::ACC_STEP>=CAR_CONST::MAX_SPD)
    {
        curSpeed=CAR_CONST::MAX_SPD;
        return;
    }
    
    curSpeed+=CAR_CONST::ACC_STEP;
}
void Car::Break()
{
    if(curSpeed<CAR_CONST::BRK_STEP)
    {
        curSpeed=0;
        return;
    }

    curSpeed-=CAR_CONST::BRK_STEP;
}


int main(void) {
    Car run99 = {"run99", 100, 0};
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();

    Car sped77 = {"sped77", 100, 0};
    sped77.Accel();
    sped77.Break();
    sped77.ShowCarState();
    return 0;
}
소유자ID: run99
연료량: 96%
현재속도: 20km/s

소유자ID: run99
연료량: 96%
현재속도: 10km/s

소유자ID: sped77
연료량: 98%
현재속도: 0km/s

앞서 언급하진 않았지만,

사실 구조체 안에 함수가 정의되어 있으면 인라인 의미가 내포되어 있으므로 굳이 인라인 지시를 안해도 되지만,
구조체 밖에 함수가 정의되어 있으면 inline을 이용해 인라인 지시를 명시적으로 해야한다.

그 이유는 위의 예제와 같이 함수를 구조체 밖으로 빼내면, 함수를 인라인으로 처리해라는 의미가 사라진다.
따라서 인라인의 의미를 그대로 유지하기 위해서는 인라인 지시를 명시적으로 해야하는 것이다.

inline void Car::ShowCarState() { . . . . }
inline void Car::Accel() { . . . . }
inline void Car::Break() { . . . . }
profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글