CS/자료구조

[자료구조] std::array with C++

nowkoes 2023. 2. 2. 14:59

<본 카테고리는 "코딩 테스트를 위한 자료 구조와 알고리즘 with C++을 기반으로 작성하였습니다>


std::array

 std::array는 cpp reference에 따르면 크기가 정해져있는 캡슐화된 컨테이너 클래스 템플릿이다. 배열 클래스의 특징으로는 원소의 타입과 배열 크기를 매개변수로 사용한다. 앞선 파트에서 선형 자료 구조는 연결된 것과 연속된 것으로 나뉜다고 배웠다. array 클래스도 결국엔 배열이므로 연속된 자료 구조다. 

 

지난 시간에 사용한 예제 그림. 크기가 n인 배열 array를 선언했을 때 데이터가 저장되는 방법.

 

 이를 이용하는 예제를 보자. 자료형은 캐릭터 타입 char고 크기가 3인 배열을 초기화하고 화면에 출력하는 코드다.

 

#include <iostream>
#include <array>
using namespace std;

int main()
{
	// [] 연산자를 이용해 초기화 하는 방법
	array<char, 3> myAry1;
	myAry1[0] = 'a'; myAry1[1] = 'b'; myAry1[2] = 'c';

	// 선언과 동시에 초기화하는 방법
	array<char, 3> myAry2 = { 'A', 'B', 'C' };

	cout << "myAry1의 원소: ";
	for (char& ch : myAry1)
		cout << ch << ' ';

	cout << '\n' << "myAry2의 원소: ";
	for (int i = 0; i < myAry2.size(); i++)
		cout << myAry2.at(i) << ' ';
}

실행 결과

 

 위의 예제를 통해 알 수 있듯이 C스타일 배열처럼 [] 연산자를 이용해 원소에 접근할 수 있고, std::array.at() 를 사용해 사용해 원소에 접근할 수 있다.

 

std::array<T,N>::at(size_type pos): pos 위치에 있는 원소에 대한 참조를 반환

객체 전달

 

 이번에는 객체를 다른 함수에 전달해보자. 전달할 때 매개변수로 값 또는 참조로 전달할 수도 있고, cosnt를 함께 사용할 수도 있다. 이때 C스타일 배열과 다르게 포인터 연산, 혹은 참조 연산을 하지 않아도 된다는 장점이 있다.

 

#include <iostream>
#include <array>
using namespace std;

template <typename T, size_t N>
void print(const array<T, N>& ary)
{
	for (auto& ch : ary)
		cout << ch << ' ';
}

int main()
{
	array<char, 5> myAry = { 'a', 'b', 'c', 'd', 'e' };
	print(myAry);
}

실행 결과

 

 코드에 보면 const 참조를 사용한 걸 볼 수 있는데, 이는 깊은 복사(자동으로 새로운 배열에 모든 원소가 복사)를 피하기 위해 사용한 것이다. 깊은 복사와 얕은 복사의 차이점은 다음과 같다.

 

  • 얕은(Shallow) 복사: 디폴트 복사 생성자에 의한 멤버 대 멤버의 복사 방식. 즉, 복사 대상의 멤버를 복사할 객체의 멤버로 그대로 복사하는 것.
  • 깊은(Deep) 복사: 원본이 소유한 모든 것을 복사하는 방식. 복사 생성자, 대입 연산자를 직접 정의하여 메모리는 복사하지 않고, 새로운 인스턴스를 만들어 복사하는 방식.

 

class Person을 얕은 복사를 진행했을 때와 깊은 복사를 진행했을 때의 차이


반복자 사용

 

 위의 예제를 보면 for문을 기존처럼 조건식을 이용해 다루지 않고, 범위를 기반으로 다루는 것을 알 수 있다.

for(char& ch : ary)
	cout << ch << ' ';

 

 이처럼 원소를 차례대로 접근하는 연산을 수행할 때, 범위 기반 for(range-based for) 문법을 이용해 빠르게 처리하는 방법을 자주 사용하게 된다. 이런 방식은 배열의 크기를 정확하게 지정하지 않아도 되고, 소스 코드의 재사용성, 유지 보수, 가독성 측면에서 이점이 존재한다.

 

 이러한 방법이 가능한 이유는 바로 반복자(iterator)를 사용할 수 있기 때문이다. 여기서 반복자란 컨테이너에 저장되어 있는 원소들을 순회할 때 사용하는 객체로서, 포인터를 일반화했다고 생각하시면 편할 것 같다. 이 iterator를 이용하기 위해선 begin()과 end()와 같은 원소의 위치를 반환하는 멤버 함수들이 필요한데, std::array에서는 이 함수들이 제공되고 있다.  

 

 따라서 반복자가 사용가능하므로 원소 접근 함수를 이용한다면 std::array 클래스의 원소 참조가 더욱 쉬워진다. 이러한 원소 접근 함수는 다음 예제를 참고하시면 될 것 같다.

 

std::array<T,N>::front() : 배열의 첫 번째 원소에 대한 참조 반환
std::array<T,N>::back() : 배열의 마지막 원소에 대한 참조를 반환
std::array<T,N>::data() : 배열 객체 내부에서 실제 데이터 메모리 버퍼를 가리키는 포인터를 반환
#include <array>
#include <iostream>
using namespace std;

int main()
{
    array<int, 5> myAry = { 1,2,3,4,5 };

    cout << myAry.front() << '\n'; // 1
    cout << myAry.back() << '\n'; // 5
    cout << *(myAry.data() + 1) << '\n'; // 2
}

깊은 비교와 깊은 복사 지원

 

 std::array는 깊은 비교(deep comparision)를 지원한다. 여기서 깊은 복사는 알겠는데, 깊은 비교가 뭘까? 이해를 위해 예제를 보며 비교해보자.

 

#include <iostream> 
using namespace std;

int main()
{
	int ary1[10] = { 1,1,1,1,1,2,2,2,2,2 };
	int ary2[10] = { 1,1,1,1,1,2,2,2,2,2 };

	if (ary1 == ary2)
		cout << "int Ary1[10] == int Ary2[10]" << endl;

	else
		cout << "int Ary1[10] != int Ary2[10]" << endl;
}

 

 해당 코드에 화면에 출력되는 결과물은 무엇일지 생각해보자. 우리가 일반적으로 사용하는 배열은 깊은 비교를 지원하지 않는다. 따라서 얕은 비교(shallow comparision)를 사용하게 되는데, 얕은 비교는 기본 타입 데이터의 경우 값의 동일 여부만, 객체의 경우 참조만 비교한다. 즉, 두 객체의 주소 값을 비교하여 같은 객체인지 판단한다. 따라서 ary1과 ary2의 모든 값들이 같더라도 참조값이 다르기 때문에 같지 않다고 출력한다.

 

#include <iostream> 
#include <array>
using namespace std;

int main()
{
	array<int, 10> myAry1 = { 1,1,1,1,1,2,2,2,2,2 };
	array<int, 10> myAry2 = { 1,1,1,1,1,2,2,2,2,2 };

	if (myAry1 == myAry2)
		cout << "array<int,10> ary1 == array<int,10> ary2" << endl;

	else
		cout << "array<int,10> ary1 != array<int,10> ary2" << endl;
}

 

 하지만 std::array일 경우는 다르다. 깊은 비교객체의 경우에도 값으로 비교하게 된다. 즉, 두 객체의 내용이 완전히 같은지를 판단한다는 의미다. 일반적으로 깊은 비교를 위해서는 객체의 내용을 비교하는 함수를 재정의해야 하는데, std::array에서는 이를 제공하고 있다고 생각하면 된다. 따라서 해당 코드의 출력은 같다고 나온다.

 

 또한 std::array는 깊은 복사를 위한 복사 할당 연산자(copy-assignment operator)도 지원한다. 따라서 관계 연산자를 이용해 std::array간의 배열을 비교하는 용도로 사용할 수 있게 된다. 하지만 주의할 점이 있다. 관계 연산자를 이용할 경우, std::array는 배열의 크기가 데이터 타입의 일부로 동작하기 때문에 크기가 다른 배열은 서로 다른 타입으로 인식되어 비교할 수 없다.

 

#include <iostream> 
#include <array>
using namespace std;

int main()
{
	array<int, 10> myAry1 = { 1,1,1,1,2,2,2,2,2,2 };
	array<int, 10> myAry2 = { 1,1,1,1,1,2,2,2,2,2 };
	array<int, 9> myAry3 = { 1,1,1,1,2,3,4,5,6 };

	if (myAry1 > myAry2)
		cout << "array<int,10> ary1 > array<int,10> ary2" << endl;

	else
		cout << "array<int,10> ary1 !> array<int,10> ary2" << endl;

	if (myAry1 > myAry3) // Error
		cout << "array<int,10> ary1 > array<int,9> ary3" << endl; 
}

std::array의 한계

 

 이러한 std::array와 같은 C스타일 배열은 몇 가지 단점이 존재한다. 이를 극복한 것이 std::vector인데 이는 다음 포스트에서 알아보도록 하자.

  • std::array의 크기는 실행중에 변경할 수 없으므로, 크기는 컴파일 시간에 결정됨
  • 크기가 고정되어 있어 원소를 추가하거나 삭제할 수 없음
  • 항상 스택 메모리를 사용함

요약

std::array<T,N>
1. 정의: 크기가 정해져있는 캡슐화된 컨테이너 클래스 템플릿
2. 선언
 - <array> 라이브러리를 include 한 후 원소의 타입과 개수를 매개변수로 사용 
3. 특징
 a. C스타일 배열처럼 [] 연산자 사용 가능
 b. 객체 전달 가능
 c. 반복자 기능 사용 가능
 d. 깊은 비교와 깊은 복사 지원
4. 단점
 a. 크기가 컴파일 시간에 결정
 b. 원소 변경 불가
 c. 스택 메모리만 사용

 

반응형