[C++] std::array vs C Style Array

hwhyeons·2024년 9월 30일

오늘은 C++ 11에 도입된 고정 크기 배열에 대해 알아보고
기존 C 스타일 배열과 어떤 차이점, 장단점이 있는지 알아보겠습니다.



std::array

std::array는 컴파일 타임에 크기를 결정해야되는 고정 크기를 가지는
배열입니다.

기본 사용 방식은

// #include <array>
array<int, 5> a;

와 같이, Array 원소의 타입과 개수를 적어주면 됩니다.

vector같은 Dynamic Array와 다르게, 기본 배열과 동일하게
컴파일 타임에 크기를 결정해야 됩니다.

따라서

int cnt = 5;
array<int,cnt> a; // error

같은 상수가 아닌 값을 배열 크기로 지정하는 방식은 불가능합니다.



이미 C++에는 이런 std::array말고도 C에서 사용하던
배열 형태가 있습니다.

int a[3];

그러면 왜 기존 배열을 놔두고
굳이 C++ 11부터 std의 array가 새로 도입된 것인지,
어느 부분에서 장점을 가지는지 궁금해서 공부해보게 되었습니다.


기존 C Style 배열과의 차이점을 위주로 알아보겠습니다.



1. C Style 배열은 크기를 알 수 없다

일반 배열은, 배열 자체만으로는 크기를 바로 얻어올 수 없습니다.

동적으로 원하는 상황에 얻어올 수가 없기 때문에,

만약 배열을 함수 등으로 인자를 넘겨서 사용하고 싶은 상황에서는
해당 배열의 크기를 같이 인자로 넘겨야 되는 번거로움을 가지고 있습니다.

(파이썬과 자바를 C보다 먼저 공부했던 저에게는, C언어를 배울 때 이 부분이 크게 불편하게 느껴졌던 부분이였습니다)

참고로, 배열이 함수의 인자로 전달받지 않았다면 sizeof를 통해
배열의 크기를 받아올 수 있으나, 포인터-배열간 전환 때문에 잘못 사용하다가는
크기를 잘못 받아올 수 있습니다.

그 예로

int a[5] = {1,2,3,4,5};
int* ptr = a;
cout << sizeof(a) << endl; // 출력 : 20 
cout << sizeof(ptr) << endl; // 출력 : 8 (또는 4)

둘다 같은 배열을 가리키지만 크기가 다르게 나옵니다.
이는 배열과 포인터의 차이에 있는데, 자세한건 나중에 글로 작성할 수 있을 때 작성해보겠습니다.

어쨋든, 이렇든 sizeof()를 이용해서 배열 크기를 구하는 것 역시 번거롭고
실수의 여지가 있는 부분입니다.


하지만, std::array는 벡터와 같은 다른 STL 컨테이너에서
크기를 받아오듯이 size()함수를 이용해서 크기를 얻어올 수 있습니다.

array<int,4> v1 = { 0,1,2,3 };
cout << v1.size() << "\n"; // 4


2. iterator 미지원

C - style array는 STL Containter에서 제공하는 Iterator을 제공하지 않습니다.

vector 등에는 begin(), end() 등의 iterator 함수를 가지고 있습니다.

하지만 C Style 배열은 그렇지 않습니다.

(사실 컴파일타임에 바로 크기가 추론이 되는 경우 iterator가 없어도iterator가 있듯이 동작하기는 합니다)
int arr[3] = {1,2,3};
for (int c : arr) { // OK
	std::cout << c << "\n";
}

그러나, 배열을 함수의 파라미터로 넘기는 경우에는
배열은 포인터로 변환되므로 for each loop를 이용할 수 없습니다.

void func(int arr[3]) {
    for (int k : arr) { // Error
        cout << k << "\n";
    }
}

비주얼스튜디오 MSVC C++20 기준으로
"this range-based 'for' statement requires a suitable "begin" function and none was found"
와 같은 오류가 발생합니다.



3. 함수에서 배열을 리턴할 수 없음

C Style 배열은 배열 자체로 리턴할 수 없습니다.
물론 배열 대신 포인터 타입으로 리턴할 수는 있지만,
이것 역시 실수를 유발할 수 있는 부분들이 많습니다.

함수 내부의 지역변수로 선언한 배열을 포인터로 리턴하면
없어질 위치를 가리킨다는 등의 문제도 생길 수 있고,
또 포인터로 배열을 리턴하면 해당 배열의 크기가 몇인지 바로 볼 수도 없습니다.

하지만 std::array는 함수 리턴 값으로 사용할 수 있습니다.

배열은 포인터처럼 사용되기 때문에 배열을 리턴한다고 해서 배열의 원소들이
복사되지는 않지만, std::array를 리턴하면 해당 array를 복사해서 리턴해줍니다.



4. == 연산의 차이

이는 장단점이라기보다는, 동작의 차이입니다.

배열의 경우, ==연산은 배열 내부 원소들의 비교가 아니라
배열의 주소를 비교합니다.

하지만 std::array는 내부 원소들이 모두 동일한지 비교하도록 되어있습니다.
(vector도 마찬가지)

int arr1[4] = { 0,1,2,3 };
int arr2[4] = { 0,1,2,3 };
array<int,4> v1 = { 0,1,2,3 };
array<int,4> v2 = { 0,1,2,3 };
cout << "C Style Array 비교 : " << (arr1 == arr2) << endl; // 거짓
cout << "STL Array비교 : " << (v1 == v2) << endl; // 참

그래서 어떤 것을 사용할까?

그래서 뭘 쓰는게 좋은건지 구글링을 해봤는데,
대부분은 "std::array를 사용하라"였습니다.

std::array는 배열을 멤버로 가지고 있는 구조체이므로 성능 차이가
기존 C Style배열과 거의 차이 없다는게 대부분의 의견이였습니다.


여담으로, 저는 알고리즘 문제를 풀 때 인덱스 범위 체크 때문에
C Style배열을 잘 사용하지 않고 보통 벡터를 이용합니다.

std::vectorstd::array는 원소에 접근하기 위한 함수로 .at()을 제공하는데,

C스타일 배열에서 사용하는 operator[]와 다르게 인덱스가 벗어나면
바로 런타임 예외가 발생하기 때문에 문제를 풀었을 때 런타임 오류로 틀린건지
아니면 반례로 틀린건지 등을 바로 알 수 있습니다.

꼭 배열이 아니여도 벡터도 마찬가지로 (컴파일러마다 다를 수는 있지만)
v[3]과 같이 접근했을 때 인덱스를 벗어나면 예외가 안뜨고 그냥 쓰레기값이
나오는 바람에,

문제를 틀렸을 때 이게 인덱스 범위에 의한 런타임 오류로 문제를 틀린건지
아니면 내가 짠 코드에 논리 문제가 있어서 반례가 있는건지
알 수가 없을 때가 있어서 답답할 때도 많았습니다.



(참고 문서들)
https://stackoverflow.com/questions/30263303/what-are-the-advantages-of-using-stdarray-over-c-style-arrays

https://en.cppreference.com/w/cpp/container/array/operator_cmp

0개의 댓글