배열은 "같은 자료형"만 묶을 수 있었다. 하지만 학생 한 명은 학번(정수), 이름(문자열), 학점(실수)처럼 서로 다른 자료형의 정보로 이뤄진다. 이렇게 여러 자료형을 하나로 묶어 새 자료형을 만드는 것이 구조체다. 여기에 실행 중 메모리를 빌리는 동적 할당까지 더하면, 마침내 연결 리스트 같은 자료구조를 만들 수 있다.
1. 구조체 (Structure)
서로 다른 자료형의 여러 변수를 하나로 묶어 새로운 자료형을 정의하는 문법이다. 관련 있는 데이터를 한 덩어리로 다룰 수 있게 해준다.

struct student {
int number; // 학번
char name[10]; // 이름
double grade; // 학점
};
struct키워드로 정의를 시작한다.student는 태그 이름으로, 이 구조체의 새 자료형 식별자가 된다.- 중괄호 안에 묶을 멤버 변수들을 선언한다.
멤버 접근: . 연산자
struct student s1;
s1.number = 20261004;
s1.grade = 4.25;
배열은 a = b 로 복사할 수 없었지만, 구조체는 s2 = s1; 로 모든 멤버가 한 번에 복사된다. 다만 == 로 두 구조체가 같은지 비교하는 건 안 되므로, 비교는 멤버를 하나씩 해야 한다.
배열은 통째 복사가 안 됐는데 구조체는 =로 된다는 게 처음엔 헷갈렸다. "배열 이름은 주소지만, 구조체 변수는 값 자체"라는 차이로 이해하니 정리가 됐다.
구조체 포인터: -> 연산자
구조체를 가리키는 포인터로 멤버에 접근할 때는 .이 아니라 ->를 쓴다.
struct student* p = &s1;
printf("%d\n", p->number); // (*p).number 와 같다
p->number는 (*p).number의 줄임 표기다. 포인터를 역참조해서 멤버에 접근하는 일이 워낙 잦아 전용 연산자를 둔 것이다.
구조체와 함수
구조체를 함수에 그냥 넘기면 일반 변수처럼 복사본이 전달된다. 그런데 구조체가 크면 통째로 복사하는 비용이 만만치 않다. 그래서 보통 구조체 포인터로 전달해 복사를 피하고, 함수가 원본을 직접 다루게 한다.
2. 메모리 동적 할당
지금까지의 배열은 크기를 컴파일 시점에 미리 정해야 했다. "사용자가 입력한 개수만큼"처럼 실행 중에 크기를 정하려면 동적 할당이 필요하다.

| 구분 | 정적 메모리 | 동적 메모리 |
|---|---|---|
| 크기 결정 | 컴파일 시점 | 실행 시점 |
| 확보 주체 | 컴파일러 자동 | 프로그래머가 직접 요청 |
| 저장 영역 | 스택·데이터 영역 | 힙(Heap) 영역 |
| 해제 | 자동 | 프로그래머가 직접 |
malloc과 free

malloc으로 힙에서 메모리를 빌리고, 다 쓰면 free로 반납한다(stdlib.h 필요).
int* p = (int*)malloc(3 * sizeof(int)); // 정수 3개 크기만큼 할당
if (p != NULL) { // 할당 성공 확인
p[0] = 10; p[1] = 20; p[2] = 30;
// ... 사용 ...
}
free(p); // 사용 후 반드시 해제
동적 할당한 메모리는 자동으로 해제되지 않는다. free를 하지 않으면 프로그램이 점점 메모리를 차지하다 결국 바닥나는 메모리 누수(memory leak)가 생긴다. 또 malloc이 실패하면 NULL을 반환하므로, 할당 직후 NULL 검사를 하는 습관이 안전하다.
malloc으로 빌린 메모리를 p[i]처럼 배열같이 쓸 수 있다는 점에서, 포인터와 배열이 한 몸이라던 게 또 확인됐다. free를 빼먹어도 짧은 프로그램에선 티가 안 나서 처음엔 대수롭지 않게 봤는데, "malloc과 free는 짝"이라는 원칙을 손에 익히기로 했다.
3. 연결 리스트 (Linked List)
구조체와 포인터, 동적 할당이 만나면 비로소 진짜 자료구조를 만들 수 있다. 대표가 연결 리스트다.

연결 리스트는 각 항목(노드)이 다음 항목을 가리키는 포인터를 품어, 노드들이 사슬처럼 이어진 구조다. 노드는 데이터 필드와 링크 필드로 이뤄진다.
typedef struct NODE {
int data; // 데이터 필드
struct NODE* link; // 링크 필드: 다음 노드를 가리킴 (자기 참조)
} NODE;
구조체 안에 같은 타입의 구조체를 가리키는 포인터가 들어 있는데, 이를 자기 참조 구조체라 한다. 맨 끝 노드의 링크에는 더 가리킬 곳이 없으니 NULL을 넣어 끝을 표시한다.
배열은 크기가 고정이고 중간 삽입·삭제가 번거롭지만, 연결 리스트는 노드를 동적으로 만들어 포인터만 바꿔 끼우면 되므로 크기와 삽입·삭제가 유연하다. 구조체·포인터·동적 할당이라는 C의 세 도구가 모두 모여야 완성되는 구조라, C 학습의 한 종착점이라 할 만하다.
4. 수업 코드로 보는 학생 구조체 배열
student_struct.c는 학생 한 명의 이름, 나이, 전공, 성적을 구조체로 묶고, 구조체 배열을 함수에 넘겨 입력·출력을 나눈 예제였다.
입력 함수 내부 흐름은 행 하나가 학생 한 명이라고 보면 된다.
#define STD_NAME_LEN 16
#define STD_NO 1
struct Student
{
char sName[STD_NAME_LEN];
int iAge;
char sDepartment[STD_NAME_LEN];
double dScore;
};
void getStudentInfo(struct Student stStudent[]);
void printStudentInfo(struct Student stStudent[]);
int main(void)
{
struct Student stStudent[STD_NO];
getStudentInfo(stStudent);
printStudentInfo(stStudent);
return 0;
}
void getStudentInfo(struct Student stStudent[])
{
int iStudentIndex;
for (iStudentIndex = 0; iStudentIndex < STD_NO; iStudentIndex++)
{
printf("학생의 이름을 입력: ");
scanf("%15s", stStudent[iStudentIndex].sName);
printf("학생의 나이를 입력: ");
scanf("%d", &stStudent[iStudentIndex].iAge);
printf("학생의 전공을 입력: ");
scanf("%15s", stStudent[iStudentIndex].sDepartment);
printf("학생의 성적을 입력: ");
scanf("%lf", &stStudent[iStudentIndex].dScore);
}
}
void printStudentInfo(struct Student stStudent[])
{
int iStudentIndex;
int iStudentNo;
for (iStudentIndex = 0; iStudentIndex < STD_NO; iStudentIndex++)
{
iStudentNo = iStudentIndex + 1;
printf("#%d번째 학생 %s은 %d살이고 %s 학과이며 성적은 %.2lf입니다.\n",
iStudentNo,
stStudent[iStudentIndex].sName,
stStudent[iStudentIndex].iAge,
stStudent[iStudentIndex].sDepartment,
stStudent[iStudentIndex].dScore);
}
}
배열에서는 stStudent[iStudentIndex]로 학생 한 명을 고르고, 점 연산자(.)로 그 학생의 멤버를 고른다. 즉 접근 순서는 몇 번째 학생인가 → 그 학생의 어떤 정보인가다.
stStudent[0].sName
stStudent[0].iAge
stStudent[0].sDepartment
stStudent[0].dScore
구조체 배열은 2차원 배열처럼 표 데이터를 다루지만, 각 열의 자료형이 달라도 된다는 점이 다르다. 이름은 문자열, 나이는 정수, 성적은 실수처럼 현실 데이터에 더 잘 맞는다.
오늘 느낀 점
- 구조체·포인터·동적 할당이 따로 배울 땐 추상적이었는데, 연결 리스트 하나에서 셋이 전부 모이는 걸 보니 "이걸 만들려고 그동안 배웠구나" 싶었다.
- 배열은 통째 복사 불가, 구조체는 가능이라는 차이가 "이름이 주소냐, 값이냐"에서 갈린다는 걸 알고 두 개념이 하나로 정리됐다.
- malloc/free를 짝으로 보는 습관이 작은 프로그램에선 안 와닿지만, 오래 도는 프로그램에선 치명적이라는 걸 머리로라도 새겨뒀다.
한 걸음 더
typedef를 쓰면struct student대신Student처럼 짧은 이름으로 자료형을 부를 수 있다. 매번struct를 붙이는 번거로움을 덜어줘, 실무 코드의 구조체는 대부분 typedef와 함께 정의된다.- 동적 할당의 메모리 누수는 작은 프로그램에선 티가 안 나지만, 오래 도는 서버에선 치명적이다. 그래서 "malloc과 free를 짝으로 생각하라"는 원칙이 강조되고, 현대 C++에선 이를 자동화하는 스마트 포인터가, 다른 언어에선 가비지 컬렉터가 이 부담을 대신 진다. C는 그 관리를 전부 프로그래머에게 맡기는 만큼, 메모리를 가장 밑바닥에서 이해하게 해준다.
- 연결 리스트의 노드들은 힙 여기저기에 흩어져 있어, 배열처럼 메모리에 연속으로 놓이지 않는다. 그래서 임의 접근(n번째를 바로 찾기)은 배열보다 느리지만, 삽입·삭제는 포인터만 바꾸면 되어 빠르다. "무엇이 빠르고 무엇이 느린가"의 이 맞교환이 자료구조 공부의 핵심 주제로 이어진다.
'language > c' 카테고리의 다른 글
| [C언어] 09. 포인터 (Pointer) (0) | 2026.08.31 |
|---|---|
| [C언어] 08. 문자열 (String) (0) | 2026.08.24 |
| [C언어] 07. 배열 (Array) (0) | 2026.08.17 |
| [C언어] 06. 함수 (Function) (0) | 2026.08.10 |
| [C언어] 05. 반복문 (0) | 2026.08.03 |