Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

원시데이터 타입

C 언어는 매우 기본적으로 사용하는 5가지 정도의 원시 데이터 타입이라는 것을 가지고 있다는 것을 앞서 배웠다. 이들 기본 타입은 다음과 같은 것들이다.
  • int, float, double, char, long long int, Pointer
인간이 다루는 매우 복잡한 데이터들도 숫자와 문자, 도형 이라는 걸 생각하면 컴퓨터가 이렇게 단지 몇가지만의 데이터 타입을 가지는 것도, 어찌보면 당연한 결과라고 할 수 있을거 같다. C 언어뿐만 아니라 거의 대부분의 언어가 6-8개정도의 원시데이터 타입만을 가지고 있을 뿐이다. 종류역시 한두개 정도만 제외하고는 C와 거의 차이가 없다.

원시데이터 타입의 구조화

인간이 다루는 데이터로 보자면, 숫자,문자,도형만 있어도 모든 정보를 다룰 수 있기는 하다. 그렇지만 너무나 비효율적이다. 그래서, 이들 데이터 타입을 구조화해서 새로운 데이터 타입을 만들어서 사용하게 된다. 예를 들자면, 주소 정보를 관리하기 위해서 주소록을 만들고, 개인신상관리를 위해서 신상카드를 만들어서 사용하는 것이다. 이렇게 구조화하게 되면, 정보를 훨씬 깔끔하게 다룰 수 있게 된다.

만약 유저정보를 관리할 목적이라면, 아래와 같이 데이터를 구조화 할 수 있을 것이다.
  +--- User Info  -----------------------+
  | Name : Text                          |
  | Age  : Number                        |
  | Address : Text                       |
  | Email   : Text                       |
  | Home    : Text                       |
  +--------------------------------------+
TextNumber만으로 유저정보 관리를 위한 User Info라는 새로운 데이터 타입을 만들었다.

구조체

C언어도 원시데이터 타입을 구조화해서 새로운 데이터 타입을 만들 수 있도록 지원하고 있다. 이것을 우리는 구조체(Structure)라고 한다. 구조체는 다음과 같은 방식으로 만들 수 있다.
struct 구조체이름 
{
   데이터타입 변수명;
   데이터타입 변수명;
   데이터타입 변수명; 
};

위에서 예로 들었던, 유저정보를 구조체로 만들어 보도록하자. 이름은 문자열이 들어가게 되므로 char의 배열이나 포인터(:12)형식으로 선언해야 할 것이다. 포인터는 좀 귀찮으니, 모든 문자열은 char의 배열로 하도록 하겠다. 나이는 int형으로 하면 될것이고, 주소, 이메일, 홈페이지는 모두 char 배열로 하면 문제없을 것이다.
struct userInfo
{
	char name[12]; 
	int age;
	char address[80];
	char email[40];
	char home[40];
};
구조체는 내부적으로 자신이 사용할 변수들을 유지하게 되는데, 이러한 변수를 멤버변수라고 한다.

구조체의 정의, 선언 그리고 사용

구조체는 원시데이터 타입을 요소로 가지는 사용자 정의 데이터타입으로 볼 수 있다. 그러므로 다른 원시데이터 타입과 마찬가지로 선언해서 사용하면 된다. 그러나 사용자 정의 데이터 타입이기 때문에, 구조체의 구조를 먼저 정의해줘야 한다. 인사기록 카드를 만들려면, 카드에 어떤 내용이 들어가야 하는지를 먼저 정의해야 하는것과 마찬가지다.

구조체의 정의는 위에서 이미 설명한바가 있다. 이제 정의를 하는 위치가 문제가 되는데, 구조체는 프로그램 전체에서 선언되고 사용될 수 있으므로, 글로벌영역에서 정의가 된다. 예를 들자면 아래와 같다.
// userInfo 구조체를 정의한다.
struct userInfo
{
	char name[12]; 
	int age;
	char address[80];
	char email[40];
	char home[40];
};

int main()
{
	struct userInfo MyUser;
}

선언은 일반데이터타입과 마찬가지다. 구조체의 이름뒤에 변수명을 적어주면 된다.
struct userInfo Myuser;

이렇게 정의와 선언이 끝났다면, 이제 사용하는 일만 남았다. 구조체는 다른 원시 데이터 타입들과는 달리, 내부에 멤버변수를 가진다. 그러므로 각각의 멤버변수별로 접근할 수 있어야 한다.

C 언어는 멤버 연산자 "."을 이용해서 멤버변수에 접근할 수 있도록 하고 있다. userInfo 구조체 선언인 MyUser에서 각각의 멤버변수는 다음과 같이 접근할 수 있다.
strcpy(MyUser.name, "yundream\0");
MyUser.age = 33;
strcpy(MyUser.email, "yundream@gmail.com\0");
strcpy(MyUser.home, "http://www.joinc.co.kr\0");

구조체와 배열

어렵게 생각할 필요는 없다. 구조체도 데이터 타입이므로, 다른 원시 데이터처럼 배열을 이용해서 동일하게 구조화할 수 있다. 만약 유저정보를 5개를 저장하는 프로그램을 만든다면, 다음과 같이 배열로 선언하면 된다.
struct userInfo Myuser[5];

접근 역시 배열첨자를 이용하면된다.
strcpy(MyUser[0].name, "yundream\0");
MyUser[0].age = 33;
strcpy(MyUser[0].email, "yundream@gmail.com\0");
strcpy(MyUser[0].home, "http://www.joinc.co.kr\0");

아주 간단하다.

구조체와 포인터

배열과 포인터는 메모리 상에서 근본적으로 동일한 구조를 가진다는 것을 배웠다. 구조체를 배열로 다룰 수 있으니, 마찬가지로 포인터로도 다룰 수 있으며, 사용하는 방법도 10장에서 배웠던것과 동일하다.

참, 다른 원시데이터 타입과 다른점이 있다. 구조체는 멤버변수를 가지고 있기 때문이다. 앞에서 구조체의 멤버변수에 접근하기 위해서 멤버연산자 .를 사용하면 된다는 것을 배웠다. 그러나 구조체를 포인터로 선언했을 경우에는 멤버연산자를 사용할 수가 없다. 멤버연산자는 을 가져오기 위해서 사용하는 연산자인데, 포인터는 이 아닌 주소를 다루기 때문이다. 그러므로 주소가 가리키는 곳의 을 가져오기 위한 새로운 연산자가 필요하게 된다. C는 구조체 멤버변수의 포인터연산을 위해서 참조연산자라는 것을 제공한다. 참조연산자는 ->를 사용하면 된다.
strcpy(MyUser->name, "yundream\0");
MyUser->age = 33;
strcpy(MyUser->email, "yundream@gmail.com\0");

당연하지만, 포인터는 주소만 가리키는 도구이므로, 실제 데이터를 저장하기 위해서는 메모리를 할당해야만 한다. 메모리 할당은 malloc(3) 함수를 이용하면 된다. 아래 코드는 userInfo 구조체를 포인터로 선언한다음, 5개의 userInfo 정보를 저장할 수 있도록 메모리를 할당하는 프로그램이다. 메모리를 할당하기 위해서는 구조체의 크기를 알아야 할것인데, 다른 데이터 타입과 마찬가지로 sizeof명령을 이용해서 알아낼 수 있다.
#include <unistd.h>
#include <stdlib.h>

struct userInfo
{
    char name[12];
    int age;
    char address[80];
    char email[40];
    char home[40];
};

int main()
{
    struct userInfo *MyUser;

    printf("structure Size is %d\n", sizeof(struct userInfo));
    MyUser = (struct userInfo *)malloc(sizeof(struct userInfo) * 5);
}

예제 프로그램

그럼 간단한 예제 프로그램을 만들어 보도록하자. 이 프로그램은 사용자 정보를 입력받아서 출력하는 일을 한다. 입력받는 정보는 다음과 같다.
  • 이름 : 문자열
  • 나이 : 숫자
다음과 같이 구조체를 정의할 수 있을 것이다.
struct userinfo
{
    char name[20];
    int  age;
};
나이는 100살을 넘기기 힘들 것이다. 그러므로 age 변수의 경우 short int로 정의를 할 수도 있을 것이다. short int는 2byte이므로 4byte의 age에 비해서 2byte의 크기를 절약할 수 있을것이라고 생각할 수 있다. 하지만 다른 여러가지 이유들 때문에, 꼭 메모리 크기를 절약할 수 있는 것은 아니다. 이에 대한 내용은 따로 기회가 되면 다루도록 하겠다. 우선은 그냥 int형으로 하겠다.

사용자 정보는 5개까지만 입력하도록 하겠다.
#include <stdio.h>
#include <string.h>

struct userinfo
{
  char name[20];
  int age;
};

int main(int argc, char **argv)
{
  int age;
  int i;
  char buf[40];
  struct userinfo myfriend[5];

  for (i = 0; i < 5; i++)
  {
    printf("Name : ");
    fgets(buf, 19, stdin);
    buf[strlen(buf)-1] = '\0';            // <--- 1
    sprintf(myfriend[i].name, "%s", buf);

    printf("Age  : ");
    fgets(buf, 19, stdin);
    age = atoi(buf);
    myfriend[i].age = age;
  }

  printf("=======================\n");
  for (i = 0; i < 5; i++)
  {
    printf("%12s : %d\n", myfriend[i].name, myfriend[i].age);
  }
}
fgets(3)은 키보드로 부터 문자열을 입력받기 위해서 사용하는 함수다. 1은 키보드로 입력된 개행문자를 제거하기 위해서 사용했다.

atoi(3) 함수는 문자열을 int형 값으로 변경하기 위해서 사용한다. 위의 예제는 나이를 숫자로 받아서 하는일이 없으니, 그냥 문자열 그대로 저장해도 상관은 없을 것이다. 그러나 나이를 가지고 비교한다던지 하는 숫자연산 작업이 있을 수 있으므로, 나중을 위해서 int 형으로 변환하는게 좋을 것이다.

다음은 테스트 결과다.
# ./userinfo
Name : yundream
Age  : 32
Name : kopete
Age  : 28
Name : dream
Age  : 31
Name : minsu
Age  : 29
Name : test
Age  : 32
=======================
    yundream : 32
      kopete : 28
       dream : 31
       minsu : 29
        test : 32

문제 위의 예제를 포인터를 사용하도록 수정해보자.

리스트

구조체 역시 일반 원시데이터타입과 마찬가지로 배열과 포인터를 이용해서 구조화 할 수 있음을 배웠다. 배열 혹은 포인터의 경우 메모리 상에 다음과 같이, 저장을 위한 공간이 만들어 질 것이다.

attachment:structarray.png

그러나 모든 데이터 타입을 원소로 가질 수 있다는 구조체의 특징은 배열보다 좀더 유연한 자료구조의 활용이 가능하게 한다. 링크드 리스트와 같은 자료구조의 활용이 가능해진다는 점이다.

배열 혹은 포인터를 이용해서 메모리를 할당하는 방식의 문제점에 대해서 생각해보도록 하자. 이 방식은 저장해야 하는 대상의 갯수를 알고 있을 때, 간단하면서도 효과적으로 사용할 수 있다. 그러나 그 크기를 알 수 없을 때에는 문제가 된다. 즉 다음과 같은 경우가 될 것이다.
  • 원소의 크기가 얼마가 될지 알수 없을 경우
명함첩 프로그램을 만들경우, 몇개의 명합을 위한 공간이 필요할지 예측하기가 힘들다. 수십개가 될 수도 있지만, 수천개가 될 수도 있다. 최대 수집가능한 명합의 갯수를 예상해서 배열을 충분히 크게 하는 방법도 있겠지만, 그럴 경우 너무 많은 메모리공간을 소비하게 된다. 또한 충분히 크게 잡았다고 해도, 공간을 초과해서 데이터가 들어올 수도 있다.
  • 중간에 데이터가 추가될 경우
100개의 데이터가 있는데, 새로추가된 데이터를 2번째 위치에 집어넣는 경우를 생각해보자. 유일한 방법은 98개의 데이터를 전부 한칸씩 뒤로 미룬다음에, 2번째 위치에 새로 추가된 데이터를 복사하는 수밖에 없다.

배열(혹은 포인터)를 이용해서 공간을 한꺼번에 할당하는 것은 너무 유연하지 못한 방법임을 알 수 있다.

그렇다면 리스트 형태로 하면 어떻게 될까. 그러니까 새로운 데이터가 들어올때마다. 데이터를 저장하기 위한 공간을 할당하는 방식이다. 만약 새로운 데이터가 추가되었고, 이를 위해서 메모리가 할당되었다면, 추가된 다음 데이터의 위치를 알고 있어야 할것이다. 우리는 포인터를 이용해서 데이터가 저장된 위치를 찾아낼 수 있음을 알고 있다. 그렇다면, 각각의 데이터가 다음 데이터의 위치를 가리킬 수 있도록 하면 될것이다. 예컨데, 구조체에 다음 저장된 데이터의 위치를 가리키는 포인터를 두는 것이다.

즉 다음과 같이 리스트형태로 만드는 것이다.

attachment:list.png

리스트는 배열에 비해서 다음과 같은 장점들을 가진다.
  1. 중간에 쉽게 데이터를 삽입할 수 있다.
데이터를 하나 생성하고, 포인터만 2번 변경해주면 된다.
  1. 메모리를 효율적으로 사용할 수 있다.
필요한 만큼만 메모리를 사용한다.
  1. 폭넓은 응용이 가능하다.
링크드리스트, 더블링크드 리스트, 환형 링크드 리스트, tree, graph 모든 고수준의 자료구조들이 리스트의 응용이다.

다음은 리스트에서 데이터를 삽입하는 방법을 보여준다. 매우 효율적으로 데이터를 삽입할 수 있음을 알 수 있다.

attachment:list_add.png

반면 배열에 비해서 사용하기가 좀 까다롭다는 단점을 가지는데, 리스트가 가지는 장점에 비할바는 아니다.

리스트 응용

그러면 위에서 다루었던 사용자 정보 관리 프로그램을 list(리스트)버전으로 바꿔보도록 하겠다. 구조체는 거의 비슷하지만, 다음 추가될 데이터의 주소를 저장해야 하므로, 포인터형 변수가 추가되어야 한다.
struct userinfo
{
  char name[20];
  int age;
  struct userinfo *NextItem;
};

다음은 완성된 프로그램이다. 약간 복잡하게 보일 수도 있지만, 몇번 실행시키면서 천천히 생각해보면 이해가 갈것이다.
#include <stdio.h>
#include <string.h>

struct userinfo
{
  char name[20];
  int age;
  struct userinfo *NextItem;
};

int main(int argc, char **argv)
{
  int age;
  int i;
  int ItemNum = 0;
  char buf[40];
  struct userinfo *myfriend;
  struct userinfo *first = NULL;  // 처음 포인터를 저장하기 위한 변수
  struct userinfo *prev = NULL; // 이전 포인터를 저장하기 위한 변수

  while(1)
  {
    printf("Name : ");
    fgets(buf, 19, stdin);
    buf[ strlen(buf)-1] = '\0';

    // strcmp는 문자열을 비교한다.
    // 두개의 문자열이 같다면 0을 리턴한다.
    // 사용자가 Name 에 quit를 입력하면 루프를 빠져나간다.
    if (strcmp(buf,"quit") == 0)
        break;
    else
        myfriend = (struct userinfo *)malloc(sizeof(struct userinfo)*1);

    sprintf(myfriend->name, "%s", buf);

    printf("Age  : ");
    fgets(buf, 19, stdin);
    age = atoi(buf);

    // 다음을 가리키는 원소가 없으므로
    // NextItem은 NULL 이된다.
    myfriend->age = age;
    myfriend->NextItem = NULL;

    // 만약에 이전 원소가 있다면,
    // 이전 원소에게 현재 원소의 포인터를 알려준다.
    if (prev != NULL)
        prev->NextItem = myfriend;

    // first 가 NULL 이라면
    // 최초입력되는 원소임을 알 수 있다.
    // 이 원소의 포인터를 저장한다.
    if (first == NULL)
        first = myfriend;

    // 현재 원소의 포인터값을 저장한다.
    prev = myfriend;
    ItemNum++;
  }
  printf("Item : %d\n", ItemNum);

  // first에는 최초입력된 원소의 포인터가 들어있다.
  myfriend = first;

  // NextItem이 NULL이 아닐때까지 루프를 돌면서
  // 원소를 출력한다.
  while(myfriend != NULL)
  {
    printf("%12s : %d\n",myfriend->name, myfriend->age);
    myfriend = myfriend->NextItem;
  }
}

union

공용체라고 부르기도 하는 union은 선언하는 방법 사용하는 방법이 구조체와 동일하다. 해서 때때로 헛갈리기도 하는데, 이런 이유로 union 자체를 아예 사용하지 않는 경우도 있다. 사실 union을 사용하지 않더라도, 프로그램을 작성하는데 큰 문제는 없다. 하지만 이런류의 문제가 그렇듯이 사용하지 않아도 문제는 없지만 사용하면 복잡한 문제를 좀더 쉽게 풀 수 있다. 그러니 일단은 알아두고 넘어갈 필요는 있을거 같다.

그렇다고 해서, 이둘의 차이가 이름에만 있는 것은 아니다. 공용체에 속한 멤버변수들은 동일한 메모리 공간을 사용한다는 점에 있어서 구조체와 분명한 차이를 보인다. 다음의 예를 보자.

먼저 구조체
struct data{
  int a; 
  char b[4];
};
이건 유니온
union data{
  int a;
  char b[4];
} 

이들은 structunion 이라는 것만 제외하고는 멤버변수의 크기까지 완전히 동일해 보이지만, 메모리 입장에서 보면 다음과 같이 분명한 차이를 보인다.

attachment:union.png

이제 sizeof 를 이용해서, 어느정도의 메모리가 할당되었는지를 확인해 보자.
#include <stdio.h>

struct stdata
{
    int a;
    char b[4];
};

union undata
{
    int a;
    char b[4];
};

int main()
{
    printf("st size : %d\n", sizeof(struct stdata));
    printf("un size : %d\n", sizeof(union undata));
}
실행시켜보면, struct stdata 는 4+4=8byte 의 크기를 차지하지만 union undata 는 4byte의 크기만을 차지하고 있음을 알수 있다. 예컨데, union은 각각의 멤버를 위한 공간을 할당방식이 아닌, 공통으로 사용할 메모리 공간을 할당하고, 하당 메모리 공간에 대한 다른 타입의 네이밍 규칙만 정하는 방식임을 알 수 있다. 4byte라는 공간을 할당해두고, 필요에 따라서 int 형으로 읽기도 하고 char 형으로 읽기도 한다는 얘기다.

굳이 이렇게까지 해서 얻을 수 있는 잇점이 어떤것이 있을지 알아보도록 하자.

형변환 작업을 없애준다. 동일한 데이터를 다양한 타입으로 볼수 있음으로, 형변환과 같은 귀찮은 과정을 생략할 수 있다. 예를 들어, int 형값을 char 형으로 4개씩 잘라서, 다시 읽는 경우를 생각해보자. 이런 경우는 충분히 발생할 수 있다. 인터넷주소(:12)는 32bit int형으로 표현될수 있다. 그렇지만 이것은 사람이 읽기에 매우 어렵기 때문에, 1byte씩 4자리로 끊어서 표현하는 방식을 사용한다. 만약 int형 인터넷 주소를 .으로 구분된 인터넷 주소로 변환하는 프로그램을 만들려고 한다면, 1byte씩 읽어들여서 표현하는 함수를 작성해야 할 것이다.

이러한 함수를 작성하는건 매우 어려운 작업이다. 그렇지만 union을 이용한다면, 간단하게 해결할 수 있다.
#include <stdio.h>

union undata
{
    int a;
    char b[4];
};

int main()
{
    int ipaddr = 91827319;
    union undata ipdata;

    ipdata.a = ipaddr;

    printf("%d.%d.%d.%d\n",ipdata.b[0], ipdata.b[1],
            ipdata.b[2], ipdata.b[3]);
}
아주 간단하게, 인터넷 주소 변환 프로그램이 만들어졌음을 알 수 있다.

눈치가 좀 빠르다면 union 이라는 것은 메모리영역을 하나 두고, 이 메모리영역을 가리키는 서로다른 타입의 포인터 변수를 선언한것과 비슷하다는 것을 알 수 있을 것이다. 위의 프로그램은 다음과 같이 변경할 수 있다.
#include <stdio.h>

int main()
{
    int ipaddr = 91827319;
    char *b;

    b = (char *)&ipaddr;

    printf("%d.%d.%d.%d\n", b[0], b[1], b[2], b[3]);
}

만약 어떤 동일한 데이터를 필요에 따라서, int, char, short int 형으로 다양하게 표현해야할 필요가 있다면, 복잡하게 포인터를 만들어서 형변환하거나 할 필요 없이, 다음과 같은 union 자료를 선언해서 사용하면 된다.
union mydata
{
    int a;
    short b[2];
    char c[4];
}
union 은 하나의 메모리영역만을 사용하는 방식이므로, 각 멤버들이 가지는 크기가 동일하도록 맞추어줄 필요가 있다. 기준은 멤버변수중에서 가장 큰 데이터 타입이 된다. 위의 경우 int 형이 가장 크기 때문에, 다른 데이터타입의 경우 배열을 이용해서 4byte가 되도록 적절히 선언을 해주어야 한다. char c[2] 와 같이 선언한다고 해서, 프로그램이 작동하지 않거나 하는 일은 없겠지만 뒤의 2바이트는 사용하지 못하게 될 것이다.

댓글

틀린부분 수정/추가해야할 부분이 있으면 코멘트 달아주세요.