C++에는 고정된 크기의 변수 그룹을 하나의 변수로 묶는 방법이 여러 가지 있습니다.
#C의 배열
C 언어에는 배열을 정의하는 세 가지 주요 방법이 있습니다:
int a[100];
int main() {
int b[100];
int *c = new int[100];
del[] c;
return 0;
}
이렇게 만들어진 변수들은 기능적으로 동일하지만, 조금씩 차이가 있습니다:
- 전역으로 정의된 배열
a는 프로그램이 실행되는 동안 내내 미리 할당된 메모리 영역에 놓입니다. 모든 원소는 처음에 기본값(int의 경우 0)으로 채워져 있습니다. - 함수 내부에서 정의된 배열
b는 스택에 — 임시 변수를 위한 특별한 메모리 영역에 — 놓이며, 함수(또는 루프 본문이나if같은 다른 블록)가 끝나는 즉시 삭제됩니다. 실행 스택의 크기는 제한되어 있으므로, 큰 배열($>10^6$)은 이런 방식으로 할당할 수 없습니다. 처음에 이 배열은 그 시점에 메모리에 남아 있던 임의의 값으로 채워져 있습니다 — 0으로 채우려면int x[100] = {}와 같이 작성하면 됩니다. 모든 원소를 지정한 값으로 채우려면int y[5] = {4, 8, 15, 23, 42}와 같이 작성하면 됩니다. new연산자로 정의한 배열c는 동적으로 할당됩니다. 이 배열은del[]연산자로 명시적으로 삭제하기 전까지 존재합니다. 또한 이 배열은 그 시점에 메모리에 남아 있던 값으로 채워져 있습니다. 앞의 두 방식과 달리, 이 배열은 미리 알 수 없는 크기라도 어떤 크기로든 만들 수 있습니다.
중요. 처음 두 가지 방식에서는 배열의 크기가 컴파일 시점에 알려진 상수여야 합니다. GCC 컴파일러는 int a[n] 형태의 표현식을 컴파일할 수 있으며, 실제로 상수가 아닌 크기의 배열이 할당됩니다. 그래서 IDE가 이를 지적하지 않을 수도 있지만, 이는 표준의 일부가 아닙니다.
배열의 모든 원소는 메모리에 연속적으로 저장되며, 변수 a, b, c 자체는 실제로 배열의 첫 번째 원소를 가리키는 포인터입니다. 대괄호는 단지 “문법적 설탕"일 뿐입니다:
a[k] <=> *(a + k)
C에는 초기화와 복사를 위한 두 가지 유용한 함수가 있는데, 각각 memset과 memcpy입니다.
첫 번째 함수는 ‘어디로’ 포인터, ‘어디에서’ 포인터, 그리고 복사해야 할 바이트 수를 받습니다:
memcpy(dest, src, sizeof src);
두 번째 함수는 “어디로"를 가리키는 포인터와 1바이트, 즉 배열 전체에 복사해 채울 값을 받습니다.
memset(arr, 0, sizeof arr);
중요. memset은 int나 float 같은 타입이 아니라 정확히 원시 바이트를 다룹니다. 따라서 memset으로는 정수형 배열을 $0$이나 $-1$처럼 “주기적인” 값으로만 채울 수 있습니다(음의 1은 이진 표기로 111..111처럼 보입니다).
또한 두 함수 모두에서 마지막 인자는 원소의 개수가 아니라 바이트 수라는 점을 기억하는 것이 중요합니다. 크기가 상수가 아닌 배열의 경우에는 타입의 크기에 배열의 크기를 곱하면 됩니다:
memcpy(dest, src, sizeof(int) * n)
여기서 sizeof(int) = 4입니다. 그냥 4라고 쓰는 대신 이렇게 쓰는 것은 코드가 스스로를 설명하도록 하기 위함입니다.
#std::array
C++11에는 고정 크기 배열을 위한 자체 클래스가 추가되었습니다:
// int a[3] = {1, 2, 3};
array<int, 3> a = {1, 2, 3};
이것에 대한 모든 연산은 C 배열과 동일한 방식으로 동작합니다. 주된 차이점은 이것이 STL 컨테이너라는 점, 즉 반복자를 가지고 있으며 표준 라이브러리의 모든 알고리즘을 이것에 사용할 수 있다는 점입니다.
sort(a.begin(), a.end());
하지만 일반 배열에서도 마찬가지입니다. 포인터가 자동으로 반복자로 변환됩니다:
sort(a, a + 3);
STL의 배열은 일반적인 고정 크기 배열과 마찬가지로 반복을 지원합니다:
for (int x : a)
cout << x << endl;
또한 다음과 같은 문법으로 반복하는 동안 배열의 원소를 수정할 수도 있습니다:
for (int &x : a)
x *= 2;
STL에는 memset보다 오류에 더 강한 대안인 std::fill도 있습니다:
fill(a.begin(), a.end(), 42);
이 함수는 이미 완전한 타입에 대해서도 동작하지만, 속도는 약간 더 느립니다.
#std::pair와 std::tuple
pair<T1, T2> 타입은 반드시 같은 타입일 필요가 없는 두 변수의 쌍을 저장합니다:
pair<int, int> interval = {0, 42};
pair<int, double> index_and_value = {7, 3.1415};
첫 번째 원소는 .first 필드로, 두 번째 원소는 .second 필드로 접근할 수 있습니다.
이를 일반화한 tuple은 임의 개수의 변수로 이루어진 튜플을 저장합니다:
tuple<int, int, int> coords_xyz = {1, 2, 3};
tuple에서는 .first, .second, .third 등을 사용하는 대신 인덱스를 사용해야 합니다.
페어와 튜플은 함수에서 반환할 때 편리합니다:
typedef tuple<double, double, double> point;
point rotate(point p) {
return {p[1], p[2], p[0]};
}
또한 이것들로 이루어진 배열은 순회하기도 편리합니다:
point points[100];
for (auto [x, y, z] : points)
cout << x << y << z << endl;
#struct
가능하면 pair나 tuple 대신 구조체를 선언할 것을 강력히 권장합니다.
struct point {
double x, y, z;
};
.first와 .second 또는 인덱스를 다루느라 고생하는 대신 이름이 붙은 필드를 얻을 수 있으며, 직접 메서드를 정의하고 연산자를 오버로딩할 수도 있습니다:
point::length() {
return sqrt(x * x + y * y + z * z);
}
point operator+(point a, point b) {
return {a.x + b.x, a.y + b.y, a.z + b.z};
}
구조체의 유일한 단점은 페어와 튜플에는 비교 함수와 해싱 함수가 정의되어 있어서 그대로 set이나 unordered_set 같은 STL 자료구조의 키로 바로 넣을 수 있지만, 구조체의 경우에는 이러한 함수들을 따로 작성해야 한다는 점입니다.