열혈 C - Chapter 22 구조체와 사용자 정의 자료형1

2024. 10. 28. 23:26Programming Language/C

프로그램을 만들면서 구조체는 100% 사용된다.

과장 하지 않고 C언어를 사용하면서 구조체를 이용하지 않은 프로그램은 존재할 수 없다.

그렇기에 이 구조체는 매우 중요하다.

그 만큼 어려운 문법은 아니지만 이 구조체를 정확히 이해하고 정확이 적용할 수 있는가가 중요하다.

22-1 구조체란 무엇인가?

구조체가 C언어를 사용해서 프로그램을 만들때 왜 100%사용되는지에 대해서 먼저 생각해보자.

사실은 우리가 제대로 프로그램을 하나 만들어보고 나면 구조체가 왜 필요한지에 대해서 매우 쉽게 이해할 수 있는데 우리가 경험해본 프로그램이 예제뿐이기에 어느정도는 상상을 기반으로 한 설명의 이해가 필요하다.

 

우리가 프로그램을 구현한다고 생각해보자.

우리의 프로그램에서 관리해야하는 데이터가 주민등록증에 존재하는 데이터들이라고 생각해보자.

이 안에는 주민등록번호, 주소, 사진, 생성일자등등의 정보가 있는데 이를 메모리 공간에 저장하고 필요하다면 검색해서 그 데이터들을 출력해야한다고 가정해보자.

 

그러면 프로그램 상에서 이 정보들을 담을 수 있는 자료형을 결정해야한다.

리스트를 나열해보자면,

  • 이름 : 문자열 = char name[]
  • 생년월일 : 정수 = int birth
  • 키(다양한 정보를 위해서 ) : 실수 : double height

등의 데이터들이 있다.

그런데 이 하나 하나의 데이터들은 주민등록 정보의 일부를 표현할 뿐이지 하나의 주민등록정보를 표현하지 않는다.

(주민등록증에 해당하는 정보는 여러가지가 있지만 이들이 주민등록증을 나타내지 않고 해당 주민등록증의 상세내용을 나나타내는 것이다라는 의미, 저 전체 내용을 묶어주는 하나의 무엇인가가 존재하지 않음을 말하는 것)

 

그 이유는 만약 열명의 주민등록정보를 저장하려고 한다. 

그러면 이건 어떻게 저장할 것인가?

한명의 주민등록정보는 하나의 set로 데이터들을 갖고 있어야한다는 것이다.

 

결국 구조체라는 것은 주민등록 정보의 상세에 있는 이름, 생년월일, 키의 정보를 한번에 저장할 수 있는 변수가 될수 있다는 것이다.

이렇게 구조체는 필요로하는 관련있는 변수들의 한 SET를 묶어 하나의 변수로 생성하는 도구이다.

(내 생각에는 자바에서 맵, 파이썬에선 딕셔너리 같은 존재로 인식된다.)

 

구조체의 정의

int xpos; //마우스의 x좌표
int ypos; //마우스의 y좌표

마우스의 좌표 정보를 저장하고 관리하기 위해서는 x좌표와 y좌표를 저장할 수 있는 두개의 변수가 필요하다.

xpos와 ypos는 서로 독립된 정보를 표현하지 않고 서로 하나의 정보를 위해 각자가 존재한다. 

그렇기에 두 정보는 항상 함께 다녀야만 한다.

 

 구조체를 선언하는 방법은 

struct point { // point라는 이름의 구조체 정의
    int xpos; // point 구조체를 구성하는 멤버 xpos
    int ypos; // point 구조체를 구성하는 멤버 ypos
};

와 같이 선언하면 된다.

 

이렇게 xpos와 ypos가 묶여서 하나의 구조체를 구성했다.

이 point라는 것은 int와 같이 자료형의 이름이고 이는 사용자가 정의한 자료형이기에 "사용자 정의 자료형(user defined data type)"이라고 한다

그래서 point라는 변수를 선언하면 이 안에 xpos와 ypos라는 변수가 생성되어 point변수 내부에 존재하게 된다.

이 구조체를 구성하는 변수를 멤버라고 부른다

이렇게 구조체를 정의할때는 그 내부에 필요한 멤버가 그 구조체를 표현하는 정보를 담아줘야만 한다.

그렇기에 구조체를 정의할때 내부 멤버를 구성할때 그 정보가 그 구조체를 잘 표현하는지에 대해서 고민을 하면서 정의해야만 한다.

 

구조체 변수의 선언과 접근

* 구조체 선언의 기본형태

// 구조체 변수 선언의 형태
struct type_name val_name;

// 실제 구조체 변수를 선언하는 예제
struct point pos;
struct person man;

이렇게 구조체를 선언할 때에는 그냥 int형에 접근하는 것 처럼 바로 사용할 수 는 없고, 사용자가 구성한 구조체임을 알리기 위해 맨 앞단에 struct라는 지시어를 붙이게 제한하고 있다.

선언한 구조체의 멤버에 접근하는 방법은

// 멤버에 접근하는 표준적인 방법
구조체의 이름.구조체 멤버의 이름

// 멤버에 접근하는 예시
pos.xpos = 20;
printf("%s", man.name);

과 같이 사용할 수 있다.

 

구조체 변수의 선언과 접근관련 예제1

이 위에서 두점 사이의 거리를 계산하는 부분에서 sqrt는 제곱근을 반환하는 함수이다.

헤더 파일 math.h를 선언하면 사용할 수 있는 수학 관련 함수이다.

 

이를 실행하면 

의 결과값을 출력한다.

 

구조체 정의와 동시에 변수 선언하기

구조체를 정의함과 동시에 변수를 선언할 수 있는 방법도 있다.

 

struct point {
    int xpos;
    int ypos;
} pos1, pos2, pos3;

와 같이 선언하면 된다.

 

이와 동일한 결과를 보이는 구조체의 정의와 변수의 선언은 

struct point {
    int xpos;
    int ypos;
};

struct point pos1, pos2, pos3;

이다.

 

구조체를 정의함과 동시에 변수를 선언하는 문장은 잘 사용되지 않는다.

그러나 문법적으로 지원 되고 간혹 사용되는 경우도 있다.

 

구조체 변수를 선언함과 동시에 초기화를 할 수도 있다.

struct point {
	int xpos;
    int ypos;
};

struct point pos = {10, 20};

struct 구조체명 구조체변수명 = {구조체 멤버1, 구조체 멤버2...};

선언과 동시에 초기화를 하는 방법은 중괄호 내부에 멤버의 순서에 맞게 데이터를 나열하면 그 순서에 맞게 멤버에 값이 할당된다.

 

 

22-2 구조체와 배열 그리고 포인터

 

이번에는 구조체를 이용해서 배열 그리고 포인터를 선언하는 방법과 유의 사항에 대해서 알아보자.

구조체 배열의 선언과 접근

구조체를 배열로 선언하는것은 크게 어려움 없이 그냥 배열로 선언하면 된다.

접근할때도 일반 배열을 접근하는것과 동일하지만 값을 넣거나 사용할때는 해당 멤버를 .(도트)연산자와 함께 붙여줘야 해당 멤버에 접근이 가능하다.

구조체 배열의 초기화

구조체 배열의 초기화는 기존 구조체의 초기화를 배열로 구성한다고 보면 쉽다.

 

*기존 구조체의 초기화

struct person man = {"홍길동", "901203", 34};

와 같이 초기화 하는걸 배열로 선언하면 된다.

 

*구조체 배열의 초기화

struct person manArr[3] = {
    {"홍길동", "901203", 34},
    {"임꺽정", "910123", 33},
    {"김흥도", "890502", 36} 
};

이렇게 선언하면 순서에 맞게 구조체가 초기화가 된다.

 

구조체 변수와 포인터

구조체 포인터 변수의 생성과 할당에 대해서 확인해보자.

구조체도 어찌되었는 데이터형이기 때문에 포인터로 선언이 가능하다.

구조체 포인터 변수의 생성 방법과 포인터 연산 및 멤버에 접근하는 방법은 아래와 같다.

struct point pos = {11, 12};
struct point * posptr = &pos; //구조체 point의 포인터 변수 선언

(*posptr).xpos = 10; // posptr이 가리키는 구조체 변수의 멤버 xpos에 접근
(*posptr).ypos = 20; // posptr이 가리키는 구조체 변수의 멤버 ypos에 접근

 

여기서 추가적으로 ->연산자에 대해서 볼텐데, -> 연산자는 화살표연산자 혹은 멤버 접근 연산자라고 불린다.

이는 구조체 포인터 변수에서 구조체에 접근하거나 구조체의 멤버에 접근할때 사용이 된다.

표준적인 사용법은 

구조체 포인터 변수명 -> 멤버명

과 같이 사용되고 이는 

(*구조체 포인터 변수명).멤버명

과 동일한 의미이다.

이는 멤버에 접근할 때 괄호와 역참조 연산자(*)를 사용하는 번거로움을 줄여준다.

 

위 예시 코드를 멤버 참조 연산자로 수정하면

posptr -> xpos = 10;
posptr -> ypos = 20;

으로 작성할 수 있다.

 

그리고 멤버 변수를 구조체 포인터 변수로 선언하는 것도 가능하다.

포인터 변수를 구조체의 멤버로 선언하기

구조체의 멤버로 구조체 포인터 변수를 선언하면 접근 할때 

(구조체 변수명.구조체멤버명) -> 구조체 맴버타입의 멤버명 

** 복잡하니 예시를 들자면 

//포인터가 될 구조체
struct 구조체A {
    int num;
    char name[20];
}

//멤버로 구조체 포인터를 담을 구조체
struct 구조체B {
    int height;
    struct 구조체A * memder;
}

int main(void){
    struct 구조체A st1 = {1, "Kim"};
    struct 구조체B st2 = {167, &st1};
    
    printf("%d", (st2.member) -> num); //이 부분에 대한 설명임
}

과 같이 사용할 수 있다.

 

위의 예시에 대한 결과이다.

 

그리고 또 구조체의 멤버로 해당 구조체의 포인터 변수를 선언할 수 도 있다.

이게 무슨 소리냐 하면 A라는 구조체를 선언할때 그 구조체의 멤버로 A구조체 포인터 변수를 사용할 수 있다는 의미이다.

struct 구조체A {
    int num;
    char name[20];
    struct 구조체A * ptr; // Type형 구조체의 멤버로 Type형 구조체 포인터 변수를 지정 가능!
};

이게 어떻게 되냐고 생각이 든다면 아마도 재귀함수 부분에 대한 내용과 일맥상통한다고 보여진다.

 

그 구조체의 사용의 예시를 보자면 

이렇게 사용이 될 수 있다.

 

구조체 변수와 첫 번째 멤버의 주소 값

구조체 변수의 주소 값과 구조체 변수의 첫번째 멤버의 주소값은 동일하다.

응용 프로그램을 만드는 쪽에서는 이런 점을 가지고 프로그램에 활용하기도 한다.

 

여기서 이것에 대한 활용 부분에서 생각해볼 점들이 많은것 같다.

아무리 생각해도 아직은 포인터와 메모리의 주소에 대해서 이해하는게 프로그램을 생성하면서 오류발생에 대한 키가 될수는 있겠지만 프로그램을 만드는데 당장 이해가 필요한지는 의문점이 드는것 같다.

(물론 프로그램을 만들면서 오래걸리는 9할은 이런 에러 때문이기에 에러 해결의 key가 된다는것은 매우 중요한 것 같긴함..)