프로그래밍 / C++ / 언리얼

Programming/C | C++ | Unreal

[C++] 자료구조 - 선형리스트(Linear List) / vector 클래스 기능 정리

아트성 2022. 2. 17. 00:32

vector 클래스 개요   

 

std::vector 클래스는 c++의 표준라이브러리 배열 기능 중 하나로, 데이터관리, 알고리즘 분석등 정말 유용하게 쓰이는 기능이다. 벡터의 저장은 자동으로 처리되며 필요에 따라 유동적으로 확장 및 축소할수있다.

 

벡터는 일반적으로 정적 배열보다 더 많은 메모리 공간을 차지한다. 앞으로 추가될 요소들을 위해 더 많은 메모리가 할당되는데, 이과정에서 vector클래스는 원소가 추가되거나 삽입될때 메모리 재할당이 발생한다.

 

 

 

 

 

vector 클래스의 특징   

 

vector 클래스는 배열에 삽입하거나 추가해서 공간을 다 차지하게 되면 원소를 담을 바구니들을 다시 생성하는 작업에 들어간다. vector클래스의 특정 멤버함수를 통해서 추가 공간들이 여유롭게 주어지는데,

이 추가된 여유공간들은 capacity(), size() 멤버함수를 사용하여 조회가 가능하다.  이 외에도 push_back(), pop_back(), insert() 과같은 멤버함수들을 이용해서 원소를 삽입하거나, 제거할수있다.

위 그림은 6개의 공간으로 할당된 벡터를 나타낸 것이다. vector 클래스는 sequence 기반의 컨테이너이므로, 원소의 저장위치가 정해지며 컨테이너가 배열 기반이므로  원소가 각각 한개의 메모리 블록에 할당된다.

 

그러나 vector 클래스의 특성상 원소를 추가하거나 삽입할때, 메모리 재할당하는 과정에 있어서 상당한 비용을 지불하게 될 수 있다. 이런 과정들을 해소시키는 방법은 직접 배열 범위를 지정하는 방법이 있다.

바로 reserve()를 사용하는 방법인데, vector 클래스에서 미리 공간을 예약할수있는 멤버함수로 아주 유용하게 사용할 수 있다. 따라서 vector 클래스의 멤버함수를 이용해서 할당작업을 통해 메모리 처리과정의 불필요한 부분들을 줄일 수 있다.

 

이처럼 vector 클래스의 몇가지 특징들을 알아 보았는데, vector의 다른 유용한 멤버함수들은 아래에서 살펴보도록 하자.

 

 

 

 

 

vector 클래스의 생성   

 

vector 클래스를 생성하기 위해서는 헤더파일인 <vector>을 추가해야한다. 

c++ vector 타입은 <vector>헤더파일에 정의돼 있다. 이 타입은 iostream과 마찬가지로 std 네임스페이스에 속한다.

 

vector 클래스의 생성은 크게 네가지로 나뉘는데, 아래 초기화 방식이 가장 보편적으로 사용하는 방식이다.

<int>타입 외에도 <string>, <double>등 다른 타입으로 초기화가 가능하며, 템플릿 타입으로 초기화도 가능하다.

vector<int> vec; // 원소수가 0인 int 타입 vector를 생성한다.

vector<int> vec(10, 100); // 초깃값이 100인 int 원소 열개를 담은 vector 생성

vector<int> inVector1 = { 1, 2, 3, 4, 5, 6 }; // vector의 유니폼 초기화 방식1
vector<int> inVector1{ 1, 2, 3, 4, 5, 6 }; // vector의 유니폼 초기화 방식2

auto elementVector = make_unique<vector<Element>>(10); // vector의 heap 할당

 

 

 

 

 

 

vector 클래스의 멤버함수들   

 

접근  ( Access )

vec.at(i) i번째 원소를 참조한다(범위 점검을 포함)
vec.front() 첫번째 원소를 참조한다.
vec.back() 마지막 원소를 참조한다.

 

vector 클래스에서 []연산자는 유효 범위를 점검하지 않아 원소 접근속도를 조금 더 높인다. 그러나 데이터에 접근할때 안전성이 떨어지는편이다. at()을 사용하게되면 유효범위를 점검하여 안전하게 원소에 접근할 수 있다.

 

 

 

삽입 / 삭제  ( Modification )

vec.push_back(x) 마지막 원소뒤에 x를 추가한다.
vec.pop_back() 마지막 원소를 제거한다.
vec.insert(p, x) p가 가리키는 위치에 x를 삽입한다.
vec.erase(p) p가 가리키는 원소를 삭제한다.
vec.clear() 원소 전체를 삭제한다.

 

vector 클래스는 push_back()pop_back() 멤버함수는 알고리즘 정렬에 매우 유용하게 사용되니 반드시 참고하도록 한다. 멤버함수의 접근 / 삽입 / 삭제를 다루는 예제는 아래 자세하게 다룬다.

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<string> vec; // vector<int> vec; vector<double>등 가능
    vec.push_back("dog");
    vec.push_back("cat");
    vec.push_back("zebra");
    vec.push_back("참새");
    vec.push_back("비둘기");
    vec.push_back("오목눈이");

    cout << "Push_back() Result : ";
    for (string& s : vec)  //  (int i = 0 ; i < vec.size(); ++i)와 동일
    {
        cout << s << " "; //output : dog cat zebra 참새 비둘기 오목눈이
    }

    cout << " " << endl;
    cout << vec.front() << endl; // output : dog
    cout << vec.back() << endl;// output : 오목눈이
    cout << vec.at(3) << endl;// output : 참새
    vec.pop_back(); // erase, clear 비슷한 기능

    cout << "pop_back() Result : ";
    for (string& s : vec) 
    {
        cout << s << " "; //output : dog cat zebra 참새 비둘기 
    }
    cout << " " << endl;

    return 0;
}

 

 

 

용량  ( Capacity )

vec.size() 원소의 개수를 확인한다.
vec.capacity() 할당된 크기를 확인한다.
vec.shrink_to_fit() 원소의 개수에 맞게 공간을 축소한다.
vec.empty() 공간이 비어있는지 확인한다. (True, False 반환)
vec.reserve(n) n개의 원소를 저장할 공간을 예약한다. (늘린공간은 비어있음)
vec.resize(n) 할당된 크기를 n으로 변경한다. (늘린 공간을 0으로 초기화)

 

vector 클래스의 멤버함수의 기능중에서 capacity와 size는 매우 유용하게 쓰인다.

아래 예제는 공간이 할당될때마다 실시간으로 원소의 개수와 할당된 크기를 파악할수있다.

그런데 크기를 주로 확인할수있는 멤버함수는 자칫하면 쉽게 헷갈릴 수 있다.

 

바로 capacity()와 size()의 차이인데 단순하게,

capacity()는 "원소가 담긴 공간의 수" 뜻하고, size()는 "순수 원소의 수" 라고 생각하면 편할것같다.

아래 예제를 통해서 원소가 늘어나고 줄어드는 데에 따라 capacity와 size의 변화과정들을 자세하게 살펴볼 수 있다.

 

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<int> vec;

    vec.reserve(8); // 8개 공간 할당
    for (int i = 0; i < 10; ++i) 
    {
        vec.push_back(5 * i);
        cout << vec[i] << " / " << "size : " << vec.size() << " " << "capacity : " << vec.capacity() << endl;
    }

    cout << " " << endl;

    vec.shrink_to_fit(); // 공간 축소
    cout << " ---- [shrink_to_fit result] ---- " << endl;
    cout << "size : " << vec.size() << " , " << "capacity : " << vec.capacity() << endl;
    cout << " -------------------------------- " << endl;
    cout << " " << endl;

    vec.resize(5); // 5개의 원소로 축소
    cout << " [resize(5) result] : ";
    for (int& i : vec)
    {
        cout << i << " ";
    }
    cout << " " << endl;
    cout << " " << endl;
    cout << " ---- [empty status] ------------ " << endl; // 공간 확인
    cout << "empty result : " << vec.empty() << " , " << "size : "
        << vec.size() << " , " << "capacity : " << vec.capacity() << endl;
    
    vec.clear(); // 원소 전체 제거
    cout << "empty result : " << vec.empty() << " , " << "size : "
        << vec.size() << " , " << "capacity : " << vec.capacity() << " / clear ok " << endl; 
    cout << " -------------------------------- " << endl;
    
    return 0;
}

 

 

 

배열에 있는 원소들이 어떤 변화가 있을까? 프로그래밍이 작동하면서 size와 capacity의 변화를 알아보자.

 

① reserve(8) □□□□□□□□ size 0 capacity 8 8개의 공간 할당
② push_back() →  ■■■■■■■■■■□□ size10 capacity 12 8개 이상일때, 여유분 할당
③ shrink_to_fit() ■■■■■■■■■■ size 10 capacity 10 원소 개수만큼 축소
④ resize(5) ■■■■■□□□□□ size 5  capacity 10    원소 5개로 축소, 공간유지
⑤ clear()  □□□□□□□□□□ size 0 capacity 10   원소 전부 삭제, 공간유지

 

1. 처음 reserve함수를 이용해 8개의 공간을 할당한다.

 

2. push_back함수와 반복문(for)를 이용해서 처음할당한 8개를 초과하게 구성한다.

    여기서 9번째 공간을 할당할때 capacity가 12개로 늘어난것을 확인할 수 있다.

    (vector 클래스는 메모리 재할당이 일어날때 이전 메모리 크기의 1/2만큼을 더 할당하는 기능이 들어있다.)

 

3. shrink_to_fit함수를 사용하면 배열안에 들어있는 원소수만큼 축소가 되다.

 

4. resize 함수를 이용하면 내부에서는 배열의 끝부분부터 n만큼 제거작업에 들어가지만, capacity의 변화는 없다.

    (resize함수는 기존할당 수보다 확대도 가능하다, 여기서 확대할때 여유공간은 모두 0으로 초기화된다)

 

5. clear 함수를 쓰면 내부의 원소는 모두 제거가 되고,  여전히 capacity의 변화는없이 빈공간으로 유지가 된다.

 

 

Vector클래스 구현   

 

MyVector.h

#pragma once

class MyVector
{
public:
	// 기본 생성자
	MyVector() = default;

	// count만큼 공간이 할당된 생성자
	explicit MyVector(size_t count);

	// 복사 생성자. 깊은 복사(deep copy)가 이뤄져야 한다.
	MyVector(const MyVector& other);

	// 할당 연산자. 깊은 복사(deep copy)가 이뤄져야 한다.
	MyVector& operator=(const MyVector& rhs);

	// 소멸자
	~MyVector();

	// 첫 번째 요소를 가리키는 반복자를 반환한다.
	int* begin();
	const int* begin() const;

	// 마지막 요소의 다음 번째를 가리키는 반복자를 반환한다.
	int* end();
	const int* end() const;

	// 컨테이너가 비었는지 검사한다.
	bool				empty() const;

	// 원소의 개수를 반환한다.
	size_t				size() const;

	// 현재 할당된 공간의 크기를 반환한다.
	size_t				capacity() const;

	// pos에 위치한 원소의 참조를 반환한다. 만약 pos가 범위에서 벗어나면 std::out_of_range 예외가 던져진다.
	int& at(size_t pos);
	const int& at(size_t pos) const;

	// pos에 위치한 원소의 참조를 반환한다.
	int& operator[](size_t pos);
	const int& operator[](size_t pos) const;

	// 컨테이너의 첫 번째 원소의 참조를 반환한다.
	int& front();
	const int& front() const;

	// 컨테이너의 마지막 원소의 참조를 반환한다.
	int& back();
	const int& back() const;

	// 컨테이너를 비운다.
	void				clear();

	// pos 이전 위치에 value를 삽입한다.
	// value가 삽입된 곳을 가리키는 포인터를 반환한다.
	int* insert(int* pos, int value);

	// pos에 위치한 원소를 지운다.
	// 삭제된 요소의 다음 포인터를 반환한다.
	int* erase(int* pos);

	// 컨테이너의 맨 끝에 원소를 추가한다.
	void				push_back(int value);

	// 컨테이너의 마지막 원소를 삭제한다.
	void				pop_back();

	// value가 존재하는지 검사한다.
	bool				contains(int value);

	// 컨테이너의 용량을 newCapacity보다 같거나 크게 늘린다.
	// newCapacity > _capacity라면 새로운 메모리를 할당하고,
	// 그렇지 않다면 아무 동작도 수행하지 않는다.
	void				reserve(size_t newCapacity);
private:
	int* _container = nullptr;
	size_t				_size = 0;
	size_t				_capacity = 0;
};

                 

MyVector.cpp

#include "MyVector.h"

#include <stdexcept>

MyVector::MyVector(size_t count)
	: _container{ new int[count] }, _size{ count }, _capacity{ count }
{
	for (size_t i = 0; i < _size; ++i)
	{
		_container[i] = 0;
	}
}

MyVector::MyVector(const MyVector& other)
	: _container{ new int[other._capacity] }, _size{ other._size }, _capacity{ other._capacity }
{
	for (size_t i = 0; i < _size; ++i)
	{
		_container[i] = other[i];
	}
}

MyVector& MyVector::operator=(const MyVector& rhs)
{
	if (this != &rhs)
	{
		MyVector temp(rhs);
		std::swap(_container, temp._container);
		std::swap(_size, temp._size);
		std::swap(_capacity, temp._capacity);
	}

	return *this;
}

MyVector::~MyVector()
{
	clear();
}

int* MyVector::begin()
{
	return _container;
}

const int* MyVector::begin() const
{
	return _container;
}

int* MyVector::end()
{
	return _container + _size;
}

const int* MyVector::end() const
{
	return _container + _size;
}

bool MyVector::empty() const
{
	if (_size == 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

size_t MyVector::size() const
{
	return _size;
}

size_t MyVector::capacity() const
{
	return _capacity;
}

int& MyVector::at(size_t pos)
{
	if (pos >= _size)
	{
		throw std::out_of_range("Out of range");
	}

	return _container[pos];
}

const int& MyVector::at(size_t pos) const
{
	if (pos >= _size)
	{
		throw std::out_of_range("Out of range");
	}

	return _container[pos];
}

int& MyVector::operator[](size_t pos)
{
	return _container[pos];
}

const int& MyVector::operator[](size_t pos) const
{
	return _container[pos];
}

int& MyVector::front()
{
	return _container[0];
}

const int& MyVector::front() const
{
	return _container[0];
}

int& MyVector::back()
{
	return _container[_size - 1];
}

const int& MyVector::back() const
{
	return _container[_size - 1];
}

void MyVector::clear()
{
	delete[] _container;
	_container = nullptr;
	_size = 0;
	_capacity = 0;
}

int* MyVector::insert(int* pos, int value)
{
	int dist = pos - begin();

	if (_size == 0)
	{
		reserve(1);

		pos = begin() + dist;
	}
	else if (_size == _capacity)
	{
		reserve(_capacity * 2);

		pos = begin() + dist;
	}

	for (int* iter = end(); iter != pos; --iter)
	{
		*iter = *(iter - 1);
	}
	*pos = value;

	++_size;

	return pos;
}

int* MyVector::erase(int* pos)
{
	if (_size == 0)
	{
		return end();
	}

	int* last = end() - 1;
	for (int* iter = pos; iter != last; ++iter)
	{
		*iter = *(iter + 1);
	}

	--_size;

	return pos;
}

void MyVector::push_back(int value)
{
	insert(end(), value);
}

void MyVector::pop_back()
{
	erase(end() - 1);
}

bool MyVector::contains(int value)
{
	for (size_t i = 0; i < _size; ++i)
	{
		if (_container[i] == value)
		{
			return true;
		}
	}

	return false;
}

void MyVector::reserve(size_t newCapacity)
{
	if (_capacity >= newCapacity)
	{
		return;
	}

	int* newContainer = new int[newCapacity];

	for (size_t i = 0; i < _size; ++i)
	{
		newContainer[i] = _container[i];
	}

	delete[] _container;

	_container = newContainer;
	_capacity = newCapacity;
}

 

MyVectorTest.cpp

#include <vector>
#include <iostream>

#include "MyVector.h"

using namespace std;

int main()
{
	vector<int> vec;
	vector<int> vec2(5);
	vector<int> vec3 = { 1, 2, 3, 4, 5 };

	vec.push_back(1);
	vec.push_back(2);
	vec.pop_back();

	for (size_t i = 0; i < vec.size(); ++i)
	{
		cout << vec[i];
	}
	cout << endl;

	try
	{
		vec.at(1);
	}
	catch (std::out_of_range& e)
	{
		cout << e.what() << endl;
	}

	vec.pop_back();

	cout << boolalpha << vec.empty() << endl;

	cout << "Cap : " << vec.capacity() << endl;
	vec.reserve(10);
	cout << "New Cap : " << vec.capacity() << endl;

	vec2.insert(vec2.begin() + 2, 3);
	for (size_t elem : vec2)
	{
		cout << elem << ' ';
	}
	cout << endl;

	vec3.erase(vec3.begin());
	for (size_t i = 0; i < vec3.size(); ++i)
	{
		cout << vec3[i] << ' ';
	}
	cout << endl;

	MyVector mvec;
	MyVector mvec2(5);

	mvec.push_back(1);
	mvec.push_back(2);
	mvec.pop_back();

	for (size_t i = 0; i < mvec.size(); ++i)
	{
		cout << mvec[i];
	}
	cout << endl;

	try
	{
		mvec.at(1);
	}
	catch (std::out_of_range& e)
	{
		cout << e.what() << endl;
	}

	mvec.pop_back();

	cout << boolalpha << mvec.empty() << endl;

	cout << "Cap : " << mvec.capacity() << endl;
	mvec.reserve(10);
	cout << "New Cap : " << mvec.capacity() << endl;

	mvec2.insert(mvec2.begin() + 2, 3);
	for (int elem : mvec2)
	{
		cout << elem << ' ';
	}
	cout << endl;

	cout << boolalpha << mvec2.contains(2) << endl;

	mvec2.erase(mvec2.begin());
	for (size_t i = 0; i < mvec2.size(); ++i)
	{
		cout << vec2[i] << ' ';
	}
	cout << endl;
}
반응형