8. 구조체
1. Intro
지금까지 우리는 변수를 사용해 데이터를 저장하고 반복문, 조건문을 이용해 데이터를 가공하며 함수를 이용해 코드를 있어보이게 다듬는 과정을 살펴보았다. 그 내용을 잘 이해하고 익숙해지면 본인이 필요한 간단한 프로그램을 만드는 것이 어렵지는 않을 것이다. 하지만 좀 더 많은 자료, 복잡한 자료를 우리가 다룬 변수와 배열만 사용해 처리하고자 한다면 코드가 상당히 복잡해질 것이다. 이러한 부분을 보완하기 위해, 어떤 구조를 가지고 있는 고차원의 변수를 생성할 수 있도록 존재하는 개념이 바로 구조체이다.
2. Struct
학생들의 성적을 관리하는 프로그램을 만들어 보자. 학생의 이름, 학번, 단과대학, 학부, 과목, 성적 정보를 입력하고, 어떤 학생의 학번을 입력했을 때 그 학생의 정보를 모두 출력하는 프로그램을 만드는 것이 목적이다. 간단하게 3명의 물리, 미적분, 자정진 성적만 관리하는 코드를 작성하자.
#include <stdio.h>
#define STD_AMT 3
int main()
{
int stdNum[STD_AMT];
char stdName[STD_AMT][100];
char depart[STD_AMT][100];
char college[STD_AMT][100];
char grade[STD_AMT][3][10];
int i, j;
for (i = 0; i < STD_AMT; i++) {
scanf("%d", &stdNum[i]);
scanf("%s", stdName[i]);
scanf("%s", depart[i]);
scanf("%s", college[i]);
for (j = 0; j < 3; j++) {
scanf("%s", grade[i][j]);
}
}
// student data saved
int number;
scanf("%d\n", &number);
printf("%d\n", stdNum[number]);
printf("%s\n", stdName[number]);
printf("%s\n", depart[number]);
printf("%s\n", college[number]);
printf("%s %s %s", grade[number][0], grade[number][1], grade[number][2]);
return 0;
}
대단히 직관적인 코드이다. 앞에서 해온 것 처럼 학생들의 정보를 입력받아 저장하고, 알고싶은 학생의 번호를 입력받아, 그 학생의 모든 정보를 출력했다. 다만 무엇인가 통일성이 없어보인다.
printf("%d\n", stdNum[number]);
printf("%s\n", stdName[number]);
printf("%s\n", depart[number]);
printf("%s\n", college[number]);
printf("%s %s %s", grade[number][0], grade[number][1], grade[number][2]);
한 학생의 정보가 stdNum, stdName, depart, college, grade에 나뉘어 저장되어 있다. 이렇게 해도 동작은 하겠지만, 아무래도 직관적이지 않고, 코드를 이해하고 수정하기 어렵다. 이렇게 특정 체계에 종속된 여러 정보를 체계적으로 저장, 관리하기 위해 사용하는 개념이 구조체이다. 구조체를 끼얹은 코드는 아래와 같다.
#include <stdio.h>
#include <string.h>
#define STD_AMT 3
struct Student
{
int number;
char name[100];
char depart[100];
char college[100];
char calcGrade[10];
char phyGrade[10];
char jjjGrade[10];
};
int main()
{
Student a;
Student b;
Student c;
printf("A\n");
scanf("%d", &a.number);
scanf("%s", a.name);
scanf("%s", a.depart);
scanf("%s", a.college);
scanf("%s %s %s", a.calcGrade, a.phyGrade, a.jjjGrade);
printf("B\n");
scanf("%d", &b.number);
scanf("%s", b.name);
scanf("%s", b.depart);
scanf("%s", b.college);
scanf("%s %s %s", b.calcGrade, b.phyGrade, b.jjjGrade);
printf("C\n");
scanf("%d", &c.number);
scanf("%s", c.name);
scanf("%s", c.depart);
scanf("%s", c.college);
scanf("%s %s %s", c.calcGrade, c.phyGrade, c.jjjGrade);
int number;
Student std;
scanf("%d\n", &number);
switch (number) {
case 0:
memcpy(&std, &a, sizeof(struct Student));
break;
case 1:
memcpy(&std, &b, sizeof(struct Student));
break;
case 2:
memcpy(&std, &c, sizeof(struct Student));
break;
}
printf("%d\n", std.number);
printf("%s\n", std.name);
printf("%s\n", std.depart);
printf("%s\n", std.calcGrade);
printf("%s %s %s", std.calcGrade, std.phyGrade, std.jjjGrade);
return 0;
}
의도적으로 반복문을 안쓰니 전보다 코드가 복잡해졌다. 반복문을 적용한 코드는 나중에 살펴보도록 하고 우선 구조체의 기본적인 구조부터 살펴보자.
struct [구조체 이름]
{
[멤버변수 자료형] [멤버변수 이름];
...
[멤버변수 자료형] [멤버변수 이름];
};
struct Student
{
int number;
char name[100];
char depart[100];
char college[100];
char calcGrade[10];
char phyGrade[10];
char jjjGrade[10];
};
구조체의 선언은 대단히 직관적이다. struct 키워드와 함께 구조체 이름을 붙이고 중괄호를 이용해 해당 구조체가 가질 값들을 선언한다. 구조체 내부에 선언되는 변수를 멤버변수라고 부른다.
이렇게 선언된 구조체는 새로운 형태의 자료형이라고 보아도 좋다. 그렇기 때문에 구조체의 형태를 가지는 변수는 아래와 같이 선언한다.
[구조체 이름] [구조체 변수 이름];
Student a;
Student b;
Student c;
일반적인 자료형의 변수를 선언하는 것 처럼 구조체 이름을 사용해 구조체 변수를 선언하면 된다.
이렇게 선언된 구조체 변수는 저마다 멤버변수를 가진다.
a는 a의 학번, 이름, 단과대, 학부, 미적성적, 물리성적, 자정진성적을 멤버변수로,
b는 b의 학번, 이름, 단과대, 학부, 미적성적, 물리성적, 자정진성적을 멤버변수로,
c는 c의 학번, 이름, 단과대, 학부, 미적성적, 물리성적, 자정진성적을 멤버변수로 갖는다. 이때 어떤 구조체변수의 멤버변수를 아래와 같이 가져올 수 있다.
[구조체변수].[멤버변수]
a.calcGrade // a의 미적 점수
a.name // a의 이름
b.number // b의 학번
b.dept // b의 단과대
c.college // c의 학부
c.jjjGrade // c의 자정진 점수
굉장히 직관적이다. "a 의 미적 점수"를 a.calcGrade처럼 [구조체변수]와 [멤버변수] 사이를 연결사 "의"의 역할을 하는 "."으로 연결해 사용한다.
구조체 변수로 배열을 선언할 수 있을까? 물론이다. 구조체 변수 배열도 일반적인 자료형의 배열을 선언하는 것과 같이 하면된다.
[구조체 이름] [구조체 변수 이름][[배열 크기]];
Student ap_ap[25];
Student ap_ap_1819[51];
Student yeol_Hyeol_Ban[273];
그럼 위의 코드를 배열을 사용해 아름답게 고칠 수 있다.
#include <stdio.h>
#include <string.h>
#define STD_AMT 3
struct Student
{
int number;
char name[100];
char depart[100];
char college[100];
char calcGrade[10];
char phyGrade[10];
char jjjGrade[10];
};
int main()
{
Student stds[STD_AMT];
int i;
for (i = 0; i < STD_AMT; i++) {
scanf("%d", &stds[i].number);
scanf("%s", stds[i].name);
scanf("%s", stds[i].depart);
scanf("%s", stds[i].college);
scanf("%s %s %s", stds[i].calcGrade, stds[i].phyGrade, stds[i].jjjGrade);
}
int number;
Student std;
scanf("%d\n", &number);
memcpy(&std, &stds[number], sizeof(struct Student));
printf("%d\n", std.number);
printf("%s\n", std.name);
printf("%s\n", std.depart);
printf("%s\n", std.calcGrade);
printf("%s %s %s", std.calcGrade, std.phyGrade, std.jjjGrade);
return 0;
}
구조체를 사용한 경우의 변수들과 사용하지 않은 경우의 변수들을 그림으로 나타내면 아래와 같다.
왼쪽은 구조체를 사용하지 않은 경우, 오른쪽은 구조체를 사용한 경우이다. 확실히 오른쪽이 "학생"을 중심으로 생각하였을 때, 더 체계적이다. 구조체는 현실 세계의 어떠한 '대상'을 프로그램에 옮길 때 유용하게 사용된다. 대상이 가지고 있는 요소를 직관적으로 표현하기 쉽기 때문이다. 위에서는 학생이라는 대상이 가지고 있는 요소인 학번, 이름, 성적등을 구조체를 통해 체계적으로 나타내었다. 그런데 경우에 따라 정말로 학번값만, 성적값만 모아야 할 경우가 있다. 그러한 경우에는 왼쪽이 더 체계적이라고 할 수 있다. 필요에 따라 적절한 자료형을 사용하는 것이 중요하다.
3. 구조체 변수 복사
일반적으로, 어떤 변수(a라 하자)에 있는 값을 동일한 자료형인 다른 변수(b라 하자)에 복사하는 구문은 아래와 같다.
b = a;
이 글을 쓰면서 알게 된 사실인데, 구조체 변수도 이와 같은 방식으로 복사 대입이 가능하다. 아래 코드를 보자.
struct Meaningful
{
int num1;
int num2;
};
int main()
{
Meaningful var1;
Meaningful var2;
var1.num1 = 10;
var1.num2 = 20;
var2 = var1;
printf("%d %d", var2.num1, var2.num2);
}
2개의 정수형 변수를 멤버변수로 갖는 의미심장한 Meaningful 구조체가 있다. Meaningful 구조체의 형태를 가지는 2개의 구조체 변수 var1과 var2가 있다. var1의 두 멤버변수에 각각 10과 20을 넣고 다음 줄을 실행한다.
var2 = var1;
그 후 var2의 멤버변수들을 출력하면 var1의 멤버변수들의 값과 동일한 값이 나오는 것을 볼 수 있다.
옛날옛적 자료라고 불리는 soen.kr에도 나오는 내용이 왜 코딩도장에 안나오는지 모르겠지만, 이러한 간단한 대입이 가능하기 때문에, 구조체 변수를 복사하기 위해 구조체의 모든 멤버변수를 각각 복사하는 번거로움을 맛보지 않아도 되는 것은 좋다.
두 군데 모두 등장하고 위의 코드에도 나오는 것이 포인터와 memcpy를 이용하는 방법이다. soen에는 위의 단순 대입이 이 방법과 동일한 기능을 한다고 하였는데, 명시적으로 구조체 변수를 복사한다는 것을 표현하기 위해 일부러 이러한 방법을 쓸 수도 있겠다고 생각한다.
memcpy(&var2, &var1, sizeof(Meaningful));
memcpy는 string.h에 등록된 함수로, 특정 주소부터(&var1) 특정 칸 수 만큼(sizeof(Meaningful))의 값을 특정 주소부터(&var2) 복사한다. 이를 그림으로 표현하면 아래와 같다.
위 그림이 정확한지는 잘 모르겠지만 아무튼 비슷한 느낌이다.
4. 멤버 함수
c에 oop 개념을 더하고 이것 저것 더해 탄생한, 비슷하지만 완전히 새로운 언어인 c++에서는 구조체의 멤버로 함수를 넣을 수 있다.
(보통 c++ 컴파일러가 c언어 또한 지원하기 때문에 둘을 혼합하여 사용할 수 있다. 하지만 c++에서 가능한 문법이 c에서 불가능한 경우가 종종 있으므로 만약 컴퓨터언어 관련된 수업에서 c언어로 시험을 볼 경우 c++문법도 사용 가능한지 담당 교수님께 꼭 여쭈어 보아야 한다.)
구조체의 멤버로 함수를 넣을 경우, 구조체 멤버 변수에 저장된 데이터를 보다 역동적으로 가공할 수 있다. 구조체는 어떤 대상의 요소를 멤버변수로 나타낸다고 했다. 비슷한 개념으로, 멤버 함수는 대상의 동작을 나타낸다.
예를 들어, 토끼와 거북이의 달리기 경주를 시뮬레이팅 하는 프로그램을 만든다고 하자. 우화 속의 토끼와는 다르게 여기의 토끼는 거북이와 마찬가지로 본인의 달리기에 항상 집중한다고 가정하자. 또한 토끼와 거북이가 스스로의 힘으로 내는 속도(기본 속도)는 일정하다고 가정한다. 토끼와 거북이가 경주하는 경기장이 입력으로 주어진다. 입력 조건은 아래와 같다.
1. 입력값은 한 칸의 길이와, 경기장을 나타내는 100개의 정수이며, 경기장의 입력값은 0이상 2이하의 값(0, 1, 2)이다.
2. 토끼와 거북이는 0번째 칸에서 출발하며, 101번째 칸이 종착지점이다.
3. 선수가 n번째 칸에 도착했을 때, n번째 칸을 지나가는 속도는 n번째 칸의 숫자에 따라 결정된다.
4. n번째 칸의 숫자가 1일 경우 토끼와 거북이는 n-1번째 칸을 지나가는 속도와 같은 속도로 해당 칸을 지나간다.
5. n번째 칸의 숫자가 2일 경우 토끼는 n-1번째 칸을 지나가는 속도의 2배의 속도로, 거북이는 4배의 속도로 해당 칸을 지나간다.
6. n번째 칸의 숫자가 0일 경우 선수는 기본 속도로 해당 칸을 지나간다.
7. 100번째 칸을 지나 101번째 칸에 도달한 경우 완주한 것으로 간주한다.
우리는 토끼와 거북이가 101번째 칸에 도착하는 시간을 각각 구해 출력하는 프로그램을 만들고 싶다.
#include <stdio.h>
int length;
int mapData[105];
struct Racer
{
int basicSpeed;
int prevSpeed;
double timeSpend;
int nowSpeed;
int multiValue;
void run(int map)
{
switch (map) {
case 0:
nowSpeed = basicSpeed;
break;
case 1:
nowSpeed = prevSpeed;
break;
case 2:
nowSpeed = prevSpeed * multiValue;
break;
}
timeSpend += length / nowSpeed;
prevSpeed = nowSpeed;
}
};
int main()
{
int i;
Racer rabbit, turtle;
rabbit.basicSpeed = 2; // 2m/s
rabbit.multiValue = 2;
turtle.basicSpeed = 1; // 1m/s
turtle.multiValue = 4;
scanf("%d", &length);
for (i = 0; i < 100; i++) {
scanf("%d", &mapData[i]);
}
for (i = 0; i < 100; i++) {
rabbit.run(mapData[i]);
turtle.run(mapData[i]);
}
printf("%lf %lf", rabbit.timeSpend, turtle.timeSpend);
return 0;
}
Racer 구조체에 run이라는 멤버 함수가 있다. 물론 이 함수를 밖으로 빼서 선언할 수도 있다. 그 코드는 아래와 같다.
#include <stdio.h>
int length;
int mapData[105];
struct Racer
{
int basicSpeed;
int prevSpeed;
double timeSpend;
int nowSpeed;
int multiValue;
};
Racer run(int map, Racer racer)
{
switch (map) {
case 0:
racer.nowSpeed = racer.basicSpeed;
break;
case 1:
racer.nowSpeed = racer.prevSpeed;
break;
case 2:
racer.nowSpeed = racer.prevSpeed * racer.multiValue;
break;
}
racer.timeSpend += length / racer.nowSpeed;
racer.prevSpeed = racer.nowSpeed;
return racer;
}
int main()
{
int i;
Racer rabbit, turtle;
rabbit.basicSpeed = 2; // 2m/s
rabbit.multiValue = 2;
turtle.basicSpeed = 1; // 1m/s
turtle.multiValue = 4;
scanf("%d", &length);
for (i = 0; i < 100; i++) {
scanf("%d", &mapData[i]);
}
for (i = 0; i < 100; i++) {
rabbit = run(mapData[i], rabbit);
turtle = run(mapData[i], turtle);
}
printf("%lf %lf", rabbit.timeSpend, turtle.timeSpend);
return 0;
}
둘을 비교해 보았을 때, 코드의 간결성과 코드의 의미의 측면에서 멤버함수가 가지는 이점을 알 수 있다.
우선 함수 내부에 존재하는 변수의 값은 함수 내부에서 선언되거나, 함수 호출시에 인자로 제공되어야 한다. 그렇기 때문에 2번째 코드에서, run 함수를 호출할 때 rabbit이나 turtle과 같은 인자를 추가로 제공해야 한다. 또한 함수 내부의 값은 함수가 종료되면 사라지기 때문에, 함수의 동작에 의한 멤버변수의 값의 변화를 저장하기 위해 run 함수에서 Racer 형 변수인 racer을 반환해 함수 호출부에서 반환값을 받아 원래 Racer형 변수인 rabbit과 turtle을 갱신해야 했다. 이렇게 하지 않으려면, rabbit과 turtle의 주소값을 넘겨주는 call by reference의 형식으로 함수를 호출하거나, rabbit과 turtle을 전역변수로 선언해야 한다.
이와 다르게, 멤버 함수는 구조체의 멤버 변수를 그대로 가져다 사용할 수 있기 때문에 함수 호출과 함께 구조체 변수의 멤버 변수의 값을 갱신할 수 있다. 덕분에 구조제 변수의 멤버 변수의 값을 일일이 바꾸어 주는 수고로움을 적게 할 수 있다.
둘째로, 코드의 의미를 보다 직관적이고 생동감있게 할 수 있다. rabbit.run()와 "토끼가 달린다"는 매우 유사한 문장 구조를 공유한다. 이렇게 프로그래밍 '언어'를 이용해 데이터의 주술 관계를 표현하면 익숙하면서도 직관적으로 코드를 작성할 수 있다.
구조체는 일반적인 자료형으로 표현하기 복잡한 데이터를 체계적으로 다룰 수 있게 도와준다는 점에서 필수는 아니지만 굉장히 필요한 개념이라 할 수 있다. 구조체를 사용하면 그렇지 않은 때 보다 고급스럽고 직관적인 코드를 작성할 수 있다. 또한 나중에 OOP의 개념을 이해하는데 도움이 될 것이다.
'Programming Language > basic C Language' 카테고리의 다른 글
5. 함수 (0) | 2018.10.06 |
---|---|
4. 제어문 - if, else if, else, switch~case (0) | 2018.10.06 |
3. 배열, 반복문 (while, for, do~while) (0) | 2018.10.06 |
2. 변수와 자료형과 연산자 (0) | 2018.10.06 |
1. 표준입출력 (0) | 2018.10.06 |