티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

구조체도 포인터를 가질 수 있고, 구조체를 배열로 만들 수도 있으며, 자기 자신을 호출하는 구조체도 만들 수 있습니다.

이번엔 이러한 구조체의 여러가지 활용법을 배웁니다.

 

또 구조체와 유사한 사용자 정의 자료형인 '공용체'와, 서로 연관된 상수를 모아 기호화된 이름으로 쓰는 '열거형'의 특징과 사용법을 배웁니다.

 

또 자료형 이름을 재정의하는 typedef에 대해서도 배웁니다.


구조체 변수는 그 안에 여러 개의 변수를 멤버로 가질 수 있으나 구조체 그 자체는 단지 하나의 변수입니다.

따라서 구조체 변수에 주소 연산자를 사용하면 특정 멤버의 주소가 아니라 구조체 변수 전체의 주소가 구해집니다.

또한 그 값을 저장할 때는 구조체 포인터를 사용합니다.

 

다음 예제를 통해 구조체 포인터의 사용법을 살펴봅니다

#include <stdio.h>

struct score
{
	int kor;
	int eng;
	int mat;
};

void main(void)
{
	struct score yuni = { 90, 80, 70 };
	struct score* pScore = &yuni;

	printf("국어 : %d\n", (*pScore).kor);
	printf("영어 : %d\n", pScore->eng);
	printf("수학 : %d\n", pScore->mat);
}

/*
실행결과

국어 : 90
영어 : 80
수학 : 70
*/
struct score* pScore = &yuni;

구조체 포인터를 선언합니다.

선언 방법은 여느 다른 변수의 포인터와 다르지 않습니다.

포인터의 자료형은 포인터에 주소를 저장할 변수의 자료형에 *을 붙여 완성합니다.

yuni의 자료형이 struct score이므로 이 구조체의 포인터 자료형은 struct score* 이 됩니다.

printf("국어 : %d\n", (*pScore).kor);
printf("영어 : %d\n", pScore->eng);
printf("수학 : %d\n", pScore->mat);

포인터를 이용해 그 안의 멤버를 사용하는 방법으로 두 가지가 사용되었습니다.

먼저 간접 참조 연산자 포인터에 사용해서 멤버를 사용할 수 있습니다.

두 번째로 ->연산자를 이용해 포인터가 저장하고 있는 주소에서 바로 멤버를 사용할 수도 있습니다.

 

간접 참조 연산자 ( * )보다 멤버 접근 연산자 ( . )의 우선순위가 더 높습니다.

따라서 간접 참조 연산보다 멤버 접근 연산이 더 먼저 수행됩니다.

그래서 포인터를 이용해 멤버에 접근할 때는 포인터에 간접 참조 연산자를 붙이고 반드시 괄호로 묶어 주어야 합니다.

만약 괄호를 하지 않으면 간접 참조 연산자는 해당 멤버 접근 연산 다음 수행되어 멤버에 포인터 연산을 하게 됩니다.

 

매번 괄호를 하는 것이 불편하다면 ->연산자를 이용할 수 있습니다.


만약 같은 형태의 구조체 변수가 많이 필요하다면 이 구조체를 배열의 형태로 선언할 수 있습니다.

주소록을 만드는 예제를 통해 구조체 배열의 사용법을 알아보겠습니다.

 #include <stdio.h>

struct address
{
	int age;
	char name[20];
	char tel[20];
	char addr[80];
};

void main(void)
{
	struct address list[5] = {
		{23, "홍길동", "111-1111", "울릉도 독도"},
		{35, "이순신", "222-2222", "서울 건천동"},
		{19, "장보고", "333-3333", "완도 청해진"},
		{15, "유관순", "444-4444", "충남 천안"},
		{45, "안중근", "555-5555", "황해도 해주"}
	};

	for (int i = 0; i < 5; i++)
	{
		printf("%10s%5d%15s%20s\n", list[i].name, list[i].age, list[i].tel, list[i].addr);
	}
}

/*
실행결과

홍길동  23    111-1111    울릉도 독도
이순신  35    222-2222    서울 건천동
장보고  19    333-3333    완도 청해진
유관순  15    444-4444      충남 천안
안중근  45    555-5555    황해도 해주
*/

구조체 배열을 사용함에 있어 어려움은 없습니다.

구조체 배열은 배열 요소가 구조체 변수일 뿐 여느 배열과 다르지 않습니다.


구조체 배열의 이름은 첫번째 배열요소의 주소입니다.

즉, 첫 번째 구조체의 주소와 같습니다.

 

따라서 구조체 배열의 이름을 인수로 받는 함수는 매개변수를 구조체 포인터로 선언해야 합니다.

예제를 통해서 살펴보겠습니다.

#include <stdio.h>

struct address
{
	int age;
	char name[20];
	char tel[20];
	char addr[80];
};

void printList(struct address* pAddress);

void main(void)
{
	struct address list[5] = {
		{23, "홍길동", "111-1111", "울릉도 독도"},
		{35, "이순신", "222-2222", "서울 건천동"},
		{19, "장보고", "333-3333", "완도 청해진"},
		{15, "유관순", "444-4444", "충남 천안"},
		{45, "안중근", "555-5555", "황해도 해주"}
	};

	printList(list);
}

void printList(struct address* pAddress)
{
	int i = 0;
	while (i < 5)
	{
		printf("%10s%5d%15s%20s\n", pAddress->name, pAddress->age, pAddress->tel, pAddress->addr);
		i++;
		pAddress++;
	}
}

/*
실행결과

    홍길동   23       111-1111         울릉도 독도
    이순신   35       222-2222         서울 건천동
    장보고   19       333-3333         완도 청해진
    유관순   15       444-4444           충남 천안
    안중근   45       555-5555         황해도 해주
*/
printList(list);

여기서 구조체 배열의 이름인 list를 인수로 주면서 printList 함수를 호출하였습니다.

구조체 배열의 이름 list는 구조체 배열의 첫 번째 구조체의 주소와 같습니다.

주소를 인수로 주었기 때문에 이를 포인터로 받아야 합니다.

void printList(struct address* pAddress);

따라서 함수의 매개변수는 위와 같이 구조체 포인터의 형태로 선언되었습니다.

printf("%10s%5d%15s%20s\n", pAddress->name, pAddress->age, pAddress->tel, pAddress->addr);

이후 printList 함수 내에서 구조체의 배열을 사용하기 위해 ->연산자를 사용하였습니다.


개별적으로 할당된 구조체 변수들을 포인터로 연결하면 관련된 데이터를 하나로 묶어 관리할 수 있습니다.

이때 '자기 참조 구조체'를 사용합니다.

자기 참조 구조체는 자신의 구조체를 가리키는 포인터를 멤버로 가집니다.

 

다음 예제를 통해 자기 참조 구조체의 의미와 활용법을 알아보겠습니다.

#include <stdio.h>

struct list
{
	int number;
	struct list * next;
};

void main(void)
{
	struct list a = { 10, 0 }, b = { 20, 0 }, c = { 30, 0 };
	struct list * head = &a, * current;

	a.next = &b;
	b.next = &c;

	printf("head->number : %d\n", head->number);
	printf("head->next->number : %d\n", head->next->number);

	printf("list all : ");
	current = head;

	while (current != NULL)
	{
		printf("%d ", current->number);
		current = current->next;
	}
	printf("\n");
}

/*
실행결과

head->number : 10
head->next->number : 20
list all : 10 20 30
*/
struct list
{
	int number;
	struct list * next;
};

우선 구조체 list가 선언되어 있습니다.

그리고 이 구조체는 자기 자신을 가리키는 포인터를 두 번째 멤버로 가지고 있습니다.

이 포인터 next에 같은 형태의 다른 구조체 변수의 주소를 저장하면

이 next를 통해 그 다른 구조체 변수의 멤버를 사용할 수 있게 됩니다.

 

struct list a = { 10, 0 }, b = { 20, 0 }, c = { 30, 0 };

같은 형태를 가지는 다른 구조체 변수 a, b, c를 각각 초기화합니다.

초기화 할때 두 번째 값으로 0을 주어 next에 아무 주소도 저장하지 않습니다.

a.next = &b;
b.next = &c;

이후 a 구조체 변수의 멤버 next에는 b 구조체 변수의 주소를 저장하고,

b 구조체 변수의 멤버 next에는 c 구조체 변수의 주소를 저장합니다.

이로써 a에서는 next를 통해 b에 접근할 수 있고, b에서는 next를 통해 c에 접근할 수 있습니다.

이후 실행 결과는 그것을 보여줍니다.

 

이렇게 구조체 변수를 포인터로 쭉 연결한 것을 연결 리스트(linked list)라고 합니다.

이 연결 리스트는 첫 번째 구조체 변수의 주소만 알면 나머지 구조체 변수는 포인터를 따라가 모두 사용할 수 있으므로

struct list * head = &a, * current;

위와 같이 첫 번째 구조체 변수의 주소를 head 포인터에 저장해두고 사용합니다.

만약 c 구조체 변수의 next에 a 구조체 변수의 주소를 저장하면 꼬리물기와 같이 무한 반복할 수 있을 것 같습니다.

 


 

공용체는 선언 방식이 구조체와 비슷하지만 저장 공간을 할당하는 방식은 전혀 다릅니다. 공용체는 모든 멤버가 하나의 저장 공간을 같이 사용합니다. 이로써 생기는 장단점이 있습니다. 예제를 통해 공용체의 장단점과 사용법을 살펴보겠습니다.

#include <stdio.h>

union student
{
	double grade;
	int num;
	char cj;
};

void main(void)
{
	union student s1;

	s1.num = 315;
	printf("학번 : %d\n", s1.num);

	s1.grade = 4.4;
	printf("학점 : %.1lf\n", s1.grade);
	printf("학번 : %d\n", s1.num);
}

/*
실행결과

학번 : 315
학점 : 4.4
학번 : -1717986918
*/

우선 공용체 변수의 크기는 멤버 중에서 크기가 가장 큰 멤버로 결정됩니다. student 공용체의 멤버 중에선 double의 크기가 8바이트로 가장 큽니다. 따라서 해당 공용체 변수의 크기는 8바이트가 됩니다.

 

공용체 변수는 이처럼 여러 개의 멤버를 가져도 전체 메모리 크기가 구조체와 달리 크지 않습니다.

하지만 하나의 저장 공간을 공유하기 때문에 한 번에 한 개의 멤버에만 데이터를 저장할 수 있습니다.

처음 학번을 315로 저장했지만 다시 학점을 4.4로 저장한 이후 다시 학번을 출력하려고 해도

학번에는 더 이상 값이 남아있지 않아 의도치 않은 다른 값이 출력되었습니다.

 


'열거형'의 선언 방식도 구조체와 비슷합니다.

그러나 열거형은 변수에 저장할 수 있는 정수값을 기호로 정의하여 나열합니다.

이게 무슨 말인지 예제를 통해 알아보겠습니다.

#include <stdio.h>

enum season { SPRING, SUMMER, FALL, WINTER };

void main(void)
{
	enum season season;
	char* pSports;

	season = SPRING;
	switch (season)
	{
	case SPRING:
		pSports = "inline";
		break;

	case SUMMER:
		pSports = "swimming";
		break;

	case FALL:
		pSports = "trip";
		break;

	case WINTER:
		pSports = "skiing";
		break;
	}

	printf("나의 레저 활동 : %s\n", pSports);
}

/*
실행결과

나의 레저 활동 : inline
*/
enum season { SPRING, SUMMER, FALL, WINTER };

열거형을 선언했습니다.

열거형을 선언하면 컴파일러는 열거형 안의 멤버들을 순서대로 0부터 차례로 하나씩 큰 정수로 치환합니다.

즉, SPRING = 0, SUMMER = 1, FALL = 2, WINTER = 3 으로 각각 치환됩니다.

 

각각 치환되는 번호를 바꿀 수도 있습니다. 

enum season { SPRING = 5, SUMMER, FALL = 10, WINTER };

다음과 같이 치환되는 값을 직접 설정할 경우,

그 이후에 나오는 멤버의 번호는 그보다 차례로 하나씩 큰 정수값이 됩니다.

즉, SUMMER는 6, WINTER는 11이 됩니다.

 

enum season season;

열거형 변수를 선언합니다.

열거형 변수의 크기는 int형과 같습니다. 


구조체, 공용체, 열거형의 이름은 항상 각각 struct, union, enum과 함께 써야 하므로 불편합니다.

특히 함수의 매개변수나 반환값의 형태에 쓰면 함수 원형이 복잡해집니다.

이때 typedef을 사용하면 자료형 이름에서 struct, union, enum을 생략할 수 있습니다.

예제를 통해 살펴보겠습니다.

#include <stdio.h>

struct student
{
	double grade;
	int num;
};
typedef struct student Student;

void printDataDef(Student *pStudent);
void printData(struct student* pStudent);

void main(void)
{
	Student s1 = { 4.2, 315 };
	struct student s2 = { 3.8, 316 };

	printDataDef(&s1);
	printData(&s1);
	
	printf("\n");

	printDataDef(&s2);
	printData(&s2);
}

void printDataDef(Student* pStudent)
{
	printf("학번 : %d\n", pStudent->num);
	printf("학점 : %.1lf\n", pStudent->grade);
}

void printData(struct student* pStudent)
{
	printf("학번 : %d\n", pStudent->num);
	printf("학점 : %.1lf\n", pStudent->grade);
}

/*
실행결과

학번 : 315
학점 : 4.2
학번 : 315
학점 : 4.2

학번 : 316
학점 : 3.8
학번 : 316
학점 : 3.8
*/

이 예제에서 먼저 눈여겨 볼 곳은 다음과 같습니다.

typedef struct student Student;

'typedef'을 이용해서 'struct student' 자료형과 똑같은 형식의 자료형인 'Student'를 만들었습니다.

쉽게 해석하자면

 

typedef(새로운 자료형을 만들겠다) struct student(새로 만들 자료형의 형식은 이것과 같다) Student(새로 만든 자료형의 이름은 이것이다)

 

typedef을 사용해 struct student와 같은 형식의 자료형을 만들었다고 해서 struct student 자료형이 사라지진 않습니다.

struct student 자료형과 같은 자료형을 하나 더 복사해서 만들었을 뿐 struct student자료형도 남아있기 때문에

이후 struct student 자료형와 Student 자료형 모두 같이 사용이 가능함을 확인할 수 있습니다.

 

만약 재정의하기 전의 자료형을 굳이 사용할 필요가 없다면 구조체의 형식 선언과 동시에 재정의할 수도 있습니다.

typedef struct
{
	double grade;
	int num;
} Student;

지금까지 배운 사용자 정의 자료형을 모두 활용하여 프로그램을 작성한 예제를 살펴보면서

구조체, 공용체, 열거형을 조화롭게 사용하는 방법을 배워보겠습니다.

 

다음 프로그램은 마켓에서 추첨하는 경품의 종류와 수량을 구조체로 묶어 입출력합니다.

경품의 종류는 열거형을 선언하여 멤버 이름을 사용하고,

경품에 따라 수량의 형태와 단위가 다르므로 수량을 저장하는 멤버는 공용체를 사용합니다.

#include <stdio.h>
#include <string.h>

typedef union
{
	double liter;
	double kg;
	int ea;
} Unit;

typedef struct
{
	Unit amount;
	enum {EGG = 1, MILK, MEAT} kind;
	char name[21];
} Gift;

void printList(Gift list);

void main(void)
{
	Gift list[5];
	
	for (int i = 0; i < 5; i++)
	{
		printf("이름 입력 : ");
		fgets(list[i].name, 21, stdin);
		list[i].name[strlen(list[i].name) - 1] = '\0';

		printf("품목 선택(1.계란, 2.우유, 3.고기) : ");
		scanf("%d", &list[i].kind);
		getchar();

		switch (list[i].kind)
		{
		case EGG:
			list[i].amount.ea = 30;
			break;
		case MILK:
			list[i].amount.liter = 4.5;
			break;
		case MEAT:
			list[i].amount.kg = 0.6;
			break;
		}
	}

	printf("# 세 번째 경품 당첨자...\n");
	printList(list[2]);
}

void printList(Gift list)
{
	printf("이름 : %s, 선택품목 : ",  list.name);
	switch (list.kind)
	{
	case EGG:
		printf("계란 %d개\n", list.amount.ea);
		break;
	case MILK:
		printf("우유 %.1lf리터\n", list.amount.liter);
		break;
	case MEAT:
		printf("고기 %.1lfkg\n", list.amount.kg);
		break;
	}
}

/*
실행결과

이름 입력 : 홍길동
품목 선택(1.계란, 2.우유, 3.고기) : 1
이름 입력 : 이순신
품목 선택(1.계란, 2.우유, 3.고기) : 2
이름 입력 : KOEY
품목 선택(1.계란, 2.우유, 3.고기) : 3
이름 입력 : 유관순
품목 선택(1.계란, 2.우유, 3.고기) : 2
이름 입력 : 안중근
품목 선택(1.계란, 2.우유, 3.고기) : 1
# 세 번째 경품 당첨자...
이름 : KOEY, 선택품목 : 고기 0.6kg
*/

예제를 살펴보겠습니다.

 typedef union
{
	double liter;
	double kg;
	int ea;
} Unit;

공용체의 형식 선언과 동시에 재정의를 하였습니다.

공용체의 멤버 중 가장 큰 크기의 자료형이 double이므로 이 공용체의 크기는 8바이트입니다.

 

typedef struct
{
	Unit amount;
	enum {EGG = 1, MILK, MEAT} kind;
	char name[21];
} Gift;

구조체의 형식 선언과 동시에 재정의를 하였습니다.

멤버들을 나열한 순서는 메모리 크기의 내림차순입니다.

Unit은 8바이트, 그 밑의 열거형은 4바이트, 마지막 이름을 저장할 char 배열은 각각의 요소가 1바이트입니다.

이 구조체의 메모리 크기는 40바이트입니다.

 

구조체 안에서 Unit 공용체를 멤버로 가지기 때문에 Unit 공용체의 형식 선언이 구조체보다 더 먼저 이루어졌습니다.

 

또 구조체 안에는 열거형을 또 멤버로 가집니다.

그런데 이 열거형의 선언(?)이 조금 특이합니다.

지금까지 배운대로 정석을 따른다면 다음과 같이 선언하고 사용할 수도 있습니다.

typedef enum {EGG = 1, MILK, MEAT} Kind;

typedef struct
{
	Unit amount;
	Kind kind;
	char name[21];
} Gift;

그런데 어떤 원리로

typedef struct
{
	Unit amount;
	enum {EGG = 1, MILK, MEAT} kind;
	char name[21];
} Gift;

이렇게도 사용할 수 있는 것인지는 저도 잘 모르겠습니다.

형태만 보자면 typedef만 빠졌지 재정의와 같은 형태를 취하고 있습니다.

열거형만 이러한 선언이 가능한 것인지, 아니면 구조체에 적용되는 typedef가 멤버인 열거형에게도 같이 적용이 될 수 있는 것인지는 저도 잘 모르겠습니다. 제가 공부하는 책에도 이에 관한 내용은 나와있지 않습니다.

그래서 몇 가지 실험을 진행해봤습니다.

struct gift
{
	Unit amount;
	enum { EGG = 1, MILK, MEAT } kind;
	char name[21];
};

먼저, 위와 같이 재정의를 하지 않고 안에 열거형을 그대로 선언했습니다.

struct gift list[5];

이후 위와 같이 구조체 배열을 만든 후 코드를 사용해봤는데 정상적으로 작동하고 있습니다.

typedef가 없어도 정상적으로 프로그램이 실행되는 것으로 보아 재정의와는 연관이 없어 보입니다.

아마 구조체 안에서 열거형을 새로 정의한 것이 아닌가 생각이 됩니다.

enum { EGG = 1, MILK, MEAT }

이게 열거형의 형식 선언과 동시에 자료형으로 사용하는 모습입니다.

enum { EGG = 1, MILK, MEAT } kind;

이후 열거형 변수명 kind를 사용하여 열거형 변수선언을 한 모습입니다.

열거형 변수 선언과 동시에 구조체의 멤버로서 사용할 수 있게 되었습니다.

 

그럼 열거형 뿐만 아니라 다른 사용자 정의 자료형 모두

구조체 안에서 선언과 동시에 변수로, 멤버로 사용이 가능한 것일까요?

struct gift
{
	union {
		double liter;
		double kg;
		int ea;
	} amount;
	enum { EGG = 1, MILK, MEAT } kind;
	char name[21];
};

구조체 안에서 공용체 변수 amount를 선언한 모습입니다.

union {
		double liter;
		double kg;
		int ea;
	}

해당 부분이 공용체의 선언과 동시에 자료형으로 사용하는 부분이며

union {
		double liter;
		double kg;
		int ea;
	} amount;

변수명으로 사용할 amount를 뒤에 붙여줌으로써 공용체 변수 선언과 동시에 구조체의 멤버로 선언되었습니다.

프로그램은 정상적으로 실행되었습니다.

이로써 구조체 안에서 공용체와 열거형의 선언과 동시에 멤버로서 사용이 가능함을 확인하게 되었습니다.

구조체 안에 구조체를 똑같이 선언하고 멤버로 사용하는 것도 같은 원리로 가능할 것이라 예상됩니다.

검증하는 과정은 따로 거치지 않겠습니다.

 

다음 시간엔 사용자 정의 자료형을 활용하는 실전 문제를 풀어보도록 하겠습니다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함