본문 바로가기
Computer Science

[C언어] C언어 구조체(struct)

by DuncanKim 2022. 7. 13.
728x90

C언어 구조체(struct)

Goal. : 구조체의 기본 활용방법부터 포인터를 이용한 구조체 값 변경방법까지 이해한다.

 

1. 구조체가 만들어진 이유.

 

C언어는 자료를 체계적으로 관리하기 위해 구조체라는 문법을 사용한다. 코드를 구현하다보면 많은 변수들이 선언이 되는데, 이 중 중복적으로 사용되는 코드들이 있다.

 

<보기만 해도 아찔한 코드>

void introduce(int age, char* name, char* hometown, char* favorite_food);
void talk(int age, char* name, char* hometown, char* favorite_food);

int main() {
  int person1_age = 20;
  char* person1_name = "홍길동";
  char* person1_hometown = "한양";
  char* person1_favorite_food = "떡볶이";

  int person2_age = 50;
  char* person2_name = "임꺽정";
  char* person2_hometown = "평양";
  char* person2_favorite_food = "떡국";

  introduce(person1_age, person1_name, person1_hometown, person1_favorite_food);
  talk(person1_age, person1_name, person1_hometown, person1_favorite_food);

  introduce(person2_age, person2_name, person2_hometown, person2_favorite_food);
  talk(person2_age, person2_name, person2_hometown, person2_favorite_food);

  return 0;
}

void introduce(int age, char* name, char* hometown, char* favorite_food) {
  printf("== 소개 시작 ==\n");
  printf("이름 : %s\n", name);
  printf("나이 : %d살\n", age);
  printf("고향 : %s\n", hometown);
  printf("== 소개 끝 ==\n");
}

void talk(int age, char* name, char* hometown, char* favorite_food) {
  printf("이야기 시작 : 안녕하세요. 저는 %d살, %s 입니다. 제 고향인 %s 에서는...\n", age, name, hometown);
}

name, age, hometown 등의 변수들이 난립하고 있는 것을 볼 수 있다. 쓰임새에 따라 숫자를 붙여 다른 변수임을 어필하지만, 내용이 두 배, 세 배 많아진다면 답이 없어질 수도 있다.

그래서 옛날의 조상 프로그래머들은 다음과 같은 생각을 했던 것 같다.

 

💡 자주 쓰는 변수를 ‘모아놓자’. 그런 다음에 모아놓은 것을 여러 개로 복사해서 거기에 변수를 집어넣고 하면 되지 않을까?

그래서 구조체가 만들어 진 것이다.

 

더 쉬운 일상의 예를 들어보자.

💡 인천항에 컨테이너 선이 있다고 하자. 러버덕을 수출한다고 하자. 야적장에 러버덕들이 한 가득 쌓여있다. 그 중에는 노란색이 아닌 것도 있고 하여튼 많다. 근데 그것들을 받아 쓸 사람이 한 명이고 미국에 있다면, 그것을 컨테이너에 담아 수출하지 않는다면? 러버덕을 한 마리씩 옮겨 실어야 할 것이고, 태평양을 건너다가 큰 파도를 만나면 다음과 같은 사태가 일어날 수도 있다.

 

이처럼 구조체는 사람들이 귀찮음을 받아들이지 않고 개선하고자 하는 과정에서 만들어졌다. 관련있는 변수들을 하나씩 묶어서 간편하게 처리하기 위해 만들어진 것이다.

 

2. 구조체의 기본 활용

 

구조체의 본질은 폴더이다. 비슷한 것을 묶어놓는 것. 하나의 디렉토리가 안에 많은 정보들을 담고 있을 때, 우리는 폴더를 나누거나 하위 폴더를 만든다. 정보를 탐색하는 것의 효율성을 높이기 위해서 폴더를 정리하는 것처럼 클래스를 정의하고 분할하는 것도 이와 같은 이유에서 이루어진다.

 

클래스나 구조체는 완벽한 설계를 하지 않아도 된다. 처음 만들 때는 나이브하게 만들었다가 필요성이 생기면 그 때 다시 팩토링을 하면 된다.

 

 

1) structure 기본구조

struct Person{
    int age;
    char* name;
    char* hometown;
    char* favorite_food;
};

 

위의 코드의 경우, 구조체의 타입을 정해둔 설계도이다. 변수들이 뭉쳐져 있는 것이라고 생각하면 된다.

이것을 만약 ‘타입화’ 한다면 아래와 같다.

 

struct Person p;

 

그럼 struct Person 이라는 타입의 크기는 어떻게 될까? 즉, 변수 p의 크기는?

printf("%d\n", (int)sizeof(p));
printf("%d\n", (int)sizeof(struct Person));

// 32
// 32

 

구조체 안에 가장 큰 변수의 크기 * 전체 변수의 개수로 크기가 정해진다.

구조체 안에 변수는 어떻게 접근할까?

char* his_home = p.hometown;

 

이런 식으로 변수를 사용할 수 있다. 구조체이름.구조체안의변수;

 

 

2) 구조체 여러 개 만들기

 

구조체는 설계도와 같다. 그렇다면 그 구조체를 타입으로 해서 여러 개의 구조체를 만들 수 있을까?

struct Person p1;
struct Person p2;
struct Person p3 = p2;

 

이렇게 변수 4개를 가지고 있는 타입을 여러 개 생성할 수 있는 것이다.

3번 라인의 p3 = p2;를 한 경우, p2가 p3 안에 들어가서 이 코드의 전체 구조체의 개수는 2개라고 생각할 수도 있는데, 값이 단순히 복사된 것이기 때문에 p1, p2, p3가 생성되었다고 보면 된다.

 

 

3) 구조체 적용 후 개선된 코드

 

구조체를 정의하면 다음과 같이 코드를 정리할 수 있다.

 

#include <stdio.h>

struct Person {
  int age;
  char* name;
  char* hometown;
  char* favorite_food;
};

void introduce(struct Person p) {
  printf("== 소개 시작 ==\n");
  printf("이름 : %s\n", p.name);
  printf("나이 : %d살\n", p.age);
  printf("고향 : %s\n", p.hometown);
  printf("== 소개 끝 ==\n");
}

void talk(struct Person p) {
  printf("이야기 시작 : 안녕하세요. 저는 %d살, %s 입니다. 제 고향인 %s 에서는...\n", p.age, p.name, p.hometown);
}

int main(void) {
  struct Person p1;
  int a;

  p1.age = 20;
  p1.name = "홍길동";
  p1.hometown = "한양";
  p1.favorite_food = "떡볶이";

  struct Person p2;

  p2.age = 50;
  p2.name = "임꺽정";
  p2.hometown = "평양";
  p2.favorite_food = "떡국";

  introduce(p1);
  talk(p1);
  introduce(p2);
  talk(p2);

  return 0;
}

 

 

3. 구조체의 활용

 

바로 위에 있는 코드에 ‘취미’를 저장하는 변수를 추가하고 싶다면?

구조체로 정리한 코드에서는 단 네 군데만 바꾸어주면 된다. 아래에 주석으로 달아놓은 ‘수정부분’을 보자.

#include <stdio.h>

struct Person {
  int age;
  char* name;
  char* hometown;
  char* favorite_food;
    char* hobby; // 수정부분
};

void introduce(struct Person p) {
  printf("== 소개 시작 ==\n");
  printf("이름 : %s\n", p.name);
  printf("나이 : %d살\n", p.age);
  printf("고향 : %s\n", p.hometown);
    printf("취미 : %s\n", p.hobby); //수정부분
  printf("== 소개 끝 ==\n");
}

void talk(struct Person p) {
  printf("이야기 시작 : 안녕하세요. 저는 %d살, %s 입니다. 제 고향인 %s 에서는...\n", p.age, p.name, p.hometown);
}

int main(void) {
  struct Person p1;
  int a;

  p1.age = 20;
  p1.name = "홍길동";
  p1.hometown = "한양";
  p1.favorite_food = "떡볶이";
    p1.hobby = "아버지와 형 부르기"; // 수정부분

  struct Person p2;

  p2.age = 50;
  p2.name = "임꺽정";
  p2.hometown = "평양";
  p2.favorite_food = "떡국";
    p2.hobby = "도둑질"; // 수정부분

  introduce(p1);
  talk(p1);
  introduce(p2);
  talk(p2);

  return 0;
}

 

네 군데만 바꿔주면 다음과 같이 취미 부분을 더 해줄 수 있다.

 

4. call-by-value, call-by-reference

 

0) call-by-value, call-by-reference란?

[C언어] call-by-reference vs call-by-value

 

[C언어] call-by-reference vs call-by-value

[C언어] call-by-reference vs call-by-value 정말 배우면 배울 수록 멱살을 잡고 흔들어 재끼고 싶은 C언어. 포인터 변수 / 포인터 상수 / 상수 포인터 등등등 무슨 비슷한 말은 많고 뜻은 전혀 다르고 처

masterpiece-programming.tistory.com

 

 

1) call-by-value

 

기본적으로 구조체는 어떤 변수 안에 들어갈 때, 원본의 값을 복사하는 방식으로 들어간다.

즉, 새로운 변수 안에 담기게 되면, 그 안에서 값을 변경한다고 해서 원본 값이 바뀌는 것은 아닌 것이다.

// 문제 : 구조체 변수에 원본이 들어있는지 참조가 들어있는지 확인해주세요.

#include <stdio.h>

struct Person {
  int age;
  char* name;
};

int main(void) {
  struct Person p1;
  p1.name = "홍길동";
  p1.age = 22;

  struct Person p2 = p1;
  p2.name = "장만월";
  p2.age = 205;

  printf("p1 : 내이름은 %s, 나이는 %d\n", p1.name, p1.age);
  printf("p2 : 내이름은 %s, 나이는 %d\n", p2.name, p2.age);

  return 0;
}

 

만약 값을 참조했다면, p1의 값도 “장만월” 205로 바뀔 것이다.

그렇지만 결과는 다음과 같다.

p1 : 내이름은 홍길동, 나이는 22
p2 : 내이름은 장만월, 나이는 205

 

 

2) call-by-reference

그런데 원본값을 바꿀 수 있는 방법도 존재한다. 포인터와 주소값을 활용하면 바꿀 수 있다.

// 문제 : 구조체 변수의 주소를 이용해서 원본값을 변경하는 함수를 구현해주세요.
// 조건 : 아래와 같이 출력하게 해주세요.

#include <stdio.h>

struct School {
  char* name;
  int birth_date;
};

// 이 함수를 변경해야 합니다.
void change(struct School s) {
  s.name = "미국초등학교";
  s.birth_date = 20180717;
}

int main(void) {
  // 힌트

  // int => 타입
  // temp => 변수명
  int temp;

  // struct School => 타입
  // s1 => 변수명
  struct School s1;

  s1.name = "한국초등학교";
  s1.birth_date = 19860404;

  printf("학교의 이름 : %s\n", s1.name);
  // 출력 => 학교의 이름 : 한국초등학교
  printf("학교의 설립일 : %d\n", s1.birth_date);
  // 출력 => 학교의 설립일 : 19860404

  change(s1); // 이 라인은 수정해도 됩니다.

  printf("학교의 이름 : %s\n", s1.name);
  // 출력 => 학교의 이름 : 미국초등학교
  printf("학교의 설립일 : %d\n", s1.birth_date);
  // 출력 => 학교의 설립일 : 20180717

  return 0;
}

 

 

위와 같이 값을 복사를 하는 경우, s1은 계속 한국초등학교이다.

학교의 이름 : 한국초등학교
학교의 설립일 : 19860404
학교의 이름 : 한국초등학교
학교의 설립일 : 19860404

 

 

그렇지만, 포인터와 주소값으로 접근을 한다면 다음과 같은 결과를 얻을 수 있다.

// 문제 : 구조체 변수의 주소를 이용해서 원본값을 변경하는 함수를 구현해주세요.
// 조건 : 아래와 같이 출력하게 해주세요.

#include <stdio.h>

struct School {
  char* name;
  int birth_date;
};

// 이 함수를 변경해야 합니다.
void change(struct School* s) {
  (*s).name = "미국초등학교";
  (*s).birth_date = 20180717; //s->birth_date로 해도 (*s).birth_date와 동일하다.
    // 여기서 s->birth_date는 java에서 s.birth_date와 같다.
    // 자바에서는 포인터를 지원하지 않기 때문에, s.birth_date와 같은 문법을 제공하는 것이다.
}

int main(void) {
  // 힌트

  // int => 타입
  // temp => 변수명
  int temp;

  // struct School => 타입
  // s1 => 변수명
  struct School s1;

  s1.name = "한국초등학교";
  s1.birth_date = 19860404;

  printf("학교의 이름 : %s\n", s1.name);
  // 출력 => 학교의 이름 : 한국초등학교
  printf("학교의 설립일 : %d\n", s1.birth_date);
  // 출력 => 학교의 설립일 : 19860404

  change(&s1); // 이 라인은 수정해도 됩니다.

  printf("학교의 이름 : %s\n", s1.name);
  // 출력 => 학교의 이름 : 미국초등학교
  printf("학교의 설립일 : %d\n", s1.birth_date);
  // 출력 => 학교의 설립일 : 20180717

  return 0;
}

 

 

change(&s1)을 통해 s1의 값이 바뀐 것을 알 수 있다.

학교의 이름 : 한국초등학교
학교의 설립일 : 19860404
학교의 이름 : 미국초등학교
학교의 설립일 : 20180717

 

 

5. 참고 : C언어와 Java와의 비교

// 자바의 인스턴스가 담긴 변수는 C언어의 주소값(포인터)과 같다.

// C언어
struct Person { int age; char* name; };
struct Person* p1 = malloc(sizeof(struct Person));
p1->age = 230; // p1이 가리키는 객체 안의 age
p1.age = 230; // p1 안에 있는 age

// 자바
class Person { int age; String name; }
Person p1 = new Person();
// Person p1; 은 주소값을 가지게 된다. 

p1.age = 230; // p1이 가리키는 객체 안의 age
728x90

댓글