1 원시데이터 타입
C 언어는 매우 기본적으로 사용하는 5가지 정도의 원시 데이터 타입이라는 것을 가지고 있다는 것을 앞서 배웠다. 이들 기본 타입은 다음과 같은 것들이다.- int, float, double, char, long long int, Pointer
2 원시데이터 타입의 구조화
인간이 다루는 데이터로 보자면, 숫자,문자,도형만 있어도 모든 정보를 다룰 수 있기는 하다. 그렇지만 너무나 비효율적이다. 그래서, 이들 데이터 타입을 구조화해서 새로운 데이터 타입을 만들어서 사용하게 된다. 예를 들자면, 주소 정보를 관리하기 위해서 주소록을 만들고, 개인신상관리를 위해서 신상카드를 만들어서 사용하는 것이다. 이렇게 구조화하게 되면, 정보를 훨씬 깔끔하게 다룰 수 있게 된다.만약 유저정보를 관리할 목적이라면, 아래와 같이 데이터를 구조화 할 수 있을 것이다.
+--- User Info -----------------------+Text와 Number만으로 유저정보 관리를 위한 User Info라는 새로운 데이터 타입을 만들었다.
| Name : Text |
| Age : Number |
| Address : Text |
| Email : Text |
| Home : Text |
+--------------------------------------+
3 구조체
C언어도 원시데이터 타입을 구조화해서 새로운 데이터 타입을 만들 수 있도록 지원하고 있다. 이것을 우리는 구조체(Structure)라고 한다. 구조체는 다음과 같은 방식으로 만들 수 있다.struct 구조체이름
{
데이터타입 변수명;
데이터타입 변수명;
데이터타입 변수명;
};
위에서 예로 들었던, 유저정보를 구조체로 만들어 보도록하자. 이름은 문자열이 들어가게 되므로 char의 배열이나 포인터형식으로 선언해야 할 것이다. 포인터는 좀 귀찮으니, 모든 문자열은 char의 배열로 하도록 하겠다. 나이는 int형으로 하면 될것이고, 주소, 이메일, 홈페이지는 모두 char 배열로 하면 문제없을 것이다.
struct userInfo구조체는 내부적으로 자신이 사용할 변수들을 유지하게 되는데, 이러한 변수를 멤버변수라고 한다.
{
char name[12];
int age;
char address[80];
char email[40];
char home[40];
};
4 구조체의 정의, 선언 그리고 사용
구조체는 원시데이터 타입을 요소로 가지는 사용자 정의 데이터타입으로 볼 수 있다. 그러므로 다른 원시데이터 타입과 마찬가지로 선언해서 사용하면 된다. 그러나 사용자 정의 데이터 타입이기 때문에, 구조체의 구조를 먼저 정의해줘야 한다. 인사기록 카드를 만들려면, 카드에 어떤 내용이 들어가야 하는지를 먼저 정의해야 하는것과 마찬가지다.구조체의 정의는 위에서 이미 설명한바가 있다. 이제 정의를 하는 위치가 문제가 되는데, 구조체는 프로그램 전체에서 선언되고 사용될 수 있으므로, 글로벌영역에서 정의가 된다. 예를 들자면 아래와 같다.
// 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 구조체와 배열
어렵게 생각할 필요는 없다. 구조체도 데이터 타입이므로, 다른 원시 데이터처럼 배열을 이용해서 동일하게 구조화할 수 있다. 만약 유저정보를 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");
아주 간단하다.
6 구조체와 포인터
배열과 포인터는 메모리 상에서 근본적으로 동일한 구조를 가진다는 것을 배웠다. 구조체를 배열로 다룰 수 있으니, 마찬가지로 포인터로도 다룰 수 있으며, 사용하는 방법도 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);
}
7 예제 프로그램
그럼 간단한 예제 프로그램을 만들어 보도록하자. 이 프로그램은 사용자 정보를 입력받아서 출력하는 일을 한다. 입력받는 정보는 다음과 같다.- 이름 : 문자열
- 나이 : 숫자
struct userinfo나이는 100살을 넘기기 힘들 것이다. 그러므로 age 변수의 경우 short int로 정의를 할 수도 있을 것이다. short int는 2byte이므로 4byte의 age에 비해서 2byte의 크기를 절약할 수 있을것이라고 생각할 수 있다. 하지만 다른 여러가지 이유들 때문에, 꼭 메모리 크기를 절약할 수 있는 것은 아니다. 이에 대한 내용은 따로 기회가 되면 다루도록 하겠다. 우선은 그냥 int형으로 하겠다.
{
char name[20];
int age;
};
사용자 정보는 5개까지만 입력하도록 하겠다.
#include <stdio.h>fgets(3)은 키보드로 부터 문자열을 입력받기 위해서 사용하는 함수다. 1은 키보드로 입력된 개행문자를 제거하기 위해서 사용했다.
#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);
}
}
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
문제
위의 예제를 포인터를 사용하도록 수정해보자.
8 리스트
구조체 역시 일반 원시데이터타입과 마찬가지로 배열과 포인터를 이용해서 구조화 할 수 있음을 배웠다. 배열 혹은 포인터의 경우 메모리 상에 다음과 같이, 저장을 위한 공간이 만들어 질 것이다.
그러나 모든 데이터 타입을 원소로 가질 수 있다는 구조체의 특징은 배열보다 좀더 유연한 자료구조의 활용이 가능하게 한다. 링크드 리스트와 같은 자료구조의 활용이 가능해진다는 점이다.
배열 혹은 포인터를 이용해서 메모리를 할당하는 방식의 문제점에 대해서 생각해보도록 하자. 이 방식은 저장해야 하는 대상의 갯수를 알고 있을 때, 간단하면서도 효과적으로 사용할 수 있다. 그러나 그 크기를 알 수 없을 때에는 문제가 된다. 즉 다음과 같은 경우가 될 것이다.
- 원소의 크기가 얼마가 될지 알수 없을 경우
명함첩 프로그램을 만들경우, 몇개의 명합을 위한 공간이 필요할지 예측하기가 힘들다. 수십개가 될 수도 있지만, 수천개가 될 수도 있다. 최대 수집가능한 명합의 갯수를 예상해서 배열을 충분히 크게 하는 방법도 있겠지만, 그럴 경우 너무 많은 메모리공간을 소비하게 된다. 또한 충분히 크게 잡았다고 해도, 공간을 초과해서 데이터가 들어올 수도 있다.
- 중간에 데이터가 추가될 경우
100개의 데이터가 있는데, 새로추가된 데이터를 2번째 위치에 집어넣는 경우를 생각해보자. 유일한 방법은 98개의 데이터를 전부 한칸씩 뒤로 미룬다음에, 2번째 위치에 새로 추가된 데이터를 복사하는 수밖에 없다.
그렇다면 리스트 형태로 하면 어떻게 될까. 그러니까 새로운 데이터가 들어올때마다. 데이터를 저장하기 위한 공간을 할당하는 방식이다. 만약 새로운 데이터가 추가되었고, 이를 위해서 메모리가 할당되었다면, 추가된 다음 데이터의 위치를 알고 있어야 할것이다. 우리는 포인터를 이용해서 데이터가 저장된 위치를 찾아낼 수 있음을 알고 있다. 그렇다면, 각각의 데이터가 다음 데이터의 위치를 가리킬 수 있도록 하면 될것이다. 예컨데, 구조체에 다음 저장된 데이터의 위치를 가리키는 포인터를 두는 것이다.
즉 다음과 같이 리스트형태로 만드는 것이다.

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

반면 배열에 비해서 사용하기가 좀 까다롭다는 단점을 가지는데, 리스트가 가지는 장점에 비할바는 아니다.
9 리스트 응용
그러면 위에서 다루었던 사용자 정보 관리 프로그램을 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;
}
}
이 문서는 수정될 수 있습니다. 최신문서는 Joinc Wiki 에서.
:::
이글과 관련된 글