삶의 공유
C++ STL 핵심 파헤치기: vector, list, deque... 대체 언제, 무엇을 써야 할까? 본문
C++ STL 핵심 파헤치기: vector, list, deque... 대체 언제, 무엇을 써야 할까?
dkrehd 2025. 11. 3. 21:49🚀 C++ STL 핵심 파헤치기: vector, list, deque... 대체 언제, 무엇을 써야 할까?
안녕하세요! C++ 전문가이자 여러분의 코딩 학습을 돕는 블로그 작성 전문가입니다. C++ 표준 템플릿 라이브러리(STL)를 사용하다 보면 vector, list, deque 등 비슷한 듯 다른 컨테이너들 때문에 고민에 빠질 때가 많습니다.
"데이터를 순서대로 저장하고 싶은데... 어떤 걸 써야 성능이 잘 나올까?"
이 질문에 대한 명쾌한 해답을 드리기 위해, 오늘은 C++의 **선형 컨테이너(Sequence Container)**들을 속속들이 파헤쳐 보겠습니다. 이들의 동작 원리와 '의도된 설계'를 이해하면, 여러분의 코드는 더 빠르고 효율적으로 변할 것입니다!
🧐 선형 컨테이너란?
가장 기본부터 짚고 넘어가죠. 선형 컨테이너는 말 그대로 "모든 요소들이 삽입된 순서대로 한 줄로 놓여있는" 자료 구조를 말합니다. 데이터를 넣은 순서가 그대로 유지되는 것이죠.
C++ STL은 대표적으로 5가지 선형 컨테이너를 제공합니다. 이들의 핵심 차이는 **'메모리를 어떻게 관리하는가'**에 있습니다.
- std::vector (벡터): [연속된 메모리]
- 가장 일반적인 배열과 유사한 구조입니다. 메모리상에 요소들이 빈틈없이 나란히 붙어있습니다.
- std::list (리스트): [이중 연결 리스트, 떨어진 메모리]
- 각 요소가 (데이터, 이전 요소 주소, 다음 요소 주소)를 갖습니다. 요소들은 메모리 여기저기에 흩어져 있고, '포인터'로 서로 연결됩니다.
- std::deque (덱, 'Deck'으로 발음): [부분적으로 연속된 메모리]
- vector와 list의 장점을 절충한 형태로, 여러 개의 연속된 메모리 덩어리(chunk)를 관리합니다.
- std::forward_list (단일 연결 리스트):
- list와 비슷하지만, '다음 요소 주소'만 가집니다. (단방향)
- std::array (배열):
- vector와 비슷하지만, 크기가 컴파일 타임에 고정됩니다.
이 컨테이너들은 내부 구조는 달라도, push_back, pop_back, size 등 대부분의 멤버 함수 이름을 동일하게 제공합니다. 이는 개발자가 필요에 따라 컨테이너 타입만 바꿔가며 성능을 테스트하기 용이하게 하려는 STL의 큰 그림입니다.
💻 코드로 보는 '의도된 설계'의 비밀
이제 예제 코드를 통해 이 컨테이너들이 어떻게 다르게 동작하는지, 그리고 왜 그렇게 설계되었는지 깊이 있게 살펴보겠습니다.
#include <iostream>
#include <vector> // 연속 메모리 배열
#include <list> // 이중 연결 리스트
#include <deque> // 부분 연속 메모리
#include <forward_list> // 단일 연결 리스트
#include <array> // 고정 크기 배열
int main()
{
// 1. 선형 컨테이너의 메모리 구조
std::vector v = {1,2,3,4,5};
std::list s = {1,2,3,4,5};
std::deque d = {1,2,3,4,5};
// 2. 대부분의 멤버 함수는 이름(사용법)이 동일합니다.
v.push_back(2); // vector의 맨 뒤에 2 추가
s.push_back(2); // list의 맨 뒤에 2 추가
d.push_back(2); // deque의 맨 뒤에 2 추가
// 3. 사용법이 다른 경우가 있다면 의도적인 설계입니다.
// v.push_front(3); // [설명 필요 1] 컴파일 에러!
s.push_front(3); // list의 맨 앞에 3 추가 (빠름)
d.push_front(3); // deque의 맨 앞에 3 추가 (빠름)
v[4] = 5; // vector의 5번째 요소에 5 대입 (빠름)
// s[4] = 5; // [설명 필요 2] 컴파일 에러!
// 4. 사용법이 유사하므로 컨테이너를 변경해도
std::vector c = {1,2,3,4}; // 만약 성능 이슈가 있다면 std::list c = ... 로 변경 가능
c.push_back(10);
c.pop_back();
}
1. 공통된 사용법: push_back
v.push_back(2);
s.push_back(2);
d.push_back(2);
- 동작 방식: push_back은 컨테이너의 맨 끝에 요소를 추가하는 함수입니다.
- 핵심 이유: vector, list, deque 모두 '맨 끝'이 어디인지 즉시 알고 있습니다.
- vector는 끝에 공간이 있으면 그냥 넣고, 없으면 더 큰 메모리를 할당받아 이사한 뒤 넣습니다. (평균적으로 $O(1)$)
- list는 '꼬리(tail)' 포인터가 가리키는 곳 뒤에 새 요소를 붙이고 꼬리 포인터를 업데이트합니다. ($O(1)$)
- deque도 마지막 청크(chunk)의 끝에 요소를 추가합니다. ($O(1)$)
이처럼 끝에 추가하는 작업은 세 컨테이너 모두 매우 효율적이라 공통으로 제공됩니다.
2. [설명 필요 1] vector에 push_front가 없는 이유
// v.push_front(3); // 컴파일 에러!
s.push_front(3);
d.push_front(3);
- 동작 방식: push_front는 컨테이너의 맨 앞에 요소를 추가합니다. list와 deque는 이 기능을 아주 잘 지원합니다.
- vector에만 없는 이유: 이는 vector의 연속된 메모리 구조 때문이며, 치명적인 성능 저하를 막기 위한 의도적인 설계입니다.
- vector가 {1, 2, 3, 4, 5}를 메모리에 저장한 방식은 [1][2][3][4][5]와 같습니다.
- 여기에 push_front(3)을 하려면, [3]을 맨 앞에 넣어야 합니다.
- 하지만 0번 인덱스에는 이미 [1]이 있죠. 방법은 하나뿐입니다.
- [5]를 한 칸 뒤로 민다.
- [4]를 한 칸 뒤로 민다.
- [3]을 한 칸 뒤로 민다.
- [2]를 한 칸 뒤로 민다.
- [1]을 한 칸 뒤로 민다.
- 비어있는 맨 앞자리에 [3]을 넣는다.
- 만약 vector에 요소가 100만 개 있다면, **100만 번의 요소 복사(이동)**가 발생합니다. 이는 엄청난 성능 낭비입니다 ($O(N)$).
- C++ STL 설계자들은 "개발자가 실수로라도 이렇게 비효율적인 동작을 시키는 것을 막자!"라는 의도로 push_front 멤버 함수 자체를 제공하지 않는 것입니다.
- 반면, list는 맨 앞(head) 포인터만 바꾸면 되고 ($O(1)$), deque는 앞쪽 메모리 청크에 추가하면 되므로 ($O(1)$) 매우 빠릅니다.
3. [설명 필요 2] list가 [] 연산자를 지원하지 않는 이유
v[4] = 5; // OK
// s[4] = 5; // 컴파일 에러!
- 동작 방식: [] (인덱스 연산자)는 "n번째 요소에 즉시 접근"하는 기능입니다.
- list에만 없는 이유: 이 역시 list의 연결 리스트 메모리 구조 때문이며, 비효율적인 연산을 막기 위한 의도적인 설계입니다.
- vector는 {1, 2, 3, 4, 5}가 [1][2][3][4][5]로 붙어있습니다.
- v[4] (5번째 요소)를 찾으라고 하면, CPU는 (시작 주소 + 4 * 요소 크기) 공식을 통해 단 한 번의 계산으로 v[4]의 메모리 주소를 찾아냅니다. 이를 **임의 접근(Random Access)**이라 하며, 믿을 수 없을 만큼 빠릅니다 ($O(1)$).
- 하지만 list는 요소들이 메모리에 [1] -> [2] -> [3] -> [4] -> [5]처럼 흩어져 있습니다.
- s[4] (5번째 요소)를 찾으라고 하면, list는 v[4]처럼 한 번에 주소를 계산할 방법이 없습니다.
- 무조건 맨 앞(head)에서 시작합니다. ([1])
- [1]의 '다음' 포인터를 따라 [2]로 이동합니다.
- [2]의 '다음' 포인터를 따라 [3]으로 이동합니다.
- [3]의 '다음' 포인터를 따라 [4]로 이동합니다.
- [4]의 '다음' 포인터를 따라 [5]에 도착합니다.
- 요소가 100만 개라면, 100만 번째 요소를 찾기 위해 99만 9999번의 포인터 이동을 해야 합니다 ($O(N)$).
- vector의 $O(1)$ 접근과 비교하면 재앙 수준의 성능 차이죠.
- STL 설계자들은 [] 연산자가 $O(1)$의 속도를 보장해야 한다고 생각했고, $O(N)$이 걸리는 list에서는 이 연산자를 아예 막아버려 개발자가 성능 문제를 인지하도록 유도했습니다.
🤔 그럼, 어떤 컨테이너를 선택해야 할까요?
각 컨테이너의 장단점을 기준으로 최적의 선택 가이드를 제시해 드립니다.
1. std::vector (기본이자 최고의 선택)
- 장점:
- 메모리 연속성: 요소들이 메모리에 나란히 붙어있어 **캐시 적중률(Cache Hit Rate)**이 매우 높습니다. CPU가 하나의 요소를 읽을 때 근처의 요소들도 함께 캐시 메모리로 가져오므로, 순차 접근 속도가 압도적으로 빠릅니다.
- 빠른 임의 접근: [] 연산자를 통한 $O(1)$ 접근이 가능합니다.
- 단점:
- 중간/앞부분 삽입 및 삭제가 매우 느립니다. (모든 요소를 뒤로 밀거나 앞으로 당겨야 함, $O(N)$)
- ✅ 추천:
- 거의 모든 상황에서 vector를 가장 먼저 고려하세요.
- 데이터의 크기를 예측하기 어렵고, 중간 삽입/삭제가 거의 없으며, 순차 접근이나 임의 접근이 빈번할 때.
2. std::list
- 장점:
- 중간/앞부분 삽입 및 삭제가 매우 빠릅니다. (포인터 몇 개만 바꾸면 됨, $O(1)$)
- 단점:
- 임의 접근이 불가능합니다. ([] 연산자 없음, n번째 요소 접근은 $O(N)$)
- 캐시 적중률이 낮습니다. (요소들이 메모리에 흩어져 있음)
- 이전/다음 포인터 때문에 요소당 메모리 오버헤드가 큽니다.
- ✅ 추천:
- 데이터의 중간(특정 위치)에서 삽입/삭제가 매우 빈번하게 일어날 때.
3. std::deque
- 장점:
- vector처럼 []를 통한 임의 접근이 빠릅니다. ($O(1)$이지만 vector보단 약간 느림)
- list처럼 맨 앞(push_front)과 맨 뒤(push_back) 모두 $O(1)$로 빠릅니다.
- 단점:
- list와 달리 중간 삽입/삭제는 vector처럼 느립니다. ($O(N)$)
- vector보다 메모리 구조가 복잡하여 캐시 효율이 약간 떨어질 수 있습니다.
- ✅ 추천:
- 양쪽 끝(앞, 뒤)에서 데이터의 삽입/삭제가 빈번하게 필요할 때. (예: 큐(Queue) 자료구조를 구현할 때)
🔑 핵심 요약
복잡한 내용을 간단히 요약해 드립니다!
- 선형 컨테이너는 데이터를 순서대로 저장하며, 대표 주자는 vector, list, deque입니다.
- std::vector (연속 메모리): 접근이 빠르고 ([]) 캐시 효율이 최고입니다. 중간 삽입/삭제는 재앙입니다. 대부분의 상황에서 정답입니다.
- std::list (연결 리스트): 중간 삽입/삭제가 빠릅니다. 대신 접근이 느리고 ([] 없음) 메모리를 더 씁니다.
- std::deque (부분 연속 메모리): 양쪽 끝(앞/뒤) 삽입/삭제가 빠르고, 접근도 빠릅니다 ([]). vector와 list의 장점을 합친 하이브리드입니다.
- vector에 push_front가 없거나 list에 []가 없는 것은, **치명적인 성능 저하를 막기 위한 STL 설계자들의 '의도적인 배려'**입니다.
이제 여러분은 각 컨테이너의 특성을 정확히 이해하셨습니다. 데이터의 사용 패턴을 분석하여 가장 효율적인 컨테이너를 자신 있게 선택하시길 바랍니다!
'Programing언어 > C++' 카테고리의 다른 글
| 배열(Array) vs 벡터(Vector), 도대체 무엇을 써야 할까? (feat. std::array) (0) | 2025.11.30 |
|---|---|
| C++ 벡터의 capacity와 size 완전 이해하기 (0) | 2025.11.18 |
| C++ 상속에서의 복사 생성자와 대입 연산자 완전 정리 (0) | 2025.10.18 |
| C++에서 Shallow Copy와 Deep Copy 완벽 정리 — 복사의 진짜 차이를 이해하자 (0) | 2025.10.16 |
| C++ ADL (Argument Dependent Lookup) 완벽 정리 (0) | 2025.10.15 |