지금까지 변수는 값 하나를 담는 상자였습니다. 그런데 한 사람의 정보처럼 이름, 나이, 점수가 늘 붙어 다니는 자료를 다루다 보면 변수를 따로 만드는 방식이 금세 불편해집니다. 구조체(structure)는 이렇게 관련 있는 여러 자료를 하나의 묶음으로 만들어 이름 하나로 다루게 해 주는 도구입니다. 왜 필요한지부터 정의하는 법과 멤버를 꺼내 쓰는 법을 예제로 익힙니다.
20.1 변수가 흩어지면 생기는 문제
학생 한 명의 정보를 담는다고 해 봅시다. 이름, 나이, 점수를 각각 변수로 두면 이렇게 됩니다.
char name[20] = "홍길동";
int age = 20;
int score = 95;
한 명일 때는 괜찮지만, 이 셋이 한 사람의 것이라는 사실은 코드 어디에도 적혀 있지 않습니다. 학생이 서른 명이 되면 변수가 아흔 개로 늘고, 함수에 학생 하나를 넘기려면 인자를 세 개씩 따로 넘겨야 합니다. 구조체는 이 셋을 한 덩어리로 묶고 그 덩어리에 이름을 붙입니다.
20.2 구조체 정의와 멤버 접근
struct 키워드로 어떤 자료들을 묶을지 설계도를 먼저 그립니다. 아래 예제를 그대로 따라 쳐서 실행해 보세요.
#include <stdio.h>
struct Student { // 학생이라는 묶음의 설계도
char name[20];
int age;
int score;
};
int main(void) {
struct Student s = {"홍길동", 20, 95};
printf("이름: %s\n", s.name);
printf("나이: %d\n", s.age);
printf("점수: %d\n", s.score);
return 0;
}
실행하면 이렇게 나옵니다.
이름: 홍길동
나이: 20
점수: 95
구조체 안에 묶인 각각의 자료(name, age, score)를 멤버(member)라고 부릅니다. 설계도인 struct Student로 실제 변수 s를 만들고, 중괄호로 멤버 값을 순서대로 채웠습니다. 멤버 하나를 꺼낼 때는 s.name처럼 변수 이름 뒤에 점을 찍고 멤버 이름을 씁니다. 이 점을 멤버 접근 연산자라고 합니다.
![[C 20] 관련 자료를 하나로 묶는 구조체](https://img.thenullpage.com/posts/6555/6555_1_726d86.webp)
20.3 점 연산자로 값 넣고 바꾸기
멤버는 읽기만 하는 것이 아니라 보통 변수처럼 값을 새로 넣을 수 있습니다. 화면의 한 점 좌표를 다루는 예제입니다.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p;
p.x = 3; // 멤버에 하나씩 대입
p.y = 4;
printf("처음: (%d, %d)\n", p.x, p.y);
p.x = p.x + 10; // x만 이동
printf("이동: (%d, %d)\n", p.x, p.y);
return 0;
}
출력은 다음과 같습니다.
처음: (3, 4)
이동: (13, 4)
정의할 때 한꺼번에 채우지 않아도, 나중에 p.x = 3;처럼 멤버마다 따로 값을 넣을 수 있습니다. 주의. 구조체 변수를 만들면서 초기화하지 않으면 멤버에는 쓰레기 값이 들어 있습니다. 값을 넣기 전에 읽으면 정의되지 않은 동작(undefined behavior)이니, 먼저 대입한 뒤에 사용합니다.
20.4 여러 개를 배열로 묶기
구조체의 진짜 쓸모는 같은 종류의 묶음이 여러 개일 때 드러납니다. 학생 세 명을 배열 하나에 담아 봅니다.
#include <stdio.h>
struct Student {
char name[20];
int score;
};
int main(void) {
struct Student cls[3] = {
{"철수", 80},
{"영희", 95},
{"민수", 70}
};
for (int i = 0; i < 3; i++) {
printf("%s: %d점\n", cls[i].name, cls[i].score);
}
return 0;
}
결과입니다.
철수: 80점
영희: 95점
민수: 70점
cls는 학생 구조체 세 칸짜리 배열입니다. i번째 학생의 점수는 cls[i].score처럼 배열 접근과 멤버 접근을 이어서 씁니다. 학생이 백 명이 되어도 반복문 한 줄이면 전부 훑습니다.
20.5 상품 재고 다루기
상품 이름, 단가, 수량을 묶어 두고 재고 금액을 구하는 예제입니다.
#include <stdio.h>
struct Product {
char name[20];
int price;
int stock;
};
int main(void) {
struct Product item = {"연필", 500, 12};
int total = item.price * item.stock;
printf("%s 재고 금액: %d원\n", item.name, total);
return 0;
}
출력은 이렇습니다.
연필 재고 금액: 6000원
멤버는 그냥 변수처럼 계산식에 넣을 수 있습니다. item.price * item.stock은 단가 곱하기 수량이라는 뜻이 코드에 그대로 드러납니다.
20.6 구조체를 함수에 넘기기
학생에게 보너스 점수를 더해 주는 함수를 만들어 봅시다. 먼저 흔히 저지르는 실수부터 보겠습니다.
void addBonus(struct Student s, int bonus) {
s.score += bonus; // 사본만 바뀜
}
함정. 구조체를 이렇게 값으로 넘기면 멤버 전체가 통째로 복사됩니다. 함수 안의 s는 원본이 아니라 사본이라, s.score를 아무리 올려도 호출한 쪽의 학생은 그대로입니다. 원본을 바꾸려면 주소를 넘겨야 합니다. 함정. 반대로 구조체 안에 포인터 멤버가 있으면, 복사할 때 그 포인터가 가리키는 데이터가 아니라 주소만 복사됩니다(얕은 복사). 두 구조체가 같은 데이터를 가리키게 됩니다.
#include <stdio.h>
struct Student {
char name[20];
int score;
};
void addBonus(struct Student *s, int bonus) {
s->score += bonus; // 포인터는 화살표로 접근
}
int main(void) {
struct Student a = {"철수", 80};
addBonus(&a, 10); // 주소를 넘김
printf("%s: %d점\n", a.name, a.score);
return 0;
}
이제 원본이 바뀝니다.
철수: 90점
여기서 s는 구조체를 가리키는 포인터입니다. 포인터가 가리키는 구조체의 멤버에 접근할 때는 점이 아니라 화살표 ->를 씁니다. s->score는 사실 (*s).score, 즉 s가 가리키는 구조체의 score라는 뜻을 짧게 쓴 것입니다. 함정. 포인터에 점을 찍어 s.score라고 쓰면 컴파일 오류가 납니다. 변수에는 점, 포인터에는 화살표. 이 구분만 기억하면 됩니다. 덤으로 구조체가 크면 통째로 복사하는 것보다 주소만 넘기는 편이 빠릅니다.
20.7 typedef로 이름 짧게 부르기
지금까지 변수를 만들 때마다 struct Student, struct Point처럼 struct를 매번 붙였습니다. typedef로 별명을 붙이면 이 struct를 생략할 수 있습니다.
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
int main(void) {
Point p = {3, 4}; // struct 없이
printf("(%d, %d)\n", p.x, p.y);
return 0;
}
출력은 (3, 4)입니다. typedef struct { ... } Point; 는 이 구조체에 Point라는 별명을 붙이라는 뜻입니다. 이후로는 struct Point 대신 그냥 Point라고 쓰면 되니 코드가 한결 깔끔해지고, 실제 라이브러리도 대부분 이 방식으로 구조체 이름을 짓습니다.
구조체는 결국 관련 있는 자료를 한 이름으로 묶는 도구이고, 멤버를 꺼내 쓰는 방법은 두 가지뿐입니다. 손에 쥔 것이 구조체 변수면 점, 구조체를 가리키는 포인터면 화살표입니다. 함수에서 원본을 바꾸려면 주소를 넘겨 화살표로 접근하고, typedef로 이름을 줄일 수 있다는 것까지 익히면 구조체의 기본은 충분합니다.