본문 바로가기
프로그래밍 언어/C, C++의 기본적인 설명

[C/C++] C언어 - 구조체

by UltraLowTemp-Physics 2021. 6. 28.
728x90

구조체 변수 (Structured Variables)

우리가 다음과 같은 변수들을 다룬다고 가정을 해보자; 도서의 타이틀, 저자명, 출판사, 출판일, 페이지 수, 정가, 권 수. 이 변수들의 타입은 정수, 문자열 등 다양하다. 이러한 다양한 변수들을 하나의 배열만을 이용하여 모든 정보를 넣고자 할 때, 구조체 변수를 사용한다. 

구조체는 다양한 종류의 정보들을 저장할 수 있기 때문에, 데이터 베이스를 구축하는데 사용하는데 쓰이는 도구이다. 
NOTE 1: 하나의 구조체에 저장되는 정보의 총 집합 = 레코드 (record)
NOTE 1: 하나의 구조체의 각각의 개별 항목 = 필드 (field)

구조체와 효율적인 데이터형 
- 효율적인 데이터 형들은 다음과 같다:
   a. 큐(queue)
   b. 바이너리 트리 (binary tree)
   c. 힙(heap)
   d. 해시 테이블(hash table)
   e. 그래프(graph)
   f. 링크드 리스트(linked list)

1. 구조체의 구조 

- 구조체는 멤버(member) 또는 필드 (field)로 구성되어 있다. 
- 구조체는 일종의 다른 데이터 형식들이 포함된 배열이라고 생각할 수 있다. 

이러한 구조체를 작성할 때, 아래와 같은 점들을 유념하자. 
  a. 구조체의 형식 또는 레이아웃 설정 
  b. 레이아웃에 맞는 변수 설정 
  c. 구조체 변수의 개별 성분에 접근 

2. 구조체의 선언

다른 변수들과 마찬가지로 구조체 변수 역시 사용하기 전에 선언을 해야한다. 이러한 구조체들의 선언을 템플릿(template)라고 부르기도 하며, 구조체에 어떻게 데이터가 저장되는지를 알려준다. 

# 구조체 선언
struct book {
    char title[40];
    char author[40];
    float price;
};

- 키워드: struct, 태그: book
- 키워드 struct로 시작하며 이것이 구조체를 정의하는 것임을 나타냄

- 중괄호({})로 둘러싸인 구조체 맴버들의 리스트들이 나옴 
- 구조체가 저장되는 순서는 배열과 비슷하며, 정의된 순서대로 메모리에 할당된다.  

이후 정의된 구조체를 사용하기 위해선 아래와 같이 구조체 변수을 생성한다. 

struct book library;

 - 이후, book이라는 구조체 형식을 가진 library라는 구조체 변수들을 사용할 수 있다. 

또한 구조체들의 배열을 아래와 같이 정의할 수 있다. 

struct book library[40];

 

 아래의 내용을 주의하라.

struct book library[40];  // book 구조체의 배열 선언
library                   // book 구조체의 배열
library[2]                // 배열의 한 원소이자 하나의 book 구조체
library[2].title          // char형 배열 
library[2].title[4]       // title 맴버의 문자열의 한 문자

 

typedef을 사용할 경우

# 정의
typedef struct _person{
	char *first_name;
    char *last_name;
    char *title;
    unsigned int age;
} Person
# 선언
Person person;

 

2.2 구조체 변수의 접근 

구조체 변수에 접근하는 방법은 크게 두 가지이다. 
  1) 배열의 인덱스를 사용하여 접근 
  2) 구조체 맴버 연산자인 도트(.)를 사용하여 접근 
    - 구조체 맴버 연산자 도트(.)는 구조체에 대해 일종의 인덱스 역활을 한다. 
    - 구조체 맴버 연산자 도트는 주소 연산자(&)보다 우선 순위가 높다. 
    → &library.price = &(library.price)

printf(library.book)  # library의 book에 접근
printf(library.title) # library의 title에 접근
printf(library.price) # library의 price에 접근

 

2.3 구조체 변수의 초기화 

배열의 초기화와 비슷한 syntax를 사용한다. 중괄호({}) 안에 콤마로 분리된 초기화자 리스트를 사용하며, 각각의 초기화자는 초기화하려는 구조체 맴버의 데이터형과 반드시 일치해야 한다. 또한 구조체를 정의한 순서에 맞춰 각각의 데이터들을 초기화해야 한다. 

struct book library = {
    "The Pirate and the Devious Damsel",
    "Renee vivotee",
    1.95
}

 

C99이후부터는 위의 초기화를 도트 연산자를 사용하면 아래와 같이 쓸 수 있다. 

struct book library = {
    .title  = "The Pirate and the Devious Damsel",
    .author = "Renee vivotee",
    .price  = 1.95
}

NOTE 1) 도트 연산자를 사용하여 초기화를 할 경우, 맨 위의 초기화와 다르게 구조체를 정의한 순서에 일치하지 않아도 된다. 

 

3. 구조체와 포인터

1) 구조체를 가리키는 포인터를 사용할 때 3가지의 장점 
  a. 구조체를 가리키는 포인터가 구조체 자체보다 다루기 쉬움 
  b. 일부 구형 시스템에서는 구조체를 전달 인자로 함수에 전달할 수 없지만, 구조체를 가리키는 포인터는 가능 
  c. 많은 데이터 표현들이 다른 구조체를 가리키는 포인터를 맴버로 가지는 구초체를 사용함 

2) 구조체 포인터의 선언과 초기화 
  a. syntax: struct <tag> *<name>; 
     - <name>이라는 포인터가 <tag>형 구조체를 가리키도록 함 
  b. <name> = &<struct variable name>; 
     - 주의: 배열과 다르게 구조체의 이름이 구조체의 주소가 아님
     - 따라서 & 연산자를 사용해서 구조체가 위치한 주소를 알려줘야 함 

ex)

$vim example.c
#include <stdio.h>
#define LEN 20
struct name {
	char first[LEN];
    char last[LEN];
};
struct guy {
	struct name handle;
    char favfood[LEN];
    char job[LEN];
	float income;
};

int main(void){
	struct guy fellow[2] = {
 		{{"Ewen", "Villard"}},
        "grilled salmon",
        "personality coach",
        100000
        },
        {{"Rodney", "Swillbelly"},
        "tripe",
        "tabloid editor",
        200000
        }
    };
    
    struct guy *him;
    him = &fellow[0];
    
    printf("address 1: %8p address 2: %8p \n", &fellow[0], &fellow[1]);
    printf("pointer 1: %8p pointer 2: %8p\n", him, him + 1);
    
    printf("him->income: $%.2f \n", him->income);
    printf("(*him).income: $%.2f \n", (*him).income);
    printf("(him+1)->favfood: $%.2f \n", (him+1)->favfood);
    printf("(him+1)->handle.last: $%.2f \n", (him+1)->handle.last);

	return 0;
}


$ gcc example.c
$ ./a.out 
address 1: 0x7ffdac407080  address 2: 0x7ffdac4070fc
pointer 1: 0x7ffdac407080  pointer 2: 0x7ffdac4070fc
him->income: $100.00
(*him).income: $100.00
(him+1)->favfood: tripe
(him+1)->handle.last: Swillbelly

NOTE 1: 구조체 포인터 him을 살펴보았을 때, 다음을 확인할 수 있다. 
     a. him = &fellow[0]
     b. him + 1 = &fellow[1] 
NOTE 2: 구조체 포인터를 사용하여 맴버의 값에 접근 
     a. "->" 연산자를 사용하여 구조체 맴버에 접근한다. 
        - 이는 구조체 이름 뒤에 도트 연산자가 오는 것과 동일한 효과를 준다. 
        - ex: 아래의 두 표현은 동일하다; him == &fellow[0]일 경우, him->income 과 fellow[0].income은 동일
     b. & 및 *연산자 사용 
        - *연산자: 해당 포인터의 주소에 저장되어있는 값을 반환 
        - ex) him == &fellow[0]일 경우, (*him).income 과 fellow[0].income은 동일

 

구조체와 관련된 이슈 

1) 구조체 안에 문자 배열을 사용해야 하는가? 문자 포인터를 사용해야 하는가? 
a. 구조체 안에 문자열을 저장해야 한다면, 문자 배열 맴버를 사용해야 함.
  예시는 아래와 같다.

struct names{
    char first[LEN];
    char last[LEN];
}

 
b. 구조체 안에 문자열 포인터를 사용하여 문자열을 다룬다고 할시, 구조체 내부에는 문자열을 저장하기 위한 공간이 할당되지 않음 
- 구조체 안의 문자열 포인터는 저장된 문자열의 주소를 가리키기 위한 용도 혹은 관리용도로 사용할 순 있지만, 구조체 내부에 문자열을 저장하기 위한 용도로는 사용하기에 적합하지 않는다. 
- 구조체 안에 문자열을 사용하기 위한 용도로써 포인터를 사용할 경우, 형태는 아래와 같다. 

struct names{
    char *fisrt;
    char *name;
}

- 런타임 시스템은 구조체  내부에 선언된 포인터에 자동으로 메모리를 할당하지 않으며, 구조체가 삭제될 때도 구조체 내부의 포인터에 할당된 포인터를 자동으로 해제하지 않음 
- 이러한 구조체는 문자열의 저장하기 위한 공간을 할당하지 않는다. 

- 구조체 안에 문자열 포인터를 사용하고자 한다면, malloc() 함수와 free() 함수를 같이 써야 한다. 
  -- malloc()함수로 문자열을 저장할 공간을 할당한 뒤, 해당 문자열이 저장되어있는 주소를 구조체의 맴버에 할당한다. 이후, 프로그램이 종료가 되면, free()함수를 사용하여 할당된 메모리를 해제한다.(중요)

  -- 예제는 Appendix 1를 참조  

 

 

4. 구조체와 함수

  함수의 인자 혹은 리턴값으로 구조체의 주소, 구조체 자체를 리턴할 수 있다. 

  구조체와 관련된 함수를 만들 때, 다음과 같은 사항을 고려하게 된다. 
    a. 전달인자로 구조체 포인터를 사용해야 하는가? 
    b. 전달인자로 구조체를 사용하고 리턴값으로 구조체를 반환해야 하는가? 

함수의 전달인자 장점 단점
구조체 포인터 - 구형 및 신형 컴파일러에서 모두 작동 
- 주소 하나만 전달하기 때문에 빠름 
- 데이터 보호가 부족함 
--> const 제한자를 사용해서 원본 데이터를 보호할 수 있음 
구조체 자체 - 함수가 원본 데이터의 복사본을 가지고 작업함 
--> 원본 데이터 자체를 가지고 작업하는 것보다 안전함
- 프로그래밍 스타일이 좀 더 명확함
- 구형 컴파일러가 허용하지 않을 수 있음
- 시간과 공간을 낭비할 수 있음 
- 한 두개의 맴버만 사용하는 함수에 커다란 구조체 전체를 전달하는 것은 낭비 

효율성을 이유로 많은 프로그래머들이 구조체 포인터를 함수의 전달인자로 많이 사용함

  1) 구조체의 주소를 인자로 사용하기 
   - NOTE: 구조체의 이름은 주소가 아니라는 점을 반드시 기억하자. 
   - 아래의 예시에서 구조체의 주소를 얻기 위해 & 연산자를 사용하였다. 

#include <stdio.h>

struct xy {
	double x;
	double y;
};

double sum(const struct xy *val){
	return (val->x + val->y);
}

int main(void){
	struct xy xy_plane = {2., 3.};
	printf("x: %f, y: %f \n", xy_plane.x, xy_plane.y);
	printf("x + y = %f \n", sum(&xy_plane));
	return 0;
}

./a.out
x: 2.000000, y: 3.000000
x + y = 5.000000

 

  2) 구조체 자체를 전달인자로 사용 

#include <stdio.h>

struct xy {
	double x;
	double y;
};

double sum(struct xy val){
	return (val.x + val.y);
}

int main(void){
	struct xy xy_plane = {2., 3.};
	printf("x: %f, y: %f \n", xy_plane.x, xy_plane.y);
	printf("x + y = %f \n", sum(xy_plane));
	return 0;
}

 - 위의 경우와 같은 결과를 줌 

 

5. 구조체와 파일 저장

아래와 같은 구조체가 있다고 가정해보자. 

#define MAX 40
struct book {
	char title[MAX];
    char author[MAX];
    float value;
}

 

1) fprintf() 사용 
  - 가장 확실한 방법이지만 반대로 효율이 가장 떨어지는 방법이기도 함 
  - 구조체의 필드의 갯수가 많으면 많아질수록 다루기 어려워짐 
  - 한 필드가 어디서 끝나고 어디에서 시작되는지 프로그램에게 알려줘야 함 
    (이 경우, 구분을 위해, 예를들어 seperator를 사용하거나 고정된 크기의 필드를 사용할 수 있다.)

fprintf(<file_stream>, "%s %s %.2f\n", primer.value, primer.author, primer.value)

 

2) fread()와 fwrite() 사용 
 - 각각의 필드를 저장하는 것이 아니라 한번에 한 레코드를 읽고 씀
 - 구조체 크기 단위를 바이너리로 읽고 저장할 수 있음

fwrite(&primer, sizeof(struct book), 1, <file_stream>);

 

Appendix 1 

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

struct namect {
	char * fname;
	char * lname;
	int letter; 
};

void getinfo(struct namect *);
void makeinfo(struct namect *);
void showinfo(const struct namect *);
void cleanup(struct namect *);

int main(void){
	struct namect person;
	getinfo(&person);
	makeinfo(&person);
	showinfo(&person);
	cleanup(&person);
	
	return 0;
}

void getinfo(struct namect * pst){
	char temp[81];
	printf("put first names\n");
	gets(temp);
	pst->fname = (char *) malloc(strlen(temp) + 1);
	strcpy(pst->fname, temp);
	printf("put last names\n");
	gets(temp);
	pst->lname = (char *) malloc(strlen(temp) + 1);
	strcpy(pst->lname, temp);
}

void makeinfo(struct namect * pst){
	pst->letter = strlen(pst->fname) + strlen(pst->lname);
}

void showinfo(const struct namect *pst){
	printf("%s %s, You have %d letters\n", pst->fname, pst->lname, pst->letter);
}
void cleanup(struct namect *pst){
	free(pst->fname);
	free(pst->lname);
}

 

728x90

댓글