Skip to content

7. 구조체(Structure Type)


1. 키워드

  • 구조체(Structure Type)와 struct 키워드
  • 멤버(Member) 또는 멤버 변수(Member Variable)
  • 바이트 패딩(Byte Padding)
  • 공용체(Union Type)와 union 키워드
  • 열거체(Enumerated Types)와 enum 키워드


2. 구조체의 기본

  • 구조체란 사용자가 C++의 기본 타입을 가지고 새롭게 정의할 수 있는 사용자 정의 타입이다.
  • 구조체는 기본 타입만으로는 나타낼 수 없는 복잡한 데이터를 표현할 수 있다.
  • 배열이 같은 타입의 변수 집합이라고 한다면, 구조체는 다양한 타입의 변수 집합을 하나의 타입으로 나타낸 것이다.
  • 이때 구조체를 구성하는 변수를 구조체의 멤버 또는 멤버 변수라고 한다.
  • C++의 구조체는 변수뿐만 아니라 함수까지도 멤버 변수로 가질 수 있다.
  • 또한, C++의 구조체는 타입일 뿐만 아니라, 객체 지향 프로그래밍의 핵심이 되는 클래스(Class)의 기초가 된다.


1) 구조체의 선언

  • C++에서 구조체는 struct 키워드를 사용하여 다음과 같이 선언한다.


struct 구조체이름
{
    멤버변수1의타입 멤버변수1의이름;
    멤버변수2의타입 멤버변수2의이름;
    ...
};


  • 다음은 book이라는 이름의 구조체를 선언하는 그림이다.


001


  • 이렇게 선언된 구조체 타입을 가지고 다음과 같이 구조체 변수를 선언할 수 있다.


struct 구조체이름 구조체변수이름;
struct Book web_book;


  • 참고로 C++에서는 구조체 변수를 선언할 때 struct 키워드를 생략할 수 있다.


2) 구조체 멤버로의 접근 방법

  • 배열에서는 인덱스를 이용하여 배열 요소에 접근할 수 있다.
  • 하지만 구조체에서 구조체 멤버로 접근하려고 할 때는 .(멤버 참조 연산자)를 사용해야 한다.


구조체변수이름.멤버변수이름
web_book.author


3) 구조체 변수의 초기화

  • C++에서 구조체 변수는 {}(중괄호)를 사용한 초기화 리스트를 사용하여 초기화한다.


구조체변수이름 = {멤버변수1의초깃값, 멤버변수2의초깃값, ...};
web_book = {"HTML과 CSS", "홍길동", 28000};


  • 이때 구조체 정의에서 멤버 변수가 정의된 순서에 따라 차례대로 초깃값이 설정되며, 나머지 멤버 변수는 0으로 초기화된다.


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

struct book {
    string title;
    string author;
    int price;
};

int main() {
    book web_book = {"HTML과 CSS", "홍길동", 28000};
    book java_book = {"Java language", "이순신"};

    cout << "첫 번째 책의 제목은 " << web_book.title
         << "이고, 저자는 " << web_book.author
         << "이며, 가격은 " << web_book.price << "원입니다." << endl;
    cout << "두 번째 책의 제목은 " << java_book.title
         << "이고, 저자는 " << java_book.author
         << "이며, 가격은 " << java_book.price << "원입니다." << endl;

    return 0;
}

// 첫 번째 책의 제목은 HTML과 CSS이고, 저자는 홍길동이며, 가격은 28000원입니다.
// 두 번째 책의 제목은 Java language이고, 저자는 이순신이며, 가격은 0원입니다.


  • C++11부터는 구조체 변수를 초기화할 때에 =(대입 연산자)를 생략할 수 있으나, Narrowing은 지원하지 않는다.


3. 구조체의 활용

1) 함수와 구조체

  • C++에서는 함수를 호출할 때 전달되는 인수나, 함수가 종료될 때 반환되는 반환값으로도 구조체를 사용할 수 있다.
  • 그 방식은 기본 타입과 완전히 같으며, 구조체를 가리키는 포인터나 구조체의 한 멤버 변수만을 사용할 수도 있다.


  • 다음의 코드는 구조체의 멤버 변수를 함수의 인수로 전달하는 것이다.


#include <iostream>
using namespace std;

struct Prop {
    int savings;
    int loan;
};

int CalcProperty(int, int);

int main(void) {
    int hong_prop;
    Prop hong = {10000000, 4000000};

    hong_prop = CalcProperty(hong.savings, hong.loan);  // 구조체의 멤버 변수를 함수의 인수로 전달함

    cout << "홍길동의 재산은 적금 " << hong.savings << "원에 대출 "
         << hong.loan << "원을 제외한 총 " << hong_prop << "원입니다." << endl;

    return 0;
}

int CalcProperty(int s, int l) {
    return (s - l);
}

// 홍길동의 재산은 적금 10000000원에 대출 4000000원을 제외한 총 6000000원입니다.


  • 위와 같이 구조체를 인수로 전달하는 방식은 함수가 원본 구조체의 복사본을 가지고 작업하므로 안전하다는 장점을 가진다.


  • 다음의 코드는 함수의 인수로 구조체의 주소를 직접 전달하는 것이다.


#include <iostream>
using namespace std;

struct Prop {
    int savings;
    int loan;
};

int CalcProperty(Prop *);

int main(void) {
    int hong_prop;
    Prop hong = {10000000, 4000000};

    hong_prop = CalcProperty(&hong);  // 구조체의 주소를 함수의 인수로 전달함

    cout << "홍길동의 재산은 적금 " << hong.savings << "원에 대출 "
         << hong.loan << "원을 제외한 총 " << hong_prop << "원입니다." << endl;

    return 0;
}

int CalcProperty(Prop *money) {
    money->savings = 100;  // 호출된 함수에서 원본 구조체의 데이터를 변경

    return (money->savings - money->loan);
}

// 홍길동의 재산은 적금 100원에 대출 4000000원을 제외한 총 -3999900원입니다.


  • 위와 같이 구조체를 가리키는 포인터를 인수로 전달하는 방식은 구조체의 복사본이 아닌 주소 하나만을 전달하므로 빠르다는 장점을 가진다.
  • 하지만 호출된 함수에서 원본 구조체에 직접 접근하므로, 원본 데이터의 보호 측면에서는 매우 위험하다.


  • 따라서 다음 코드의 CalcProperty() 함수처럼 const 키워드를 사용하여 함수에 전달된 인수를 함수 내에서는 직접 수정할 수 없도록 하는 것이 좋다.


#include <iostream>
using namespace std;

struct Prop {
    int savings;
    int loan;
};

Prop InitProperty(void);
int CalcProperty(const Prop *);

int main(void) {
    int hong_prop;
    Prop hong;

    hong = InitProperty();
    hong_prop = CalcProperty(&hong);  // 구조체의 멤버 변수를 함수의 인수로 전달함

    cout << "홍길동의 재산은 적금 " << hong.savings << "원에 대출 "
         << hong.loan << "원을 제외한 총 " << hong_prop << "원입니다." << endl;

    return 0;
}

Prop InitProperty(void) {
    Prop hong_prop = {10000000, 4000000};

    return hong_prop;  // 구조체를 함수의 반환값으로 반환함
}

int CalcProperty(const Prop *money) {  // const 키워드를 사용하여 구조체의 데이터를 직접 수정하는 것을 방지함
    // money->savings = 100;           // 호출된 함수에서 원본 구조체의 데이터를 변경. 컴파일 오류 발생

    return (money->savings - money->loan);
}

// 홍길동의 재산은 적금 10000000원에 대출 4000000원을 제외한 총 6000000원입니다.


  • 위의 코드에서 CalcProperty() 함수 내의 주석 처리된 부분을 실행하여 원본 구조체에 대한 수정을 시도할 경우 C++ 컴파일러는 오류를 발생시킨다.
  • 또한, 위의 코드에서 InitProperty() 함수의 반환값으로 구조체를 반환한다.
  • 기본적으로 C++의 함수는 한 번에 하나의 데이터만을 반환할 수 있다.
  • 하지만 이렇게 구조체를 사용하면 한 번에 여러 개의 데이터를 반환할 수 있게 된다.


2) 중첩된 구조체

  • C++에서는 구조체를 정의할 때 멤버 변수로 또 다른 구조체를 포함할 수 있다.


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

struct Name {
    string first;
    string last;
};

struct Friends {
    Name firts_name;
    string address;
    string job;
};

int main(void) {
    // 구조체 변수 선언과 동시에 초기화
    Friends hong = {.firts_name.first = "길동", .firts_name.last = "홍", .address = "서울시 강남구 역삼동", .job = "학생"};

    // 구조체 변수 선언 후 초기화 리스트 사용
    // Friends hong;
    // hong = {{"길동", "홍"}, "서울시 강남구 역삼동", "학생"};

    cout << hong.address << endl
         << endl
         << hong.firts_name.last << hong.firts_name.first << "에게," << endl
         << "그동안 잘 지냈니? 아직도 " << hong.job << "이니?" << endl
         << "다음에 꼭 한 번 보자." << endl
         << "잘 지내." << endl;

    return 0;
}

// 서울시 강남구 역삼동
//
// 홍길동에게,
// 그동안 잘 지냈니? 아직도 학생이니?
// 다음에 꼭 한 번 보자.
// 잘 지내.


  • 위의 코드에서 Friends 구조체는 Name 구조체를 멤버 변수로 포함하고 있다.


3) 구조체의 크기

  • 구조체의 크기는 멤버 변수들의 크기에 따라 결정된다.
  • 하지만 구조체의 크기가 언제나 멤버 변수들의 크기 총합과 일치하는 것은 아니다.


#include <iostream>
using namespace std;

struct TypeSize {
    char a;
    int b;
    double c;
};

int main(void) {
    cout << "구조체 TypeSize의 각 멤버의 크기는 다음과 같습니다." << endl;
    cout << sizeof(char) << ", " << sizeof(int) << ", " << sizeof(double) << endl;

    cout << "구조체 TypeSize의 크기는 다음과 같습니다." << endl;
    cout << sizeof(TypeSize) << endl;

    return 0;
}

// 구조체 TypeSize의 각 멤버의 크기는 다음과 같습니다.
// 1, 4, 8
// 구조체 TypeSize의 크기는 다음과 같습니다.
// 16


  • 위의 코드에서 구조체 멤버 변수의 크기는 각각 1, 4, 8바이트이다.
  • 하지만 구조체의 크기는 멤버 변수들의 크기 총합인 13바이트가 아니라 16바이트가 된다.


  • 구조체를 메모리에 할당할 때 컴파일러는 프로그램의 속도 향상을 위해 바이트 패딩이라는 규칙을 이용한다.
  • 구조체는 다양한 크기의 타입을 멤버 변수로 가질 수 있는 타입이다.
  • 하지만 컴파일러는 메모리의 접근을 쉽게 하려고 크기가 가장 큰 멤버 변수를 기준으로 모든 멤버 변수의 메모리 크기를 맞추게 된다.
  • 이것을 바이트 패딩이라고 하며, 이때 추가되는 바이트를 패딩 바이트라고 한다.


002


  • 위의 코드에서는 크기가 가장 큰 double 타입의 크기인 8바이트가 기준이 된다.
  • 맨 처음 char 타입 멤버 변수를 위해 8바이트가 할당되며, 할당되는 1바이트를 제외한 7바이트가 남게 된다.
  • 그다음 int 타입 멤버 변수는 남은 7바이트보다 작으므로, 그대로 7바이트 중 4바이트를 할당하고 3바이트가 남게 된다.
  • 마지막 double 타입 멤버 변수는 8바이트인데 남은 공간은 3바이트뿐이므로 다시 8바이트를 할당 받는다.
  • 따라서 이 구조체의 크기는 총 16바이트가 되며, 그중에서 패딩 바이트는 3바이트가 된다.


4. 공용체와 열거체

1) 공용체

  • 공용체는 union 키워드를 사용하여 선언하며, 모든 면에서 구조체와 같다.
  • 하지만 모든 멤버 변수가 하나의 메모리 공간을 공유한다는 점만이 다르다.
  • 모든 멤버 변수가 같은 메모리를 공유하기 때문에 공용체는 한 번에 하나의 멤버 변수밖에 사용할 수 없다.


003


  • 공용체는 순서가 규칙적이지 않고, 미리 알 수 없는 다양한 타입의 데이터를 저장할 수 있도록 설계된 타입이다.
  • 이러한 공용체는 크기가 가장 큰 멤버 변수의 크기로 메모리를 할당 받는다.
  • 따라서 공용체 배열을 사용하면, 같은 크기로 구성된 배열 요소에 다양한 크기의 데이터를 저장할 수 있다.


  • 다음의 코드는 공용체의 한 멤버 변수만을 초기화하면, 나머지 멤버 변수들도 모두 같은 데이터를 공유한다는 것을 보여준다.


#include <iostream>
using namespace std;

union Sharedata {
    unsigned char a;
    unsigned short b;
    unsigned int c;
};

int main(void) {
    Sharedata var;
    var.c = 0x12345678;

    cout << hex;
    cout << var.a << endl;
    cout << var.b << endl;
    cout << var.c << endl;

    return 0;
}

// x
// 5678
// 12345678


  • 공용체에 저장된 값의 의미는 값을 저장할 때 공용체의 어떤 멤버 변수를 사용했는지에 따라 달리 해석된다.
  • 따라서 공용체가 어떤 멤버 변수를 사용하여 저장했는지를 별도의 변수로 저장하든가 하여, 사용할 때에도 같은 멤버 변수를 사용해야 한다.


2) 열거체

  • 열거체는 새로운 타입을 선언하면서 동시에 그 타입이 가질 수 있는 정수형 상숫값도 같이 명시한다.
  • 이러한 열거체를 이용하면 프로그램의 가독성이 높아지고, 변수가 지니는 값에 의미를 부여할 수 있다.


#include <iostream>
using namespace std;

enum Weather {
    SUNNY = 0,  // 상숫값을 지정하지 않으면 0부터 1씩 증가함
    CLOUD = 10,
    RAIN = 20,
    SNOW = 30
};

int main(void) {
    int input;
    Weather wt;

    cout << "오늘의 날씨를 입력해 주세요:" << endl;
    cout << "(SUNNY=0, CLOUD=10, RAIN=20, SNOW=30)" << endl;
    cin >> input;
    wt = (Weather)input;  // 예를 들어 타입 변환에 의해 wt에는 SUNNY 입력 시 0으로 저장됨

    switch (wt) {
        case SUNNY:
            cout << "오늘의 날씨는 아주 맑아요!" << endl;
            break;
        case CLOUD:
            cout << "오늘의 날씨는 흐리네요!" << endl;
            break;
        case RAIN:
            cout << "오늘의 날씨는 비가 오네요!" << endl;
            break;
        case SNOW:
            cout << "오늘의 날씨는 눈이 내리네요!" << endl;
            break;
        default:
            cout << "정확한 상숫값을 입력해 주세요!" << endl;
            break;
    }

    cout << endl
         << "열거체 Weather의 각 상숫값은 "
         << SUNNY << ", " << CLOUD << ", " << RAIN << ", " << SNOW
         << "입니다." << endl;

    return 0;
}

// 오늘의 날씨를 입력해 주세요:
// (SUNNY=0, CLOUD=10, RAIN=20, SNOW=30)
// SUNNY (입력)
// 오늘의 날씨는 아주 맑아요!
//
// 열거체 Weather의 각 상숫값은 0, 10, 20, 30입니다.

// 오늘의 날씨를 입력해 주세요:
// (SUNNY=0, CLOUD=10, RAIN=20, SNOW=30)
// 0 (입력)
// 오늘의 날씨는 아주 맑아요!
//
// 열거체 Weather의 각 상숫값은 0, 10, 20, 30입니다.


  • C++에서 열거체는 enum 키워드를 사용하여 선언한다.
  • 이때 상숫값을 따로 명시하지 않으면 0부터 시작되며, 그 다음은 바로 앞의 상숫값보다 1만큼 증가되어 정의된다.

References