[C]구조체(struct)[1]


  • 이번 포스트는 구조체(struct)에 관한 내용입니다.

1️⃣ 구조체(struct)를 사용하는 이유

  • 실수를 줄이기 위해: 같은 형의 데이터 여러개를 매개변수로 받을 때 순서가 바뀌면 컴파일러가 실수를 찾을 방법이 없습니다.
  • 수정이 용이: 나중에 누군가 매개변수를 바꾸면 문제가 생기고 일리리 수정하는 것도 쉽지 않습니다.

2️⃣ 구조체 사용

< 구조체 선언 >

struct date 
{
    int year;
    int month;
    int day;
};

(1) 기본적인 구조체일때

int main(void)
{
    struct date birth;

    birth.year = 1993;
    birth.month = 05;
    birth.day = 25;
    
    printf("My birthday is %d.%d.%d", birth.year, birth.month, birth.day);
}
/*---출력---*/
My birthday is 1993.5.25

(2) 구조체(포인터)일때

int main(void)
{
    struct date *my_birth;

    birth->year = 1993;
    birth->month = 05;
    (*birth).day = 25;  //이런식으로 사용이 가능하지만 잘 사용하지않는 방법
    
    printf("My birthday is %d.%d.%d", birth->year, birth->month, birth->day);
}
/*---출력---*/
My birthday is 1993.5.25

(3) 구조체(배열)일때

int main(void)
{
    /* 이런식의 선언은 실수할 가능성이 큼 */
    struct date birth[2] = { {1993, 05, 25}, {2000, 06, 11}};
    
    /* 각값을 변경하는 것이 실수를 줄이는 방법*/
    // birth[0].year = 1993;
    // birth[0].month = 05;
    // birth[0].day = 25;
    // birth[1].year = 2000;
    // birth[1].month = 06;
    // birth[1].day = 11;
    for (int i = 0; i < 2; i++)
        printf("birthday is %d.%d.%d\n", birth[i].year, birth[i].month, birth[i].day);
}
/*---출력---*/
birthday is 1993.5.25
birthday is 2000.6.11

(4) 구조체(포인터배열)일때

int main(void)
{
    struct date *birth[2];
    struct date Timo = { 1997, 12, 03};
    struct date Nono = { 2000, 02, 23};

    birth[0] = &Timo;
    birth[1] = &Nono;
    for (int i = 0; i < 2; i++)
        printf("birthday is %d.%d.%d\n", birth[i]->year, birth[i]->month, birth[i]->day);
}
/*---출력---*/
birthday is 1997.12.3
birthday is 2000.2.23

3️⃣ typedef를 이용해서 구조체 선언하기

/* typedef사용 example */
typedef unsigned int size_t;
  • 위의 코드는 unsigned int에 새로운 별명을 준 것 입니다.
  • usigned intsize_t어느 것을 사용해도 상관없습니다.
  • C에서 _t로 끝나는 자료형은 보통 이런식으로 typedef로 선언된 것 입니다.
/* 방법 1 */
struct date {
    int year;
    int month;
    int day;
};

typedef struct date date_t;

/* 방법 2 */
typedef struct date {
    int year;
    int month;
    int day;
} date_t;

/* 방법 3 */
typedef struct {
    int year;
    int month;
    int day;
} date_t;

위의 코드와 같이 3가지 방법으로 typedef를 사용하여 구조체를 선언할 수 있지만 방법 3의 방법이 제일 깔끔하고 많이 쓰입니다.


4️⃣ 특징 & 주의사항

(1) 구조체 변수 초기화

  • 구조체는 일반 변수와 마찬가지로 선언과 동시에 초기화가 안됩니다.(스텍에 남아있는 데이터를 그대로 사용)
  • 사실 기계는 구조체라는 개념자체를 모릅니다.(전처리기를 통해 일반변수와 같이 확장)
    ->구조체는 프로그래밍 언어가 개발자들 편하라고 제공 해준 개념입니다.
  • body_t body = { 51, 163};와 같이 구조체를 선언과 동시에 초기화하면 실수를 할 가능성이 큽니다.
    typedef struct {
      float height;
      float weight;
    } body_t;
    

    만약 위와 같이 구조체가 선언되어 있었다면 키가 51cm, 몸무게가 163kg이 될 겁니다.
    이런 실수를 줄이기 위해 구조체를 사용한 것인데 사용의 의미가 없어지게 됩니다.

  • const 멤버 변수는 사용하지 않는 것이 좋습니다. (쓸일도 별로없고 업계에서도 쓰지말라는 쪽)

(2) 구조체 매개변수 전달(값 vs 주소)

  • 기본 자료형을 전달할 때는 기본 데이터 크기가 작으니 원본 바꿀 때만 주소로 전달하면 됬었습니다.

  • 구조체를 주소로 전달하는 것이 좋습니다.

< 멤버 변수가 엄청많을 경우>

  • 구조체의 경우 데이터 크기가 클수도 있습니다.(int형 멤버 변수가 수만개 => 수백KB)
    typedef struct {
      int point1;
      /* ... */
      int porint30000;
    } characteristic_t;
    

    다음과 같이 구조체의 주소로 전달

    void note_characteristic(const characteristic_t *crt);
    

< 매개변수가 많은 경우 >

typedef struct {
    id_t  id;
    char  name[20];
    int   age;
    float height;
    float weight;
    /* ... */
    char  address[50];
} person_info_t

다음과 같이 전달하면 복잡하고 실수할 가능성이 큽니다.

void note_person_info(id_t id, char *name, int age,
        float height, float weight, ..., char *address);

그 대신 다음과 같이 주소로 구조체를 전달

void note_person_info(const person_info_t *person);

(3) 여러 자료형 변수를 반환하고 싶을 때(구조체 반환)

  • C언어의 함수는 반환값이 한개 입니다. 하지만 구조체를 반환하면 실질적으로 여러 개의 값을 반환하는 격입니다.
date_t get_day(void)
{
    date_t date;

    date.year = 2021;
    date.month = 02;
    date.day = 22;

    return date;
}

int main(void)
{
    date_t date;

    date = get_day();
    printf("today is %d.%d.%d\n", date.year, date.month, date.day);
}
/*---출력---*/
today is 2021.2.22

5️⃣ 구조체의 멤버변수가 포인터일 때 생길 수 있는 실수

< 실수가 난 케이스(얕은 복사) >

#include <stdio.h>
#define NUM_CHAMP (2)

typedef struct {
    char *name;
} champion_t;

int main(void)
{
    champion_t champ[NUM_CHAMP];
    FILE *stream;

    stream = fopen("test.txt", "wb");
    champ[0].name = "Teemo";
    champ[1].name = "Nono";

    fwrite(champ, sizeof(champ[0]), NUM_CHAMP, stream); 

    fclose(stream);
}
/*---test.txt 적힌 내용---*/
P@@?V@@?  // 주소값이 내용에 적힘

프로그램 종료 후에도 test.txt에 적힌 주소값은 그대로 이지만 그 문자열은 다른 주소에 저장되어 있습니다.(보안상의 이유로 주소값이 임의로 바뀜)
즉 주소값을 저장한 파일은 쓸모가 없게 됩니다.

< 해결방법(깊은 복사) >

  • 포인터 멤버변수대신 배열형 멤버변수를 사용하여 값을 복사하면 됩니다.
#include <stdio.h>
#include <string.h>
#define NUM_CHAMP (2)
#define NAME_LEN (20)

typedef struct {
    char name[NAME_LEN];
} champion_t;

int main(void)
{
    champion_t champ[NUM_CHAMP];
    FILE *stream;

    stream = fopen("test.txt", "wb");
    const char* name1 = "Teemo";
    const char* name2 = "Nono";

    strncpy(champ[0].name, name1, NAME_LEN);
    champ[0].name[NAME_LEN - 1] = '\0';
    strncpy(champ[1].name, name2, NAME_LEN);
    champ[0].name[NAME_LEN - 1] = '\0';

    fwrite(champ, sizeof(champ[0]), NUM_CHAMP, stream); 

    fclose(stream);
}

위와 같이 주소가 아닌 을 test.txt에 복사 합니다. 그리고 프로그램을 종료한 뒤에 다시 파일을 읽으면 정상적으로 값을 읽을 수 있습니다.

int main(void)
{
    FILE *stream;
    char champ[NUM_CHAMP][NAME_LEN];

    stream = fopen("test.txt", "rb");
    /* 오류처리 생략 */
    fread(champ, sizeof(champ[0]), NUM_CHAMP, stream);

    for (int i = 0; i < NUM_CHAMP; i++)
        printf("champ%d name is %s\n", i + 1, champ[i]);
}
/*---출력---*/
champ1 name is Teemo
champ2 name is Nono
  • 결론적으로 파일에 저장할 때는 구조체 뿐만아니라 일반 변수들도 포인터로 복사하면 안됩니다.
  • 가능하면 구조체 멤버변수로 포인터를 사용하지 않는 것이 좋습니다.




© 2021.02. by kirim

Powered by kkrim