이번에는 구조체에 대해 알아보자.
우선 게임 캐릭터의 정보를 저장하는 프로그램을 만든다고 가정해보자.

만약 캐릭터가 100명이 된다면 이대로 계속 변수를 300개나 만들어야 한다.
심지어는 이름, HP, MP가 서로 다른 변수라 한 눈에 보이지도 않는다.
이렇게 파편화된 변수들을 하나로 묶어주는 일종의 '양식'이 있다면, 굳이 캐릭터마다 변수 이름을 달리 할 필요 없이 서류 한 장에 이름 HP, MP 칸을 다 만들어 두고 새 캐릭터를 생성할 때마다 그 서류 양식을 그대로 찍어내면 되지 않을까?
이 기능을 하는 것이 바로 '구조체'이다.
구조체란, 하나 이상의 변수를 묶어서 좀 더 편리하게 사용할 수 있도록 도와주는 도구이다.
구조체는 쉽게 설명해서 일종의 카테고리를 만들어서 하나의 자료형으로 사용한다.
[int, float, char 등 자료형을 모아서 새로운 커스텀 자료형을 만든다고 생각하면 된다]
물론 배열을 만들고 반복문을 사용하여 배열에 저장해도 되지만, 이름을 저장할 배열, 학번을 저장할 배열 등등 필요한 배열들이 많을 것이다. 그렇다면 이중 for문으로 2차원 배열을 만들 것인가?
그것보다는 구조체를 사용해서 조금 더 편하고 가독성 있게 사용하는 것이 더 좋다고 생각한다.
따로 변수를 계속 선언할 필요 없이 하나의 카테고리를 만들어서 변수를 넣어놓고, 필요할 때마다 카테고리에 이름을 붙이고 꺼내서 사용하는 것이 좋다고 생각한다.
예시로 게임 캐릭터의 정보를 저장하는 '캐릭터' 카테고리를 한 번 만들어보자
그러기 위해서는 우선 구조체를 '정의' 해야 하는데, 구조체 정의는 메모리를 차지하지 않고 어떤 모양으로 만들지 설계도만 그리는 단계라고 생각하면 된다. 맨 뒤에 세미콜론( ; )을 꼭 붙여야 한다.

구조체 정의는 "struct 구조체 이름 { 구조체 멤버 };" 로 구성되어 있다
기존의 변수를 사용하던 방식에 카테고리만 만들어 묶어 사용하는 방식이다.

구조체 정의는 새로운 자료형을 만드는 것과 같기 때문에 보통 main( )함수 전에 작성한다. 지역변수처럼 어떤 함수 안에 구조체를 선언하면 그 함수 안에서만 사용할 수 있기 때문이다.
main( ) 함수가 시작되고 나서 struct Character CharInfoA;라고 구조체 변수를 선언해 주었다.
struct Character라는 자료형(카테고리)에 CharInfoA라는 변수 이름을 지어준 것이다.
(구조체 변수를 선언할 때는 구조체 이름과 변수이름을 써주면 되는데, struct [구조체 이름] 까지가 자료형이라고 생각하면 된다)

구조체를 선언하면 구조체 멤버를 사용할 수 있는데, "변수이름.구조체 멤버이름" 처럼 .을 사용하여 구조체 멤버에 접근하여 사용하면된다.
위 방법처럼 사용자가 직접 입력하여 구조체 변수의 멤버 값을 대입할 뿐만 아니라 구조체 멤버를 초기화 할 수도 있다.

구조체 멤버의 값을 main( )함수 내에서 선언할 때 직접 초기화 할 수 있다.

초기화 할 때는 {.멤버이름 = 값} 과 같은 형태로 초기화 할 수도 있으며, 멤버 이름을 적지 않고 초기화 할 수도 있다.
(멤버 이름을 적지 않을 때는 구조체 멤버 순서대로 값이 들어간다. 또한 값을 넣어주지 않은 멤버는 0으로 초기화 된다)
구조체를 선언할 때 struct [구조체 이름] [변수 이름];으로 선언했었지만 사실 구조체 선언을 조금 더 편하게 하는 방법이 있다.
바로 typedef 키워드를 사용하는 것이다.
typedef 키워드는 C언어에서 자료형의 이름을 새롭게 붙일 때 사용하는 키워드이다.
typedef를 이용하면 main( )함수에서 구조체를 선언할 때 매번 struct를 써줄 필요가 없다.

이 typedef를 사용할 때는 구조체 별칭이 필요한데, 구조체 별칭은 구조체를 정의할 때 중괄호 뒤에 써주면 된다.
구조체 이름인 _Student는 별칭과 이름이 같아도 되지만 중복의 경우 구조체 이름 앞에는 _를 붙여 구별한다.
위와 같이 typedef와 별칭을 써주면 struct [구조체이름]을 써줄 필요 없이 별칭만 써도 구조체 선언이 가능하다.
(구조체 별칭은 구조체 이름과 동일하게 사용해도 되지만, 일반적으로 구조체 이름과 별칭을 둘 다 쓸 때는 구조체 이름 앞에 _를 붙여준다)
그렇다면 우리가 사용하는 것은 실제로 별칭 뿐인데 매번 구조체 이름까지 적어야 할까?
사실 구조체 이름도 생략이 가능하다.

이렇게 구조체 이름을 사용하지 않고 별칭만 사용하는 것을 '익명 구조체' 라고 한다. typedef를 이용해 구조체 별칭을 사용할 때도 문법은 똑같으므로 멤버에 접근할 때는 똑같이 .을 이용해 멤버에 접근하면 된다.
일반 변수에 배열이 있듯이 변수의 집합체인 구조체에도 배열이 존재한다. 이를 '구조체 배열'이라고 한다.
학생 정보를 저장하는 구조체를 만들어서 수십 수백명의 정보를 저장해야 한다고 하면, 구조체도 일일히 선언하는 것보다 배열을 쓰는 것이 훨씬 편리할 것이다. 구조체 배열을 선언하는 방법은 일반적인 배열을 선언하는 방법과 같다.

특정 배열에 있는 구조체 멤버값을 수정할 수도 있다.
sizeof( )함수를 사용해서 구조체 배열의 크기도 구하였다.

코드를 실행해보면 수정된 체력과 배열의 크기를 확인할 수 있다.
그리고 당연히 구조체도 주소가 있기 때문에 포인터를 사용할 수 있다.
구조체 포인터는 말 그대로 구조체의 주소를 가져와서 사용하는 포인터이다.

예시로 Character라는 별칭의 구조체를 만들고 CharA라는 이름의 구조체 변수를 선언했고,
또 이 구조체 변수의 주소를 담기 위해 CharAPtr 구조체 포인터 변수를 선언하고 CharA의 주소를 대입하였다.
그 후 *을 이용하여 주소에 접근한 후 값을 변경해주고 출력까지 하였다.
여기서 일반 포인터 사용과는 다르게 구조체에서 포인터를 사용할 때 (*CharAPtr).hp 처럼 괄호를 이용했는데,
이것도 연산자 우선순위와 연관이 있다. *CharAPtr.hp로 사용하면 구조체가 아닌 포인터 변수를 참조하려고 하기 때문에 오류가 발생해버린다.
그렇지만 구조체 포인터를 매번 이런식으로 괄호를 붙이는 것은 조금 귀찮을 수 있다.
그래서 C언어 개발자들이 기호를 하나 만들었는데, 바로 -> 기호이다.
이 기호를 사용하면 *과 괄호, . 까지도 안써도 되서 훨씬 편리하고 가독성도 좋다. (주소에 바로 접근하겠다! 라는 느낌으로 받아들이면 된다)

이런식으로 바로 접근해서 사용해도 되고 괄호를 쓰는 것이 좋다고 생각하는 사람은 괄호를 써도 무관하다

하지만 코드를 실행하면 에러가 발생하는데, 문자열을 바로 수정했기 때문이다.
C언어 기초-17 <문자열> 편에서 = 연산자를 이용하여 문자열을 바로 수정하는 것은 불가능해서 다른 방법으로 문자열을 수정하는 방법에 대해 다루었을 것이다. 그 중 배열 포인터를 사용하여 문자열을 수정하는 방법을 알아보자.
이 방법은 구조체를 정의할 때 자료형을 크기가 고정된 배열이 아니라 문자열의 주소를 가리키는 포인터(char* role)로 만드는 것이다.

이렇게 설계하면 배열이 아니기 때문에 = 연산자로 언제든지 자유롭게 문자열을 변경할 수 있다.
%s는 컴퓨터에게 시작주소를 알려주면 그 주소로 가서 첫글자부터 차례대로 읽어서 종료문자를 만날때까지 화면에 출력하는 역할을 한다. 즉 문장 자체를 복사하는 것이 아니라, 메모리 어딘가에 있는 새로운 문자열 상수의 "주소"만 갈아끼우는 방식이다
이 방법에는 단점도 존재한다.
"마법사"가 저장되어 있는 메모리 주소는 읽기 전용(Read-Only)메모리 이므로, 이에 접근하여 글자를 수정하려고 하면 컴퓨터가 프로그램을 강제로 종료시킨다.
파일로 저장하거나 네트워크로 전송할 때 데이터가 증발한다.
예를들어 게임 캐릭터 정보를 파일에 저장하거나 네트워크를 통해 다른 유저에게 보낸다고 하면, 컴퓨터는 구조체의 크기만큼 메모리 주소를 그대로 복사해서 전송할 것이다.
그런데 포인터 방식은 진짜 데이터가 저장되는 것이 아닌 메모리 주소만 저장된다. 컴퓨터마다 메모리 주소가 다 다르기 때문에 나중에 게임을 다시 켜서 그 주소를 열어보거나, 다른 사람 컴퓨터에서 그 주소를 열어보면 엉뚱한 값(쓰레기 값)이 들어 있거나 실행 자체가 되지 않을 것이다.
파일 수정 면에서는 편하고 쉽지만, 파일의 안정성 문제에서는 굉장히 떨어지기 때문에 상황에 맞게 사용하는 것이 좋다.

코드를 실행해보면, 에러 없이 정상적으로 문자열이 출력된 것을 확인할 수 있다.
게임 캐릭터의 정보를 만드는데, 이 캐릭터가 장착하고 있는 무기의 정보도 세부적으로 넣고 싶다고 가정해보자.
무기는 이름, 공격력, 내구도 등의 여러 데이터가 필요할 것이다.
따라서 무기 정보를 저장하는 양식을 만들어놓고, 캐릭터 정보 양식을 만들 때 무기 적는 칸에 아예 무기 정보 양식을 통째로 넣으면 코드의 재사용성이 극대화 될 것이다.
그렇기 위해서는 구조체 안에 구조체를 선언하는데, 이를 '중첩 구조체' 라고 한다.
쉽게 말해서 상위 카테고리(부모 구조체) 안에 하위 카테고리(자식 구조체)를 만든다고 생각하면 된다.

이런 식으로 다른 구조체를 멤버로 포함할 수 있다.
Character안의 Weapon 구조체 멤버에 접근하고 싶으면 player.item.power와 같이 연속으로 멤버를 참조해야 사용할 수 있다. 물론 Weapon 구조체 멤버만 따로 선언해서 사용할 수도 있다.

구조체를 중첩해서 사용하면 여러가지 정보를 관리하기 용이해진다.
구조체의 또 하나 특별한 기능이 있는데, 다른 구조체 뿐만 아니라 심지어 자기 자신을 멤버로 가질 수 있다
이를 "자기 참조 구조체" 라고 한다.

자기 자신을 참조할 때에는 반드시 포인터(*) 형태여야 한다.
아직 구조체 선언이 끝나지도 않았는데 어떻게 구조체 안에 자기자신을 선언할 수 있으며, 중첩 구조체처럼 어떠한 정보를 저장하는 것도 아니고, 자기 자신을 포인터로 가리켜서 어디에 쓰는 것일까?
자기 참조 구조체는 연결 리스트나 트리를 만들 때 사용되는데, 이 연결리스트와 트리는 자료구조에서 나오는 내용이기 때문에 이런 특성이 있구나 하는 것만 알아두면 된다.
함수를 배운 순간부터 함수는 프로그램과 떨어질 수 없는 관계이다. 프로그램의 코드가 길어질수록 기능을 함수로 분리하는 것이 좋은데, 함수에서도 구조체를 사용할 일이 있을 것이다.
구조체를 인자로 전달하는 방법은 2가지가 있다. 바로 포인터로 전달하는 것(Call by Reference)과 구조체 그대로 전달하는 방법(Call by Value)이다.
포인터를 사용하지 않고 넘겨주게 되면 매개변수에 복사되는 것이므로 원본 값에는 영향을 끼치지 않는다는 것을 알고 있을 것이다.
따라서 어떤 변수의 값을 가져와서 출력을 한다던가, 원본값을 수정할 필요가 없는 경우에는 포인터를 사용하지 않고 그냥 복사해서 넘겨주었다.
하지만 구조체의 경우는 여러가지 자료형을 묶어서 새로운 자료형으로 만든 하나의 카테고리이기 때문에, 구조체 크기가 커질 수록 복사할 공간이 더 필요하게 된다. 따라서 공간이 낭비되어 비효율적이기 때문에 매개변수로 구조체를 전달할 때에는 보통 포인터를 사용한다. 값을 바꿀 필요가 없을 경우에도 마찬가지 이다.
포인터 없이 구조체를 통째로 받는 함수(Call by Value)와 구조체의 주소를 받는 함수(Call by Reference)를 비교해보자.

복사본을 가지고 아무리 함수 안에서 값을 변경한다고 하더라도(Call by Value) 함수가 끝나는 순간 복사본은 사라지고 main( )함수 안에 있는 원본 캐릭터의 멤버 변수는 그대로 유지된다. 게다가 구조체가 커져버리면 메모리 낭비도 심해진다.

구조체와 함수를 사용할 때는 조금 귀찮더라도 함수에게 주소(&)를 알려주고, 함수는 그 주소로 찾아가 접근하여 원본을 고치게 하는 것이 좋다.
마지막으로 반대로 함수가 구조체를 return(반환)할 때에는 어떻게 해야할까?
함수에서 구조체를 변경한 뒤 돌려주는 방법도 2가지가 존재한다.
첫 번째는 구조체 덩어리 자체를 리턴하는 것이다.

이 방법은 안전하지만 구조체 크기가 커지만 복사 비용(메모리 부담)이 발생한다.
두 번째는 내부에서 만든 구조체의 주소를 리턴하는 것이다.

이 방법은 좋지 않다. 그 이유는 함수 내부에서 만든 변수(mage)는 함수가 끝나는 순간 메모리에서 소멸된다.
청소되어 사라진 방의 주소(&mage)를 main( )함수에 돌려줘봤자, 나중에 그 주소를 열어보면 쓰레기 값만 가득하거나 프로그램이 터진다. (이를 댕글링 포인터(Dangling Pointer)라고 한다)
그렇다면 함수에서 구조체 포인터를 return 하고 싶을 때는 어떻게 해야 할까?
다음 글에서 다룰 '동적할당(malloc)'을 이용해 컴퓨터가 강제로 청소하지 못하는 수동 메모리 구역(힙)에 구조체를 생성해야만 주소를 안전하게 return 할 수 있다.
VR게임 개발을 위한 언리얼엔진/C++ 공부한 내용 끄적이기...
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!