스택은 LIFO(Last-in First-Out)규칙을 가지고 있어서 스택원소를 반환할 때 마지막에 들어온 데이터를 반환하게 됩니다.

마치 차곡차곡 쌓인 무언가와 비슷합니다. 가장 먼저들어간 물체는 제일 밑에 있어서 가장 마지막에 꺼내야하죠. 그리고 당연히 중간에 있는 원소를 가져올 수도 없습니다.

 

이런 방법은 함수를 호출할 때와 동일합니다.

함수가 호출되면 스택메모리영역에 쌓이게 되고 모든 함수의 호출이 완료되었으면 마지막(가장 최근)에 호출했던 함수부터 차례대로 반환하게 됩니다.

 

스택은 LIFO규칙만 생각하고 만들면 굉장히 쉽게 구현할 수 있습니다.

벡터처럼 모든원소에 접근할 필요가 없고, 반복자도 필요없습니다.

물론 필요에 의해서 구현할 수는 있지만, 필수는 아닙니다.

 


1. PushBack()

원소를 추가할 때 사용되는 유일한 함수입니다.

 

2. PopBack()

마지막으로 추가된 원소를 반환하고 삭제합니다.

 

3. Num()

스택의 원소개수입니다. 원소가 0개라면 1,2번 함수를 호출하면 런타임에러를 발생하기 때문에

해당 함수로 원소개수를 체크해야할 상황이 생깁니다.

 

이 정도의 함수면 스택을 구현할 수 있습니다. 빡 조이면 3번을 빼고 1번과 2번함수만으로 구현할 수 있죠.

그러나 편의성을 위해서 몇개 더 구현하겠습니다.

 

4. Top()

마지막으로 추가된 원소를 반환하지만, 원소를 삭제하지 않습니다.

 

5. IsEmpty()

스택이 비어있으면 true, 1개 이상의 원소가 있으면 false를 반환합니다

예제코드에서는 5번함수가 구현되어있지 않습니다. 한번 직접 구현해보세요.


template <typename Data>
class Container_Master
{
protected:
	Data* my_base = nullptr;
	size_t my_capacity = 0;
	size_t my_size = 0;

	//캐퍼시티를 늘릴 때 지정된 값이 없다면 해당 값만큼 늘립니다
	const size_t additive_capacity = 32;
public:
	//컨테이너의 공간을 새로 생성합니다
	void Reserve(size_t capacity)
	{
		//기존 캐퍼시티가 더 크다면 재할당을 하지 않습니다
		if (my_capacity >= capacity)
			return;

		Data* temp = new Data[capacity];
		my_capacity = capacity;

		//컨테이너에 원소가 있다면 새로 할당한 주소로 옮겨줍니다
		if (my_size >= 1)
		{
			for (size_t i = 0; i < my_size; i++)
			{
				temp[i] = my_base[i];
			}
			delete[] my_base;

		}

		my_base = temp;
	}

	//배열의 원소를 삭제하지않고
	//사이즈를 0으로 만듭니다
	//Clear()함수후에 원소룰 추가하면 기존에 있던 원소를 덮어씌웁니다
	void Clear()
	{
		my_size = 0;
	}

	//컨테이너의 원소개수입니다
	size_t Num()
	{
		return my_size;
	}

	//컨테이너의 캐퍼시티개수입니다
	size_t Max()
	{
		return my_capacity;
	}
	
	//인덱스 유효성검사를 합니다
	//유효하지 않으면 throw
	bool CheckValidIndex(const size_t index)
	{
		if ((index >= 0) && (index < this->my_size))
		{
			return true;
		}
		else
		{
			throw printf("범위를 벗어난 인덱스를 사용하였습니다. 접근하려는 인덱스: %d, 배열크기: %d", index, this->Num());
		}
	}
};
#include "Container_Master.h"

template <typename Data>
class MyStack : public Container_Master<Data>
{

};

이전에 포스팅했던 벡터와 중복되는 함수는 부모클래스에서 다룹니다.

 

내부적으론 벡터와 동일하게 '동적 배열'을 사용합니다.

다만 중간삽입, 삭제가 없이 마지막위치에서 삭제, 삽입을 하기 때문에 당연히 모든 원소가 메모리에서 인접해 있고, 원소의 순서가 보장되어 있습니다.

 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

이터레이터는 '원소를 순서대로 접근하는 반복 오브젝트' 정도로 이해하면 되겠다.

'순서'는 거꾸로 혹은 임의의 방식으로 돌아갈 수 있다.

 

우리는 가장 기본적인 '처음부터 마지막'순서의 이터레이터만 구현해보자

 


template <typename Data>
class MyVector : public Container_Master<Data>
{

public:

	class iterator
	{
	private:
		Data* pos = nullptr;
	public:
		iterator(Data* pos = 0)
		{
			this->pos = pos;
		}
		Data& operator* ()const
		{
			return *pos;
		}

		bool operator!=(const iterator& iter)const
		{
			return pos != iter.pos;
		}
		bool operator==(const iterator& iter)const
		{
			return pos == iter.pos;
		}

		//전위
		iterator& operator++()
		{
			++pos;
			return (*this);
		}
	};
    
public:
    	iterator begin()
	{
		iterator iter(this->my_base);
		return iter;
	}
	iterator end()
	{
		iterator iter(this->my_base + this->my_size);
		return iter;
	}
};

나는 벡터클래스 내부에 이터레이터클래스를 생성했다.

따로 이터레이터클래스를 만들어도 되는데 이터레이터는 이정도만 구현해도 될거같아서 그랬다.

 

코드에서 나오지만 이터레이터는 '원소를 가리키는 포인터'정도로 이해하면 된다.

포인터는 +1을 하면 포인터의 크기만큼 메모리상에서 이동하게 되는데

포인터의 크기는 곧 주소의 크기고 32비트 환경에서는 4바이트, 64비트 환경에서는 8바이트의 사이즈를 가지게 된다.

 

빈공간없이 붙어있는 벡터의 경우

begin()를 보면 벡터 내부의 배열을 이터레이터생성자로 넘기면

배열의 첫 부분을 가리키는 포인터를 이터레이터가 가지게 된다.

거기서 ++iter를 하게 된다면, 이터레이터 내부의 포인터도 ++pos을 하게 되고,

포인터의 크기만큼 이동하게 되므로 당연히 다음 포인터를 가리키게 되고

그 결과 이터레이터는 다음 원소를 가리키게 된다.

이런 방식이라고 보면된다.

 

이터레이터로 루프돌리는 방법은

for (MyVector<int>::iterator it = my_vector.begin(); it != my_vector.end(); ++it)
{
	cout << (*it) << endl;
}
    
for (auto it = my_vector.begin(); it != my_vector.end(); ++it)
{
	cout << (*it) << endl;
}

for (auto it : vector)
{
	cout << (it) << endl;
}

MyVector<int>::iterator를 직접 선언해서 사용해도 되지만 상당히 귀찮기 때문에

c++11이후로 변경된 'auto'키워드를 사용하서 간편하게 다뤄도 된다

 

range-based for loop방식을 사용하면 더 간편하게 사용할 수 있다.

이 방법도 c++11에서 생긴 방법이다

 


이제 거의 모든 기능을 구현했지만 하나의 기능을 아직 구현하지 않았다.

그것은 벡터의 원소에 빠르게 접근하기 위한 operator[]

	Data& operator[] (const size_t index)
	{
		//인덱스가 유효하면 인덱스 위치에 보관한 자료를 반환하세요.
		this->CheckValidIndex(index);

		//데이터를 반환합니다
		//반환 형식이 참조 형식임을 주의하세요.
		return this->my_base[index];
	}

어렵지 않다. 벡터에 []을 사용하면 내부에서 배열에 []을 사용해서 원소를 가져오는 방법이다.

인덱스가 유효한지 검사하는 함수만 추가되었을 뿐이다.

 


이렇게 벡터의 기본적인 함수를 구현했다.

예제코드에 있지만 여기선 설명하지 않은 함수가 있다. 어려운 코드가 아니니 금방 이해하실 수 있다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

벡터의 특징중 하나인 어느 위치에서든 원소의 삽입과 삭제를 고려해서 짜야한다


삽입코드

	//마지막 위치에 원소를 추가합니다
	void Add(const Data& data)
	{
		Insert(this->Num(), data);
	}
	
	//정해진 위치에 원소를 추가합니다
	//원소 삽입시 기존 위치의 원소부터 마지막원소까지 전부 인덱스가 +1씩 밀리기 때문에
	//원소가 많고 0의 가까운위치에 삽입될수록 느려질 수 있습니다
	void Insert(const size_t insert_index, const Data& data)
	{
		//컨테이너에 비어있는 자리가 있는지 확인
		if (this->my_size == this->my_capacity)
		{
			//빈자리 없음. 재할당을 시도합니다
			this->Reserve(this->my_capacity + this->additive_capacity);
		}


		//컨테이너 중간에 원소가 들어간다면 원소를 해당 지점부터 한칸씩 뒤로 이동시킵니다.
		for (size_t i = this->my_size; i > insert_index; --i)
		{
			this->my_base[i] = this->my_base[i - 1];
		}
		

		//추가합니다
		this->my_base[insert_index] = data;
		++this->my_size;
	}

Add()는 마지막위치에 원소를 삽입하는 함수이므로 Insert()만 보겠다.

 

Insert()는 아래 순서로 작동한다

1. 컨테이너(배열)에 빈자리가 있는지 확인-> 빈자리가 없다면 재할당

2. 삽입해야할 위치의 원소부터 마지막원소까지 전부 한칸씩 이동

3. 삽입해야할 위치가 비었으니 원소추가

 

여기서 우린 2번항목을 잘 봐야한다. 왜 중간에 원소를 삽입하면 퍼포먼스가 떨어지는지...

단순히 원소를 옮기는 작업이기에 가끔 한두번 삽입하는건 일반적인 환경에선 문제가 없지만,

자주, 빈번하게, 매 프레임 중간삽입을 한다면 분명히 문제를 가져올것이라고 생각한다.

 

원소가 0번째 인덱스에 가까울수록 퍼포먼스가 떨어진다. 반대로 말하면

마지막에 원소를 추가하는것은 퍼포먼스에 문제가 없다. 

 

중간삽입은 작업내용을 고려해서 사용해야 한다.

 

 


 

삭제코드

	//해당 위치의 원소를 삭제합니다
	void RemoveAt(const size_t index)
	{
		this->CheckValidIndex(index);

		--this->my_size;
		for (size_t i = index; i < this->my_size; ++i)
		{
			this->my_base[i] = this->my_base[i + 1];
		}
	}

	//원소이동의 부하를 줄이기 위해 삭제될 원소와 마지막 원소의 위치만 변경하고 삭제합니다
	//마지막 원소가 삭제된 원소의 위치로 오기 때문에 원소의 순서는 보장되지 않습니다
	void RemoveAtSwap(const size_t index)
	{
		--this->my_size;
		this->my_base[index] = this->my_base[this->my_size];
	}

RemoveAt(): 해당 위치(인덱스)의 원소 삭제.

RemoveAtSwap(): 해당 위치의 원소를 삭제후 빈공간을 마지막 원소로 대체. 순서가 보장되지 않음.

 

RemoveAt()을 보면 Insert()와 동일하게 원소를 이동시킨다. 역시 마지막 원소를 삭제하는것은 퍼포먼스에 문제가 없다.

 

벡터는 순서를 보장하는 장점이 있지만, 순서상관없이 사용하는 경우도 많이 있다.

가령 쉽게 원소에 접근하기 위해 등등...

그런 사람들을 위해 RemoveAtSwap()을 사용하면 순서는 보장되지 않지만 어느 위치의 원소를 삭제해도 퍼포먼스에 문제가 없다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

생성자와 소멸자 코드

	MyVector()
	{
		//my_base가 nullptr지만
		//사이즈및 캐퍼시티가 0이므로 원소를 추가할때 Reserve()함수에서 할당이 이뤄집니다
		this->my_base = nullptr;
		this->my_capacity = this->my_size = 0;
	}
	MyVector(std::initializer_list<Data> list)
	{
		this->my_capacity = this->my_size = 0;
		this->Reserve(list.size());

		for (auto& element : list)
		{
			this->my_base[this->my_size++] = element;
		}
	}
    //MyVector<int> my_vector({ 1, 2, 3, 4 });
	~MyVector()
	{
		if (this->my_base)
		{
			delete[] this->my_base;
		}
	}

벡터를 생성할 때 즉시 원소를 넣고싶으면 2번째 생성자처럼

MyVector<int> my_vector({ 1, 2, 3, 4 });

이렇게 넣으면된다. 

 

소멸자가 호출되면 벡터 내부에 포인터로 작성된 배열을 해제하는것을 볼 수 있다.

 

 

캐퍼시티(Capacity)란?

캐퍼시티: 배열의 빈공간을 포함한 최대크기.

사이즈: 배열의 원소갯수.

 

std::vector는 원소를 추가할 때 사이즈가 캐퍼시티를 초과하면 새롭게 캐퍼시티를 할당합니다.

 

그냥 배열에 추가하면 알아서 되는거아냐?

새롭게 캐퍼시티를 할당하는게 왜?

어쨌든 잘 돌아가면 되는거 아냐?

 

라고 생각하면 당신은 주니어(라고 포프님이 그랬습니다)

 

캐퍼시티의 할당을 고려하지 않고 배열을 생성하게 되면 퍼포먼스의 악영향을 끼칠 수 있습니다.

단순히 10개 100개 1000개를 만드는건 크게 체감되지 않을테지만, 특정한 상황에선 고려해야합니다.

왜그럴까요? 우선 코드부터 봅시다

template <typename Data>
class Container_Master
{
protected:
	Data* my_base = nullptr;
	size_t my_capacity = 0;
	size_t my_size = 0;

	//캐퍼시티를 늘릴 때 지정된 값이 없다면 해당 값만큼 늘립니다
	const size_t additive_capacity = 32;
public:
	//컨테이너의 공간을 새로 생성합니다
	void Reserve(size_t capacity)
	{
		//기존 캐퍼시티가 더 크다면 재할당을 하지 않습니다
		if (my_capacity >= capacity)
			return;

		Data* temp = new Data[capacity];
		my_capacity = capacity;

		//컨테이너에 원소가 있다면 새로 할당한 주소로 옮겨줍니다
		if (my_size >= 1)
		{
			for (size_t i = 0; i < my_size; i++)
			{
				temp[i] = my_base[i];
			}
			delete[] my_base;

		}

		my_base = temp;
	}
};

Reserve()함수 입니다.

 

간단하게 문장으로 표현하면 이렇습니다

1. new를 사용해 새롭게 할당.

2. for루프로 기존 원소를 새롭게 할당한 곳에 전부 이동.

3. 기존에 할당된 배열(Data*)은 삭제하고 새롭게 할당된 배열을 등록합니다

 

이미지로 보자면...

기존 배열의 원소가 하나씩 이동하는것을 볼 수 있다.

첫번째 이미지 처럼 원소를 가진상태로 배열의 사이즈가 꽉차서 재할당이 이뤄지면 모든 원소가 새로운 배열로 이동하는것을 볼 수 있다. 이미지는 7개밖에 되지 않지만

만약 원소가 만개라면? 십만개라면?

게다가 재할당할때 마다 캐퍼티시는 여러개가 추가되는게 아니라 std::vector를 기준으로 1개만 추가된다.

그러면 만개의 원소를 하나 추가한다고 가정하면 1+2+3+4+5+...+9999 만큼 원소가 이동하게 되고 10000번의 재할당이 이뤄진다. 메모리 파편화의 가능성도 있다.

언리얼엔진같은 경우는 더 많이 캐퍼시티가 증가하지만 그것이 근본적인 해결책이 되진 않는다.

 

그렇기에 배열(벡터)을 생성하게 된다면 꼭 미리 할당을 해야한다.

원소를 10개 채울려면 원소를 추가하기 전에 Reserve(10)함수를 사용하면된다.

만약 원소를 몇개 채울지 모르겠다면 추정치라도 Reserve()해준다.

 


std::vector는 예기치 않게 캐퍼시티를 늘릴 경우 1만 늘어나는것을 나는 32개의 캐퍼시티를 늘리게 구현해놨다.

처음엔 기존원소개수의 2배씩 캐퍼시티를 늘릴까 생각했지만, 그렇게까지 할 필요를 못느껴서 32개로 고정했다.

32개로 설정한 이유는... 없다. 그냥 느낌이 32개가 좋다. 뭔가 적절한느낌 이다.

 

 

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

size_t는 unsigned_int와 동일합니다

그렇다면 굳이 size_t가 존재하는 이유가 무엇일까요?


 

size_t의 선언부를 보게되면 이렇게 되어있습니다

#ifdef _WIN64
    typedef unsigned __int64 size_t;
#else
    typedef unsigned int     size_t;
#endif

64비트일땐 64바이트, 32비트일땐 32바이트의 unsigned_int로 정의되어 있습니다.

환경에 따라서 size_t의 크기가 보장되지만

int는 64비트환경이더라도 64바이트를 보장받을 수 없습니다.

이것이 size_t와 unsigned int의 차이점입니다.

하지만 이것은 size_t의 사용목적을 제대로 알려주진 않습니다

 

size_t를 사용해야하는 가장 큰 이유는...

 

size_t는 메모리나 문자열등의 길이를 표현할 때 사용한다

 

메모리와 문자열은 0미만의 사이즈는 존재할 수 없습니다. 그런데 자신이 직접 컨테이너를 작성했는데 size_t를 사용하지 않고 int, short 등의 signed 자료형을 사용하고, 다른 컨테이너는 unsigned를 사용하게 되어 중구난방이 되면 어떤 문제를 가져올지 알 수 없습니다. 

그래서 특정상황에서 통일된 자료형을 사용하기 위해 size_t를 사용하고 있습니다.

이것이 size_t를 사용하는 중요한 이유라고 생각됩니다

 

자신이 size_t를 사용하지 않고 unsigned_int를 사용해서 컨테이너를 만들어도 자료형 때문에 문제가 생기진 않을겁니다. 다만, size_t를 사용하면 이것이 컨테이터의 메모리사이즈라는 의미를 '명확'하게 주기 때문에

코드 가독성을 더 높여줄 것이라고 기대합니다.

 

 

벡터는 '동적 배열 클래스'입니다

 

1. 벡터는 배열(array)과 다르게 크기 수정이 언제는 가능하다

배열은 한번 크기를 설정한 이후 수정이 불가능하지만, 벡터는 언제는 크기 수정이 가능합니다.

이렇게 보면 크기수정이 매우 마법처럼 보이지만 사실은,

크기가 수정되면 새로운 배열을 만들어 기본 배열의 원소를 이동시키게 됩니다

사실은 하나의 배열을 늘렸다 줄였다 하는것이 아니라 !새로운!배열을 만들어 버립니다.

 

그렇기 때문에 벡터생성시 캐퍼시티를 미리 생성해주는것이 매우 중요합니다.

캐퍼시티를 미리 생성하지 않아도 벡터가 꽉차면 새로운 배열을 생성해서 늘려주긴 합니다만, 

상황에 따라서 퍼포먼스에 악영향을 미칠 수 있습니다.

여기서 캐퍼시티란, 벡터의 원소개수가 아니라 빈공간을 포함한 벡터의 크기입니다.

 

예를 들자면, 벡터라는 아파트에 캐퍼시티라는 빈 집을 미리 만들어놔야 원소라는 입주민을 언제든 받을 수 있습니다

만약 한층에 하나의 입주만 받는 20층짜리 아파트는 20가구의 입주민만 받을 수 있죠.

21번째 가구가 입주한다고 했을 때 이미 지어진 아파트에 1층을 올릴순 없습니다.

그래서 21층짜리 아파트를 새로 짓습니다.

그리고 이전 아파트에 살던 입주민들을 새로운 아파트로 재입주 시키게 됩니다.

그렇기 때문에 처음 아파트를 지을때 미리 캐퍼시티라는 빈 집을 입주민에 맞게 지어놔야 다시 아파트를 지을일이 없습니다.

 

꼭 추가될 원소개수를 고려해 캐퍼시티를 고려합시다.

 

2. 원소가 메모리상에 인접해 있어서 벡터의 어떤 원소든 쉽게 접근할 수 있지만,

항상 인접해야있어야하므로 벡터의 중간원소의 삽입,삭제가 일어나면 빈공간을 메꿔야하므로

빈번하게 일어나면 비효율적이다.

벡터는 모든 원소가 메모리상에 순서대로 붙어있습니다. 덕분에 원소의 순서가 보장되어 있지만, 중간에 원소가 삽입, 삭제가 발생하면 빈공간을 메꿔야하기 때문에 비효율적이고 이유는 해당 지점을 기준으로 모든 원소가 옆 메모리로 이동하게 됩니다.

 

중간삭제이슈:

위에 설명했듯이 20층짜리 벡터가 있고 입주민이 꽉차있는 상황에서 10층 입주민이 삭제라는 이사를 하게 되었습니다. 그럴때 10층을 빈 공간으로 두지 않고 11층 입주민은 10층으로, 12층입주민은 11층으로...20층입주민은 19층으로 이동하게 됩니다.

그저 원소 하나를 삭제했을 뿐인데 10개의 원소가 모두 이동하게 되었습니다.

환경에 따라서 가끔 한두번 그러는건 문제 없겠지만, 빈번하게 일어나면 퍼포먼스에 악영향을 끼치게 됩니다.

 

중간삽입이슈:

마찬가지로 20층짜리 벡터가 있고 입주민이 꽉차있는 상황에서 10층에 새롭게 입주하려는 입주민이 있습니다.

그럼 역시 기존10층입주민은 11층으로... 11층입주민은 12층으로... 20층입주민은 21층으로....

삭제와 마찬가지로 11개의 원소가 이동하게 됩니다. 게다가 21층을 필요로하니 아파트를 새로 짓기까지 해야합니다.

 

이렇게 모든 원소를 꼭 붙혀놓으려고 하는 이유는 언제는 쉽게 벡터의 원소에 접근하기 위해서 입니다.

만약 10번째 원소를 삭제했을 때 빈공간으로 놓으면 우리는 원소를 가져올 때마다 비어있는지 확인해야하고, 원소를 삽입할 때 마다 빈공간을 찾아야하고 굉장히 바빠질것입니다. 

 

순서를 보장받고싶진 않지만 벡터를 사용하고 싶을 때 사용할 수 있는 꼼수가 하나 있습니다.

순서를 보장받을 필요가 없다면 중간삽입을 할 필요가 없으므로 중간삭제에 대해서만 확인하면됩니다.

그 방법은 원소삭제후 빈 공간을 벡터의 마지막원소만 이동시켜 채워넣으면 됩니다.

 

마지막원소의 위치가 변경되었으니 순서는 바뀌었지만, 대신 중간삭제에 대해선 문제없이 할 수 있습니다.

해당 방법은 예제에 있습니다.


제가 구현한 벡터의 함수들입니다.

 

생성자()

생성자(std::initializer_list<Data>)

소멸자()

 

Reserve(): 캐퍼시티할당

Clear(): 배열의 모든원소 삭제(실제로 삭제되지 않고 배열사이즈를0으로 변경합니다. 캐퍼시티는 그대로입니다)

Num(): std::vector.size()와 동일한 함수로 배열의 원소개수를 알려줍니다

Max(): 배열의 캐퍼시티를 알려줍니다

 

Add(): 배열 마지막위치에 원소추가

Insert(): 배열의 원하는 위치에 원소추가

RemoveAt(): 배열의 원하는 위치의 원소를 삭제합니다(실제로 삭제되지 않고 배열사이즈를 1줄입니다)

RemoveAtSwap(): 배열의 원하는 위치의 원소를 삭제하고, 그 자리에 마지막원소를 가져와 채웁니다.(실제로 삭제되지 않고 배열사이즈를 1줄입니다). 해당방법을 사용하면 원소의 순서는 보장할 순 없지만, 삭제된 위치부터 마지막위치까지 원소를 +1만큼 이동시키지 않아도 됩니다.

PopBack(): 스택의 Pop()처럼 마지막 원소를 삭제하고 반환합니다.

 

그외에...

이터레이터

오퍼레이터[], =


기능을 추가로 넣는다면 Clear()는 원소사이즈를 0으로 변경하지만 새로운 함수를 만들어서 캐퍼시티까지 0으로 변경하는 방법도 있겠습니다.

Add, Num, Max등 기존 벡터함수이름와 다르게 언리얼엔진의 TArray를 참고해서 함수명을 작성했습니다.

제가 언리얼엔진을 써서 그런지 TArray의 함수명이 더 직관적이라 편하다고 생각됩니다

 

앞으로 시간될 때 공부도 할겸 Stack, LinkedList등등 기초 자료구조를 만들어서 포스팅할 생각입니다.

 


우선 클래스를 먼저 보여드리겠습니다

#pragma once

template <typename Data>
class Container_Master
{
protected:
	Data* my_base = nullptr;
	size_t my_capacity = 0;
	size_t my_size = 0;

	//캐퍼시티를 늘릴 때 지정된 값이 없다면 해당 값만큼 늘립니다
	size_t base_capacity = 32;
public:
	//컨테이너의 공간을 새로 생성합니다
	void Reserve(size_t capacity)
	{
		//기존 캐퍼시티가 더 크다면 재할당을 하지 않습니다
		if (my_capacity > capacity)
			return;

		Data* temp = new Data[capacity];
		my_capacity = capacity;

		//컨테이너에 원소가 있다면 새로 할당한 주소로 옮겨줍니다
		if (my_size >= 1)
		{
			for (size_t i = 0; i < my_size; i++)
			{
				temp[i] = my_base[i];
			}
			delete[] my_base;

		}

		my_base = temp;
	}

	//배열의 원소를 삭제하지않고
	//사이즈를 0으로 만듭니다
	//Clear()함수후에 원소룰 추가하면 기존에 있던 원소를 덮어씌웁니다
	void Clear()
	{
		my_size = 0;
	}

	//컨테이너의 원소개수입니다
	size_t Num()
	{
		return my_size;
	}

	//컨테이너의 캐퍼시티개수입니다
	size_t Max()
	{
		return my_capacity;
	}
	
    	//인덱스 유효성검사를 합니다
	//유효하지 않으면 throw
	bool CheckValidIndex(const size_t index)
	{
		if ((index >= 0) && (index < this->my_size))
		{
			return true;
		}
		else
		{
			throw printf("범위를 벗어난 인덱스를 사용하였습니다. 접근하려는 인덱스: %d, 배열크기: %d", index, this->Num());
		}
	}
};
#include "Container_Master.h"

template <typename Data>
class MyVector : public Container_Master<Data>
{

};

Container_Master를 부모를 두고 MyVector클래스가 있습니다.

벡터를 구현하다보니까 앞으로 스택을 구현할 때 중복될거같은 부분을 부모클래스로 올렸습니다.

아직 스택을 구현해본적이 없지만... 그럴거같아서요...

 

std::vector의 템플릿방식으로 클래스를 생성했습니다.

 

템플릿에 대한 자세한 내용은 여기서 확인해주세요

https://modoocode.com/219

 

https://modoocode.com/219

모두의 코드 씹어먹는 C++ - <9 - 1. 코드를 찍어내는 틀 - C++ 템플릿(template)> 작성일 : 2017-04-07 이 글은 77006 번 읽혔습니다. 에 대해서 배웁니다. 안녕하세요 여러분! 지난번 강좌 생각해보기는 잘

modoocode.com

 

벡터의 사이즈를 int, unsigned int를 사용하지 않고 size_t를 사용한 이유는 여기서 확인해주세요

https://forestbird0.tistory.com/37

 

[C++] size_t의 역할

size_t는 unsigned_int와 동일합니다 그렇다면 굳이 size_t가 존재하는 이유가 무엇일까요? size_t의 선언부를 보게되면 이렇게 되어있습니다 #ifdef _WIN64 typedef unsigned __int64 size_t; #else typedef unsigned int size_

forestbird0.tistory.com


https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 


//새탭 생성
_driver.ExecuteScript("window.open();");

//*탭 포커싱
//마지막 탭(우측) 포커싱하기
_driver.SwitchTo().Window(_driver.WindowHandles.Last());

//첫번째 탭(좌측) 포커싱하기
_driver.SwitchTo().Window(_driver.WindowHandles.First());

//*특정 탭정보 저장, 특정 탭 포커싱

//현재 탭정보 저장
//string window = _driver.CurrentWindowHandle;

//모든 탭정보중 0번째 인덱스탭 저장
string window = _driver.WindowHandles.ToList()[0];
_driver.SwitchTo().Window(window);

//현재 탭 닫기
//탭을 닫은경우 포커싱을 잃어버리기 때문에 다른 탭으로 포커싱을 잡아줘야 합니다!!!
_driver.Close();

탭을 닫았으면 크롬드라이버는 포커싱중인 탭이 없기 때문에 문제가 생깁니다

꼭 다른 탭으로 포커싱을 잡아주세요!!

 

https://github.com/ForestBird1/TestSelenium.git

 

GitHub - ForestBird1/TestSelenium

Contribute to ForestBird1/TestSelenium development by creating an account on GitHub.

github.com

 


스크린샷

//*스크린샷 찍기
ITakesScreenshot _takesScreenshot = (ITakesScreenshot)_driver;
Screenshot screenshot = _takesScreenshot.GetScreenshot();
screenshot.SaveAsFile("screenshot.png", ScreenshotImageFormat.Png);

저장된 파일을 확인하면 웹 전체스크린샷이 찍혀있습니다.

그런데 만약 특정요소부분만 짤라서 사진을 찍고싶다면??

 


스크린샷 자르기

정확히 로고만 스크린샷을 찍고싶은데 어떻게 해야할까요?

어렵지 않습니다 로고이미지의 요소를 가져온뒤

가로, 세로, 위치 값을 가져와서 그만큼 전체스크린샷에서 잘라 저장하면 됩니다. 

//네이버 로고요소 가져오기
_web_elem = _driver.FindElement(By.XPath("//*[@id='header']/div[1]/div/div[1]/h1/a"));

//스크린샷을 비트맵으로 변경
Bitmap bitmap_screenshot = new Bitmap(new System.IO.MemoryStream(screenshot.AsByteArray));

//*전체스크린샷에서 로고크기에 맞춰자릅니다
Rectangle rect_crop_size = new Rectangle(_web_elem.Location.X, _web_elem.Location.Y, _web_elem.Size.Width, _web_elem.Size.Height);
bitmap_screenshot = bitmap_screenshot.Clone(rect_crop_size, bitmap_screenshot.PixelFormat);
bitmap_screenshot.Save(String.Format(Environment.CurrentDirectory + "//screenshot_2.png", ImageFormat.Png));

//*비트맵은 사용후 꼭 Dispose해야 메모리누수가 발생하지 않습니다
//*Dispose를 사용하지 않으려면 using문으로 사용해도 됩니다
bitmap_screenshot.Dispose();

차근차근 보시면 절대 어렵지 않습니다

 


화면에 보이지 않는 요소 스크린샷

로고는 대부분 사이트 상단에 있어서 웹 접속시 바로 보이기 때문에 스크린샷을 찍을 수 있습니다

그런데 꼭 스크롤을 해야할 정도로 밑에 있는 요소는 스샷을찍고 자르려고하면

System.OutOfMemoryException: '메모리가 부족합니다.'

라고 크래시가 납니다

위에서 우리가 비트맵에 스크린샷바이트값을 입력했고 그 범위를 넘긴것이라고 생각하면됩니다

우리가 직접 웹에서 스크롤을 해도 마찬가지로 크래시가 납니다

 

이유는 아무리 스크롤을해도 스크린샷의 사이즈인 가로,세로길이는 강제로 늘리지 않는 한 모니터사이즈만큼 고정되어 있습니다

그러나 요소의 위치는 스크린샷의 사이즈와 별개로 있기 때문에 단순히 요소의 위치만 구하고 스샷을 자르려고하면 문제가 생기기 마련입니다

전체스크린샷의 사이즈는 1000x1000이라면 스크롤하려는 요소의 위치는 1000,1300으로 스크린샷 사이즈,비트맵사이즈의 범위밖으로 넘어가게되기 때문에 문제가 생깁니다.

 

그래도 해결하는 방법인 다 존재하니 알아봅시다

우선 전체스샷을 찍을때 요소가 보여야겠죠? 이미 이런 기능은 존재합니다!!

개발자모드에서 바로 해당 요소로 스크롤이 됩니다 이걸 우리는 코드에서 작동시켜야하는데 방법은 말이죵

_web_elem = _driver.FindElement(By.XPath("//*[@id='footer']/div/div[3]/div[2]/div[1]/a/img"));
_driver.ExecuteScript("arguments[0].scrollIntoView(true);", _web_elem);

 

딱~ 해보시면 스크롤이 됩니다

이제 안보이던 요소를 스크린샷찍을 수 있게 되었습니다

그 다음 우리는 스크롤된 길이(y축)만큼 추가로 계산을 해야합니다

 

스크린샷은 왼쪽위를 (0,0)으로 기준점을 잡습니다

그런데 스크롤된 웹의 기준점은 (0, 스크롤길이) 입니다

이 격차만큼 우리는 계산을 해줘서 웹 기준점을 0, 0으로 맞춰줘야합니다

 

자 그럴려면 스크롤된 y축값을 가져옵시다

//웹 페이지의 Y기준점 찾기
var y_offset_object =_driver.ExecuteScript("return window.pageYOffset;");
int y_offset = (int)float.Parse(y_offset_object.ToString());

코드가 좀 이상하죠? int로 받고있는데 왜 굳이 float를 거치는지,,,

가끔 특정상황에서는 페이지Y오프셋이 소수점으로 떨어지는 경우가 있습니다.

그래서 바로 int.Parse를 하면 이상하게 작동하더라구요.

안전장치로 일단 float로 불러온뒤 int로 캐스팅후 가져왔습니다

물론 float로 가져와서 바로 사용해도 됩니다만 이따 뺄셈을 해야해서 정확한 계산을 하기 위해 int로 캐스팅해줬습니다

 

y축값을 가져왔으니 우리가 스샷을 찍으려는 요소의 위치를 y축값만큼 뺍니다

//*전체스크린샷에서 요소크기에 맞춰자릅니다
Rectangle rect_crop_size = new Rectangle(
    _web_elem.Location.X,
    _web_elem.Location.Y - y_offset, 
    _web_elem.Size.Width, 
    _web_elem.Size.Height);
bitmap_screenshot = bitmap_screenshot.Clone(rect_crop_size, bitmap_screenshot.PixelFormat);
bitmap_screenshot.Save(String.Format(Environment.CurrentDirectory + "//screenshot_scroll_crop.png", ImageFormat.Png));

y값만 계산했습니다

 

이 방법은 최대스크린샷사이즈보다 큰 요소는 찍기 어렵다는게 단점입니다...

 


분명 Capture node screenshot이라는 편한 기능이 있는데...

개발자모드에서 원하는 요소를 우클릭하면 Capture node screenshot이라는 기능이 있습니다

이걸사용하면 스크롤과 요소사이즈 상관없이 곧바로 요소를 스크린샷할 수 있습니다

하...근데 코드상에서 돌리는 방법은 아무리 삽질해도 안나오네요 아는사람있으면 댓글 부탁드립니다

 

 

https://github.com/ForestBird1/TestSelenium.git

 

GitHub - ForestBird1/TestSelenium

Contribute to ForestBird1/TestSelenium development by creating an account on GitHub.

github.com

 

이전편까지의 셀레니움기능들로 대부분의 간단한 매크로를 만들 수 있습니다.

게시글 제목만 가져오기, 특정기간내의 게시글 가져오기, 여러계정들의 정보바꾸기 등등

 

하지만 다음 페이지로 넘어가고 페이지 로딩이 되지도 않았는데 프로그램은 페이지가 로딩 안된지 몰라서 다음 단계로 진행할 수 있습니다. 

 


웹 로딩 대기

1.

_driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);

해당코드는 페이지로딩까지 최대 10초까지 기다린다는 뜻 입니다

10초내로 페이지로딩이 완료되면 다음 작업을 진행하고 10초가 넘어가면 더이상 기다리지않고 다음 작업을 진행하게됩니다

한번만 설정하면 모든 드라이버에서 적용됩니다

원하는 '초'단위의 숫자를 입력하시면 됩니다


2.

WebDriverWait wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
IWebElement SearchResult = wait.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementExists(By.XPath(xpath)));

10초동안 xpath요소가 존재할때까지 대기한다는 뜻입니다

여기서 특별한 셀레니움기능이 추가되는데

해당 패키지를 먼저 설치하셔야 합니다


3.

Thread.Sleep(1000);

특정 상황에서는 페이지로딩을 기다려도 특정한 이유로 더 기다려야하는 상황이 생길 수 있습니다

이럴 땐 강제로 슬립을 걸어서 기다리는 방법이 있습니다.

ms단위 이므로 1초를 기다리려면 위의 코드처럼 1000으로 입력해야합니다

 


자바스크립트

IJavaScriptExecutor _js = (IJavaScriptExecutor)_driver;
_js.ExecuteScript(js);

이건 별거 없고 이렇게 바로 실행해주시면 됩니다

 

https://github.com/ForestBird1/TestSelenium.git

 

GitHub - ForestBird1/TestSelenium

Contribute to ForestBird1/TestSelenium development by creating an account on GitHub.

github.com

 


쓰기, 읽기, 지우기

이전의 3번게시글을 읽어서 XPath값을 가져오는 방법으로 아이디텍스트박스의 XPath값을 가져옵니다

private IWebElement _web_elem = null;

//쓰기
_web_elem = _driver.FindElement(By.XPath("//*[@id='id']"));
_web_elem.SendKeys("id");

//읽기
_web_elem = _driver.FindElement(By.XPath("//*[@id='id']"));
Console.WriteLine(_web_elem.Text);

//지우기
_web_elem = _driver.FindElement(By.XPath("//*[@id='id']"));
//둘중에 하나만 사용해도 됩니다
_web_elem.SendKeys("");
_web_elem.Clear();

꼭 XPath의 큰따옴표를 작은따옴표로 바꿔주셔야합니다!


이제 기본적인 매크로를 만들 수 있습니다

우리는 특정한 요소의 내용을 가져올 수 있고 텍스트박스에 입력할 수 있으며 클릭을통해 모든 행동을 제어할 수 있습니다.

아직 고급단계가 남아있긴하지만 어지간한 매크로를 만드는데는 충분합니다. 

물론 아직 완벽하게 만들기는 무리가 있습니다. 자바스크립트도 사용못하고 쓰레드슬립도 모르고 웹로딩을 대기하는 방법도 모릅니다. 특별히 스크린샷기능도 모르죠

다음 포스팅엔 조금더 고급단계로 넘어가보겠습니다

 

https://github.com/ForestBird1/TestSelenium.git

 

GitHub - ForestBird1/TestSelenium

Contribute to ForestBird1/TestSelenium development by creating an account on GitHub.

github.com

 

+ Recent posts