임베디드 환경에서는 ArrayList 같은 편한 도구를 쓸 수 없다. RAM이 수백 KB밖에 안 되는 MCU에서는 데이터가 메모리 어디에, 어떻게 올라가는지 직접 따져야 한다. 힙 할당도 단편화 위험 때문에 웬만하면 쓰지 않는다.
배열은 가장 기본이면서 임베디드에서도 현역으로 쓰이는 자료구조다. C로 직접 구현하면서 메모리 레이아웃과 포인터 동작을 같이 짚어본다.
정적 배열 (Static Array)
| |
이 한 줄이 선언되는 순간 스택(Stack) 에 5 × 4 = 20바이트가 연속으로 잡힌다.
arr은 첫 번째 요소의 주소(0x1000)를 가리키는 포인터다. arr[2]에 접근한다는 건 *(arr + 2), 즉 0x1000 + 2×4 = 0x1008 주소를 직접 읽는 것이다.
메모리 설계
| 항목 | 값 |
|---|---|
| 요소 타입 | int (4바이트) |
| 요소 개수 | 5 |
| 총 메모리 사용량 | 20바이트 |
| 저장 위치 | Stack |
| 접근 시간복잡도 | O(1) |
정적 배열의 한계
크기가 컴파일 타임에 고정된다. 사용자 입력에 따라 크기가 달라져야 하는 상황이라면 쓸 수 없다.
VLA는 스택 오버플로우 위험이 있고, C11부터는 선택사항(optional)이다. 실무나 코딩테스트에서는 쓰지 않는 게 낫다.
동적 배열 (Dynamic Array)
런타임에 크기를 결정해야 할 때 malloc()으로 힙(Heap) 에 메모리를 직접 할당한다.
C++의 std::vector, Java의 ArrayList가 바로 이 방식으로 구현된 자료구조다. 내부적으로 동일하게 힙 할당 + Doubling Strategy를 사용한다. 이 글에서는 그 구조를 C로 직접 구현해본다.
스택에는 DynamicArray 구조체(포인터 + int 2개 = 16바이트)만 올라가고, 실제 데이터 블록은 힙에 연속으로 저장된다. da->data가 그 힙 주소를 가리키는 포인터다.
메모리 설계
| 항목 | 값 |
|---|---|
| 포인터 크기 (스택) | 8바이트 (64bit) |
| 데이터 크기 (힙) | sizeof(int) × n 바이트 |
| n = 5일 때 총 사용량 | 8 + 20 = 28바이트 |
| 저장 위치 | 포인터 → Stack, 데이터 → Heap |
| 접근 시간복잡도 | O(1) |
구현
구조체 정의
size와 capacity를 분리한 이유가 있다. 요소를 추가할 때마다 realloc()을 호출하면 성능이 나쁘다. capacity를 2배씩 늘리는 방식(Doubling Strategy)으로 realloc() 호출 횟수를 O(log n)으로 줄인다.
함수 구현
| |

realloc() 동작 방식: 현재 블록 뒤에 공간이 있으면 확장, 없으면 새 위치에 복사 후 이전 블록 해제.
Main (테스트 및 성능 측정)
| |
실행하면 아래와 같은 결과를 볼 수 있다.
1,000만 개를 넣는데 realloc()은 단 23번. capacity가 4 → 8 → 16 → … → 16,777,216으로 두 배씩 늘어나기 때문이다. 매번 realloc()을 호출했다면 1,000만 번 호출했을 것이다.
정적 배열 vs 동적 배열 비교
| 항목 | 정적 배열 | 동적 배열 |
|---|---|---|
| 크기 결정 시점 | 컴파일 타임 | 런타임 |
| 저장 위치 | Stack | Heap |
| 크기 변경 | 불가 | realloc()으로 가능 |
| 접근 속도 | O(1) | O(1) |
| 메모리 해제 | 자동 (스코프 종료 시) | free() 필수 |
| 사용 상황 | 크기가 고정된 경우 | 크기가 가변적인 경우 |