C 스타일의 배열은 몇가지 단점이 존재한다.
- 메모리 할당과 해제와 수동으로 처리해야 한다. 메모리를 해제하지 못하면 메모리 릭(memory leak)이 발생하여 해당 메모리 영역을 사용할 수 없다.
- []연산자에서 배열의 크기보다 큰 원소를 참조하는 것을 검사하지 못한다. 잘못 사용하면 세그멘테이션 결합(segmentation fault) 또는 메모리 손상으로 이어질 수 있다.
- 배열을 중첩해서 사용할 경우 문법이 복잡해진다.
- 깊은 복사(deep copy)가 기본으로 동작하지 않아 수동으로 구현해야 한다.
깊은 복사: 객체를 복사 할 때, 해당 객체와 인스턴스 변수까지 복사하는 방식. 전부를 복사하여 새 주소에 담기 때문에 참조를 공유하지 않는다.
C++은 C스타일을 배열을 대체하는 std::array을 제공한다.
std::array는 원소의 타입과 배열의 크기를 매개변수로 사용하는 클래스 템플릿이다.
std::array<int, 10> arr1;
arr1[0] = 1;
std::cout << "arr1 배열의 첫 번째 원소: " << arr1[0] <<std::endl;
std::array<int, 4> arr2 = {1, 2, 3, 4};
std::cout << "arr2의 모든 원소: " << arr1[0] << std::endl;
for (int i = 0; i < arr2.size(); i++){
std::cout << arr2[i] <<" ";
}
std::cout << std::endl;
at(index): 인자로 전달된 index값이 유효하지 않으면 std::out_of_range 예외를 발생시킨다.
배열 인덱스를 사용자의 입력으로 받는다던가 , 또다른 이유로 유효하지 않은 인덱스에 접근할 수 있는 상황이라면 다음과 같음 예외처리 코드를 만들 수 있다.
std::array<int, 4> arr3 = {1, 2, 3, 4};
try{
std::cout << arr3.at(3) << std::endl; //에러 아님
std::cout << arr3.at(4) << std::endl; //std::out_of_range 예외 발생
}
catch(const std::out_of_range& ex)
{
std::cerr << ex.what() << std::endl;
}
std::array 객체를 다른 함수에 전달하는 방식은 기본 데이터를 전달하는 것과 유사하다. 값 또는 참조(reference)로 전달할 수 있다. C 스타일 배열을 함수에 전달할 때처럼 포인터 연산을 사용한다거나 참조, 역참조(de-reference) 연산을 하지 않아도 된다.
다음은 사용자 정의 함수 print()에 std::array 배열을 값으로 전달하는 예제 코드다.
void print(std::array<int, 5> arr){
for (auto ele : arr)
std::cout << ele << ", ";
}
std::array<int, 5> arr = {1, 2, 3, 4, 5};
print(arr);
auto 키워드를 사용하면 초깃값의 형식에 맟춰 선언하는 인스턴스(변수)의 형식이 '자동'으로 결정된다. 이를 타입 추론(type inference)이라고 한다.
위의 코드는 배열의 크기가 고정되어 있어 다른 크기의 배열은 전달할 수가 없다.
만약 다양한 크기의 std::array 객체에 대해 동작하는 범용적인 배열 출력 함수를 만들고 싶다면 print()를 템플릿으로 선언하고, 배열의 크기를 템플릿의 매개변수로 전달하면 된다.즉, print()함수를 다음과 같은 형태로 작성한다.
템플릿: 함수나 클래스가 개별적으로 다시 작성하지 않고도 각기 다른 수많은 자료형에서 동작할 수 있게 한다.
template <size_t N>
void print(const std::array<int, N>& arr);
size_t는 typedef unsigned int size_t;로 정의된다.
함수에 std::array 객체를 전달할 경우, 기본적으로 새로운 배열에 모든 원소가 복사된다. 즉, 자동으로 깊은 복사가 동작한다. 이러한 동작을 피하고 싶다면 참조 또는 const 참조를 사용할 수 있다.
배열의 원소를 차례대로 접근하는 연산은 매우 자주 발생한다.
std::array는 반복자(iterator)와 범위 기반 for(renge-based for)문법을 이용하여 원소에 차례대로 접근할 수 있다. 범위 기반 for 반복문을 사용하여 std:array의 모든 원소를 접근할 수 있는 것은 반복자를 사용하기 때문이다.
std::array는 begin()과 end()라는 이름의 멤버 변수를 제공한다.
begin(): 가장 첫 번째 원소의 위치 반환
end(): 가장 마지막 원소의 위치(정확하게는 마지막 원소 다음 위치) 반환
범위 기반 for 반복문은 begin() 위치부터 증가연산자(++) 또는 덧셈 연산자(+) 같은 산술 연산을 이용해 차례대로 원소를 이동하다가 end() 위치에 도달하면 종료한다.
for (auto it = arr.begin(); it != arr.end(); it++){
auto element = (*it);
std::cout << element << ' ';
}
반복자는 std::array, std::map, std::set, std::list처럼 반복 가능한 모든 STL 컨테이너에 대해 사용할 수 있다. 반복자를 사용함으로써 소스 코드의 재사용성, 유지 보수, 가독성 측면에서 이점을 얻을 수 있다.
const_iterator 또는 reverse_iterator 같은 형태의 반복자도 사용할 수 있다.
const_iterator: 일반 반복자의 const버전. const로 선언된 배열에 대해 begin(), end() 같은 함수를 사용하 면 const_iterator를 반환한다.
reverse_iterator: 배열을 역방향으로 이동할 수 있습니다. 이 반복자를 ++같은 증가 연산자와 함께 사용할 경우, 일반 반복자와 반대로 이동하게 됩니다.
[]연산자와 at()함수 외에 std::array에서 원소 접근을 위해 사용할 수 있는 멤버 함수
front(): 배열의 첫 번째 원소에 대한 참조를 반환한다.
back(): 배열의 마지막 원소에 대한 참조를 반환한다.
data(): 배열 객체 내부에서 실제 데이터 메모리 버퍼를 가리키는 포인터를 반환한다.
(배열의 첫번째 주소를 반환)
std::srray<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.front() << std::endl; //1 출력
std::cout << arr.back() << std::endl; //5 출력
std::cout << *(arr.data() + 1) << std::endl; //2 출력
'코딩테스트를 위한 자료 구조와 알고리즘 with C++' 카테고리의 다른 글
1장 리스트, 스택, 큐 - std::vector (0) | 2021.08.23 |
---|---|
1장 리스트, 스택, 큐 - 자료 구조의 유형 (0) | 2021.07.22 |