본문 바로가기

프로그래밍 언어/C언어

[C언어] 18. 구조체와 여러가지 자료형

※ 이 포스팅은 VISUAL STUDIO 2019를 사용합니다.

 

안녕하세요. 루미아입니다.

 

오늘은 구조체와 여러가지 자료형에서 배워보겠습니다.

 


구조체란?

우리는 이때까지 자료형에 대해 공부했습니다. (예를 들면, int, double 등) 그런데, 자료형은 더욱 세분화 시켜 나타낼 수 있습니다. 무엇이 있는지 알아볼까요?

자료형
기초 자료형 파생 자료형 사용자정의 자료형

int

char

double

void

...

배열

포인터

구조체

공용체

typeof

enum

 

여태까지는 동일한 종류의 데이터를 하나로 묶을 수 있는 배열을 사용했지만, 다른 종류의 데이터를 하나로 묶어야 한다면 어떻게 할까요? 즉, int, double, char형을 모두 하나로 묶어야 한다면 어떻게 해야할까요?

이럴때에는 구조체로 하나로 묶어주어야 합니다.

 

구조체는 여러개의 변수들을 묶어서 새로운 자료형을 만들 수 있는 자료형입니다.

배열과 다른점은 배열은 자료형이 같은 자료들을 하나로 묶는거라면, 구조체는 자료형이 달라도 묶을 수 있다는 것입니다.

구조체 선언

구조체는 다음과 같이 선언합니다.

struct student {
	int num;
	char name[10];
	double grade;
};

여기서 struct는 student의 키워드이고 student는 태그(tag)라고 합니다. 그리고 { }(중괄호) 안에 쌓여 있는 것을 구조체 멤버라고 합니다.

 

그런데, 구조체를 다음과 같이 선언했다고 해서 변수 선언이 되지는 않습니다. 즉, 구조체 선언은 구조체 안에 어떤 변수가 들어간다만 정의하는 틀과 같습니다. 그래서 데이터를 저장할 수 있는 상태는 아닙니다.

 

참고 : 구조체는 함수 밖에서 생성하면, 전체 함수에서 사용할 수 있는 구조체를 선언합니다.

         함수 안에서 생성하면, 그 함수 안에서만 사용하는 구조체를 선언합니다.

구조체 변수 생성

위에서 선언을 했으면 구조체 변수 생성을 해봅시다. 구조체 변수를 선언하는 방법은 함수 안에서 선언합니다.

구조체는 다음과 같이 설정합니다.

struct student {
	int num;
	char name[10];
	double grade;
};

int main(void){
	struct student s1;
	...
}

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

위의 전체 메모리는 구조체 s1에 할당된 메모리를 나타냅니다.

여기서는 노란색이 num, 파란색이 name[10], 초록색이 grade를 나타냅니다.

그런데, 엑세스 속도 향상을 위해 더 많은 메모리를 할당하는 경우도 있으므로 sizeof를 사용해서 크기를 보는 것이 정확합니다.

 

변수를 여러개 초기화 하는 방법은 ,(콤마 연산자)로 분리해줍니다.

struct student s2,s3;

 

구조체는 구조체 정의와 선언을 동시에 할 수 있습니다.

struct student{
	int num;
	char name[10]
	double grade;
} s1;

정의와 다른점은 마지막 세미콜론 앞에 s1이 붙었다는 점입니다.

구조체의 초기화

위에서는 선언만 하고 구조체를 초기화하지 않았습니다. 첫 번째와 같이 구조체와 선언이 분리된 경우에는 배열의 선언과 비슷하게 선언해줍니다.

struct student s1 = {24, "Kim", 4.3};

그런데, 변수는 참조를 할 수 있어야하는데, 구조체는 어떻게 참조할까요?

s1.grade = 3.0;

s1은 구조체 변수이고 grade는 구조체에 있는 멤버입니다. 이것을 .(온점)으로 이어주면 참조할 수 있습니다.

 

여기서 주의할 점은 char name[10]은 문자 배열이라 문자 배열이라면 strcpy()함수를 사용해야 합니다.

strcpy(s1.name, "Kim");

이름없는 구조체?

위에서는 모두 구조체의 이름을 student로 선언했습니다. 그런데, 구조체는 이름이 없어도 상관이 없습니다. 대신, 구조체의 이름이 없으므로 변수를 생성할 수 없습니다.

이름을 쓰고싶지 않다면 필요한 구조체 변수를 구조체 정의와 함께 선언해야 합니다.

struct {
	int num;
	char name[10];
	double grade;
} s1, s2, s3;

여기서 s4를 함수 안에서 초기화하려면 위와 같은 구조체를 한번 더 선언해야 합니다.

구조체의 활용

구조체를 멤버로 가지는 구조체

지금까지는 구조체에 대해서 간단히 알아봤습니다. 그런데, 구조체 안에 구조체가 또 들어갈 수 있습니다.

struct date{
 	int year;
	int month;
	int day;
};

struct student{
	int num;
	char name[10];
	struct date dob; // date of birth
	double grade;
};

다음과 같이 선언된 두 개의 구조체에 struch date dob는 구조체 안에 구조체가 들어가 있는 구조입니다.

 

이렇게 선언된 구조체를 초기화 하려면 다음과 같이 초기화 해야합니다.

s1.dob.year = 1983;
s1.dob.month = 03;
s1.dob.day = 29;

s1에 있는 dob안에 있는 year, month, day를 초기화 할 때 이렇게 씁니다.

구조체의 연산

구조체를 다른 구조체에 대입하는 연산은 가능합니다. 즉, 구조체에 들어있는 데이터를 다른 구조체에 복사할 수 있습니다.

struct point{
	int x;
	int y;
};
struct point p1 = {10, 20};
struct print p2 = {30, 40};

다음과 같이 두 점에 대한 구조체를 생성했다고 가정하면,

p2 = p1; // p1에 있는 정보를 모두 p2에 복사

// 또는

p2.x = p1.x; // p1의 x값을 p2 x에 복사
p2.y = p1.y; // 위와 동일

다음과 같이 p1에 있는 데이터를 p2에 복사할 수 있습니다.

이 경우는 변수가 많은 구조체일수록 시간을 절약할 수 있는 방법입니다.

 

비교를 할 때에는 구조체를 하나씩 비교해주어야 합니다.

if (p1 == p2){
	...
}   // 이 방법은 컴파일 오류가 발생합니다.

if ((p1.x == p2.x) && (p1.y == p2.y)){
	...
} // 이렇게 구조체 하나마다 비교를 해 주어야 합니다.

즉, 구조체의 변수가 많아지면 비교수식을 많이 적어야합니다.

구조체의 배열

구조체 안에서는 배열을 선언할 수 있었습니다. 그렇다면 구조체를 여러 개 이어 만든 배열로 만들 수 있을까요?

구조체의 배열은 다음과 같이 선언합니다.

struct student{
	int num;
	char name[10];
	double grade;
};
struct student list[100];

이렇게 선언하면 구조체 배열을 100개 만든다는 의미입니다.

 

배열 안에 있는 구조체를 초기화 할 때도 위 처럼 초기화 하면 됩니다.

list[2].num = 24;
strcpy(list[2].name = "홍길동"); // 문자열 저장은 항상 strcpy()를 이용!
list[2].grade = 4.3;

 

구조체 배열은 초기화도 가능합니다. 다만 중괄호 안에 다시 중괄호로 묶어주어야 합니다.

struct student list[3] = {
	{1, "Park", 3.42}
	{2, "Kim", 4.31}
	{3, "Lee", 2.98}
}

구조체와 포인터

포인터는 아주 광범위하게 쓰이는데, 구조체에 쓰이는 포인터는 2가지 입니다.

1. 구조체를 가리키는 포인터

2. 포인터를 멤버로 가지는 구조체

 

구조체를 가리키는 포인터

변수에도 포인터를 만들 수 있듯이 구조체에도 포인터를 만들 수 있습니다.

struct student s = {24, "Kim", 4.3};

struct student *p;

p = &s;

printf("학번 = %d 이름 = %s 학점 = %f \n", (*p).number, (*p).name, (*p).grade);
// 여기서는 포인터를 통해 구조체에 접근합니다. (*p)가 구조체가 됩니다.

여기서는 *p가 구조체 전체를 가리키고 .(온점)뒤에 멤버를 붙여 사용합니다.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 
p

 

그런데, 이런 경우는 프로그램에 자주 등장하기 때문에 다음과 같이 쓸 수 있습니다.

p->number;

이 뜻은 위에 있는 (*p).number와 같은 말이 됩니다.

즉, p->number의 뜻은 "포인터 p가 가리키는 구조체의 멤버 number"가 됩니다.

 

구조체 변수와 구조체 포인터의 조합은 헷갈릴 수 있습니다. 그러므로 표로 정리해 드리겠습니다.

번호 구조체 수식 의미
1 (*p).number 포인터 p가 가리키는 구조체의 멤버 number
2 p->number (*p).number와 완전히 같습니다.
3 *p.number 연산자의 우선순위에 의해 *(p.number)로 해석합니다. 의미는 "구조체 p의 멤버 number가 가르키는 것"으로 해석합니다. 이 때, number는 반드시 포인터여야하고, 아니면, 오류가 발생합니다.
4 *p->number 연산자의 우선순위에 의해 *(p->number)로 해석합니다. 의미는 "p가 가르키는 구조체의 멤버 number가 가르키는 내용"으로 해석합니다. 이 때, number는 반드시 포인터여야하고, 아니면 오류가 발생합니다.

 

포인터를 멤버로 가지는 구조체

구조체 안에서도 다음과 같이 포인터를 선언할 수 있습니다.

struct date{
	int month;
	int day;
	int year;
};

struct student{
	int num;
	char name[20];
	double grade;
	struct date *dob; // 구조체 안에 포인터가 들어갔습니다.
};

int main(void){
	struct date d = {3, 20, 1990};
	struct student s = {20190001, "kim", 4.3);
    
	s.dob = &d; // 포인터 dob에 구조체 d의 주소를 저장
}

s.dob->day를 입력하면 dob에 있는 주소를 가지고 date안에 있는 day 값을 출력합니다.

문자 배열과 문자 포인터의 차이점

포인터 파트에서 문자 배열과 문자 포인터는 차이가 없다고 했습니다. 여기서도 문자 배열과 문자 포인터를 바꿔 쓸 수 있을까요?

struct studentA {
	int num;
	char name[10];
	double grade;
}

struct studentB {
	int num;
	char *p; // 문자 포인터 *p
	double grade;
}

둘다 선언하는데에는 문제가 없습니다.

struct studentA s1 = {20190001, "홍길동", 4.3};
struct studentB s2 = {20190002, "김유신", 4.2};

여기서 구조체 변수를 생성하고 초기화 하는 것도 문제가 없습니다.

 

그런데, 이것을 다음과 같이 사용자에게 받아들이게 하면 차이가 납니다.

struct studentA s1;
scanf("%s", s1.name); // 이것은 가능!

struct studentB s2;
scanf("%s", s2.p);    // 포인터가 올바른 주소로 초기화되지 않음!

 

대신, 이런 문장은 가능합니다.

struct studentB s2;
s2.p = "강감찬";

구조체와 함수

구조체를 함수의 매개변수로 넘길 수 있을까요? 구조체는 함수의 인수로 사용 가능하고, 구조체의 반환값으로 변환할 수 있습니다.

구조체는 인수나 반환값으로 사용할 때에는 "값에 의한 호출"로 이루어집니다. 즉, 구조체의 값은 함수에서 돌아다니지만, 구조체 자체에 영향을 미치지는 않습니다.

단점으로는 구조체의 크기가 클 수록 시간이 많이 걸리므로 포인터를 사용하는 것이 바람직합니다.

구조체를 함수의 인수로 넘기는 방법

구조체를 함수의 매개변수로 쓰는 경우는 다음과 같이 써주면 됩니다.

int structure(struct student s1, struct student s2){ // 구조체와 동일하게 선언해주면 됩니다.
	...
}

int main(void){
	equal(a,b); // 만약 a,b라는 구조체를 선언해서 초기화 했다면, 인수에 a,b를 넣어 값을 넘길 수 있습니다.
	...
}

그런데, 구조체가 큰 경우에는 시간이 오래 걸린다고 위에서 설명했습니다. 위의 내용을 포인터로 바꾸면 다음과 같습니다.

int structure(struct student *p1, struct student *p2){ // 다음과 같이 참조에 의한 호출을 표시합니다.
	if(p1->number == p2->number) // ->로 구조체에 접근합니다.
    ...
}

int main(void){
	equal(&a,&b); // 주소를 함수로 보내줍니다.
	...
}

이 방법도 장점만 있는 것은 아닙니다. 앞 단원에서 설명했듯이, 포인터는 값을 그 자리에서 바꾸므로 값을 훼손할 수 있습니다. 그러므로 const 지정자를 앞에 붙어주어 변경을 금지시킵니다.

const를 붙이는 위치는 구조체 이름과 포인터 사이 즉, student const *p1 처럼 붙여주면 됩니다.

구조체를 함수의 반환값으로 넘기는 방법

구조체를 반환값으로 사용하는 경우는 다음과 같습니다.

struct student create(){
	struct student s;
	s.num; = 3;
	strcpy(s.name,"Park");
	s.grade = 4.0;
	return s;
}

int main(void) {
	struct student a;
	a = create();  // create()에서 생성된 구조체를 a에 대입
	...
}

이렇게 하면 구조체를 반환값으로 사용할 수 있고, 또한 한 번에 여러개의 값을 출력할 수 있습니다.

위의 예제는 int형, char형, double형을 구조체로 사용해서 3개의 값을 출력하고 있습니다.

공용체

지금까지는 모든 변수가 다른 메모리에 저장되었다면, 여기에서 선언되는 변수들은 전부 메모리를 공유합니다.

구조체와 같은 방법으로 선언하고 구조체와 똑같이 태그를 붙여 사용합니다.

 

공용체는 다음과 같이 선언합니다.

union example{
	char c;
	int i;
};

다음과 같이 공용체는 구조체와 동일하게 선언하고 사용방법도 비슷합니다.

union example v;
union example v = { 'A' };

공용체는 메모리를 가장 많이 요구하는 자료형으로 메모리를 할당합니다. 즉, 공용체 안에 double형이 있으면 공용체의 메모리는 8바이트를 할당합니다. 위의 경우는 int가 제일 크므로 4바이트를 할당합니다.

#include <stdio.h>

union example { // 공용체 틀을 선언합니다.
	int i;
	char c;
}

int main(void){
	union example v; // 공용체 변수를 선언합니다.
    
	v.c = 'A'; // 공용체 변수 v에 있는 c에 'A'를 저장합니다. 이 때, v.i를 사용하면 쓰레기값이 출력됩니다.
   	v.i = 10000; // 공용체 변수 v에 있는 i에 10000을 저장합니다. 이 때, v.c를 사용하면 이상한 문자가 출력됩니다.
}

위와 같은 예제를 보고 공용체를 이해할 수 있습니다.

열거형

열거형은 변수가 가질 수 있는 값들을 나열해놓은 자료형입니다. 즉, 변수가 가질 수 있는 값들을 나타내는 상수를 모아놓은 자료형입니다.

예를 들어, 요일을 나타내는 변수 d를 선언한다고 했을 때, 변수 d는 MON~SUN까지의 상수만 가질 수 있을 때, 열거형이 사용됩니다.

 

열거형은 다음과 같이 선언합니다.

enum days { SUN, MON, TUE, WED, THU, FRI, SAT };

다음과 같이 SUN~SAT는 모두 기호 상수이므로 열거형은 기호 상수를 모아놓은 자료형이라고 할 수 있습니다.

다음과 같이 선언하는 것은 구조체와 같이 틀만 잡아놓은 것이고, 여기에 변수를 선언해야 합니다.

 

위와 같이 정의된 열거형으로 다음과 같은 수식을 쓸 수 있습니다.

enum days today; // 열거형 변수 생성
today = SUN; // 열거형에서 선언된 기호상수는 열거형 변수에 대입할 수 있습니다.

today = MY_DAY; // 다음과 같이 열거형 선언에 없는 기호 상수는 컴파일 오류가 발생합니다.

열거형은 다음과 같이 숫자가 결정되어 있습니다.

enum days { SUN, MON, TUE, WED, THU, FRI, SAT };
// SUN = 0, MON = 1 ... 처럼 1씩 증가합니다.

enum days { SUN = 1, MON, TUE, WED, THU, FRI, SAT };
// 만약 이렇게 설정하면 SUN = 1, MON = 2 ... 처럼 1씩 증가합니다.

enum days { SUN = 7, MON = 1, TUE, WED, THU, FRI, SAT = 6 };
// 필요한 경우에는 다음과 같이 모든 식별자들의 값을 지정할 수 있습니다.

typedef 자료형

이 자료형은 사용자가 새로운 자료형을 정의하는데 사용하는 키워드입니다.

즉, 자료형(type) 를 정의(define) 한다고 해서 typedef라고 합니다.

 

typedef는 다음과 같이 사용합니다.

typedef unsigned char BYTE;

위의 경우는 unsigned char이라는 자료형을 BYTE로 만드는 코드입니다. unsigned char은 문자가 아닌 정수를 저장한다는 것을 강조하기 위해 BYTE라고 이름을 새로 지어주어 프로그래머가 보기 편하게 만드는 것입니다.

 

typedef int INT32;
typedef short INT16;

INT32 i; // i는 int형과 같습니다.
INT16 k; // k는 short형과 같습니다.

다음과 같은 코드를 쓰는 경우는 32비트 컴퓨터가 있는 반면, 16비트 컴퓨터에서는 int형이 2바이트이므로 혼동할 수 있으므로 다음과 같이 써주었습니다. 즉, 프로그래머들이 혼동하기 쉬운 자료형을 다시 정의해서 이해하기 쉽게 만들어줍니다.

구조체로 새로운 자료형 만들기

위에서는 기존의 자료형에 이름을 붙이는 경우인데, 사실상 별 다른 의미가 없어보입니다. 그런데, typedef문은 상당히 복잡한 형식도 새로운 자료형을 만들 수 있는 능력이 있습니다.

다음과 같은 코드를 살펴봅시다.

struct point {
	int x;
	int y;
};

typedef struct point POINT;

다음의 코드는 point라는 구조체를 POINT라는 새로운 타입으로 지정하는 것입니다. 

POINT라는 새로운 자료형이 생겼으므로 POINT는 앞에 struct를 붙일 필요가 없습니다.

POINT a,b;

위에서 typedef와 구조체의 선언을 같이 사용할 수 있습니다.

typedef struct point{
	int x;
	int y;
} POINT;

예를 들어 다음과 같이 복소수(complex number)를 새로운 타입으로 선언해봅시다.

// 복소수는 a + bi로 나타낼 수 있습니다. 즉, a는 real이고 b는 imag로 볼 수 있습니다.
typedef struct complex {
	double real;
	double imag;
} COMPLEX;
COMPLEX x, y;

위에서 살펴본 열거형으로 다음과 같은 자료형도 정의 할 수 있습니다.

typedef enum { FALSE, TRUE } BOOL;
BOOL condition; // enum { FALSE, TRUE } condition; 과 같습니다.

typedef의 장점

1. 이식성을 높여줍니다.

위에서 INT32,INT16을 살펴보았을 때, int형은 컴퓨터에 따라 4바이트가 될 수 있고, 2바이트가 될 수 있습니다. 이럴 때에는 프로그래머가 봤을 때, 4바이트인지 2바이트인지 확실히 알 수 있고, 컴퓨터 시스템이 바뀌면 typedef만 바꾸면 됩니다.

 

2. #define과의 차이점

사실, #define을 이용해도 typedef와 비슷한 효과를 낼 수 있습니다. 그런데 #define보다 typedef가 훨씬 좋은 이유는 typedef는 컴파일러가 직접 처리하기 때문에 #define처럼 문자열을 다른 문자열로 대치하는 것과는 다릅니다.

다음과 같은 자료형의 정의는 #define으로는 할 수 없습니다.

typedef float VACTOR[2]

 

3. 문서화의 역할도 합니다.

typedef를 사용하면 주서거을 붙이는 효과를 가지게 됩니다. 즉, POINT나 MATRIX와 같은 이름들을 사용하면 구조체가 무엇을 표현하는지 확실하게 알 수 있습니다.


자, 여기까지 구조체와 여러가지 자료형에 대해 알아보았습니다.

 

다음은 그 어렵다던 포인터를 더욱 자세히 활용해보겠습니다.

 

댓글로 모르시는 내용을 말해주세요! 무엇이든지 알려드리겠습니다.

'프로그래밍 언어 > C언어' 카테고리의 다른 글

[C언어] 20. 스트림  (0) 2019.11.30
[C언어] 19. 포인터의 활용  (0) 2019.11.29
[C언어] 17. 문자와 문자열  (0) 2019.11.21
[C언어] 16. 포인터  (0) 2019.11.20
[C언어] 15. 배열  (0) 2019.11.20