티스토리 뷰
주의 사항!
- 이 일지는 작성하고 있는 현시점, 공부와 병행하면서 작성되고 있습니다.
- 공부 중에 떠오른 생각이나 그때그때의 개념정리 같은 내용이 포함됩니다.
- 따라서 이 일지의 내용은 제가 공부하고 이해한 대로 작성되기 때문에 실제 사실과는 다를 수 있습니다
이번 시간은 '사용자 정의 자료형'에 관한 내용입니다.
변수가 많이 필요하면 주로 배열을 사용합니다.
하지만 변수들의 자료형이 다 다르다면 배열을 만들 수 없습니다.
그럴 때는 '구조체'를 사용합니다.
구조체가 무엇인지 바로 예제를 통해 확인해보겠습니다.
#include <stdio.h>
struct student
{
int num;
double grade;
};
void main(void)
{
struct student s1;
s1.num = 2;
s1.grade = 2.7;
printf("학번 : %d\n", s1.num);
printf("학점 : %.1lf\n", s1.grade);
}
/*
실행결과
학번 : 2
학점 : 2.7
*/
저는 게임을 만들고 싶어서 유니티 스크립트를 다루면서 C#언어를 조금 다룬 적이 있습니다.
거기에는 '클래스'라고 불리는 것이 있습니다.
제 생각에 이 클래스는 바로 이 C언어의 '구조체'를 다른 말로 부르는 것 같습니다.
코드를 하나하나 살펴보겠습니다
struct student
{
int num;
double grade;
};
여기서 구조체의 형식을 선언합니다.
구조체의 이름은 student이며 구조체라는 것을 알리기 위해 앞에 struct를 붙여줍니다.
그리고 중괄호로 묶인 부분에는 학생의 학번을 저장하는 int형의 num와
학생의 학점을 저장하는 double형의 grade가 선언되어 있습니다.
이로써 student라는 구조체 안에 int형 변수를 저장할 공간과 double형 변수를 저장할 공간이 마련되었습니다.
여기까지를 '구조체의 형식 선언'이라고 합니다.
형식 선언 안에서 선언된 변수들 num와 grade는 이 구조체의 '멤버'라고 부릅니다.
직접 만든 함수를 사용하기 위해서는 코드 윗줄에 함수의 형식을 선언해 주어야 했습니다.
구조체도 마찬가지 입니다.
직접 만든 구조체를 사용하기 위해서는 코드 윗줄에 구조체의 형식을 선언해 주어야 합니다.
struct student s1;
구조체를 사용하기 위해 변수를 선언했습니다.
구조체는 앞서 형식을 선언해주어야 하는 것에서 함수와 비슷했지만
함수와는 달리 구조체를 사용하기 위해서는 변수 선언을 해주어야 합니다.
student라는 이름의 구조체는 학생에 대한 정보를 담고 있고, 그 정보에는 학번과 학점이 있습니다.
그러면 이제 그 학생이 누구인지 선언을 해주는 것이 변수 선언에 해당합니다.
위 코드는 s1이라는 변수명을 사용하여 's1'이라는 학생의 정보를 다룰 것임을 선언합니다.
이때 s1 앞에 struct student는 하나의 자료형 역할을 하게 됩니다.
정수형 변수를 선언할 때는 int, 실수형 변수를 선언할 때는 double을 사용했던 것과 마찬가지로
si이라는 변수에 학생에 관한 정보를 저장하고자 할 때 앞서 만들었던 struct student를 자료형으로 사용합니다.
s1.num = 2;
s1.grade = 2.7;
이후 변수를 저장하는 부분입니다.
s1이라는 학생의 학번은 2다;
s1이라는 학생의 학점은 2.7이다;
라고 하면서 변수를 저장합니다.
s1과 num 사이에 온점( . )은 '멤버 접근 연산자'라고 하는데 그냥 '~의' 라고 생각하면 좋을 것 같습니다.
구조체의 형식 선언에서 멤버들을 선언할 때는 한 가지 고려하면 좋을 점이 있습니다.
멤버들은 자료형의 크기 순서대로 차례로 정렬해주는 것이 좋습니다.
같은 멤버들을 선언한다고 해도 자료형의 크기를 고려하지 않고 아무렇게나 선언한다면
구조체의 메모리 크기는 더 커질 수 있습니다.
구조체가 가지는 멤버는 동일한데 선언 순서에 따라서 크기가 달라진다니 무슨 말일까요?
이는 '패딩바이트'라고 하는 것과 관련이 있습니다.
모든 시스템은 메모리를 빠르게 읽고 쓰기 위해 메모리를 일정 단위로 쪼개서 보는 것을 좋아합니다.
그런데 구조체는 그 안의 멤버들의 자료형이 다 다를 경우 메모리를 일정 단위로 쪼개서 보는 것이 어렵습니다.
어떤 구조체의 멤버가 char형, int형, double형 하나씩 차례로 선언되어 있다고 가정하겠습니다.
이것을 시스템이 하나씩 읽어갈 때,
처음 char형을 읽으면 시스템은 '아, 1바이트 단위로 정리하면 되겠다'고 생각합니다.
그리고 그 다음 int형을 읽으면 int형은 4바이트기 때문에 1바이트 단위로 정리했다간 데이터가 손실될 수 있습니다.
따라서 시스템은 '4바이트 단위로 정리하자'고 생각합니다.
그런데 앞서 1바이트인 char형 하나만 저장해놓은 상태이기 때문에 이대로 int형을 이어 붙이게 되면
총 공간은 5바이트가 되고 이걸 4바이트 단위로 쪼개서 읽었다간 int형 데이터는 손실됩니다.
그래서 이 단위를 맞추기 위해 시스템은 앞서 저장했던 char형 뒤에 3바이트의 별도의 메모리를 추가합니다.
3바이트를 추가함으로써 총 4바이트가 되었고, 이제 int형을 이어 붙일 수 있게 되었습니다.
여기서 단위를 맞추기 위해 추가한 3바이트를 '패딩바이트'라고 부릅니다.
택배 상자에 크기가 서로 다른 물건들을 넣을 때 빈 공간에 넣는 '에어캡'과 같다고 보면 이해가 쉽습니다.
이어서 double형을 읽는 경우도 생각해보겠습니다. double형은 8바이트의 크기를 가집니다.
앞서 int형을 저장하면서 메모리 단위를 4바이트로 생각했지만
double형을 4바이트 단위로 읽었다간 데이터가 손실됩니다. 따라서 시스템은 단위를 8바이트 단위로 수정합니다.
그리고 지금까지 저장된 메모리를 살펴보면 char형1개, 3바이트의 패딩바이트, 4바이트의 int형이 저장되어 있어
딱 8바이트인 상황입니다.
여기에 bouble형을 그대로 이어 붙여도 총 16바이트가 됩니다.
8 바이트 단위로 읽어도 double형의 데이터가 손실될 일은 없습니다.
따라서 별도의 패딩바이트를 추가하지 않아도 됩니다.
int형 뒤에 바로 double형을 저장하고 총 바이트는 16바이트가 되었습니다.
위의 예에서 실제 데이터를 저장하고 있는 메모리 공간은 총 13바이트이고, 패딩바이트가 3바이트를 차지합니다.
만약 멤버의 선언 순서가 반대로 double형부터 int, chat형 순서로 선언되었다면 어떨까요?
시스템은 먼저 double형을 저장하면서 메모리 단위를 8바이트로 생각합니다.
그리고 int형을 이어 붙이면 총 12바이트가 됩니다.
이를 8바이트 단위로 읽어도 int형의 데이터는 손실되지 않기 때문에 패딩바이트 없이 그대로 저장합니다.
그리고 이어서 char형을 이어 붙이면 총 13바이트가 됩니다.
이를 8바이트 단위로 읽어도 char형의 데이터는 손실되지 않기 때문에 패딩바이트 없이 그대로 저장합니다.
이제 더 이상 저장할 멤버가 없습니다.
총 메모리 크기는 13바이트가 되었지만 8바이트 단위로 나뉘어 떨어지지 않아 모양이 이쁘지 않습니다.
여기에 3바이트의 패딩바이트를 추가해 모양을 이쁘게 잡아줍니다.
이로써 이 구조체의 총 메모리 크기는 16바이트가 되었고 패딩바이트가 3바이트 차지하게 됩니다.
위와 같이 구조체의 멤버를 정렬할 때는 크기가 커지는 순서나 작아지는 순서나 상관없이
하나 편한 순서를 선택해서 정렬해주면 됩니다.
하지만 멤버가 정렬되지 않고 중구난방이 되면 어떨까요?
char, double, int 순서로 저장한다고 가정해보겠습니다.
그렇게 되면 구조체의 전체 메모리 구조는 다음과 같이 됩니다.
char 1바이트, 패딩바이트 7바이트, double 8 바이트, int 4바이트, 패딩바이트 4바이트
따라서 총 메모리 크기는 24바이트가 되며 이 중 절반가량인 11바이트를 쓸모없는 패딩바이트가 차지합니다.
위의 예처럼 구조체 안에서 멤버를 선언할 때는 자료형의 크기 순서대로 선언해주어야
구조체의 메모리 크기가 최소가 되며, 메모리 관리가 더 효율적이게 됩니다.
이를 한 번 예제를 통해 확인해보겠습니다.
#include <stdio.h>
struct strt1
{
char ch1;
double db1;
int in1;
};
struct strt2
{
char ch1;
int in1;
double db1;
};
struct strt3
{
double db1;
int in1;
char ch1;
};
void main(void)
{
struct strt1 strt1;
struct strt2 strt2;
struct strt3 strt3;
printf("%d\n", sizeof(strt1));
printf("%d\n", sizeof(strt2));
printf("%d\n", sizeof(strt3));
}
/*
실행결과
24
16
16
*/
그런데 이것 외에 다른 방법으로 구조체의 메모리를 최소화할 수도 있습니다.
#pragma pack(1);
위와 같이 컴파일러에 패딩 바이트를 넣지 않도록 지시할 수 있습니다.
하지만 시스템이 메모리를 읽고 쓰는 과정이 비효율적이게 되므로, 메모리를 읽고 쓰는 시간은 더 길어집니다.
구조체는 그 멤버로 배열, 포인터는 물론이고 이미 선언된 다른 구조체까지도 사용할 수 있습니다.
다음은 배열과 포인터를 멤버로 갖는 구조체의 사용 예제입니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct profile
{
double height;
int age;
char* intro;
char name[20];
};
void main(void)
{
struct profile yuni;
strcpy(yuni.name, "KOEY");
yuni.age = 17;
yuni.height = 164.5;
yuni.intro = (char*)malloc(sizeof(char) * 80);
if (yuni.intro == NULL)
{
printf("메모리가 부족합니다.\n");
exit(1);
}
printf("자기소개 : ");
fgets(yuni.intro, 80, stdin);
yuni.intro[strlen(yuni.intro) - 1] = '\0';
printf("이름 : %s\n", yuni.name);
printf("나이 : %d\n", yuni.age);
printf("키 : %.1lf\n", yuni.height);
printf("자기소개 : %s\n", yuni.intro);
free(yuni.intro);
}
/*
실행결과
자기소개 : hello world, my name is KOEY!
이름 : KOEY
나이 : 17
키 : 164.5
자기소개 : hello world, my name is KOEY!
*/
예제를 한 번 살펴보겠습니다.
struct profile
{
double height;
int age;
char* intro;
char name[20];
};
먼저 구조체의 형식 선언입니다.
구조체의 멤버들을 보면 자료형의 크기가 큰 것부터 작은 것 순서로 정렬되어 있습니다.
참고로 char*, int*, double* 과 같은 포인터의 자료형은 크기가 모두 4바이트입니다.
그리고 name같은 경우 char자료형의 배열의 형태로 이루어져 있어 그 크기가 총 20바이트에 해당하지만
배열은 각각의 요소들을 하나씩 저장하기 때문에 20개의 char로 보면 됩니다.
나머지는 앞에서도 다루었던 부분으로 어려움이 없을 것으로 보입니다.
이제 다른 구조체를 멤버로 갖는 구조체를 사용하는 예제를 살펴보겠습니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct profile
{
double height;
int age;
};
struct student
{
struct profile profile;
double grade;
int id;
};
void main(void)
{
struct student yuni;
yuni.profile.age = 17;
yuni.profile.height = 164.5;
yuni.id = 315;
yuni.grade = 4.3;
printf("나이 : %d\n", yuni.profile.age);
printf("키 : %.1lf\n", yuni.profile.height);
printf("학번 : %d\n", yuni.id);
printf("학점 : %.1lf\n", yuni.grade);
}
/*
실행결과
나이 : 17
키 : 164.5
학번 : 315
학점 : 4.3
*/
어려운 건 없습니다.
구조체 안에서 구조체 변수 선언하듯이 똑같이 멤버로 선언해주시면 됩니다.
그리고 구조체 안의 구조체의 멤버를 사용하기 위해 멤버 접근 연산자를 두 번 사용하면서
차례로 접근해가는 것을 알 수 있습니다.
구조체 안에 멤버로서 구조체를 넣기 위해서는 멤버로 넣을 구조체가 보다 먼저 형식 선언되어 있어야 합니다.
student 구조체 안에 멤버로서 profile 구조체를 넣을 것이므로
profile 구조체가 student 구조체 보다 먼저 형식 선언될 수 있게 합니다.
구조체의 멤버로 구조체가 들어가 있을 때 그 구조체의 크기는 어떻게 정해질까요?
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct strt1
{
char ch;
short sh1;
short sh2;
int in;
};
struct strt2
{
short sh3;
struct strt1 strt1;
};
struct strt3
{
short sh1;
char ch1;
short sh2;
short sh3;
int in1;
};
void main(void)
{
struct strt1 strt1;
struct strt2 strt2;
struct strt3 strt3;
printf("%u\n", sizeof(strt1));
printf("%u\n", sizeof(strt2));
printf("%u\n", sizeof(strt3));
}
/*
실행결과
12
16
12
*/
구조체 strt1의 크기는 12바이트이고 메모리를 읽는 단위는 4바이트입니다.
그 다음 구조체 strt2에는 구조체strt1을 멤버로 주었습니다.
이 때 멤버로 들어간 구조체의 크기는 12바이트가 아닌 4바이트로 계산하면 편합니다.
이유는 배열과 비슷합니다. 구조체 strt1의 단위가 4바이트고, 이런 단위가 3개 연속 있다고 생각하면 됩니다.
앞서 char[20]배열은 총 크기가 20바이트지만 구조체의 멤버로서 메모리에 저장될 때는 각 요소 하나하나씩 저장되어
크기가 1바이트인 char 20개와 같다고 보는 것과 비슷한 원리입니다.
그 증거로 구조체 strt2에는 구조체 strt1을 멤버로 주고,
구조체 strt3는 strt1를 멤버로 넣지 않고 strt1의 멤버를 그대로 가져와 넣어보았습니다.
메모리에 저장되는 멤버들은 모두 같지만 strt2와 strt3의 크기를 비교한 결과 다르게 계산됨을 알 수 있습니다.
다음은 구조체의 멤버들을 초기화 하는 방법에 대해 알아보겠습니다.
최고 학점을 받은 학생의 데이터를 출력하는 예제입니다.
#include <stdio.h>
struct student
{
char name[20];
int id;
double grade;
};
void main(void)
{
struct student s1 = { "홍길동", 315, 2.4 }, s2 = { "이순신", 316, 3.7 }, s3 = { "세종대왕", 317, 4.4 };
struct student max;
max = s1;
if (s2.grade > max.grade) max = s2;
if (s3.grade > max.grade) max = s3;
printf("학번 : %d\n", max.id);
printf("이름 : %s\n", max.name);
printf("학점 : %.1lf\n", max.grade);
}
/*
실행결과
학번 : 317
이름 : 세종대왕
학점 : 4.4
*/
구조체를 초기화 할 땐 배열과 같이 해당하는 멤버에 맞게 초기화할 수 있습니다.
중괄호{ } 안에 넣는 초기화 값의 순서는 구조체의 멤버의 순서와 동일하게 합니다.
구조체의 형식을 선언함과 동시에 변수 선언과 초기화를 하는 방법도 있습니다.
struct student
{
char name[20];
int id;
double grade;
} s1 = { "KOEY", 315, 4.5 };
하지만 위와 같이 형식 선언과 동시에 변수 선언 및 초기화를 진행하면 해당 변수는 전역변수가 됩니다.
전역 변수는 프로그램이 종료되기까지 그 값이 메모리에 남는 특징이 있습니다.
따라서 메모리를 효율적으로 관리하기 위해서라면 이와 같은 선언은 하지 않는 것을 추천합니다.
구조체 변수를 함수의 매개변수로 사용하는 것을 예제로 다루어 보겠습니다.
로봇의 왼쪽눈과 오른쪽눈의 시력을 입력하고, 함수를 이용해 두 눈의 시력을 바꾸는 예제입니다.
#include <stdio.h>
struct vision
{
double left;
double right;
};
struct vision exchange(struct vision);
void main(void)
{
struct vision robot;
printf("시력 입력 : ");
scanf("%lf%lf", &(robot.left), &(robot.right));
robot = exchange(robot);
printf("바뀐 시력 : %.1lf, %.1lf\n", robot.left, robot.right);
}
struct vision exchange(struct vision robot)
{
double vision;
vision = robot.left;
robot.left = robot.right;
robot.right = vision;
return robot;
}
/*
실행결과
시력 입력 : 0.1 1.2
바뀐 시력 : 1.2, 0.1
*/
struct vision exchange(struct vision);
구조체의 형식 선언이 먼저 이뤄지고 이어서 함수의 형식 선언이 이루어 집니다.
함수는 vision 구조체를 매개변수로 받고 vision 구조체로 반환할 것이기 때문에
매개변수와 반환형의 자료형은 struct vision이 됩니다.
robot = exchange(robot);
함수를 실행하면서 robot을 매개변수로 입력하고 반환되는 구조체를 robot에 다시 저장합니다.
다음 시간에는 이어서 '비트 필드 구조체'에 관해 배웁니다.
'공부 일지 > C언어 공부 일지' 카테고리의 다른 글
구조체 활용, 공용체, 열거형 (0) | 2021.01.31 |
---|---|
사용자 정의 자료형2 (0) | 2021.01.29 |
동적 메모리 할당 및 활용 관련 문제2 (0) | 2021.01.28 |
동적 메모리 할당 및 활용 관련 문제1 (0) | 2021.01.28 |
동적 할당 저장 공간의 활용 (0) | 2021.01.28 |