template <class Ty, std::size_t N>
class array;
array
는 기존 정적 배열을 구현한 컨테이너이다. <array>
헤더에서 정의된다. Ty
에는 배열의 타입이, N
에는 정의할 크기가 들어간다. msvc++ 기준으로는 array
컨테이너 안에 실제로 값이 저장되는 정적 배열 Ty _Elems[N]
가 public으로 선언 되어 있다. (왜 public으로 드러나 있는지는 잘 모르겠다.)
단순히 생각하면 기존 정적 배열 대신 array
를 사용할만한 경우는 없어보인다. 헤더도 추가로 써야하고, 타이핑 길이도 더 길게해야하고 귀찮다. 그러나 몇 가지 이점이 있다.
- 함수를 통해 배열을 주고 받을 때, 배열의 크기를 유지할 수 있음.
(크기를 따로 전달할 필요 없음.) - 경계 인덱스(배열의 끝)에서 안전한 연산 가능
- 일부 연산(함수)이 미리 구현되어 있음
자세한 설명와 예시 코드는 기본적인 사용법을 확인한 다음인 2. 이점에서 계속하겠다.
1. 사용
array
는 타입 정의를 제외하고는 배열과 완전히 똑같이 사용할 수 있다. 두드러지는 차이점은 템플릿을 이용한다는 것과 사이즈를 기억하고 있다는 것이다. 템플릿 문법을 사용해서 타입과 생성할 크기를 명시해야 한다. 이 때 명시한 크기를 size()
함수에서 다시 얻을 수 있다.
#include<iostream>
#include<array>
using namespace std;
int main() {
array<int, 10> arr;
for (int i = 0; i < arr.size(); i++) {
arr[i] = i+1; // 0 1 2 3 4 5 6 7 8 9
}
arr[4] = 55;
for (auto& v : arr) {
cout << v << ' ';
}
cout << endl;
}
1 2 3 4 55 6 7 8 9 10
생성
List-initialization으로 원소들을 초기화 할 수 있다. 배열의 크기는 생략할 수 없다. 별도로 작성된 생성자가 없다. 기본 생성자와 = 연산자를 사용하는 것으로 보인다.
array<int, 5> arr1; // 쓰레기 값
array<int, 5> arr2 = { 0 }; // 0 0 0 0 0
array<int, 5> arr3 = { 1,2,3,4,5 }; // 1 2 3 4 5
array<int, 5> arr4 = { 1,2 }; // 1 2 0 0 0
array<int, 5> arr5{ 1 }; // 1 0 0 0 0
array<int, 5> arr6{ 1,2,3,4,5 }; // 1 2 3 4 5
array<int, 5> arr7({ 1,2,3,4,5 }); // 1 2 3 4 5
array<int, 5> arr8(arr7); // 1 2 3 4 5
// array<int, > arr9={ 1,2,3 }; //Error
이터레이터
begin()
, end()
로 얻을 수 있는 array iterator는 이터레이터의 모든 연산을 지원한다.
접근
배열처럼 []
연산자를 이용해 원하는 인덱스에 직접 접근할 수 있다. 동일한 함수로 .at()
을 사용할 수 있다. 만약 지정해줬던 밖의 인덱스를 접근하고자 했을 때에는 예외를 발생시켜 프로그램을 중단한다.
array<int, 5> arr{ 1,2,3,4,5 };
cout << arr[1] << endl; // 2
arr[3] = 7; // 4 -> 7
cout << arr.at(3) << endl; // 7
arr.at(0) = 99; // 1 -> 99
// ERROR!!
// cout << arr.at(10) << endl;
// cout << arr[10] << endl;
front()
와 back()
을 이용해 첫 번쨰 원소와 마지막 원소의 레퍼런스를 얻을 수도 있다. (이터레이터 begin/end와 헷갈리지 말자) data()
를 이용해서 _Elems의 포인터를 직접 얻을 수도 있지만, 이 방법을 추천하지는 않는다.
// .front() == arr[0]
// .back() == arr[arr.size()-1]
cout << arr.front() << ", " << arr.back() << endl; // 99, 5
// data()
int* p = arr.data();
for (int i = 0; i < arr.size(); i++)
cout << p[i] << ' '; // 99 2 3 7 5
용량
size()
이용해 배열의 크기를 확인할 수 있다. size 값은 template의 상수를 이용한다. 컨테이너의 인터페이스 일관성을 위해 max_size()
도 구현이 되어있는 것 같은데 이는 size()
와 같다.
array<int, 5> arr{ 1,2,3,4,5 };
cout << arr.size() << endl; // 5
cout << arr.max_size() << endl; // 5
array
는 size를 저장하는 변수를 따로 만들지 않는데, 그래서 일반적인 배열을 만들 때와 같은 크기만큼만 사용한다.
// size 내부 구현 예시
template<class Ty, size_t N>
class array {
...
size_t size() { return N; }
};
empty()
함수를 이용해 크기가 0인 배열인지 확인할 수 있지만, 정적 배열에서 크기가 0인 배열을 생성할 일은 잘 없다.
array<int, 5> arr{ 1,2,3,4,5 };
array<int, 0> zero;
cout << arr.empty() << endl; // false
cout << zero.empty() << endl; // true
수정
수정 연산은 컨테이너에 추가적인 원소를 삽입하거나 삭제하는 연산인데, array
는 정적 메모리이기 때문에 이러한 연산을 지원하지 않는다.
기타
array
는 원소들을 단일한 값으로 채울 수 있도록 fill()
함수를 제공한다. 내부적으로는 C언어 라이브러리의 memset()
을 호출하는 것으로 보인다.
array<int, 3> arr2; // 쓰레기 값이 들어가 있다.
arr2.fill(100); // 100 100 100
2. 이점
1. 함수를 통해 배열을 주고 받을 때, 배열의 크기를 유지
함수에 배열을 넘겨주고자 할 때, 기존 배열을 이용하면 배열이 포인터로 붕괴되기 떄문에 배열의 원소의 개수를 반드시 같이 넘겨줘야 했다. 이는 해당 함수를 사용하기 번거롭게 만들고, 실수를 유발할 가능성이 존재한다. 그에 반에 array
컨테이너의 레퍼런스를 넘겨주면, size()를 사용할 수 있기 때문에 이러한 문제를 해결할 수 있다. 단, 해당 함수를 템플릿 함수로 작성해야 한다는 번거로움이 있다.
#include<iostream>
#include<array>
using namespace std;
// 기존 기본 배열을 이용하는 sum
int sum(int* arr, size_t size) {
int result = 0;
for (int i = 0; i < size; i++) {
result += arr[i];
}
return result;
}
// array 컨테이너를 이용하는 sum
template<typename Ty, size_t N>
int sum(array<Ty, N>& arr) {
int result = 0;
for (auto& v : arr) {
result += v;
}
return result;
}
int main() {
constexpr int SIZE = 5;
int arr1[SIZE] = { 1,2,3,4,5 };
array<int, SIZE> arr2 = { 1,2,3,4,5 };
// 배열에서 정상적인 사용
cout << sum(arr1, sizeof(arr1) / sizeof(int)) << endl;
cout << sum(arr1, SIZE) << endl;
cout << sum(arr1, 5) << endl;
cout << sum(arr1, 6) << endl; // 실수하는 경우
// array 컨테이너 이용
cout << sum(arr2) << endl;
}
15
15
15
-858993445 (알 수 없음)
15
2. 경계 인덱스(배열의 끝)에서 안전한 연산 가능
기존 배열/포인터를 사용하는 경우, 배열의 범위를 넘어가는 포인터 연산에 대해서도 상관없이 작동했다. 이를 의도한 것이 아니라면 예상하지 못한 결과가 나오거나, 프로그램 자체가 잘못될 수도 있는데, array
는 범위를 넘어가는 연산에 대해서 debug Assertion을 수행한다. 따라서, 디버깅 모드에서 인덱스에 대한 실수를 바로잡을 수 있다.
int arr1[] = { 1,2,3,4,5 };
array<int, 5> arr2{ 1,2,3,4,5 };
arr1[6]; // 실행됨
// 중단됨.
//cout << arr2[6] << endl;
//auto iter = arr2.begin() + 7;
3. 일부 연산(함수)이 미리 구현되어 있음
fill()
이나, 배열 자체끼리의 비교, 배열 전체 복사 등에서 컨테이너에서 미리 구현된 연산이 존재하기 떄문에 이런 연산을 직접 구현해야 하는 배열에 비해 array
를 쓰는 것이 좋을 수 있다.
비어있는 배열
msvc++의 std는 array<Ty,N>
과 array<Ty, 0>
를 따로 정의 한다. 즉, 크기가 0인 array
는 다른 타입으로 새로 만들었다. 따라서, 빈 배열에 대한 오류처리를 함수를 오버로드해서 따로 처리할 수 있다. 이 경우 메인과 오류 처리에 대해서 분리되어 있기 때문에 가독성이 오를 여지가 있다. (계속 말하지만, 크기가 0인 배열을 쓸 일이 있을지는 모르겠다.)
- 일반적인 경우
template<typename Ty, size_t N>
void func(array<Ty, N> arr) {
// ...
if (arr.empty()) {
// 비어있는 경우이 대한 액션을 취한다.
}
// main code
}
- 오버로딩을 활용한 경우
template<typename Ty, size_t N>
void func(array<Ty, N> arr) {
// main code
}
template<typename Ty>
void func(array<Ty, 0> arr) {
// 비어있는 경우이 대한 액션을 취한다.
}