구조체

CHOI·2021년 7월 13일
0

C 언어

목록 보기
20/28
post-thumbnail

우리가 이번에는 심즈라는 게임을 만들고 있다고 생각해보자

무슨 게임인지 잘 모르는 사람들을 위해서 조금 설명해주자면 이 게임은 사람 캐릭터를 육성하는 게임이다. 근데 게임을 플레이 하는 유저가 사람 한 명을 추가 했다고 생각해보자. 그러면 어떻게 처리해야 할까?

일단 가장 먼저 머리속에서 스쳐가는 생각은 다음과 같을 수 있다.

음, 그럼 이전에 만들어 놓은 문자열 배열의 i 번 째 원소에 이름을 등록하고 int 형 배열의 i 번 째 원소로 나이를 등록하고 성격은...

이런식의 방법일 것이다. 이는 앞서 우리가 했었던 도서 관리 프로그램의 방식이다. i 번째 책에 대한 정보는 book_name[i-1], auth_name[i-1], publ_name[i-1], borrow[i-1] 배열에 넣어서 보관했었다.

그런데 사실 이 방법은 조금 문제가 있다. 바로 책의 정보를 수정하고 인자로 넘겨줄 때 상당히 불편하다는 것이다. 앞서 우리가 사용했었던 도서 추가 함수의 원형을 살펴보자.

int add_book(char (*book_name)[30], char (*auth_name)[30],
             char (*publ_name)[30], int *borrowed, int *num_total_book);

이렇게 보면 인자가 너무 길다.. 그래도 간단한 도서 관리 시스템이니까 이정도인거지 위의 심즈같은 경우는 사람 한 명에 대한 정보가 수없이 많다. 예를 들어서 나이, 이름, 성별, 종교, 성격, 직업, 가치관, 재산, 가족... 등등 수많은 정보가 있다. 우리가 사람의 정보를 수정하거나 함수를 호출할 때 마다 이 무지막지한 정보를 인자로 전달하면 손가락도 아프고 눈도 빠질 것이다.

우리가 배열을 배우기 전 때로 한 번 돌아가보자. 우리는 배열을 배우기 전에 예를 들어서 10명의 학생의 점수를 보관하기 위해서 10개의 변수를 각각 선언해서 보관했었다. 그런데 배열을 배우고 나서 우리는 단순히 int arr[10]; 을 해주면 int 형 변수 10개를 쉽게 다룰 수 있었다. 뿐만 아니라 함수에 int 형 변수를 전달할 때, int 형 변수 10개를 전달하면

int fnnc(int a, int b, int c...);

이런식으로 해주었지만 int 형 변수 10개를 배열을 사용하면 다음과 같이 쉽게 할 수 있었다.

int func(int *arr);

그렇다면 위 배열과 같은 논리가 여기에도 적용될 수 있지 않을까? 원소의 크기가 제각각인 배열을 만드는 것이다. 한 사람의 정보를 한 개의 배열로 만드는 것이다. 첫 번째 원소는 int 로 나이를 보관하고 두 번째 원소는 char [30] 로 이름을 보관하는 것이다.

듣기만 해서는 정말 괜찮은 아이디어 인 것 같다. 하지만 C 언어 에서는 배열의 원소의 타입은 모두 동일해야 한다. 다시 말해서 동일한 배열에서 어떤 원소는 char 이고 어떤 원소는 int 일 수 없다는 것이다. 여기서 포기해야 하나 생각할 수 있지만 다행스럽게도 C 언어 에서는 배열에서 해결하지 못한 것들을 구조체 를 이용해서 해결할 수 있다.

멤버

#include <stdio.h>
struct Human {
  int age;    /* 나이 */
  int height; /* 키 */
  int weight; /* 몸무게 */
};            /* ; 붙이는 것 주의하세요 */
int main() {
  struct Human Psi;

  Psi.age = 99;
  Psi.height = 185;
  Psi.weight = 80;

  printf("Psi 에 대한 정보 \n");
  printf("나이   : %d \n", Psi.age);
  printf("키     : %d \n", Psi.height);
  printf("몸무게 : %d \n", Psi.weight);
  return 0;
}

실행 결과

Psi 에 대한 정보
나이   : 99
키     : 185
몸무게 : 80

새로운 것들이 많이 등장해서 두려울 수 있지만 이 강좌가 끝날 때 쯤 위 사실들을 자유롭게 다룰 수 있을 것이니까 걱정하지 말고 일단 다음 부분을 보자.

struct Human {
  int age;    /* 나이 */
  int height; /* 키 */
  int weight; /* 몸무게 */
};            /* ; 붙이는 것 주의하세요 */

앞서 말했듯이 구조체는 "각각의 원소의 타입이 제각각인 배열"라고 하였다. 이 때문에 배열에서는 배열의 타입만으로 모든 원소의 타입을 알 수 있었다. (예를 들어서 int arr[10] 이라고 하면 arr 의 모든 원소가 int 형이라는 것을 알 수 있었다.)

그러나 구조체는 그렇지 않다. 따라서 구조체를 정의할 때 모든 원소의 타입을 지정해줘야 한다. 위와 같이 말이다. 이 Human 이라는 구조체는 3 개의 멤버를 가지고 있는데 (구조체에서는 원소라고 하지 않고 멤버(Member)라고 부른다) 각 멤버는 int age, int height, int weight 로 3 개가 있다.

구조체의 일반적인 정의는 다음과 같다.

struct 구조체이름 {
  멤버들..예를 들면 char str[10];
  int i;
}; /* 마지막에 꼭 ; 를 붙인다. */

다음으로 main 함수의 내부를 살펴보자.

struct Human Psi;

다음과 같이 Human 이라는 구조체의 구조체 변수로 Psi 를 정의 하였다. 여기서 놀라운 점은 struct Human 이라는 것이 마치 int 형 변수를 선언할 때 int 를 사용한 것 처럼 이용되었다는 것이다. 아무튼 이처럼 Psi를 정의하고 나면, Psi 의 타입은 sturct Human 즉, Human 구조체 가 된다. int a 했을 때, a 의 타입이 int 인 것 처럼.

그렇다면 배열에서 [] 로 원소에 접근한 것 처럼 구조체에서도 멤버에 접근하는 방법이 있을 것이다. C 언어에서는 . 을 이용하여 멤버에 접근할 수 있다. 예를 들어 Psi 의 height 멤버에 접근하려면 Psi.height 이라고 하면 된다. 이는 마치 배열에서 arr[3] 과 같이 원소에 접근하는 것과 동일하다. 다만 구조체는 . 을 사용하고 멤버가 무엇인지 특별히 명시 해줘야 한다.

Psi.age = 99;
Psi.height = 185;
Psi.weight = 80;

따라서 위 작업은 Psi 라는 구조체의 변수의 각 멤버의 값을 대입하는 작업이다. 이는 마이 arr[2] = 99, arr[3] = 90 과 같은 작업이다.

예제

지금 구조체에 대해서 처음 배워서 살짝 이해가 안되고 혼동이 올 수 있다. 다음 예제를 통해서 감을 확 잡아보자.

#include <stdio.h>
char copy_str(char *dest, const char *src);
struct Books{
    char name[30];
    char auth[30];
    char publ[30];
    int borrowed;
};
int main(){
    static Books Harry_Potter;
    
    copy_str(Harry_Potter.name,"Harry Potter");
    copy_str(Harry_Potter.auth,"J.K. Rolling");
    copy_str(Harry_Potter.publ, "Scholastic");
    Harry_Potter.borrowed = 0;
    
    printf("책 이름 : %s \n", Harry_Potter.name);
    printf("저자 이름 : %s \n", Harry_Potter.auth);
    printf("출판사 이름 : %s \n", Harry_Potter.publ);

    return 0;
}

char copy_str(char *dest, const char *src) {
  while (*src) {
    *dest = *src;
    src++;
    dest++;
  }

  *dest = '\0';

  return 1;
}

실행 결과

책 이름 : Harry Potter
저자 이름 : J.K. Rolling
출판사 이름 : Scholastic

일단 위를 보면 저번 문자열 여러 함수를 만들때 사용했던 문자열 복사 함수를 사용하였다. 이렇게 이전에 사용했던 함수를 가져오면 시간도 절약되고 편리하다.

구조체에서 변수의 초기화

먼저 구조체를 정의 한 부분을 보자

struct Books {
  /* 책 이름 */
  char name[30];
  /* 저자 이름 */
  char auth[30];
  /* 출판사 이름 */
  char publ[30];
  /* 빌려 졌나요? */
  int borrowed;
};

흥미로운 점은 구조체가 우리가 이전에 만들었던 도서 관리 프로그램과 유사하게 생겼다는 것이다. 그 때에는 각 책을 배열의 한 개의 원소로 표현했는데, 책 이름의 경우 name[100][30] 의 한 문자열 name[i] ( i는 임의의 수 ), 빌렸는 지에 대한 유무는 borrow[100] 의 한 문자열 broow[i] 로 표현했다. 하지만 구조체에서는 책의 각각 정보를 따로따로 배열로 표현할 필요가 없다. main 함수를 보면

struct Books Harry_Potter;

copy_str(Harry_Potter.name, "Harry Potter");
copy_str(Harry_Potter.auth, "J.K. Rolling");
copy_str(Harry_Potter.publ, "Scholastic");
Harry_Potter.borrowed = 0;

여기서 우리는 먼저 Harry_Potter 라는 struct Books 의 구조체 변수를 만들었다. 그러면 이제 Harry_Potter 의 각 멤버에 값을 대입해줘야할 것이다. 먼저 Harry_Potter.name 에 "Harry Potter"를, 마찬가지로 저자와, 출판사도 넣어 주었다. 그리고 Harry_Potter.borrowed 에 빌렸는지에 유무 확인을 위해 0 을 넣어주었다.

그런데 borrow 의 멤버의 값은 항상 처음에는 0일 것이다. 그렇다면 굳이 책을 매번 등록할 때 마다 borrowed = 0 을 해줄 필요없이 구조체 자체에서 바꿔주면 안될까?

한 번 구조체의 정의 부분을 다음과 같이 바꿔보자.

struct Books {
  /* 책 이름 */
  char name[30];
  /* 저자 이름 */
  char auth[30];
  /* 출판사 이름 */
  char publ[30];
  /* 빌려 졌나요? */
  int borrowed = 0;
};

컴파일이 문제 없이 잘 될줄 알았는데

컴파일 오류

error C2143: 구문 오류 : ';'이(가) '=' 앞에 없습니다.
error C2059: 구문 오류 : '='
error C2059: 구문 오류 : '}'
error C2079: 'Harry_Potter'은(는) 정의되지 않은 struct 'Books'을(를) 사용합니다.
error C2224: '.name' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2198: 'copy_str' : 호출에 매개 변수가 너무 적습니다.
error C2224: '.auth' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2198: 'copy_str' : 호출에 매개 변수가 너무 적습니다.
error C2224: '.publ' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2198: 'copy_str' : 호출에 매개 변수가 너무 적습니다.
error C2224: '.borrowed' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2224: '.name' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2224: '.auth' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.
error C2224: '.publ' 왼쪽에는 구조체/공용 구조체 형식이 있어야 합니다.

다음과 같은 오류의 향연을 볼 수 있다.

이유는 간단하다. 구조체의 정의에서 변수를 초기화 할 수 없기 때문이다. 그냥 받아들이자. 구조체 정의 내부에서는 변수를 초기화할 수 없다. 특히, 위와 같이 실수하면 어디서 오류가 났는지도 찾기 힘들다. 따라서 매우 주의해야 한다.


구조체에서 배열의 사용

#include <stdio.h>
struct Books {
  /* 책 이름 */
  char name[30];
  /* 저자 이름 */
  char auth[30];
  /* 출판사 이름 */
  char publ[30];
  /* 빌려 졌나요? */
  int borrowed;
};
int main() {
  struct Books book_list[3];
  int i;

  for (i = 0; i < 3; i++) {
    printf("책 %d 정보 입력 : ", i);
    scanf("%s%s%s", book_list[i].name, book_list[i].auth, book_list[i].publ);
    book_list[i].borrowed = 0;
  }

  for (i = 0; i < 3; i++) {
    printf("------------------------------- \n");
    printf("책 %s 의 정보\n", book_list[i].name);
    printf("저자 : %s \n", book_list[i].auth);
    printf("출판사 : %s \n", book_list[i].publ);

    if (book_list[i].borrowed == 0) {
      printf("안 빌려짐\n");
    } else {
      printf("빌려짐 \n");
    }
  }
  return 0;
}

실행 결과

책 0 정보 입력 : ChewingC Psi itguru
책 1 정보 입력 : ChewingCPP Psi ModooCode
책 2 정보 입력 : asdf asdf as
-------------------------------
책 ChewingC 의 정보
저자 : Psi
출판사 : itguru
안 빌려짐
-------------------------------
책 ChewingCPP 의 정보
저자 : Psi
출판사 : ModooCode
안 빌려짐
-------------------------------
책 asdf 의 정보
저자 : asdf
출판사 : as
안 빌려짐

바로 main 부분을 보자.

struct Books book_list[3];

이 부분이 이해가 잘 안될 수 있으니 한 번 더 설명해보겠다. 우리가 int arr[3] 은 익숙할 것이다. 이때 int 가 하나의 타입 이듯이, struct Books 도 하나의 타입이라고 생각하면 된다. 그런데 int arr[3] 를 하면 arrint 형 원소 3 개 만들어지듯이, book_liststruct Books 형 변수가 3개 만들어지는 것이다.

for (i = 0; i < 3; i++) {
  printf("책 %d 정보 입력 : ", i);
  scanf("%s%s%s", book_list[i].name, book_list[i].auth, book_list[i].publ);
  book_list[i].borrowed = 0;
}

그리고 for 문을 통해서 각각의 struct Books 변수의 멤버의 값을 대입해주고 borrowed 의 값도 0으로 초기화해주었다.

for (i = 0; i < 3; i++) {
  printf("------------------------------- \n");
  printf("책 %s 의 정보\n", book_list[i].name);
  printf("저자 : %s \n", book_list[i].auth);
  printf("출판사 : %s \n", book_list[i].publ);

  if (book_list[i].borrowed == 0) {
    printf("안 빌려짐\n");
  } else {
    printf("빌려짐 \n");
  }
}

그리고 for 문을 통해서 book_list 의 각 원소의 멤버들을 출력하였다.

구조체의 포인터

포인터 라는 것만 보고 눈살 찌푸리게 된 사람도 있을 것이다. 구조체 또한 역시 포인터를 사용할 수 있다. 포인터의 개념에 대해서만 잘 이해하고 있다면 어려운 부분이 전혀 아니다.

#include <stdio.h>
struct test {
  int a, b;
};
int main() {
  struct test st;
  struct test *ptr;

  ptr = &st;

  (*ptr).a = 1;
  (*ptr).b = 2;

  printf("st 의 a 멤버 : %d \n", st.a);
  printf("st 의 b 멤버 : %d \n", st.b);

  return 0;
}

실행 결과

st 의 a 멤버 : 1
st 의 b 멤버 : 2

먼저 구조체의 포인터에 대해서 이야기 하기 전에 확실히 짚고 넘어가야할 부분이 있다. 앞서 누누히 말했지만 struct test 역시 하나의 타입(형) 이라는 것이다. 위의 예제들의 struct Human 이나 stuct books 역시 하나의 타입이였다.

즉, 구조체는 하나의 타입을 창조하는 것과 마찬가지이다. 마치 intchar 처럼 말이다. 그런데 이러한 타입을 가리킬 때 우리가 포인터를 어떻게 사용했었나? 바로, int *char * 로 사용했다. 구조체도 똑같다.

struct test st;
struct test *ptr;

위의 두 번째 문장과 같이 struct test *ptr; 의 의미는" struct test 형을 가리키는 포인터 ptr "이다. 여기서 주의해야할 점은 ptr절대로 구조체가 아니다. ptr 역시 다른 모든 포인터와 마찬가지로 8바이트 공간을 차지한다(64비트 컴퓨터의 경우).

ptr = &st;

그리고 위와 같이 ptrst 의 주소값을 넣는 것이다. 그런데 눈치가 빠른 사람들은 다음과 같은 질문을 할 수 있다.

아까 구조체는 단순히 원소의 크기가 제각각인 배열이라고 하지 않았나? 그러면 구조체도 배열 처럼 변수의 이름이 그 주소값이 되어야 하는 것 아닌가? 다시 말해서 int arr[10]; 라고 정의 했다면 포인터를 정의 할 때 int *p = arr 이라고 하지 int *p = &arr 라 하지 않잖아?

상당히 좋은 질문이다. 하지만 조금 아래에 보면 구조체 변수의 이름은 살짝 다르다는 것을 알게 될 것이다. 그냥 보통 변수 처럼,(그래서 "구조체 변수"라고 부르지 "구조체 배열"이라고 하지 않았다.) 주소 연산자 & 를 사용하며 구조체가 정의된 메모리의 주소값을 불러 온다고 생각해주면 된다.

그러면 ptrst 를 가리키는 포인터가 된다.

(*ptr).a = 1;
(*ptr).b = 2;

그러면 구조체의 멤버 값을 변경한 부분을 보자 일단 (*ptr)st 과 동일하다고 볼 수 있다. 왜냐하면 prtst 를 가리키는 포인터이기 때문이다. 따라서 (*ptr).a = 1st.a = 1 과 정확히 100% 동일하다. 따라서 아래 printf 문에서 st.a 가 1 이 출력되는 것을 볼 수 있다.

그런데 위처럼 굳이 괄호를 사용해야 할까? 그냥 *ptr.a = 1 을 하면 무슨 문제가 있을까?

포인터의 괄호를 써야할까?

(*ptr).a = 1; 을
*ptr.a = 1; 로 바꿔서 컴파일 해보자.

컴파일 오류

error C2231: '.a' : 왼쪽 피연산자가 'struct'을(를) 가리킵니다. '->'를 사용하십시오.
error C2100: 간접 참조가 잘못되었습니다.

그러면 이러한 오류가 발생한다.

왜 그럴까? 이에 대한 답을 하기전에 연산자 우선 표를 확인해보자.

여기서 맨 위를 보면 . 이 있는 것을 볼 수 있다. 여기서의 . 은 구조체의 멤버를 지칭할 때 사용하는 . 을 말한다. 즉 (*ptr).a 에서의 . 을 말한다는 것이다. 그런데 바로 아래에 *(포인터) 라고 적혀있다. 여기서 즉, (*ptr).a 에서의 * 를 말한다. 주목해야할 점은 .* 보다 우선순위가 높다라는 것이다.

따라서 *ptr.a 를 하면 . 가 먼저 실행되어 ptr.a 를 먼저 실행한 뒤에 그 값에 * 를 한 것에 1이 들어가게 된다는 것이다. 따라서 *ptr.a*(ptr.a) 과 동일한 문장이 된다.

그런데 위에서 말했지만 ptr 은 단순히 포인터에 불과하다. 즉, ptr 은 구조체가 아니라는 것이다. 따라서 구조체도 아닌 것에 a 라는 멤버에 접근하라고 하니까 오류가 발생하게 된다.

결과적으로 구조체의 포인터를 사용해서 멤버에 접근하기 위해서는 반드시 괄호를 사용해야 한다. 이는 상당히 귀찮지 아니할 수 없다. 그래서 똑똑한 C 프로그래머들은 이 문제를 해결하기 위해서 다음과 같은 아름다운 기호를 만들었다.

#include <stdio.h>
struct test {
  int a, b;
};
int main() {
  struct test st;
  struct test *ptr;
  ptr = &st;
  ptr->a = 1;
  ptr->b = 2;
  printf("st 의 a 멤버 : %d \n", st.a);
  printf("st 의 b 멤버 : %d \n", st.b);
  return 0;
}

실행 결과

st 의 a 멤버 : 1
st 의 b 멤버 : 2

다음 문장을 보자

ptr->a = 1;
ptr->b = 2;

이와 같이 *(ptr.a) = 1 이라는 문장을 단순히 ptr -> a = 1 로 간단히 표현할 수 있다. 그 아래 문장 또한 마찬가지이다. 단순히 사용자의 편의를 위해서 -> 라는 새로운 기호를 도입하였다. (이는 위의 연산자 우선 표 에서도 볼 수 있다.)

구조체는 정말 잘 쓰면 보물같은 존재이다. 포인터와 더불에 C 언어의 양대 산맥을 이루는 기능이다.

구조체 포인터 연습하기

구조체 포인터에 대해서 연습하기 전에 -> 다시 한번만 집고 넘어가보자 pt -> b = 3 의 의미는 포인터 pt 가 가리키고 있는 구조체 변수의 멤버 b 의 값에 3을 대입하라는 의미이다. 즉

#include <stdio.h>
struct box{
    int a=0;
    int b=0;
};
int main(){
    struct box name1;
    struct box *p;
    p = &name1;
    p -> a = 99;
    
    printf("a : %d \n",name1.a);
    
    return 0;
}

라면 p -> a 의 의미는 포인터 p 가 가리키는 구조체 변수 name1 의 멤버 a 라는 의미이다. 그러면 p -> a = 3 의 의미는 포인터 p 가 가리키는 구조체 변수 name1 의 멤버 a 의 값에 3을 대입하라는 의미라고 생각할 수 있다.

예제 1

여기까지 잘 이해가 된다면 이제 다음 예제를 보자.

#include <stdio.h>
struct TEST {
  int c;
  int *pointer;
};
int main() {
  struct TEST t;
  struct TEST *pt = &t;
  int i = 0;

  /* t 의 멤버 pointer 는 i 를 가리키게 된다*/
  t.pointer = &i;

  /* t 의 멤버 pointer 가 가리키는 변수의 값을 3 으로 만든다*/
  *t.pointer = 3;

  printf("i : %d \n", i);

  /*

  -> 가 * 보다 우선순위가 높으므로 먼저 해석하게 된다.
  즉,
  (pt 가 가리키는 구조체 변수의 pointer 멤버) 가 가리키는 변수의 값을 4 로
  바꾼다. 라는 뜻이다/

  */
  *pt->pointer = 4;

  printf("i : %d \n", i);
  return 0;
}

실행 결과

i : 3
i : 4

상당히 복잡한 예제이다. 이 예제를 보고 이해했다면 더이상 구조체 포인터가지고 헷갈리지 않을 듯 하다.

먼저 TEST 구조체를 보자.

struct TEST {
  int c;
  int *pointer;
};

구조체 안에 포인터가 들어있다. 일단 상당히 쫄리지만 괜찮다. 우린 포인터를 많이 상대해 봤으니까

struct TEST t;
struct TEST *pt = &t;
int i = 0;

마찬가지로 ptt 를 가리키게 된다.

t.pointer = &i;

위 문장에서 t 의 멤버 pointeri 의 주소값이 들어가게 된다. 따라서 t 의 멤버 pointeri 를 가리키게 된다. 그러면 우리는 pointer 를 가지고 i 의 값을 바꾸며 놀 수 있게 되겠다.

*t.pointer = 3;

흠. 일단 우선 순위를 고려하면 .* 보다 우선 순위가 높으니까 t.pointer 가 먼저 해석되고 그 다음에 *(t.pointer) 의 형태로 해석될 것이다. 따라서, *t.pointer 를 통해 구조체 변수 t 의 멤버 pointer 가 가리키는 변수를 지칭할 수 있게 되는 것이다.

*pt->pointer = 4;

. 과 마찬가지로 -> 또한 * 보다 우선 순위가 높다. 따라서 위는 *(pt -> pointer) = 4 와 동일하다. 아무튼, pt -> pointer 로 통해 "포인터 pt 가 가리키는 구조체 변수의 멤버 pointer ", 즉 t.pointer 를 의미 하고 *(pt -> pointer) = 4 를 통해 pointer 가 가리키는 값을 4로 바꿀 수 있게 된다.

예제 2

#include <stdio.h>
int add_one(int *a);
struct TEST {
  int c;
};
int main() {
  struct TEST t;
  struct TEST *pt = &t;

  /* pt 가 가리키는 구조체 변수의 c 멤버의 값을 0 으로 한다*/
  pt->c = 0;

  /*
  add_one 함수의 인자에 t 구조체 변수의 멤버 c 의 주소값을
  전달하고 있다.
  */
  add_one(&t.c);

  printf("t.c : %d \n", t.c);

  /*
  add_one 함수의 인자에 pt 가 가리키는 구조체 변수의 멤버 c
  의 주소값을 전달하고 있다.

  */
  add_one(&pt->c);

  printf("t.c : %d \n", t.c);

  return 0;
}
int add_one(int *a) {
  *a += 1;
  return 0;
}

실행 결과

t.c : 1
t.c : 2

앞서 예제 1을 잘 이해 했다면 위를 이해하는데 문제가 없을 것이다. 혹시 모르니까 다음 부분만 보자.

add_one(&t.c);

add_one 함수의 인자로 t 의 멤버 c 의 주소값을 전달하였다. 역시 & 보다 . 의 우선 순위가 높아 &(t.c) 를 한 것과 동일하다. 아무튼 이 함수에 의해 멤버 c 의 값이 1 증가한다.

구조체의 대입

"구조체의 복사" 라고 하면 뭔가 거창할 것 같지만 사실 별거 없다.

바로, 구조체도 보통 변수들과 같이 = 를 사용할 수 있다는 것이다.(= 가 대입 연산자라는 건 알고 있을 거라고 믿고싶다.)

#include <stdio.h>
struct TEST {
  int i;
  char c;
};
int main() {
  struct TEST st, st2;

  st.i = 1;
  st.c = 'c';

  st2 = st;

  printf("st2.i : %d \n", st2.i);
  printf("st2.c : %c \n", st2.c);

  return 0;
}

실행 결과

st2.i : 1
st2.c : c

위 예제를 이해하는데 아무 문제가 없을 것이다.

st2 = st;

변수 ij 에 대입하면 i 의 값이 그대로 j 에 복사되듯이, st2 의 멤버 i 에는 st 의 멤버 i 값이 그대로 복사되고 st2 의 멤버 c 에는 st 의 멤버 c 값이 그대로 복사된디. 이는 상당히 합리적으로 대입연사자가 자신의 역할을 하는것 같다.

#include <stdio.h>
char copy_str(char *dest, char *src);
struct TEST {
  int i;
  char str[20];
};
int main() {
  struct TEST a, b;

  b.i = 3;
  copy_str(b.str, "hello, world");

  a = b;

  printf("a.str : %s \n", a.str);
  printf("a.i : %d \n", a.i);

  return 0;
}
char copy_str(char *dest, char *src) {
  while (*src) {
    *dest = *src;
    src++;
    dest++;
  }

  *dest = '\0';

  return 1;
}

실행 결과

a.str : hello, world
a.i : 3

위 코드 역시 구조체의 대입이 무엇인지 잘 이해 했다면 이해하는데 무리가 없을 것이다.

#include <stdio.h>
char copy_str(char *dest, char *src);
struct TEST {
  int i;
  char str[20];
};
int main() {
  struct TEST a, b;

  b.i = 3;
  copy_str(b.str, "hello, world");

  a = b;

여기까지 보면 구조체 TEST 를 정의하고 구조체 변수 b 의 멤버 i 에 3을 대입하고 이전에 우리가 만들었었던 copy_str 함수를 통해 b 의 멤버 str 배열에 문자열 "hello, world" 를 대입하고 대입 연산자를 통해 a 구조체에 b 구조체를 대입하였다. 따라서 b 구조체의 모든 멤버의 데이터가 a 구조체에 일일이 대응되어 값이 복사된다. 즉, ii 끼리 strstr 의 각 원소끼리 쭈르륵 복사된다

구조체를 인자로 전달하기

예제 1

#include <stdio.h>
struct TEST {
  int age;
  int gender;
};
int set_human(struct TEST a, int age, int gender);
int main() {
  struct TEST human;
  set_human(human, 10, 1);
  printf("AGE : %d // Gender : %d ", human.age, human.gender);
  return 0;
}
int set_human(struct TEST a, int age, int gender) {
  a.age = age;
  a.gender = gender;
  return 0;
}

이렇게 성공적으로 컴파일 했다면 오류가 발생할 것이다... 혹은 아래와 같이 나올 것이다.

AGE : 0 // GENDER : 0

오류의 내용을 살펴보면 human 이라는 구조체의 변수의 값이 초기화 되지 않은채로 사용했다는 내용 같다.

우리는 TEST 구조체를 정의 했고

int set_human(struct TEST a, int age, int gender) {
  a.age = age;
  a.gender = gender;

  return 0;
}

set_human 함수를 만들어서 TEST 구조체 변수들의 값을 초기화 하도록 했다. 따라서

set_human(human, 10, 1);

와 같이 한다면 humanagegender 멤버들이 초기화 되는 것 같이 보이는데 사실 그렇지 않다. 왜그럴까? 여태까지 강좌를 잘 따라 왔다면 짐작할 수 있을 것 같다.

바로 함수 강좌에서 말한 규칙,

특정한 변수의 값을 다른 함수를 통해서 변화시키려고 하면 그 변수의 주소값을 인자로 전달해야 한다.

라는 규칙을 지키지 않았기 때문이다.

따라서 위에 함수에서 a.age = age; 를 했을 때, 실제로 바뀌는 건 main 함수의 human 이 아니라 set_human 함수의 a 가 바뀐다. 즉, human 과 별개의 구조체 변수 a 의 멤버 age 의 값이 바뀐다는 것이다.

따라서 human 구조체 변수의 멤버들은 전혀 초기화 되지 않은 채 출력을 실행하게 된다.

이를 해결 하기 위해서 구조체 변수의 주소값을 인자로 받는 함수를 만들어야 한다.

예제 2

아래의 예제를 보기 전에 스스로 만들어 보는 것을 추천한다.

#include <stdio.h>
struct TEST {
  int age;
  int gender;
};
int set_human(struct TEST *a, int age, int gender);
int main() {
  struct TEST human;

  set_human(&human, 10, 1);

  printf("AGE : %d // Gender : %d ", human.age, human.gender);
  return 0;
}
int set_human(struct TEST *a, int age, int gender) {
  a->age = age;
  a->gender = gender;

  return 0;
}

실행 결과

AGE : 10 // Gender : 1

위와 같이 구조체 변수의 멤버의 값이 제대로 변경되었음을 볼 수 있다.

int set_human(struct TEST *a, int age, int gender) {
  a->age = age;
  a->gender = gender;

  return 0;
}

함수를 구조체 변수의 주소값을 인자로 받게 수정하였다.

set_human(&human, 10, 1);

그리고 함수를 호출할 때도 인자로 구조체 변수의 주소값을 전달하였다. 따라서 ahuman 을 가리키게 된다.(역시 주의할 점은 a구조체 변수가 아니다 라는 사실이다. a 는 단순히 구조체 변수 human 의 메모리상의 시작 주소값을 보관하고 있을 뿐이다. )

그리고 a->age = age; 부분에서 a->ageage 는 다르다는 것이다. a->age 는 구조체 변수 human 의 멤버 age 를 지칭하고 그냥 ageset_human 함수의 인자로 받는 int 형 변수 age 라는 변수를 가리키는 것이다. 이 둘은 다른 것이고 컴퓨터 내부에서도 다르게 처리된다.

예제 3

#include <stdio.h>
struct TEST {
  int age;
  int gender;
  char name[20];
};
int set_human(struct TEST *a, int age, int gender, const char *name);
char copy_str(char *dest, const char *src);

int main() {
  struct TEST human;

  set_human(&human, 10, 1, "Lee");

  printf("AGE : %d // Gender : %d // Name : %s \n", human.age, human.gender,
         human.name);
  return 0;
}
int set_human(struct TEST *a, int age, int gender, const char *name) {
  a->age = age;
  a->gender = gender;
  copy_str(a->name, name);

  return 0;
}
char copy_str(char *dest, const char *src) {
  while (*src) {
    *dest = *src;
    src++;
    dest++;
  }

  *dest = '\0';

  return 1;
}

실행 결과

AGE : 10 // Gender : 1 // Name : Lee

기본적으로 이전 예제와 비슷하지만 name 이라는 멤버 하나가 추가 되었다.

int set_human(struct TEST *a, int age, int gender, const char *name) {
  a->age = age;
  a->gender = gender;
  copy_str(a->name, name);

  return 0;
}

함수 set_human 에서 인자로 받은 문자열을 구조체 멤버 name 배열에 넣기 위해서는 앞서 우리가 만들었던

copy_str 를 통해서 넣어야 한다. 그런데 이 함수의 인자로 주소값을 보내야 하기 때문에 a->name 를 통해 구조체의 멤버 name 의 값을 전달 했는데 name 은 배열의 이름이기 때문에 배열의 시작 주소로 값이 전달된다.

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글