std::map
은 C++ 표준 라이브러리에서 제공하는 핵심적인 연관 컨테이너(associative container) 중 하나다.
키(Key)와 값(Value)을 쌍으로 묶어 저장하며, 키를 기반으로 값을 효율적으로 검색, 삽입, 삭제할 수 있게 해준다.
1. 이론
1.1. std::map의 개념과 특징
std::map
은 데이터를 키-값(Key-Value) 쌍으로 저장한다. 사전에서 단어(키)를 찾아 뜻(값)을 찾는 것과 유사하다.
주요 특징은 다음과 같다.
- 고유한 키:
std::map
내의 모든 키는 유일해야 한다. 중복된 키를 허용하지 않는다. - 자동 정렬: 삽입되는 모든 원소는 키를 기준으로 자동 정렬된다. 기본적으로는 오름차순(
std::less
)으로 정렬되며, 사용자가 정렬 기준을 직접 지정할 수도 있다. - 연관 컨테이너: 배열처럼 인덱스를 통해 원소에 접근하는 것이 아니라, 키를 통해 값에 접근한다.
1.2. 내부 구현과 계층 구조 (Red-Black Tree)
std::map
은 일반적으로 레드-블랙 트리(Red-Black Tree)라는 자가 균형 이진 탐색 트리(Self-Balancing Binary Search Tree)로 구현된다.
레드-블랙 트리의 5가지 규칙
- 모든 노드는 레드(Red) 또는 블랙(Black) 색상을 가진다.
- 루트 노드는 항상 블랙이다.
- 모든 리프 노드(NIL,
nullptr
)는 블랙이다. - 레드 노드의 자식은 반드시 블랙이다. (즉, 레드 노드가 연속으로 나타날 수 없다.)
- 임의의 노드에서부터 그 자손인 리프 노드에 이르는 모든 경로에는 동일한 개수의 블랙 노드가 존재한다.
이 규칙들 덕분에 트리는 항상 유사 균형(Approximately Balanced) 상태를 유지한다. 트리의 높이가 O(log N)
(N은 노드의 개수)으로 유지되므로, 검색, 삽입, 삭제 연산의 시간 복잡도가 O(log N)
임을 보장할 수 있다.
동작 계층 구조
- 사용자 영역 (User Code):
my_map[key] = value;
와 같은 코드를 작성. - C++ 표준 라이브러리 (
<map>
):operator[]
호출 시,std::map
내부의 레드-블랙 트리에서key
를 탐색.- 키 존재 시: 해당 노드의 값(value)에 대한 참조를 반환.
- 키 부재 시:
- 새로운 (키, 기본값) 쌍으로 노드를 생성.
std::allocator
를 사용해 힙(Heap) 메모리에 노드를 할당.- 레드-블랙 트리의 규칙에 맞게 트리를 재조정(회전, 색상 변경)하여 균형을 맞춤.
- 새로 생성된 값에 대한 참조를 반환.
- C 런타임 라이브러리 (CRT):
std::allocator
는 내부적으로new
연산자를 호출하고, 이는 다시malloc()
과 같은 저수준 메모리 할당 함수를 호출한다. CRT는 자체적인 메모리 풀을 관리하며 효율적인 할당을 시도한다. - 운영체제 (OS Kernel): CRT의 메모리 풀이 부족해지면, 운영체제에 시스템 콜(System Call, 예: Windows의
VirtualAlloc
, Linux의brk
또는mmap
)을 통해 추가적인 메모리 페이지를 요청한다. 커널은 물리 메모리를 프로세스의 가상 주소 공간에 매핑해준다. - 하드웨어 (MMU): CPU의 메모리 관리 장치(MMU)가 가상 주소를 실제 물리 주소로 변환하여 최종적으로 메모리에 접근한다.
1.3. std::map
vs std::unordered_map
특징 | std::map |
std::unordered_map |
---|---|---|
내부 구현 | 레드-블랙 트리 (자가 균형 이진 탐색 트리) | 해시 테이블 (Hash Table) |
정렬 | 키에 따라 자동 정렬됨 | 정렬되지 않음 (무작위 순서) |
시간 복잡도 | 검색, 삽입, 삭제: O(log N) | 검색, 삽입, 삭제: 평균 O(1), 최악 O(N) |
메모리 사용 | 노드마다 포인터(부모, 자식)를 저장하므로 추가 오버헤드 발생 | 해시 테이블 구조로 인해 더 많은 메모리를 사용할 수 있음 |
주요 사용처 | - 데이터의 정렬된 순회가 필요할 때 - 범위 기반 검색( lower_bound , upper_bound )이 필요할 때- 예측 가능한 성능이 중요할 때 |
- 가장 빠른 평균 속도로 데이터를 조회해야 할 때 - 데이터의 순서가 중요하지 않을 때 - 캐싱(Caching) 구현 등 |
언제 무엇을 사용해야 할까?
std::map
: 아이템 ID, 몬스터 ID 순으로 정렬된 목록을 출력해야 하거나, 특정 점수 범위 내의 플레이어를 찾는 등 순서가 중요하고 범위 검색이 필요할 때 적합하다.std::unordered_map
: 캐릭터의 스킬 쿨타임을 ID로 빠르게 조회하거나, 리소스 파일 경로를 이름으로 즉시 찾아 로드하는 등 순서와 상관없이 최대한 빠른 접근이 필요할 때 좋다.
2. 실습 코드 (게임 인벤토리 관리)
다음은 std::map
을 사용하여 게임 캐릭터의 인벤토리를 관리하는 예제다. 아이템의 고유 ID를 키로, 아이템의 수량을 값으로 관리한다. 또한, 아이템의 상세 정보는 별도의 std::map
으로 관리하여 데이터베이스처럼 활용한다.
#include <iostream>
#include <map>
#include <string>
// 아이템의 상세 정보를 담는 구조체
struct Item {
std::string name;
std::string description;
int value;
};
// 게임 캐릭터의 인벤토리를 관리하는 클래스
class Inventory {
public:
// 아이템 데이터베이스에 아이템 원본 정보를 추가하는 함수
void RegisterItem(int itemID, const std::string& name, const std::string& desc, int value) {
// 아이템 정보를 생성하여 데이터베이스 맵에 저장
item_database_[itemID] = {name, desc, value};
}
// 인벤토리에 아이템을 추가하는 함수
void AddItem(int itemID, int quantity = 1) {
// Q1: 먼저 item_database_.count(itemID)로 아이템이 등록된 정보인지 확인하는 이유는 무엇일까요?
// 만약 이 확인 절차를 생략한다면 어떤 문제가 발생할 수 있을까요?
// A. 등록되지 않은 ID를 인벤토리에 넣으려고 하면, 존재하지 않는 아이템을
// 참조하게 되기 때문.
// 잘못된 ID로 접근 시 예외 발생
if (item_database_.count(itemID) == 0) {
std::cout << "[에러] 등록되지 않은 아이템 ID: " << itemID << std::endl;
return;
}
// Q2: 'item_counts_[itemID] += quantity;' 코드는 맵에 해당 itemID가 없을 때 어떻게 동작할까요?
// 이 동작이 std::map의 어떤 특징 때문에 가능한지 설명해보세요.
// A. 기본 값을 넣어준다. int이기 때문에 0이 들어가고 += quantity가 실행됨.
// std::map의 operator[]가 자동 삽입을 제공해서 가능하다.
item_counts_[itemID] += quantity;
std::cout << "'" << item_database_.at(itemID).name << "' " << quantity << "개를 획득했습니다." << std::endl;
}
// 인벤토리에서 아이템을 사용하는 함수
void UseItem(int itemID, int quantity = 1) {
auto it = item_counts_.find(itemID);
// Q3: 여기서 'find' 함수를 사용한 결과를 'end()'와 비교하는 이유는 무엇인가요?
// 'operator[]'나 'at()'을 바로 사용하지 않는 이유는 무엇일까요?
// A. 키가 없으면 find는 end() 반복자를 반환한다.
// operator[]를 사용하면 자동 삽입으로 원하지 않는 아이템이 생기고, at()은
// 없는 키를 조회할 시 'out_of_range' 예외를 발생시킨다.
if (it != item_counts_.end()) {
if (it->second >= quantity) {
it->second -= quantity;
std::cout << "'" << item_database_.at(itemID).name << "' " << quantity << "개를 사용했습니다." << std::endl;
// 아이템을 모두 사용했다면 인벤토리에서 제거
if (it->second == 0) {
// Q4: 'erase(it)'를 사용하는 것과 'erase(itemID)'를 사용하는 것의 성능상 차이가 있을까요?
// 왜 여기서는 반복자(it)를 사용한 버전이 더 효율적일까요?
// A. it을 사용하면 검색 과정을 생략하고 바로 삭제가 가능하다.
item_counts_.erase(it);
std::cout << "'" << item_database_.at(itemID).name << "'을(를) 모두 소진했습니다." << std::endl;
}
} else {
std::cout << "[부족] '" << item_database_.at(itemID).name << "'의 수량이 부족합니다." << std::endl;
}
} else {
std::cout << "[없음] 인벤토리에 해당 아이템이 없습니다." << std::endl;
}
}
// 인벤토리 전체를 출력하는 함수
void PrintInventory() const {
std::cout << "--- 인벤토리 ---" << std::endl;
if (item_counts_.empty()) {
std::cout << "비어있음" << std::endl;
} else {
// Q5: 이 반복문은 왜 항상 아이템 ID 순서대로 인벤토리를 출력할까요?
// std::map의 어떤 내부 구조적 특징 때문인지 설명해보세요.
// A. 내부적으로 레드 블랙 트리를 사용한다. 그래서 키를 삽입할 때 자동
// 으로 정렬 상태를 유지한다.
for (const auto& pair : item_counts_) {
int itemID = pair.first;
int quantity = pair.second;
// 아이템 데이터베이스에서 아이템 정보를 가져옴
const Item& item = item_database_.at(itemID);
std::cout << " ID: " << itemID << " | 이름: " << item.name
<< " | 수량: " << quantity << " | 설명: " << item.description << std::endl;
}
}
std::cout << "----------------" << std::endl;
}
private:
// 아이템의 원본 정보를 저장하는 맵 (ID -> Item 정보), 일종의 게임 데이터베이스 역할
std::map<int, Item> item_database_;
// 캐릭터가 소유한 아이템의 수량을 저장하는 맵 (ID -> 수량)
std::map<int, int> item_counts_;
};
int main() {
// 인벤토리 시스템 생성
Inventory myInventory;
// 게임에 존재하는 아이템들을 미리 등록
myInventory.RegisterItem(101, "체력 물약", "체력을 50 회복시켜준다.", 20);
myInventory.RegisterItem(102, "마나 물약", "마나를 30 회복시켜준다.", 25);
myInventory.RegisterItem(201, "강철 검", "기본적인 공격을 할 수 있는 검.", 150);
myInventory.RegisterItem(301, "가죽 방패", "공격을 방어할 수 있는 방패.", 100);
// 게임 플레이: 아이템 획득
myInventory.AddItem(101, 5); // 체력 물약 5개 획득
myInventory.AddItem(201); // 강철 검 1개 획득
myInventory.AddItem(101, 3); // 체력 물약 3개 추가 획득
myInventory.AddItem(999); // 등록되지 않은 아이템 획득 시도
myInventory.PrintInventory();
// 게임 플레이: 아이템 사용
myInventory.UseItem(101, 2); // 체력 물약 2개 사용
myInventory.UseItem(201, 2); // 강철 검 2개 사용 시도 (수량 부족)
myInventory.UseItem(102); // 마나 물약 사용 시도 (인벤토리에 없음)
myInventory.PrintInventory();
// 게임 플레이: 아이템 모두 사용
myInventory.UseItem(101, 6); // 남은 체력 물약 6개 모두 사용
myInventory.PrintInventory();
return 0;
}
3. 요약
std::map은 키(Key)와 값(Value)을 하나의 쌍으로 묶어서 저장하는 C++의 연관 컨테이너입니다.
가장 큰 특징은 키를 기준으로 항상 자동으로 정렬된다는 점입니다. 내부적으로는 '레드-블랙 트리'라는 균형 잡힌 이진 탐색 트리로 구현되어 있어서, 데이터가 얼마나 많든 상관없이 검색, 삽입, 삭제 연산을 O(log N)의 안정적인 시간 복잡도로 수행할 수 있습니다.
std::unordered_map과 비교하자면, unordered_map은 해시 테이블을 사용해서 평균 O(1)의 매우 빠른 조회가 가능하지만 순서를 보장하지 않습니다.
반면 std::map은 unordered_map보다 약간 느릴 순 있어도, 데이터를 항상 정렬된 상태로 유지해야 하거나 특정 범위의 데이터를 찾아야 할 때 매우 유용합니다.
예를 들어, 게임에서 아이템 ID 순서대로 인벤토리를 보여주거나, 점수 랭킹 보드를 관리하는 것처럼 데이터의 순서가 중요할 때 std::map을 사용하는 것이 좋습니다.
'C++' 카테고리의 다른 글
std::vector vs std::map 비교 (0) | 2025.09.12 |
---|---|
std::vector (0) | 2025.09.12 |
C++ 스마트 포인터 (Smart Pointers) (0) | 2025.09.10 |
C++의 4대 캐스팅 (0) | 2025.09.09 |
RAII (Resource Acquisition Is Initialization) (0) | 2025.09.08 |