92 컨테이너와 알고리즘
먼저 알아야 할 것
돌아보기
37장에서 C의 배열은 크기가 컴파일 시간에 정해지고, 45장에서 realloc 으로 늘릴 수 있다고 배웠다. 그러면 “자라는 배열”을 직접 만들 때 가장 틀리기 쉬운 자리는 어디인가?
답. 세 곳이다. 첫째, 늘릴 크기 계산 — 88장에서 본 곱셈 감김이 여기서 일어난다. 둘째, 실패했을 때의 상태 — realloc이 실패하면 원본 포인터는 그대로 유효한데, 반환값을 곧바로 원본 변수에 대입하는 흔한 코드는 그 원본을 잃어버린다(누수다). 셋째, 늘어난 뒤의 포인터 — 원소를 가리키던 포인터는 재할당 후 무효가 된다. 컨테이너를 직접 만들 때마다 이 셋을 다시 맞히는 것보다, 한 번 제대로 만든 것을 쓰는 편이 낫다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 키드 해시를 쓰면 얼마나 느려지는가? 그리고 OS가 없으면 무작위 비밀은 어디서 오는가?
92.1 네 컨테이너의 한살이
먼저 넷을 한 화면에서 본다. 만들고, 넣고, 훑고, 되돌리는 과정이 서로 어떻게 다른지가 이 예제에 다 들어 있다.
examples/ch92/tour.c
/* 네 컨테이너의 한살이 — 만들고, 넣고, 훑고, 되돌린다.
array · list · ring · map 을 한 화면에서 비교한다. */
#include <proven.h>
/* 침습적 리스트: 노드가 자료 안에 산다 */
typedef struct {
int id;
proven_list_node_t link; /* ← 이 한 칸이 리스트에 매달릴 고리다 */
} task_t;
int main(void)
{
proven_allocator_t alloc = proven_heap_allocator();
/* ── ① 자라는 배열 ───────────────────────────────────────── */
proven_result_array_t ar = PROVEN_ARRAY_INIT(alloc, int, 2);
if (!proven_is_ok(ar.err)) return 1;
proven_array_t arr = ar.value;
for (int i = 1; i <= 5; i++)
(void)PROVEN_ARRAY_PUSH(&arr, int, i * i);
proven_println("array len={} cap={} elem_size={}",
PROVEN_ARG(arr.len), PROVEN_ARG(arr.cap), PROVEN_ARG(arr.elem_size));
/* 훑기는 인덱스로 — 포인터를 들고 다니지 않는다 */
proven_print(" contents:");
for (proven_size_t i = 0; i < arr.len; i++)
proven_print(" {}", PROVEN_ARG(*PROVEN_ARRAY_GET(&arr, int, i)));
proven_println("");
int last = 0;
(void)PROVEN_ARRAY_POP(&arr, int, &last);
proven_println(" pop -> {} (len {})", PROVEN_ARG(last), PROVEN_ARG(arr.len));
PROVEN_ARRAY_DESTROY(&arr);
/* ── ② 침습적 리스트 — 할당이 한 번도 없다 ───────────────── */
task_t items[3] = { { .id = 10 }, { .id = 20 }, { .id = 30 } };
proven_list_t list;
proven_list_init(&list);
for (int i = 0; i < 3; i++)
proven_list_push_back(&list, &items[i].link);
proven_list_node_t *node, *tmp;
proven_print("list from the front:");
PROVEN_LIST_FOR_EACH(node, &list) {
task_t *t = PROVEN_LIST_ENTRY(node, task_t, link);
proven_print(" {}", PROVEN_ARG(t->id));
}
proven_println(" (the node lives inside the data, so no allocation is needed)");
/* 훑으면서 떼어내려면 SAFE 판을 쓴다 */
PROVEN_LIST_FOR_EACH_SAFE(node, tmp, &list) {
task_t *t = PROVEN_LIST_ENTRY(node, task_t, link);
if (t->id == 20) proven_list_remove(node);
}
proven_print(" after removing 20:");
PROVEN_LIST_FOR_EACH(node, &list) {
proven_print(" {}", PROVEN_ARG(PROVEN_LIST_ENTRY(node, task_t, link)->id));
}
proven_println("");
/* ── ③ 링 버퍼 — 고정 크기, 지나간 것은 버린다 ───────────── */
proven_result_ring_t rr = PROVEN_RING_INIT(alloc, int, 4);
if (proven_is_ok(rr.err)) {
proven_ring_t ring = rr.value;
for (int i = 1; i <= 4; i++) (void)proven_ring_push(&ring, &i);
int five = 5;
proven_err_t full = proven_ring_push(&ring, &five);
proven_println("ring push when full -> err={} (it tells you instead of overwriting)",
PROVEN_ARG((int)full));
int v = 0;
(void)proven_ring_pop(&ring, &v);
proven_println(" pop -> {} (the one put in first)", PROVEN_ARG(v));
proven_ring_destroy(&ring);
}
/* ── ④ 해시 맵 — 키를 소유할지 빌릴지 고른다 ─────────────── */
/* 키를 맵이 복사해 소유한다(U8_OWNED) — 빌리는 판도 있다 */
proven_result_map_t mr = PROVEN_MAP_INIT_U8_OWNED(alloc, int, 8);
if (proven_is_ok(mr.err)) {
proven_map_t map = mr.value;
int a = 1, b = 2;
(void)proven_map_set_u8_owned(&map, PROVEN_LIT("alpha"), &a);
(void)proven_map_set_u8_owned(&map, PROVEN_LIT("beta"), &b);
const int *found = proven_map_get(&map,
(proven_map_key_t){ .str = PROVEN_LIT("beta") });
proven_println("map get(\"beta\") -> {}",
PROVEN_ARG(found ? *found : -1));
proven_println(" get(\"absent\") -> {} (null when there is none)",
PROVEN_ARG((bool)(proven_map_get(&map,
(proven_map_key_t){ .str = PROVEN_LIT("absent") }) == nullptr)));
proven_map_destroy(&map);
}
return 0;
}
실행 결과
array len=5 cap=8 elem_size=4
contents: 1 4 9 16 25
pop -> 25 (len 4)
list from the front: 10 20 30 (the node lives inside the data, so no allocation is needed)
after removing 20: 10 30
ring push when full -> err=2 (it tells you instead of overwriting)
pop -> 1 (the one put in first)
map get("beta") -> 2
get("absent") -> true (null when there is none)
| 컨테이너 | 만들기 | 할당 | 훑기 | 되돌리기 |
|---|---|---|---|---|
array | PROVEN_ARRAY_INIT(alloc, T, n) | 자란다 | 인덱스로 | PROVEN_ARRAY_DESTROY |
list | proven_list_init(&l) | 없음 | PROVEN_LIST_FOR_EACH | 불필요 |
ring | PROVEN_RING_INIT(alloc, T, n) | 한 번, 고정 | pop 으로 꺼내며 | proven_ring_destroy |
map | PROVEN_MAP_INIT_U8_OWNED(alloc, T, n) | 자란다(재해싱) | 키로 조회 | proven_map_destroy |
표 92.1
리스트만 할당이 없다는 점이 눈에 띈다 — 노드가 자료 안에 있기 때문이다. 그래서 리스트는 기억 부족으로 실패할 수 없는 유일한 컨테이너이고, 임베디드에서 특별히 사랑받는다(94장).
92.2 자라는 배열
proven_array_t는 원소 크기와 정렬을 알고 있는 바이트 버퍼다. 속을 보면 왜 타입을 매크로로 채우는지가 분명해진다.
typedef struct {
proven_allocator_t alloc; /* 만들 때 받은 할당자를 기억한다 */
proven_byte_t *data; /* 바이트 버퍼 */
proven_size_t len; /* 지금 든 원소 수 */
proven_size_t cap; /* 담을 수 있는 원소 수 */
proven_size_t elem_size; /* 원소 하나의 크기 */
proven_size_t align; /* 원소의 정렬 요구 */
} proven_array_t;배열이 할당자를 기억한다는 점이 문자열과 다르다. 문자열은 연산마다 할당자를 받지만(90장), 배열은 만들 때 한 번 받아 안에 넣어 둔다 — push가 자랄 때마다 할당자를 넘기게 하면 코드가 시끄러워지기 때문이다. 그래서 파괴할 때도 할당자를 다시 주지 않는다(PROVEN_ARRAY_DESTROY(&arr)).
C에는 제네릭이 없으므로 타입은 매크로가 채워 준다.
examples/ch92/arr.c
#include <proven.h>
#include <stdio.h>
int main(void)
{
proven_allocator_t alloc = proven_heap_allocator();
proven_result_array_t made = PROVEN_ARRAY_INIT(alloc, int, 4);
if (!proven_is_ok(made.err)) {
printf("array creation failed\n");
return 1;
}
proven_array_t arr = made.value;
for (int i = 1; i <= 6; i += 1) { /* 용량 4를 넘겨 자동으로 자란다 */
if (!proven_is_ok(PROVEN_ARRAY_PUSH(&arr, int, i * i))) {
printf("push failed\n");
PROVEN_ARRAY_DESTROY(&arr);
return 1;
}
}
printf("count: %zu\n", arr.len);
for (size_t i = 0; i < arr.len; i += 1) {
printf("%d ", *PROVEN_ARRAY_GET(&arr, int, i));
}
printf("\n");
PROVEN_ARRAY_DESTROY(&arr);
return 0;
}
실행 결과
count: 6
1 4 9 16 25 36
PROVEN_ARRAY_INIT(alloc, int, 4)는 “int를 담을 배열, 초기 용량 4”라는 뜻이고, PROVEN_ARRAY_PUSH는 타입을 다시 적어 컴파일 시간에 맞는지 확인한다. 용량 4를 넘긴 다섯째 원소에서 배열은 스스로 늘어났다 — 그리고 그 성장은 할당자를 통해 일어난다(89장의 규칙 그대로, 배열이 자기가 만들어질 때 받은 할당자를 기억한다).
반례. 원소 포인터를 들고 있다가 push 하기
int *first = PROVEN_ARRAY_GET_MUT(&arr, int, 0);
(void)PROVEN_ARRAY_PUSH(&arr, int, 42); /* 여기서 버퍼가 옮겨갈 수 있다 */
*first = 7; /* 옛 주소에 쓴다 — 사용 후 해제 */배열이 자라면 내용이 새 버퍼로 옮겨가고, 옛 주소를 가리키던 포인터는 무효가 된다. 45장에서 배운 사용 후 해제가 컨테이너 위에서 나타나는 형태다. 규칙은 하나다 — 컨테이너를 바꾼 뒤에는 인덱스로 다시 얻는다. 포인터가 아니라 인덱스를 들고 다니는 습관이 여기서 방어가 된다.
92.3 침습적 리스트 — 할당 없는 연결
proven_list_t는 침습적(intrusive) 연결 리스트다 — 노드가 자료를 품는 것이 아니라, 자료 구조체 안에 링크 필드를 심는다. 46장에서 만든 struct 안에 proven_list_node_t link;를 한 칸 두는 식이다.
typedef struct proven_list_node_t {
struct proven_list_node_t *next;
struct proven_list_node_t *prev;
} proven_list_node_t;
typedef struct {
proven_list_node_t head; /* 자기 자신을 가리키는 보초(sentinel) */
} proven_list_t;head가 보초라는 점이 이 구현의 요령이다. 빈 리스트에서 head.next와 head.prev는 자기 자신을 가리키고, 그래서 삽입·삭제 코드에 “널인가” 검사가 한 번도 나오지 않는다 — 리눅스 커널이 오래 쓴 무늬다.
무엇이 좋은가. 리스트에 넣기 위해 따로 할당할 것이 없다 — 노드가 곧 자료이기 때문이다. 힙이 없는 환경에서도 리스트를 쓸 수 있고(94장), 같은 객체를 두 리스트에 동시에 매다는 것도 링크 필드를 둘 두면 된다. 대가는 자료 구조체가 리스트를 알아야 한다는 것이다.
되찾는 방향이 문제인데, 그것을 매크로 하나가 해결한다.
task_t *t = PROVEN_LIST_ENTRY(node, task_t, link);link 멤버의 주소에서 구조체의 시작 주소를 역산하는 매크로다 — 46장에서 본 offsetof가 여기서 실물로 쓰인다. 노드에서 자료로 돌아가는 이 한 줄이 침습적 리스트의 전부라 해도 좋다.
훑는 매크로는 둘이다.
| 매크로 | 하는 일 | 언제 |
|---|---|---|
PROVEN_LIST_FOR_EACH(it, &l) | 앞에서부터 훑는다 | 읽기만 할 때 |
PROVEN_LIST_FOR_EACH_SAFE(it, tmp, &l) | 다음 노드를 미리 잡아 둔다 | ★ 훑으면서 떼어낼 때 |
표 92.2
별표가 중요하다. 훑는 도중에 노드를 떼어내면 그 노드의 next가 무의미해져 루프가 길을 잃는다. _SAFE 판은 다음 노드를 미리 손에 쥐고 도므로 지금 노드를 떼어내도 안전하다. 예제에서 20번 항목을 떼어낼 때 이 판을 쓴 이유다.
두 매크로 모두 반복 변수를 미리 선언해 두어야 한다는 점도 알아 두면 좋다(proven_list_node_t *node, *tmp;). 매크로가 for의 초기화 자리에 선언을 넣지 않기 때문인데, C89 시절부터 이어진 커널 관행이다.
92.4 링 버퍼 — 고정 크기의 흐름
proven_ring_t는 고정 크기 원형 버퍼다. 생산자와 소비자가 오가는 자리, 로그의 최근 N개, 오디오·센서 샘플처럼 지나간 것은 버려도 되는 흐름에 쓴다. 크기가 고정이므로 가득 차면 어떻게 할지가 계약의 일부다 — 여기서도 기본은 조용히 덮지 않고 알리는 쪽이다(예제의 err=2가 그 확인이다).
push와 pop 둘뿐이고, 둘 다 원소를 복사한다. 넣을 때는 주소를 주고, 꺼낼 때도 받을 곳의 주소를 준다.
int v = 42;
proven_err_t e = proven_ring_push(&ring, &v); /* 값을 복사해 넣는다 */
int out;
e = proven_ring_pop(&ring, &out); /* 값을 복사해 꺼낸다 */이 설계 덕에 링 버퍼 안의 원소를 가리키는 포인터가 밖으로 새지 않는다 — 원형 버퍼에서 포인터를 들고 있다가 그 자리가 덮이는 것이 고전적인 사고인데, 그것을 인터페이스로 막았다.
92.5 해시 맵, 그리고 공격받는 자료구조
proven_map_t는 열린 주소 방식(open addressing) 해시 맵이다. 키는 정수나 바이트열이고, 문자열 키는 두 가지 모드가 있다 — 빌린 키(호출자가 바이트를 살려 둔다)와 소유한 키(맵이 복사해 들고 있다). 89장의 소유·빌림 구분이 여기서도 그대로 나타난다.
키의 갈래를 만들 때 정한다.
| 만들기 매크로 | 키 | 주의 |
|---|---|---|
PROVEN_MAP_INIT_INT(alloc, T, n) | 정수(key.id) | 가장 빠르다 |
PROVEN_MAP_INIT_U8_BORROWED(alloc, T, n) | 문자열(빌림) | ★ 키 바이트를 맵보다 오래 살려 둬야 한다 |
PROVEN_MAP_INIT_U8_OWNED(alloc, T, n) | 문자열(복사) | 넣을 때 복사 비용. 대신 안전 |
표 92.3
키는 공용체 하나로 넘긴다 — 정수면 .id, 문자열이면 .str이다.
proven_map_key_t k = { .str = PROVEN_LIT("beta") };
const int *v = proven_map_get(&map, k); /* 없으면 널 */조회는 널로 답한다. 꾸러미가 아니라 포인터를 돌려주는 이유는 “없음”이 실패가 아니라 정상적인 답이기 때문이다(87장의 에러 갈래와 구별된다). 그리고 그 포인터는 맵 안을 가리키므로, 다음에 맵이 자라면 무효가 된다 — 배열에서와 같은 규칙이다.
넣는 함수는 셋이다. proven_map_set(일반), proven_map_set_u8_owned(문자열 키를 복사해 넣기), proven_map_set_with_scratch(임시 기억을 따로 주기). 마지막 것은 재해싱 같은 내부 작업의 임시 버퍼를 다른 할당자에서 얻고 싶을 때 쓴다 — 아레나 위에서 맵을 돌릴 때 죽은 기억이 쌓이지 않게 하는 장치다.
examples/ch92/wordcount.c
#include <proven.h>
#include <stdio.h>
/* 낱말을 세어 정렬해 찍는다 — 맵과 배열과 정렬을 한 번에 */
typedef struct { proven_u8str_view_t word; int count; } entry_t;
/* 비교자는 전순서여야 한다: 동점도 일관되게 갈라 준다 (53장의 반례를 피한다) */
static int by_count_desc(const void *a, const void *b)
{
const entry_t *x = a, *y = b;
if (x->count != y->count) return (x->count < y->count) - (x->count > y->count);
proven_size_t n = x->word.size < y->word.size ? x->word.size : y->word.size;
int c = proven_memcmp(x->word.ptr, y->word.ptr, n);
if (c != 0) return c;
return (x->word.size > y->word.size) - (x->word.size < y->word.size);
}
int main(void)
{
proven_allocator_t alloc = proven_heap_allocator();
const char *text = "the quick fox the lazy dog the fox";
/* 문자열 키 맵: 기본은 HashDoS 에 견디는 키드 해시를 쓴다 */
proven_result_map_t made_map =
proven_map_create(alloc, 16, PROVEN_KEY_TYPE_U8_OWNED, sizeof(int), alignof(int));
if (!proven_is_ok(made_map.err)) return 1;
proven_map_t counts = made_map.value;
proven_u8str_view_t all = proven_u8str_view_from_cstr(text);
proven_u8str_view_t space = proven_u8str_view_from_cstr(" ");
proven_size_t start = 0;
for (;;) {
proven_size_t hit = proven_u8str_view_find(all, start, space);
proven_size_t end = (hit == PROVEN_INDEX_NOT_FOUND) ? all.size : hit;
proven_u8str_view_t w = proven_u8str_view_slice(all, start, end - start);
const int *seen = proven_map_get(&counts, (proven_map_key_t){ .str = w });
int next = seen ? *seen + 1 : 1;
if (!proven_is_ok(proven_map_set(&counts, (proven_map_key_t){ .str = w }, &next)))
break;
if (hit == PROVEN_INDEX_NOT_FOUND) break;
start = hit + 1;
}
printf("distinct words: %zu\n", counts.len);
/* 세어 둔 것을 배열에 모아 정렬한다 */
proven_result_array_t made_arr = PROVEN_ARRAY_INIT(alloc, entry_t, 8);
if (!proven_is_ok(made_arr.err)) return 1;
proven_array_t list = made_arr.value;
const char *words[] = {"the", "quick", "fox", "lazy", "dog"};
for (size_t i = 0; i < sizeof words / sizeof words[0]; i++) {
proven_u8str_view_t w = proven_u8str_view_from_cstr(words[i]);
const int *c = proven_map_get(&counts, (proven_map_key_t){ .str = w });
entry_t e = { .word = w, .count = c ? *c : 0 };
if (!proven_is_ok(PROVEN_ARRAY_PUSH(&list, entry_t, e))) break;
}
proven_array_sort(&list, by_count_desc); /* 최악에도 O(n log n) 보장 */
for (size_t i = 0; i < list.len; i++) {
const entry_t *e = PROVEN_ARRAY_GET(&list, entry_t, i);
printf(" %.*s = %d\n", (int)e->word.size, (const char *)e->word.ptr, e->count);
}
PROVEN_ARRAY_DESTROY(&list);
proven_map_destroy(&counts);
return 0;
}
실행 결과
distinct words: 5
the = 3
fox = 2
dog = 1
lazy = 1
quick = 1
이 예제 하나에 이 장의 도구가 다 들어 있다 — 맵으로 세고, 배열에 모으고, 정렬해서 찍는다. 낱말을 자르는 데는 90장의 뷰를 썼으므로 문자열 복사는 맵이 키를 소유할 때 한 번뿐이다.
실제 사례. HashDoS — 해시 맵이 공격 대상이 된 사건
2011년, 여러 웹 프레임워크가 동시에 같은 취약점으로 무너졌다. 공격자가 같은 버킷으로 몰리는 키들을 골라 요청 하나에 수천 개의 매개변수로 실어 보내면, 평균 이던 삽입이 이 되고 전체가 으로 퇴화해 서버 한 대가 요청 몇 개로 멎었다. 해시 함수가 공개되어 있어 충돌을 계산할 수 있었다는 것이 원인이었다.
오늘의 처방은 키드 해시(keyed hash)다 — 프로세스마다 무작위 비밀을 뽑아 해시에 섞으면 공격자가 충돌을 미리 계산할 수 없다. proven의 proven_map_create가 문자열 키에 SipHash-2-4와 무작위 씨앗을 기본으로 쓰는 이유이고, 반대로 키가 전부 내 코드에서 오는 경우를 위해 더 빠른 FNV-1a를 쓰는 proven_map_create_trusted를 따로 둔 이유다. 기본값은 안전한 쪽, 빠른 쪽은 이름을 밝혀서 — 이 부에서 반복해 만난 원칙이다.
문. 키드 해시를 쓰면 얼마나 느려지는가? 그리고 OS가 없으면 무작위 비밀은 어디서 오는가?
답. SipHash는 FNV-1a보다 느리지만 문자열 길이에 비례하는 정도이고, 맵 연산 전체에서 해시 계산이 차지하는 몫은 대개 크지 않다. 무작위 비밀은 운영체제의 난수원에서 한 번 뽑는다 — 그리고 OS가 없는 환경(94장)에서는 뽑을 곳이 없으므로 FNV-1a로 물러난다. 라이브러리는 이 사실을 숨기지 않고 문서에 적어 두는데, 근거가 분명하다: 공격자가 없는 곳에는 공격자 모델도 필요 없다. 펌웨어 안에서 키를 고르는 외부인은 존재하지 않는다.
92.6 최악을 보장하는 정렬
85장에서 본 두 가지 문제 — 검사되지 않는 비교자, 그리고 최악에서 무너지는 알고리즘 — 을 여기서 함께 다룬다.
proven_array_sort는 introsort다. 빠른 퀵소트로 시작하되, 재귀가 너무 깊어지면 힙소트로 갈아탄다. 그래서 평균은 퀵소트만큼 빠르고 최악에도 이 보장된다 — 85장에서 본 복잡도 공격이 통하지 않는다. 헤더의 문구를 그대로 옮기면 “은 평균이 아니라 보장”이다.
비교자 쪽은 언어가 도와줄 수 없으므로 계약을 문서와 예제로 밝힌다. 예제의 by_count_desc가 그 본보기다 — 개수로 내림차순, 동점이면 낱말로 가른다. 85장의 반례(첫 글자만 보는 비교자)와 정확히 대비된다.
흔한 오해. “동점은 어떻게 처리해도 상관없다”
상관있다. 비교자는 전순서(total order)를 이뤄야 한다 — 같으면 0, 일관되게 크고 작아야 하며,cmp(a,b)와 cmp(b,a)의 부호가 반대여야 한다. 이것을 어기면 결과가 뒤죽박죽이 되는 정도가 아니라, 구현에 따라 배열 밖을 침범할 수도 있다(파티션 알고리즘이 경계를 비교 결과로 판단하기 때문이다). “동점을 아무렇게나” 돌려주는 비교자는 그래서 버그이지 취향이 아니다.92.7 바이트를 글자로 — 해시와 인코딩
같은 상자 안에 든 나머지 도구들도 짚어 둔다.
- 용도별 해시 — 맵의 내부용(빠른 혼합), 무결성 확인용(CRC-32), 암호학적 용도(SHA-256), 그리고 앞서 본 키드 해시(SipHash). 라이브러리가 이들을 구분해 두는 이유는 같은 “해시”라는 낱말이 전혀 다른 요구를 뜻하기 때문이다 — 빠르기만 하면 되는 자리에 SHA-256을 쓰면 낭비이고, 적대적 입력이 오는 자리에 FNV-1a를 쓰면 위험하다.
- hex와 Base64 — 바이트를 텍스트로 옮기는 두 표준. 여기서도 원칙은 같다. 잘못된 입력(홀수 길이 hex, 잘못된 패딩)은 추측해 고치지 않고
PROVEN_ERR_INVALID_ENCODING으로 거부한다(90장의 그 규범이다).
복습 정리
컨테이너 요약.
| 도구 | 모양 | 맞는 자리 | 주의 |
|---|---|---|---|
array | 자라는 연속 배열 | 순서 있는 목록 | 자라면 포인터 무효 |
list | 침습적 연결 리스트 | 할당 없이 잇기 | 자료가 링크를 품는다 |
ring | 고정 크기 원형 | 흐름·최근 N개 | 가득 찼을 때의 계약 |
map | 열린 주소 해시 | 키로 찾기 | 키 소유 여부를 고른다 |
array_sort | introsort | 무엇이든 정렬 | 비교자는 전순서 |
| 해시 4종 | 용도별 | 맵·무결성·암호·반공격 | 용도를 섞지 않는다 |
표 92.4
여기까지가 순수한 계산의 세계다 — 운영체제가 없어도 전부 돈다. 다음 장에서는 바깥으로 나간다. 파일, 스트림, 시간, 난수 — OS와 닿는 자리다.