
Container and Array
만약 한 회사의 전 직원의 나이를 기록하고 평균 나이를 계산한다고 생각해보자
컨테이너나 배열이 없다면 전 직원이 100명이라고 가정할 때 100개의 변수를 정의하고 계산해야 한다
매우 반복적이며 휴먼에러가 발생하기 너무 좋은 상황이다, 또한 만약 콘솔창 출력을 한다고 하면 이 이름들을 전부 입력해서 콘솔창 출력을 해야한다
또한 데이터를 추가, 삭제하면 사이드 이펙트가 발생하기 쉽다
이럴때 사용할 수 있는게 컨테이너이다
컨테이너란 여러개의 관련된 이름없는 값들을 하나의 단위로 묶어서 저장하고 관리하는 자료구조이다
앞서 자주 예시로 작성한 string도 문자열 컨테이너 (유사 컨테이너)로서 문자들의 모음을 위한 저장공간을 제공하는 데이터타입이다
std::string name{ "kelvin" };
std::cout << name;
위에서 작성한대로 컨테이너의 element들은 이름이 존재하지 않는다 (unnamed), 하지만 컨테이너 객체 자체에는 이름이 존재한다 (string 컨테이너 객체 자체는 name이라는 이름이 있지만 k, e, l, v, i, n은 이름이 없다는 뜻)
그렇기 때문에 컨테이너에 많은 element를 동적으로 넣을 수 있는것이다 (struct와 같은 멤버 변수 이름이 반드시 필요한 자료구조는 추가,삭제 시 recompile이 필요하기 때문)
이 정의에 따라 struct는 컨테이너가 될 수 없다 (멤버 데이터 이름이 필요하기 때문에)
컨테이너의 elements들의 갯수를 length나 count로 부른다
std::string name{ "kelvin" };
name.length(); //6
C++에는 다양한 컨테이너가 존재하고 각각의 컨테이너들이 지원하는 연산과 연산을 했을때 성능도 다르다
(중간 삽입/삭제가 성능상 유리한 컨테이너가 있고 불리한 컨테이너가 있다, 또한 중간 삽입/삭제를 지원하지 않을 수 있다)
모든 컨테이너에는 장단점이 존재하고 어떤 케이스에서 어떤 컨테이너를 사용하는게 가장 적합할 지 선택하는것이 중요하다 (유지보수성 향상, 성능 향상)
element type
대부분의 언어에서 컨테이너의 element들은 모두 같은 타입을 가진다
std::string과 같은 일부 컨테이너는 미리 element 타입이 정해져있지만 대부분의 컨테이너는 class template으로 구현되기 때문에 프로그래머가 원하는 타입으로 사용이 가능하다 (원하는 타입마다 컨테이너를 하나하나 만들필요 없기 때문에 유연하다)
C++ container
C++의 컨테이너 라이브러리는 STL의 일부며 몇 가지 일반적인 유형의 컨테이너를 구현하는 다양한 클래스 타입을 가진다
https://en.cppreference.com/w/cpp/container
C++에서의 컨테이너는 일반 프로그래밍에서의 컨테이너의 개념보다 좀 좁다, 오직 컨테이너 라이브러리에 있는 클래스 타입만 C++에서는 컨테이너로 간주된다
이 세가지는 정확히는 유사 컨테이너라고 부른다 (100% 완벽하게 컨테이너의 요구사항을 구현하지는 않지만 대부분 구현하기 때문에)
array
배열은 elements들을 연속적으로 저장하는 컨테이너 데이터 타입이다 (각 element가 간격 없이 인접한 메모리 위치에 배치된다)
배열은 element에 대한 빠르고 직접적인 접근을 허용한다
C++에는 3가지 주요 배열 타입이 존재한다
C++에서 배열을 더 안전하고 사용하기 쉽게 만들기 위해서 C++03에서 std::vector 컨테이너 클래스를 도입하였다, std::vector는 위 3가지 배열 타입 중 가장 유연하고 다른 배열 타입에는 없는 기능들도 가지고 있다
모든 컨테이너 클래스들은 유사한 interface를 가지고 있다, 따라서 하나의 컨테이너 사용법을 잘 배우면 나머지 컨테이너도 비슷하게 사용이 가능하다
std::vector
std::vector는 배열을 구현하는 C++ 표준 컨테이너 라이브러리의 컨테이너 클래스이다
< vector > 헤더에 클래스 템플릿으로 정의되어 있고 element의 타입을 지정하는 템플릿 타입 매개변수를 가진다
ex) std::vector< int > int타입 element vector를 선언
std::vector 타입 객체를 인스턴스화 하려면 다음과 같이 진행한다
#include <vector>
std::vector<int> foovector{}; //기본 생성자 사용, 빈 중괄호이기 때문에 값 초기화임, 0개의 int element를 가진 vector
다음과 같이 리스트 초기화를 사용하여 특정 초기화 값들과 함께 vector타입 객체를 만들 수 있다
std::vector<int> foovector{ 2, 3, 4, 5 }; //리스트 초기화, 리스트 생성자를 호출한다
std::vector namevector{ 'k', 'e', 'l', 'v', 'i', 'n' };
//CTAD사용으로 타입 추론 가능
컨테이너는 일반적으로 리스트 생성자 (list constructor)가 존재한다, 따라서 초기화 리스트를 사용하여 컨테이너 인스턴스를 생성할 수 있다
리스트 생성자는 다음과 같은 작업을 수행한다
따라서 컨테이너를 초기화 리스트로 초기화하면 리스트 생성자가 호출되고 컨테이너의 element들이 초기화 값으로 초기화 되어 컨테이너 타입 객체가 생성된다
operator [ ]
C++에서 배열 element에 접근하는 가장 기본적인 방법이 바로 배열이름[ ]를 사용하는 것이다
[ ]에는 특정 element의 번호 index를 넣어주고 사용한다
C++의 배열은 0부터 시작한다 (zero-based)
이때 이러한 index는 절대적인 위치가 아닌 첫번째 element로부터의 거리인 상대적인 거리를 의미한다
std::vector<int> foovector{ 2, 3, 4, 5 };
foovector[0]; //2
foovector[1]; //3
배열의 element에 접근할 때 index는 반드시 0부터 Length-1 사이의 값이어야 한다
(undefined behavior 발생, assert로 인해 크래시 발생 가능)
하지만 operator[ ]는 bound check를 전혀 하지 않는다, 그 말은 즉 index가 0 ~ Length-1인지 확인하지 않는다는 것이다
배열은 메모리상에서 연속적이다, 즉 element들이 메모리 상에서 바로 옆에 인접해 있다는 의미이다
주소를 확인해보면 쉽게 이해할 수 있다
std::vector<int> foovector{ 1, 2, 3, 4 };
std::cout << &(foovector[0]) << std::endl;
std::cout << &(foovector[1]) << std::endl;
std::cout << &(foovector[2]) << std::endl;
값이 4차이로 계속 이어지게 된다 (사용자 컴퓨터의 int 크기만큼)
값이 바로 인접해있기 때문에 배열 내의 어떤 요소의 주소든 빠르게 계산이 가능하다 (인접해 있기 때문에 주소 계산이 쉽다)
배열이 자주 사용되는 큰 이유중 하나가 바로 이 operator [ ]를 이용한 임의 접근이 가능하기 때문이다
그렇다면 특정 갯수만큼 std::vector를 만들고 싶다면? { }를 이용하여 특정 갯수만큼 초기화 값을 넣는 방법도 있지만 불편하다, 다음과 같이 처리한다
std::vector<int> data(10); //10개의 int element를 가지는 vector(값은 0으로 초기화 됨)
std::vector<int> data(10, 7); //10개의 int element를 가지는 vector(값은 7로 초기화 됨)
각 element들은 값 초기화가 되고 만약 element가 class type으로 초기화 되었다면 각각의 생성자들이 호출된다 (10개면 10번 생성자가 호출됨)
이럴때는 직접 초기화를 사용하는걸 강력하게 권장한다
std::vector<int> data{ 10 };
이는 length가 1이고 초기화 값이 10인 vector를 생성하기 위한 리스트 생성자 호출이 될수도 있고 바로 위와 같은 경우로 length가 10이고 element가 0으로 초기화 된 vector를 생성하는 의미가 될 수 있기 때문에 모호하다
클래스 타입 객체를 생성할 때 초기화 리스트가 비어있으면 { } 기본 생성자가 리스트 생성자보다 선호되고 { }가 비어있지 않다면 리스트 생성자가 다른 일치하는 생성자들보다 선호된다
// 복사 초기화
std::vector<int> v1 = 10; // 초기화 리스트가 아님, vector(size_type) 생성자가 explicit으로 되어 있기 때문에 암시적 타입 변환이 불가능하다 (일치하는 생성자가 없다)
// 직접 초기화
std::vector<int> v2(10); // 10은 초기화 리스트가 아님, 명시적 단일 인수 생성자와 일치함 (vector(size_type))
// 리스트 초기화
std::vector<int> v3{ 10 }; // { 10 }은 초기화 리스트로 해석됨, 리스트 생성자와 일치함
// 복사 리스트 초기화
std::vector<int> v4 = { 10 }; // { 10 }은 초기화 리스트로 해석됨, 리스트 생성자와 일치함
std::vector<int> v5({ 10 }); // { 10 }은 초기화 리스트로 해석됨, 리스트 생성자와 일치함 (v4와 동일, 직접 초기화 아님)
// 기본 초기화
std::vector<int> v6 {}; // {}는 빈 초기화 리스트, 기본 생성자와 일치함
std::vector<int> v7 = {}; // {}는 빈 초기화 리스트, 기본 생성자와 일치함
하지만 만약 std::vector가 클래스 타입의 멤버라면 () 초기화는 사용할 수 없다 (멤버 데이터 초기화에는 직접 초기화가 허용되지 않기 때문 -> 함수 호출과 문법적으로 모호해지기 때문에 C++에서 막아놓음)
class Foo
{
public:
std::vector<int> temp(10); //error
};
따라서 복사 초기화나 리스트 초기화를 사용해야 하고 CTAD가 허용되지 않기 때문에 타입을 명시해야 한다 (특정 클래스의 크기를 알려면 멤버 데이터의 크기들을 전부 알아야 하는데 이때 CTAD가 들어가게 되면 굉장히 복잡해지기 때문에 막아놓음)
class Foo
{
public:
std::vector<int> temp{ std::vector<int>(9) }; //임시벡터를 만들어 초기화
};
또한 std::vector 타입의 객체는 const로 만들 수 있다
const std::vector<int> data{ 1, 2, 3, 4, 5 };
const이기 때문에 vector의 element는 const취급을 받아 수정이 불가능하다
이때 vector의 element type은 const로 사용이 불가능하다
const std::vector<const int> data{}; //error
하지만 std::vector타입 객체는 constexpr로 만들 수 없다, 만약 constexpr 배열이 필요하다면 std::array를 사용해야 한다