C++ 구조체 | C++ 자습서 9

C++ 구조체

구조체는 복합 자료형입니다. int형 변수가 4바이트 메모리에 저장하는 것이면 구조체는 int, char, float 등 여러 자료를 섞어서 사용할 수 있습니다. 배열도 사용할 수 있고 구조체안에 구조체를 사용할 수 있습니다. 굉장히 자유도가 높은 자료형으로 볼 수 있죠.

C++은 구조체 보다 클래스를 많이 사용하는데 데이터 구조적으로 보면 구조체와 클래스는 같은 것이라고 합니다. 물론 클래스에는 private과 public 접근제어자가 있고 상속과 추상클래스 등 다른 개념이 있습니다. 어디까지나 데이터 구조 측면에서 기계적으로 같다는 말입니다.

구조체를 배우면 클래스의 본질을 더 잘 이해할 수 있습니다. 그래서 좀 어려워도 자바로 처음 프로그래밍을 시작하는 것 보다 C++을 추천하기도 합니다.

구조체의 개념은 매우 직관적입니다. 선형으로 길게 늘어선 메모리를 생각해봅니다. int 형이라면 4바이트죠? 0으로 초기화 시켜서 비트로 나열하면 0000 0000 0000 0000 0000 0000 0000 0000 이렇게 되어 있을겁니다. 이제 int 1개 char 1개 double 1개를 구조체로 만들어 봅니다. int 의 4바이트, char 1바이트, double 8바이트 총 13바이트의 구조체가 만들어 집니다.

메모리가 1300 바이트라면 이 구조체 100개가 들어갈 수 있습니다. 이런식의 디테일한 계산법은 중요한데 프로그램 실행 전에도 메모리 사용량을 예상할 수 있기 때문입니다. 프로그램의 규모가 커지면 동적 메모리 할당을 사용하는데 용량이 정확히 나오면 메모리를 배치하는 것 만으로도 많은 데이터를 처리할 수 있습니다.

객체 지향 프로그래밍 언어인 자바가 많이 사용되고 컴퓨터 하드웨어가 발전하면서 메모리 관리에 대한 개념을 많이 잊고 사는데요. 여전히 C++에서는 메모리를 직접 관리해야 합니다. 물론 메모리의 용량이 기가 단위로 늘어나면서 찔찔하게 메모리를 쓰지 않아도 됩니다.

닌텐도의 고전 게임기 NES는 64KB 의 용량만으로도 수많은 게임을 구현할 수 있었습니다. 지금은 MEGA도 아니고 GIGA 단위의 메모리가 기본입니다. 그러니까 메모리가 부족해서 충돌이 나는 경우는 이제 좀처럼 보기 힘들어진 거죠. 그리고 소프트웨어 기술도 함께 발전하면서 과거엔 문제였던 것들이 이젠 문제가 되지 않는 경우가 많습니다.

역시 그렇다 하더라도 메모리 관리는 중요합니다. C++ 의 모든 주제에서 메모리와 포인터 이야기는 빠질 수가 없는데요. C++자체가 메모리를 투명하게 보면서 만드는 프로그래밍이라서 그렇습니다. C++을 모두에게 추천하는 것은 컴퓨터의 본질을 배울 수 있기 때문입니다.

컴퓨터의 본질은 메모리라는 거죠. 우리가 실행하는 모든 프로그램은 일단 메모리에 올라와야 실행이 됩니다. 하드디스크 등 대용량 저장장치가 있어도 그 데이터를 CPU가 접근하려면 메모리에 올려놔야 합니다. 메모리에 올라오고 나서야 CPU가 메모리의 데이터를 레지스터로 올려서 비로소 연산을 할 수 있습니다.

결국 컴퓨터 구조를 생각하다 보면 어셈블리어까지 배우게 됩니다. 리눅스 커널의 창시자 리누즈 토발즈는 C언어 코드를 보면 어셈블리어로 머리속에서 변환이 가능하다고 합니다. 어셈블리어는 기계어에 1대1 대응이 가능한 니모닉(의미를 가진 명령어) 입니다. 이런 것이 가능한 것이 C++ 언어의 장점이죠. high level language이지만 low level 이 투명하게 보이는 것 입니다. 메모리를 볼 수 있기 때문이죠.

구조체 작성 및 사용

아래의 예제는 구조체를 작성하는 예시입니다.

C++은 문법적으로 편의성을 더했습니다. C언어에서는 구조체 정의시 struct을 써야해서 typedef를 사용하지 않으면 문법이 좀 번거로웠는데 C++에서는 그냥 클래스처럼 이름으로 정의하면 됩니다.

#include <iostream>

using namespace std;

struct theProduct{
    char name[20];
    int price;
    char description[30];
};

int main()
{
    theProduct earphone = {
        "Good Ear",
        25000,
        "for smartphone"
    };

    theProduct headphone = {
        "my Headset",
        35000,
        "high quality sound"
    };

    cout << "- product name: " << earphone.name << endl;
    cout << "- price : " << earphone.price << endl;
    cout << "- description : " << earphone.description << endl;

    cout << endl;

    cout << "- product name: " << headphone.name << endl;
    cout << "- price : " << headphone.price << endl;
    cout << "- description : " << headphone.description << endl;


    return 0;
}
- product name: Good Ear
- price : 25000
- description : for smartphone

- product name: my Headset
- price : 35000
- description : high quality sound

구조체의 데이터에 접근하기 위해서 도트 연산자를 사용했습니다. 구조체 포인터를 사용하면 -> 화살표로 바뀝니다.

구조체 초기화와 배열

구조체 초기화 방법은 몇가지가 있습니다. C++은 항상 프로그래머들에게 다양한 선택권을 줍니다. 어떤 스타일을 따를지는 자유지만 일관성을 가지는 것은 중요합니다. 초기화 방법이 여러개라고 하나의 프로그램에서 여러가지 초기화 방법을 쓴다면 가독성이 떨어질 것 입니다.

소스코드는 프로그램을 작동시키는 명령어들이라 컴퓨터가 알아만 들으면 문제가 없지만, 결국 이것도 사람들이 읽는 문서이기 때문에 대체적으로 많은 사람들에게 가독성이 좋은 문서란 것이 존재합니다.

IDE에서 들여쓰기 기능이 있기 때문에 아래 처럼 어느 정도 보기 좋은 형식이란게 있습니다. 가독성 부분에 있어서는 들여쓰기가 가장 중요하긴 합니다.

구조체 배열은 일반 배열과도 비슷합니다. { } 괄호를 한번 더 치고 쉼표로 요소를 구분하면 됩니다. 포인터를 배운 후에는 구조체 배열도 포인터로 사용하게 됩니다. 포인터를 사용하면 구조체를 더욱 자유롭게 사용할 수 있습니다.

#include <iostream>

using namespace std;


int main()
{
    // 구조체 내부 선언
    struct student
    {
        char name[20];
        short age;
    };
    
    student dongSoo = {"Kim Dong Soo", 17};
    // 할당연산자 = 생략가능
    student miNa {"Park Mi Na", 18};
    // 아무것도 없는 초기화 - NULL 과 0
    student wanJae {};

    cout << "\n[------- struct basic -------]" << endl;
    cout << dongSoo.name << " | " << dongSoo.age << endl;
    cout << miNa.name << " | " << miNa.age << endl;
    cout << wanJae.name << " | " << wanJae.age << endl;

    cout << "\n[------- struct array -------]" << endl;
    
    student st[3] = {

        {"Han Woo Gil", 19},
        {"Ma Hong Chul", 18},
        {"Gang Do Woon", 17}
        
        };

    cout << st[0].name << " | " << st[0].age << endl;
    cout << st[1].name << " | " << st[1].age << endl;
    cout << st[2].name << " | " << st[2].age << endl;

    return 0;

}
[------- struct basic -------]
Kim Dong Soo | 17
Park Mi Na | 18
 | 0

[------- struct array -------]
Han Woo Gil | 19
Ma Hong Chul | 18
Gang Do Woon | 17

위에서 { } 로 초기화를 하면 NULL 값과 0으로 초기화가 됩니다.

구조체 특성

구조체의 멤버는 C++의 모든 데이터형이 가능합니다. 기본자료형, 복합자료형, 클래스 등 모든 것이 가능합니다.

sizeof 연산자로 각 멤버들의 사이즈를 측정해보면 바이트를 메모리에서 어떻게 사용하는지 알 수 있습니다. 비주얼 스튜디오의 메모리 덤프 기능을 활용하면 실제 메모리에 어떻게 배치되어 있는지 확인이 가능합니다. 16진수가 표기된 메모리를 볼 수 있는데 윈도우는 리틀엔디안 시스템으로 바이트가 역순으로 저장되어 있습니다. 예를들어 00 01 00 01 이면 01 00 01 00 처럼 순서가 뒤집힙니다.

비주얼 스튜디오의 메모리 덤프 기능을 몇번 사용하다 보면 금방 메모리의 개념이 잡힙니다. 좀 번거로워도 직접 해보는 것을 추천합니다. 같이 C++를 배워도 그거 한 사람과 안한 사람의 차이는 큽니다.

#include <iostream>

using std::cout;
using std::endl;

struct person
{
    // string 객체도 멤버가 될 수 있다
    std::string name;
    int age;
    float height;
};

int main()
{
    person p1 {"Mr Kim", 25, 175};

    cout << "[-------- this is p1 --------]" << endl;
    cout << p1.name << endl;
    cout << p1.age << endl;
    cout << p1.height << endl;

    person p2;

    // Memberwise 멤버별 대입
    p2 = p1;

    cout << "[-------- this is p2 --------]" << endl;
    cout << p2.name << endl;
    cout << p2.age << endl;
    cout << p2.height << endl;

    cout << "[-------- memory address --------]" << endl;

    printf("p1 address : %p\n",p1);
    printf("p2 address : %p\n",p2);

    cout << "^ struct person size: " << sizeof(person) << endl; 
    cout << "^ struct p1 size: " << sizeof(p1) << endl; 
    cout << "^ struct p2 size: " << sizeof(p2) << endl;
    cout << "^ struct p1.name   size: " << sizeof(p1.name) << endl;
    cout << "^ struct p1.age    size: " << sizeof(p1.age) << endl;
    cout << "^ struct p1.height size: " << sizeof(p1.height) << endl;

    return 0;

}
[-------- this is p1 --------]
Mr Kim
25
175
[-------- this is p2 --------]
Mr Kim
25
175
[-------- memory address --------]
p1 address : 0061FED0
p2 address : 0061FEF0
^ struct person size: 32
^ struct p1 size: 32
^ struct p2 size: 32
^ struct p1.name   size: 24
^ struct p1.age    size: 4
^ struct p1.height size: 4

구조체 비트필드 사용하기

구조체를 비트로 사용할 수 있습니다. 이것은 low level에서 하드웨어 장치 관련한 프로그래밍을 할 때 사용하는 기법입니다. 일반 응용 프로그램을 만들 때는 좀처럼 사용할 일이 없을 겁니다. 그렇지만 알아둬서 나쁠 것은 없겠죠.

#include <iostream>

using std::cout;
using std::endl;


int main()
{

    struct bitField
    {
        unsigned int FB : 10;
        unsigned int : 4; // 비워두는 필드
        bool ST1 : 1;
        bool ST2 : 1;
    };

    bitField myBit {1023, true, false};

   cout << myBit.FB << endl;
   cout << myBit.ST1 << endl;
   cout << myBit.ST2 << endl;
   
   cout << "myBit size byte: " << sizeof myBit << endl;

//    비트필드는 sizeof 연산 불가능
//    cout << "FB field size: " << sizeof(myBit.ST1) << endl;
   
   return 0;

}
1023
1
0
myBit size byte: 8

요약

C++ 구조체에 관하여 알아봤습니다.

객체 지향 프로그래밍의 시대이므로 클래스와 구조체의 차이를 알고 싶다면 Cherno 의 유튜브 영상 Classes vs Structs in C++ 를 추천합니다. (아래 참고문서 링크)

프로그래밍은 복합 데이터형에 들어가야 재미가 있어집니다. 맨날 int char 같은 단순한 자료만 다루다가 복합 데이터형인 구조체를 사용하면 다양한 데이터를 만들 수 있습니다.

예를 들어 시간, 날짜, 달력 이런 것들은 복합 데이터형이죠. 현실 세계에 좀더 의미가 있는 자료구조는 거의 복합 데이터형입니다. Person 을 구조체 한다. 그런 이름과 나이 키 등이 멤버 변수가 됩니다. 이 방식이 그대로 클래스로 바뀌는 거죠. 그런데 바뀌는것은 struct 이란 키워드를 class로 바뀌고 몇가지 접근 제한자 등의 차이밖에 없습니다.

키워드가 바뀌는 것의 기계적 차이는 크지 않습니다. 오히려 우리가 클래스 객체라는 단어를 떠올리면 생각을 바꾸는게 객체 지향 프로그래밍입니다. 즉 0과 1을 처리하는 컴퓨터는 그대로입니다. 중요한 것은 사람이 컴퓨터 프로그래밍을 설계하는 방식을 바꾼다는 것 입니다.

기계적인 방식을 이해하는 것은 구조체가 좋고 객체지향 프로그래밍을 하려면 클래스가 좋습니다.

어느 방식이 적합한가 까지 생각할 수 있다면 이 주제를 충분히 커버한 것이라 할 수 있습니다.

참고문서

Data structures – C++ Tutorials (cplusplus.com)

C++ 구조체 (programiz.com)

C++ 구조체 With Example (guru99.com)

C++ 구조체 – 자료구조 – Tutorialspoint

8.4 — Structs | Learn C++ (learncpp.com)

C 언어 코딩 도장: 48.1 구조체를 만들고 사용하기 (dojang.io)

CLASSES vs STRUCTS in C++ – YouTube

Advanced C++: Struct Vs. Class – YouTube

Leave a Comment