I'm FanJae.

[C++] Vector I 본문

C++/Basic

[C++] Vector I

FanJae 2024. 8. 13. 18:07

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

- 배열의 단점은 크기를 변경할 수 없다는 것이다.

- 크기를 변경하려면, 처음에 필요한 만큼의 메모리를 동적 할당해서 사용해야 한다.

 

1. 동적 배열(vector)의 구현

- 메모리 동적 할당 기능을 활용해서, 크기 변경이 가능한 배열을 직접 생각해보자.

 

1-1. 기본적인 buffer의 생성

 

동적 할당된 buffer

- 대략 이런식으로 할당을 한다고 생각하여, 코드를 작성하면 아래와 같다.

#include <iostream>
int main(void)
{
    int* buff = new int[5];
    
    buff[0] = 1;
}

 

 

1-2. 크기의 변동

- 1은 넣은 뒤의 값은 이와 같을 것이다. 이제 이것의 '크기'를 변동한다고 생각해보는 것이다.

- 사이즈를 10개로 늘린다고 가정하자.

- 크기를 늘릴때는 재할당 했다고, 이전에 있던 데이터가 삭제 되어서는 안된다. 

 

① 새로운 크기의 할당

temp에 새로운 크기를 할당한다.

- 우선, 새롭게 할당 받을 크기 만큼의 메모리를 동적할당 한다.

- 코드로 작성해보면 아래와 같다.

int* temp = new int[10];

 

② 기존 값의 복사

- 이제 기존 buff안에 있는 데이터가 모두 복사 되어야 한다.

- memcpy를 이용해서 복사를 해주면 된다.

- 참고 : void *memcpy(void *dest, const void *src, size_t n); // 목적지, 원본 위치, 복사할 바이트 수.

- 코드로 작성하면 아래와 같다.

memcpy(temp, buff, sizeof(int)*5);

- 위 작업까지 모두 수행이 끝나면, 아래 그림과 같이 복사가 끝날 것이다.

- 위와 같이 복사가 됨을 확인이 가능하다. 

③ 기존 값의 삭제

- 이제 buff가 할당 받았던 기존 동적 메모리는 다시 반납해주어야 한다.

- delete를 사용해서 이를 반납한다.

- 코드로 작성하면 아래와 같다.

delete[] buff;

- 이 작업이 종료되면, buff가 포인트 하고 있던 메모리는 회수될 것이다.

회수 이후 상황

- 회수 이후 상태는 위와 같다.

 

④ buffer에 확장된 동적 메모리 할당 

- 이제 buff가 temp가 가리키는 메모리를 가리키게 만들기 위해서, temp가 가리키는 주소값을 얻어와야 한다.

- 2개 모두 포인터 변수이므로 어렵지 않게 얻어올 수 있다.

buff = temp;
temp = nullptr; // temp의 해제

- 위 코드 까지 완료되고 나면, 최종적으로 이와 같을 것이다.

- 여기서 만약 조금 더 완벽하게 작성하고 싶다면, temp에 nullptr을 넣으면 될 것 같다.

ㅇㄴㅇㅁㅇㄹㄹ-ㅇ0x2000

- 전체 코드를 정리 하면 아래와 같이 정리 될 것이다.

#include <iostream>
#include <cstring> // memcpy

int main(void)
{
	int* buff = new int[5];
	buff[0] = 1;

	std::cout << buff[0] << std::endl;

	int* temp = new int[10];
	memcpy(temp, buff, sizeof(int) * 5);

	delete[] buff;
	
	buff = temp;
	temp = nullptr;
	
	std::cout << buff[0] << std::endl;
}

2. myvector 구현

- 객체지향 프로그래밍의 철학 중 하나로 타입을 먼저 만들자는 취지가 있다.

- 이를 잘 생각하면, 처음 만들때는 조금 고생을 할 수 있지만, 한번 잘 구현만 해놓으면 유용하게 사용 가능할 것이라는 의미이다.

- 이에 따라서 myvector를 설계해보고자 한다.

 

① class 설계

- 앞서 우리가 만들었던 vector를 생각해보면 vector는 주소를 가질 포인터 변수와 buffer의 크기 정보가 필요했다.

- 따라서 vector 객체는 이와 같이 만들어질 것이다.

vector v
*buff
size

- 이것에 대한 기본적인 클래스를 만들면 아래와 같이 만들 수 있다.

#include <iostream>

class myvector
{
    int* buff;
    int size;
};

 

② 생성자의 생성

- 앞서, 특정 크기 만큼의 메모리 동적 할당을 해준 것을 구현해주면된다.

- 코드로 작성하면 아래와 같다.

vector(int sz)
{
    size = sz;
    buff = new int[size];
}

 

③ 소멸자 생성

- 앞서 우리는 buff에 동적 메모리를 할당하였다. 이는 객체가 파괴될 때 함께 소멸될 수 있도록 처리가 필요하다.

- 즉, 소멸자는 생성자에서 자원 할당시 소멸자에서 자원을 회수하는 개념이다.

~vector() { delete[] buff; }

 

④ 1차 테스트 코드 작성

class myvector
{
    int* buff;
    int size;
public:
    vector(int sz)
    {
        size = sz;
        buff = new int[size];
    }
    ~vector() { delete[] buff; }
}
int main(void)
{
    vector v(5);
}

- 이와 같이 정리가 가능하다.

 

⑤ resize() 구현

- 이제 앞서 생각해보았던, 크기의 확장의 개념을 다시 적용해보자.

v.resize(10);

- 이와 같이 불렀다면, 앞서 우리가 가졌던 buffer의 크기가, 5에서 10으로 늘어나야 한다는 것이다.

- 우리는 앞서 크기의 확장에 대해 얘기를 나눴었다. 이를 기억하여 순서대로 적용해보자.

- 여기서는 '축소'도 다뤄보고자 한다. 다만 한번에 두가지를 다루면 복잡하니 일단 먼저 다뤘던 확장 부터 논해보자.

 

⑴ 확장의 구현

- 우선 아래와 같이 확장부터 생각해보자. 

 

 

void resize(int newsize)
{
     
}

 

- 앞서 확장은 새로운 크기의 할당, 기존 값의 복사, 기존 값의 삭제, 확장된 동적 메모리 할당 순서였다.

- 이를 그대로 작성해주면 된다. 아래와 같이 작성이 가능하다. 주석으로 순서를 구분하였다.

- 추가된 것이 있지만, 크게 어렵지 않게 이해할 수 있다.

void resize(int newsize)
{
     int* temp = new int[newsize] // 1. 새로운 크기의 할당
     memcpy(temp, buff, sizeof(int) * size); // 2. 기존 값의 복사
     
     delete[] buff; // 3. 기존 값의 삭제
     buff = temp; // 4. buff에 새로운 메모리 공간 할당
     
     temp = nullptr; // 5. temp가 가리키는 곳이 없도록 변경.
     size = newsize; // 6. 사이즈 값을 새로운 값으로 변경.
}

 

 축소의 구현

- 우린 확장에 대해 이미 꽤 깊이 생각했다.

- 축소와 확장일때 차이를 보이는 부분은 기존 값의 복사에 있다.

 

- 복사가 될때, 데이터가 유실되는 문제가 존재할 것이다. 다만, 이는 사용자가 resize()를 수행한 시점에서 불가피하다.

- 여기서, 중요한 것은 복사를 할때, 기존 배열의 사이즈 만큼 복사하는 것이 아닌 새로운 사이즈 만큼 복사해야 한다.

memcpy(temp, buff, sizeof(int) * size); // 확장에서의 복사

- 기존 확장에서의 복사를 생각해보면, 5칸 만큼 복사를 해야하는데, 새로운 temp는 그만큼 그걸 넣을 공간이 없다.

- 따라서, 새로운 사이즈 만큼 복사를 해줘야 한다.

memcpy(temp, buff, sizeof(int) * newsize); // 확장에서의 복사

- 이를 기존, resize()에 적용하면 아래와 같이 정리된다.

void resize(int newsize)
{
     if(newsize == size) return ; // 크기가 같을때는 실행되지 않도록 처리
     
     int* temp = new int[newsize] // 1. 새로운 크기의 할당
     
     // 확장인 경우
     if(newsize > size) 
         memcpy(temp, buff, sizeof(int) * size); // 2. 기존 값의 복사
         
     // 축소인 경우    
     if(newsize < size) 
         memcpy(temp, buff, sizeof(int) * newsize); // 2. 기존 값의 복사
     
     delete[] buff; // 3. 기존 값의 삭제
     buff = temp; // 4. buff에 새로운 메모리 공간 할당
     
     temp = nullptr; // 5. temp가 가리키는 곳이 없도록 변경.
     size = newsize; // 6. 사이즈 값을 새로운 값으로 변경.
}

- 축소와 확장에 대해서만 복사를 구분하였고, resize시 기존 사이즈와 동일하면, 크기 재할당을 하지 않도록 처리하였다.

 

⑥ 테스트 코드 예시

int main()
{
    vector v(5);
    
    v.resize(10);
    
    v.resize(3);
}

- 만드는 사람은 다소 복잡할 수 있겠지만, 사용하는 입장에서는 가져다 쓰기가 매우 간편해진다.

 

⑦ get,set 기능 구현

- vector의 값을 넣어주는 함수와 이를 반환해주는 기능을 구현해서 넣어준다.

int getAt(int idx)
{
    if(idx < size)	return buff[idx];
}
void setAt(int idx, int value)
{
    if(idx < size)	buff[idx] = value;
}

 

⑧ Template 처리

- 현재까지의 vector의 단점은 integer에 한정된다는 것이다.

- class를 만드는 틀인 template을 이용하면 활용도가 커진다.

 

⑴ class 위에 template 문법 추가

//template <typename T>
template <class T>
class myvector
{
    ...
}

 

⑵ Type이 달라지는 경우를 생각해서 변환

- 전체 코드를 보면서 T로 바꿔야 하는 곳을 찾아내야 한다.

int* buff;
int size;

- 위 2개에 대해서 생각해보면, 배열의 크기는 데이터 타입이 바뀐다고 해서 변하는게 아니다.

- 하지만 메모리에 담는 데이터 타입은 double,char,그외 다른것에 따라 충분히 달라질 수 있다.

- 따라서, buff는 변환이 필요하다.

T* buff;
int size;

 

⑨ 최종 변환 이후 코드

#include <iostream>
template <class T>
class myvector
{
private:
	T* buff;
	int size;
public:
	myvector(int sz)
	{
		size = sz;
		buff = new T[size];
	}
        ~myvector()	delete[] buff;
	void resize(int newsize)
	{
		if (newsize == size) return; 

		int* temp = new T[newsize]; 

		if (newsize > size)	memcpy(temp, buff, sizeof(T) * size);
		else if (newsize < size) memcpy(temp, buff, sizeof(T) * newsize); 

		delete[] buff; 
		buff = temp; 

		temp = nullptr;
		size = newsize;
	}
	void setAt(int idx, const T& value)
	{
		if(idx < size)	buff[idx] = value;
	}
	T getAt(int idx)
	{
		if (idx < size) return buff[idx];
	}
};

int main()
{
    vector <int> v(3);
    vector <double> v2(5);
    
    v.setAt(0, 3);
    std::cout << v.getAt(0) << std::endl;
    
    v2.setAt(0, 3.5);
    std::cout << v2.getAt(0) << std::endl;

- 이와 같이 구현할 수 있다.사실, 이미 C++ 표준에 vector라는 것이 존재한다.

- 그럼에도 이를 구현한 것은, 이것을 직접 구현해보면서 이해하는 과정이 매우 중요하다.


3. STL vector

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v1(5);
    std::vector<int> v2(5, 3);
    std::vector<int> v3 = {1,2,3,4,5};
    
    v3[0] = 99;
    
    v3.push_back(3); // 맨 끝에 추가
}

- STL은 C++ 언어의 표준 라이브러리로 대부분 Template으로 구현되어 있다.

 

std::vector

- 크기 변경이 가능한 동적 배열이다. 사용법은 배열과 거의 동일하며, [] 연산자로 요소에 접근 가능하다.

- 이후, STL에 대해 얘기할때 조금 더 별도 포스트로 다뤄보고자 한다.


4. myvector의 파일 분리

- myvector는 Template이다.

- 만약, 이를 분리할 것이라면 반드시 헤더 파일에 정의하는 것만으로 끝내면 안되며, 구현 정보까지 있어야한다.

- 이는 Template instantiation(템플릿 인스턴스화)  때문이다. (링크의 1-2-1) 참고)

 

템플릿의 인스턴스화가 되는 상황

 

- 컴파일 단계에서 myvector의 타입을 보고 각 타입에 맞게 변환의 과정을 거쳐줘야 한다.

- 즉, int 타입을 사용하면 컴파일 단계에서 int로 변환을 일으키고, double 타입을 사용하면 double로 변환을 일으킨다.

 

정리하면 다음과 같이 구현될 수 있다.

// myvector.h
#pragma once
template <class T>
class myvector
{
private:
	T* buff;
	int size;
public:
	myvector(int sz)
	{
		size = sz;
		buff = new T[size];
	}
	~myvector()
	{
		delete[] buff;
	}
	int get_vector_value(int index)
	{
		return buff[index];
	}
	void resize(int newsize)
	{
		if (newsize == size) return; // 크기가 같을때는 실행되지 않도록 처리

		int* temp = new T[newsize]; // 1. 새로운 크기의 할당

		if (newsize > size)	memcpy(temp, buff, sizeof(T) * size); // 2. 기존 값의 복사 (확장)
		else if (newsize < size) memcpy(temp, buff, sizeof(T) * newsize); // 2. 기존 값의 복사 (축소)

		delete[] buff; // 3. 기존 값의 삭제
		buff = temp; // 4. buff에 새로운 메모리 공간 할당

		temp = nullptr; // 5. temp가 가리키는 곳이 없도록 변경.
		size = newsize;
	}
	void setAt(int idx, const T& value)
	{
		if (idx < size)	buff[idx] = value;
	}
	T getAt(int idx)
	{
		if (idx < size) return buff[idx];
	}
};

 

// main.cpp
#include <iostream>
#include "myvector.h"

int main()
{
	myvector<int> dat(5);
	for (int i = 0; i < 5; i++)
	{
		dat.setAt(i,i+5);
		std::cout << dat.getAt(i) << std::endl;
	}
	std::cout << std::endl;
	dat.resize(10);

	for (int i = 0; i < 10; i++)
	{
		dat.setAt(i, i+10);
		std::cout << dat.getAt(i) << std::endl;
	}
	std::cout << std::endl;

	myvector<double> dat2(5);
	dat2.setAt(0, 3.4);
	std::cout << dat2.getAt(0) << std::endl;
}

- myvector.cpp에 구현하더라도, 컴파일 단위가 달라서 이를 인지할 방법이 없다는 것에 항상 유의해야 할 것 같다.

'C++ > Basic' 카테고리의 다른 글

[C++] Explicit Constructor  (0) 2024.08.14
[C++] Member initializer list, Default member initializer  (0) 2024.08.14
[C++] Constructor / Destructor  (0) 2024.08.13
[C++] OOP(Object Oriented Programming) I  (0) 2024.08.12
[C++] Explicit casting  (0) 2024.08.11
Comments