일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- suffix return type
- increment operator
- member function pointer
- dynamic_cast
- constructor
- diamond inheritance
- std::ostream
- return by reference
- placement new
- conversion constructor
- pointer to member data
- virtual function
- vector size
- virtual function table
- this call
- std::endl
- virtual inheritance
- std::cout
- vector capacity
- virtual destructor
- 더 지니어스 양면포커
- c++ basic practice
- operator overloading
- discord bot
- new&delete
- c++ multi chatting room
- delete function
- std::vector
- base from member
- C++
- Today
- Total
I'm FanJae.
[C++] Vector I 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
- 배열의 단점은 크기를 변경할 수 없다는 것이다.
- 크기를 변경하려면, 처음에 필요한 만큼의 메모리를 동적 할당해서 사용해야 한다.
1. 동적 배열(vector)의 구현
- 메모리 동적 할당 기능을 활용해서, 크기 변경이 가능한 배열을 직접 생각해보자.
1-1. 기본적인 buffer의 생성
- 대략 이런식으로 할당을 한다고 생각하여, 코드를 작성하면 아래와 같다.
#include <iostream>
int main(void)
{
int* buff = new int[5];
buff[0] = 1;
}
1-2. 크기의 변동
- 1은 넣은 뒤의 값은 이와 같을 것이다. 이제 이것의 '크기'를 변동한다고 생각해보는 것이다.
- 사이즈를 10개로 늘린다고 가정하자.
- 크기를 늘릴때는 재할당 했다고, 이전에 있던 데이터가 삭제 되어서는 안된다.
① 새로운 크기의 할당
- 우선, 새롭게 할당 받을 크기 만큼의 메모리를 동적할당 한다.
- 코드로 작성해보면 아래와 같다.
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 |