STL Container (1) 'array'
STL의 대표적인 라이브러리로는 다음과 같다.
1) 배열 개념의 임의 타입의 객체를 보관할 수 있는 Container 라이브러리
2) 인덱스 개념의 컨테이너에 보관된 원소에 접근할 수 있는 반복자 itorator 라이브러리
3) 반복자들을 가지고 일련의 작업을 수행하는 알고리즘
그중에서 Container 라이브러리를 먼저 알아볼 것이다.
Container 라이브러리 중에서도 sequence container 에 대해서 먼저 알아볼 것이고
sequence container 에는 array, vector list, 등 이 있다.
먼저 array 부터 알아보겠다.
https://learn.microsoft.com/ko-kr/cpp/standard-library/array-class-stl?view=msvc-170#at
array 클래스(C++ 표준 라이브러리)
자세한 정보: 배열 클래스(C++ 표준 라이브러리)
learn.microsoft.com
C++ 레퍼런스 - std::array (안전한 배열)
모두의 코드 C++ 레퍼런스 - std::array (안전한 배열) 작성일 : 2020-07-17 이 글은 24286 번 읽혔습니다. 이 컨테이너는 마치 C 언어에서의 배열인 T[N] 과 비슷하게 작동하는데, 예를 들어서 C 배열 처럼 {}
modoocode.com
위 두 사이트를 많이 참고하며 공부했다. 이곳에서 설명이 부족한 부분들은 위 두 사이트들에서 보다 더 자세하게 설명되어 있으니 해당 사이트에 들어가 부족한 부분을 채우기를 바란다.
array 고정된 크기의 배열을 사용하고 싶을때 라이브러리에 정의되어 있는 array를 불러와 사용할 수 있다면
고정된 크기의 배열을 생성하는데 있어서 좀 더 효율적으로 사용할 수 있음을 알 수 있다.
템플릿을 공부하고 array이라는 클래스 템플릿이 어떻게 동작하는지에 관해서 코드를 분석해보고 싶었고 또 할 수 있다면
array 클래스 템플릿이 동작하는 예를 구현해보며 스스로가 이해하고 있음을 직접 확인하고 싶었다.
template <class T, std::size_t N>
class array { ''''''' }
위는 array 클래스가 라이브리러에서 어떻게 정의 되어있는지를 확인할 수 있는 좋은 코드이다.
우선 tmeplate 이 사용됐고 자료형을 받을 수 있는 T 와 배열의 사이즈를 받을 수 있는 std안에 있는 size_t 타입의 N이 받고 있다. 그럼 위 예시를 보고 array 클래스를 불러와 템플릿 array 클래스의 객체를 생성할 대략
다음과 같이 생성해야 함을 알 수 있다.
#include <array>
int main() {
array<int,5> aa {1,2,3,4,5};
array<char,3> c = {'a','b','c'};
array<double,2> d = {1.1,2.2};
}
템플릿 array 클래스 객체를 생성할떄 초기값을 선정하는데 있어 = 대입이 생략된체 선언 할 수 있음을 추가로 알린다.
그렇다면 위 메인에서 템플릿 클래스 객체를 생성하고 그 클래스 내에 선언되어 사용할 수 있는 멤버들은 다음과 같다.
다음의 코드는 직접 array 클래스를 정의해보고 구현해본 코드들이다.
template <class T, typename std::size_t N>
class Myarray {
T len;
T array[N];
public :
Myarray() :len(0) {
}
template <typename ...Types>
Myarray(T num, Types ...nums) : len(0) {
array[len++] = num;
init(nums...);
}
void init(T num) {
array[len++] = num;
}
template <typename ...Types_init>
void init(T num, Types_init ...nums_init ) {
array[len++] = num;
init(nums_init...);
}
/*존재하는 멤버 함수*/
T& at(std::size_t n) {
if (n>=N || n<0) {
throw std::out_of_range("out_of_range");
}
return array[n];
}
void fill(T n) {
for(std::size_t i =0; i<N; i++) {
array[i] = n;
}
}
/*연산자 오버로딩 */
std::size_t size() { return N; }
T & operator[](T n) {
return array[n];
}
void operator = (Myarray <int, 5> & ref) {
for (std::size_t i =0; i<N; i++) {
array[i] = ref.array[i];
}
}
};
실제 라이브러리에 들어가 array 클래스를 보면 위와 같이 유사하게 작성되어 있음을 알 수 있다.
메인에서 객체를 생성하고 생성자가 호출된다.
template <typename ...Types>
Myarray(T num, Types ...nums) : len(0) {
array[len++] = num;
init(nums...);
}
그런데 생성자도 템플릿으로 되어 있는데 생성자도 함수의 일종으로 템플릿 작성이 가능하며
매개변수로 T num 과 Types ... nums 로 되어 있는 것이 보이고 인덱스 len을 0으로 초기값을 정해주고
array[len++] 인덱스에 해당하는 배열번지에 num 값을 보내주고 있다.
그런데 Types ... nums 이라는 것을 처음 볼 것이다
잘 생각해보면 알 수 있다.
1만 값을 받는 것이 아닌 2,3,4,5 라는 뒤에 남아있는 숫자들도 있는데 이들의 값을 받아야 한다.
하지만 가변적인 저 값들은 몇개가 남아있을지 몇개가 더 인자로 전달 될 수 있는지 알 수 없다.
따라서 매개변수에서 값을 입력 받을때 가변적으로 입력 받을 수 있는 가변 인자라는 사실을 컴파일러에게 명시해야 한다.
그것이 int ... 이라는 뜻이고 ...이라는 것이 가변인자. (가변 매개변수) 라고 부르며
이것을 템플릿에 사용할 경우 템플릿 파라미터 팩화 시켯다 라고 말하는 것이다.
즉, 위 템플릿으로 사용하였으니 템플릿 파라미터 팩화 시켜 nums는 가변적으로 데이터를 받을 수 있다 라는 의미이며
가변적으로 받을 수 있는 매개변수의 자료형은 int도 받을 수 있고 double도 받을 수 있고 char도 받을 수 있으니 Types라고 정의 한 것이다.
그러고 init(nums...) 라는 함수를 호출하고 인자값에 nums... 이라는 것이 보인다.
이는 현재 남아있는 가변적 데이터값들을 init함수에 전부 전달하겠다라는 의미이며. 생성자에서는 재귀호출을 하며 가변적 데이터들을 처리하기에 어려움이 있으니 함수를 호출하는 것이다.
void init(T num) {
array[len++] = num;
}
template <typename ...Types_init>
void init(T num, Types_init ...nums_init ) {
array[len++] = num;
init(nums_init...);
}
매개변수가 T num과 Types_init ... nums_init 이라는 매개변수가 받는다.
즉, 생성자에서 init( )함수를 호출하여 전달한 인자를 num 과 nums_init이 받는다.
생성자의 num은 1을 init 함수에서 num은 2를 받고
생성자의 nums는 2,3,4,5를 init 함수에서 nums_init은 3,4,5 를 받는 것이다.
그러고 그에 해당하는 값을 배열번지에 맞게 저장을 해주고
다시 한번 init 함수를 호출하고 있다 재귀호출을 하고 있는 것인데 호출을 하면서 nums_init...을 인자 전달한다.
즉 3,4,5 를 전달하는 것이고 3을 num이 받는다. 4,5는 nums_init이 받는다. 그렇게 반복해서
그러고 다시 한번 init 함수를 호출하는데 값이 마지막 5뿐이라면 ?
여기서 중요하다.
nums_init이 5이후의 값이 없다면 가변적으로 보내는 것이 아닌 함수 오버로딩을 시켜
void init(T num) {
array[len++] = num;
}
위의 값이 호출되게 하여 마지막 남은 가변 데이터를 저장해주는 과정을 담은 것이다.
왜냐하면 5 이후의 값이 없고 남아 있는 값들이 없어 nums_init에서 받을 수 없다.
따라서 매개변수 하나만 필요한 시점이고 그 매개변수는 num이면 된다. 따라서 같은 함수에서 오버로딩 된 것이다.
이로써 STL array 클래스에서 객체를 생성하고 생성된 객체가 배열로 저장될떄 어떻게 저장되는지 알 수 있었다.
----------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
또 실제 라이브러리 array 클래스에서는 operator [] 와 = 가 정의되어 있는 것을 알 수 있다.
이는 다음과 같이 사용되기 위함이다.
Myarray <int, 5> a2;
a2 = a1;
a2 객체를 생성한다. 디폴트 생성자가 호출되거나 혹은 정의되어 있다면 그 정의된 생성자가 호출될 것이다.
a2 = a1 이 보인다 객체에 객체를 대입하려한다. 즉 대입연산자 오버로딩이 된다.
void operator = (Myarray <int, 5> & ref) {
for (std::size_t i =0; i<N; i++) {
array[i] = ref.array[i];
}
}
다음과 같이 말이다.
아 for 문에서 i 의 자료형을 std:size_t 라고 해준 이유는 입력 받은 배열의 크기 N의 타입이 size_t이기 때문이다.
이를 auto 로 사용하여 자료형을 추론하게끔 사용할 수도 있으며 int도 ok
또한 저장된 값을 출력하고 싶다면 ?
다음과 같이 작성할 것이다.
for (std::size_t i = 0; i<a1.size(); i++) {
std::cout<<a1[i];
//std::cout<<a1.at(i);
}
맞다 a1은 객체일뿐 배열이 아니다. 하지만 [] 을 사용하고 인덱스를 주고 있다 그렇다 오버로딩되고 있다.
T & operator[](T n) {
return array[n];
}
배열의 번지 그대로 반환하고 있다.
그런데 코드를 보면 알겠지만
std::cout<<a1[i];
//std::cout<<a1.at(i);
두가지 중에서 아래의 코드는 무엇일까? at(i)
at(i) 역시 멤버함수로 존재한다.
a1 함수는 배열 범위에서 벗어난 경우 예외처리를 해주기 위해 throw를 던져주는 함수이다.
예외처리를 위한 함수이다.
즉, a1(4)라고 입력을 한다면 4번지에 해당하는 값이 출력이 된다. 하지만 a1(6) 이나 a1(-1)이라고 입력하게 된다면
이는 입력된 정보가 잘못된 것이니 이에 대한 예외 처리한 함수라는 뜻이다.
이를 위해 try{}와 catch 함수를 사용한다면 그 던진 throw를 catch가 받아 예외 처리를 할 수 있게 된다.
T& at(std::size_t n) {
if (n>=N || n<0) {
throw std::out_of_range("out_of_range");
}
return array[n];
}
다음과 같이 던져졌다?
try {
for (std::size_t i = 0; i<a1.size(); i++) {
//std::cout<<a1[i];
std::cout<<a1.at(i);
}
std::cout<<std::endl;
Myarray <int, 5> a2;
a2 = a1;
for (std::size_t i = 0; i<a1.size(); i++) {
std::cout<<a2[i];
}
std::cout<<std::endl;
}
catch (std::out_of_range &ref) {
std::cout<<ref.what();
}
다음과 같이 예외를 처리할 수 있다.
아 이때 at 함수내에서 던져지는 throw는 out_of_range 의 개체이다. 그 개체를 catch에서 받고
what() 어떤 이유로 종료됐는지 문자열로 찍어라 라는 함수.
그러면 a1.at()와 그냥 a1[i] 와의 차이는 무엇이냐?
a1[1]은 throw를 발생시키지 않는다. 컴파일러가 실행속도를 위해서 예외가 있는지 없는지에 대한 안정성 검사를 하지 않고 실행속도에 초점을 맞추지만 at는 throw를 발생시켜 실행속도는 떨어질지라도 안정성을 더 높인다.
그것이 at 와 a1[i]에 대한 차이이다.
#include <iostream>
template <class T, typename std::size_t N>
class Myarray {
T len;
T array[N];
public :
Myarray() :len(0) {
}
template <typename ...Types>
Myarray(T num, Types ...nums) : len(0) {
array[len++] = num;
init(nums...);
}
void init(T num) {
array[len++] = num;
}
template <typename ...Types_init>
void init(T num, Types_init ...nums_init ) {
array[len++] = num;
init(nums_init...);
}
/*존재하는 멤버 함수*/
T& at(std::size_t n) {
if (n>=N || n<0) {
throw std::out_of_range("out_of_range");
}
return array[n];
}
void fill(T n) {
for(std::size_t i =0; i<N; i++) {
array[i] = n;
}
}
/*연산자 오버로딩 */
std::size_t size() { return N; }
T & operator[](T n) {
return array[n];
}
void operator = (Myarray <int, 5> & ref) {
for (std::size_t i =0; i<N; i++) {
array[i] = ref.array[i];
}
}
};
int main() {
Myarray <int, 5> a1 = {1,2,3,4};
try {
for (std::size_t i = 0; i<a1.size(); i++) {
//std::cout<<a1[i];
std::cout<<a1.at(i);
}
std::cout<<std::endl;
Myarray <int, 5> a2;
a2 = a1;
for (std::size_t i = 0; i<a1.size(); i++) {
std::cout<<a2[i];
}
std::cout<<std::endl;
}
catch (std::out_of_range &ref) {
std::cout<<ref.what();
}
a1.fill(5);
for (std::size_t i = 0; i<a1.size(); i++) {
std::cout<<a1[i];
}
}
따라서 코드가 array 클래스의 정의되어 있는 일부분의 멤버들이다.
실질적으로 선언되어 동작하는 멤버들은 보다 더 많다. 일부분임을 반드시 알고 있어야 한다.
더 자세한 정보는
https://learn.microsoft.com/ko-kr/cpp/standard-library/array-class-stl?view=msvc-170#at
이곳에서 확인하기를 바란다.