2026-03-25 TIL (22일차)
C++
pair을 이용하여 vector 선언
vector는 연속된 메모리 공간에 데이터를 저장하는 배열 기반 컨테이너입니다. 여기에 pair를 넣으면, 각 칸마다 두 개의 데이터가 묶인 하나의 쌍이 저장됩니다.
메모리 저장 형태와 특징
- 저장되는 형태: 메모리 상에
(첫 번째 데이터, 두 번째 데이터), (첫 번째 데이터, 두 번째 데이터)순서로 빈틈없이 배치됩니다. - 자료형의 자유: first와 second에 서로 다른 자료형을 넣을 수 있습니다. (예: int와 string, float와 bool 등)
1
2
3
4
5
6
7
8
9
10
11
12
std::vector<std::pair<int, std::string>> userList;
userList.push_back(std::make_pair(1, "Alice"));
userList.push_back({2, "Bob"});
userList.emplace_back(3, "Charlie");
for (int i = 0; i < userList.size(); ++i) {
// 벡터의 i번째 요소(pair)의 first와 second에 접근
std::cout << "ID: " << userList[i].first
<< ", Name: " << userList[i].second << std::endl;
}
}
장점
① 연관된 데이터 묶기
서로 관련이 있는 두 데이터를 하나의 리스트로 관리하고 싶을 때 유용합니다.
② 자동 정렬 (Sorting)
std::pair는 기본적으로 비교 연산자가 정의되어 있습니다. std::sort를 사용하면 편하게 정렬할 수 있습니다.
해시 테이블(Hash Table)
해시 테이블(Hash Table)은 컴퓨터 과학에서 데이터를 르게 찾아내기 위해 고안된 ‘키(Key)-값(Value)’ 기반의 자료구조입니다. 쉽게 비유하자면, 수만 권의 책이 꽂힌 도서관에서 책 제목(Key)을 말하자마자 사서가 “그 책은 7번 선반의 3번째 칸에 있습니다”라고 단번에 알려주는 안내 시스템과 같습니다.
해시 테이블의 상세 정의 및 핵심 구성 요소
해시 테이블은 해시 함수(Hash Function)를 사용하여 키를 특정 Index으로 변환하고, 이 Index을 주소 삼아 데이터를 저장하는 배열 형태의 자료구조입니다.
- 키(Key): 데이터를 찾기 위한 고유한 식별자 (예: 유저 닉네임, 학번).
- 해시 함수(Hash Function): 임의의 길이를 가진 키를 고정된 크기의 숫자(Index)로 바꿔주는 수학적 연산입니다.
- 해시 값(Hash Value/Code): 해시 함수의 결과물로 나온 숫자입니다. 이 숫자가 배열의 Index가 됩니다.
- 버킷(Bucket/Slot): 데이터가 저장되는 각각의 공간입니다.
해시 테이블의 특징
해시 테이블이 다른 자료구조(배열, 리스트, 트리)와 구별되는 특징은 탐색의 효율성입니다.
- 상수 시간 탐색 ($O(1)$): 일반 배열은 처음부터 끝까지 훑어야 하지만($O(N)$), 해시 테이블은 키를 함수에 넣는 즉시 주소가 나오므로 데이터 양과 무관하게 거의 즉시 찾아냅니다.
- 공간과 시간의 교환: 빠른 속도를 얻는 대신, 데이터를 띄엄띄엄 저장하므로 메모리 공간을 상대적으로 더 많이 차지합니다.
해시 충돌(Collision)
해시 테이블의 단점은 서로 다른 두 키가 해시 함수를 거쳤는데 똑같은 방 번호(Index)가 나오는 ‘해시 충돌’입니다. 이 현상이 발생하는 이유는 다음과 같습니다.
- 비둘기집 원리 (Pigeonhole Principle)
- 입력될 수 있는 키(Key)의 종류는 무한대(예: 세상의 모든 문자열 조합)에 가깝지만, 해시 테이블의 방(Bucket) 개수는 컴퓨터의 한정된 메모리 때문에 유한합니다.
- 10개의 비둘기집에 11마리의 비둘기를 넣으려면 반드시 한 집에는 2마리 이상이 들어가야 하듯, 무한한 키를 유한한 배열 인덱스로 압축하다 보면 필연적으로 겹치는 주소가 생깁니다.
- 해시 함수의 한계
- 어떤 키가 들어와도 완벽하게 100% 다른 주소로 분산시켜주는 ‘완벽한 해시 함수’를 만드는 것은 현실적으로 불가능하거나 연산 비용이 너무 큽니다. 따라서 효율성을 위해 적당히 빠르고 잘 분산되는 함수를 쓰다 보면 우연히 같은 값이 계산될 확률이 존재합니다.
충돌 해결 방법
- 체이닝 (Chaining): 충돌이 난 방 안에 연결 리스트(Linked List)를 만들어 데이터를 줄 세우는 방식.
- 개방 주소법 (Open Addressing): 충돌이 나면 규칙에 따라 비어있는 옆 방을 찾아 들어가는 방식.
활용 예시
- ① 데이터베이스 인덱싱: 수백만 명의 회원 정보 중 ‘ID’ 하나로 특정 회원을 즉시 찾아야 할 때 사용합니다. 로그인 시 비밀번호 대조 과정에서도 쓰입니다.
- ② 컴파일러의 심볼 테이블: 코드 안의 수많은 변수 이름과 함수 이름을 메모리 주소와 연결해 줘야 할 때 사용합니다.
- ③ 캐싱 (Caching): 웹 브라우저나 서버가 연산 결과를 저장해 두었다가, ‘URL’이나 ‘검색어’를 키로 하여 데이터를 빠르게 다시 가져올 때 사용합니다.
- ④ 게임 개발 (Object Lookup): 수만 개의 아이템 중 특정 고유 번호(UID)를 가진 객체를 즉시 찾아 로직을 실행해야 할 때 필수적입니다. (
std::unordered_map)
요약 해시 테이블은 무엇(Key)을 넣으면 어디(Index)에 있는지 바로 계산해 내는 자료구조입니다. 데이터가 정렬되어 있을 필요가 없고, 오직 찾는 속도($O(1)$)가 생명인 시스템 입니다.
String
게임 클라이언트/서버 개발 시 채팅, 세이브 파일, UI 텍스트 등 문자열을 다룰 때 필수적으로 알아야 할 C++ string의 핵심 기능과 성능 최적화 포인트 정리.
1. string의 본질: vector<char>의 사촌
C++에서 string은 연속된 메모리 공간에 문자(char)들이 모여 있는 컨테이너로, vector와 구조가 거의 동일하다.
- 인덱스 접근 ($O(1)$):
s[0],s[1]처럼 배열과 똑같이 빠른 속도로 특정 위치 문자에 접근 가능. - 메서드 공유:
vector처럼.size()로 길이를 구하거나,.push_back('!')으로 맨 뒤에 문자를 추가할 수 있다. - 문자열 전용 연산자:
+: 문자열 이어 붙이기 ("Hello" + " World")==,<,>: 사전순(알파벳순) 비교 가능.
2. 문자열 검색과 추출: find, npos, substr
① find("검색어") 와 string::npos (절대 규칙)
find는 문자열 안에서 검색어가 시작되는 인덱스(위치)를 반환한다.string::npos의 정체:find가 검색어를 찾지 못했을 때 반환하는 값.find의 반환형은 부호 없는 정수(size_t)이므로,-1이 들어가면 언더플로우가 발생해 아주 거대한 쓰레기값이 된다.- 주의:
find사용 직후if (pos != string::npos)로 검사하지 않고 결과값을 그대로 배열 인덱스나substr에 넣으면 메모리 에러(Crash)가 발생한다.
② substr(시작위치, 길이) 콤보 활용
- 원본은 그대로 둔 채, 원하는 부분만 복사해서 새로운 문자열을 만들어낸다.
- 활용 (파일명/확장자 분리):
game_save.dat에서find('.')로 점의 위치를 찾은 뒤,substr을 이용해 파일명과 확장자를 분리할 때 유용하다.
3. 문자열 수정과 성능: replace vs substr
① replace(시작위치, 길이, "새문자")
substr과 달리 원본 문자열 자체를 직접 수정한다.- 성능 주의점: 중간 내용을 지우고 길이가 다른 문자를 끼워 넣으면, 뒤에 있는 문자를 전부 밀거나 당겨야 하는 메모리 이동 비용이 발생한다. 루프 안에서 빈번하게 사용 시 성능 저하 우려가 있다.
- 무한 루프 주의: 전체 치환 루프를 돌릴 때, 다음 검색 시작 위치(
pos)를 반드시 ‘새로 끼워 넣은 단어의 길이’만큼 뒤로 밀어줘야 한다.
② 무엇이 더 빠를까?
- 문자열을 이리저리 많이 뜯어고쳐야 한다면, 원본을
replace로 계속 건드리는 것보다 필요한 부분만substr로 떼어내어 새로운 빈 문자열에 이어 붙이는 것이 성능상 유리할 때가 많다.
4. 대소문자 무시 검색: tolower
- 원리: 아스키코드(ASCII) 테이블에서 영문 대·소문자는 정확히 32만큼 차이가 난다. (
'A'=65,'a'=97) tolower(문자): 대문자가 들어오면 소문자로 변환해 주며, 이미 소문자거나 기호면 그대로 둔다.- 활용: 게임 채팅 필터 시스템에서 유저가 “Hack”, “HACK” 등 섞어 써도 잡아낼 수 있도록, ‘검색용 소문자 복사본’을 만들어 필터링을 진행할 때 필수적이다.
5. 데이터 파싱과 형변환: stringstream, stoi, to_string
네트워크 패킷이나 세이브 파일(텍스트)을 실제 게임 데이터(구조체)로 변환하는 핵심 과정.
stringstream+getline:- 긴 문자열을 스트림에 넣고 쉼표(
,)나 공백 등 구분자를 기준으로 데이터를 조각(토큰)낸다.
- 긴 문자열을 스트림에 넣고 쉼표(
stoi(문자열 $\rightarrow$ 숫자):- 조각난
"45"는 아직 글자일 뿐이므로,stoi("45")를 통해 실제 수학 계산이 가능한 정수 45로 변환해야 HP, 골드 등에 적용할 수 있다.
- 조각난
to_string(숫자 $\rightarrow$ 문자열):- 반대로 변경된 스탯(정수)을 다시 세이브 파일(텍스트)로 저장할 때 사용한다.
요약표
| 도구 | 주요 역할 및 특징 | 실무/게임 개발 활용 예시 |
|---|---|---|
string | 배열(vector)처럼 연속된 메모리를 쓰는 문자 컨테이너 | UI 텍스트, 캐릭터 이름 저장 |
string::npos | 찾기 실패 시 반환되는 쓰레기값 (조건문 체크 필수) | 검색 결과 검증 (버그 및 크래시 방지) |
find / substr | 특정 문자의 위치를 찾고 복사본으로 추출 | 에셋 파일명과 확장자 분리 |
replace | 원본 문자열을 직접 수정 (메모리 이동에 따른 성능 유의) | 채팅 금칙어를 ***로 치환 |
tolower | 문자를 소문자로 변환 (아스키코드 활용) | 대소문자 구별 없는 명령어/채팅 검색 |
stringstream | 긴 문자열을 특정 구분자(쉼표 등) 기준으로 조각냄 | 네트워크 패킷 분해, CSV 데이터 읽기 |
stoi | 문자열(Text)을 실제 계산용 정수(Int)로 변환 | 세이브 파일에서 HP, 골드 수치 읽기 |
to_string | 계산용 정수(Int)를 다시 문자열(Text)로 변환 | 변경된 스탯을 세이브 데이터로 쓰기 |