[C++] vector 내부에 iterator 클래스 구현 (+ 연산자 오버로딩, 복사 생성자)
2024. 4. 3. 10:01ㆍProgramming Language/C++
vector 내부에 iterator 클래스 만드는데, 클래스 템플릿이기 때문에 내부에 있는 iterator도 템플릿이 된다.
iterator는 데이터의 시작 주소와 가리키고 있는 데이터가 시작 주소로부터 몇 번째인지 알아야 한다.
즉, iterator는 데이터의 시작 주소와 인덱스(시작 주소로부터 몇 칸 떨어져 있는지)만 알면 된다.
ex) 3번째를 가리키는 경우, 시작 주소와 인덱스 2의 주소만 알면 된다.
또한 자료형도 알아야 하는데, 템플릿이기 때문에 데이터의 시작 주소 타입은 `T*`이다.
▷ 코드
CArr.h
#pragma once
#include <assert.h>
// 클래스 템플릿
template<typename T>
// T는 가변 배열에 저장될 데이터 타입이다.
class CArr // CArr로 vector를 만들어도 CArr과 iterator 클래스는 별개의 자료형이기 때문에 이 클래스 크기는 총 16byte 자료형이다.
// 객체는 `CArr::iterator`로 만들면 된다.
{
private:
T* m_pData; // 8byte (64bit이기 때문에) // 일단 템플릿 부분을 지우거나 주석 처리 후 <Ctrl + R + R>로 전체적으로 해당 이름을 바꾸자.
int m_iCount; // 현재 들어와있는 데이터 개수
int m_iMaxCount; // 최대 개수
public:
void push_back(const T& _Data); // 데이터 추가
void resize(int _iResizeCount); // 재할당 => 몇 칸으로 늘릴 것인지를 _iResizeCount로 받는다.
// 데이터 타입과 무관한 가변 배열의 개수이기 때문에 int가 맞다.
T* data() { return m_pData; } // 시작 주소 반환
int size() { return m_iCount; } // 데이터 개수
int capacity() { return m_iMaxCount; } // 현재 기준으로 허용 범위
T& operator[] (int idx);
class iterator;
iterator begin(); // (아래 쪽에 정의했다.)
iterator end(); // (아래 쪽에 정의했다.)
public:
CArr(); // 생성자
~CArr(); // 소멸자
// 이너 클래스를 포함하고 있는 클래스의 private 멤버에 접근 가능
class iterator
{
private:
// 그래서 벡터를 똑같이 구현하려면 아래와 같이 멤버들이 있어야 한다.
CArr* m_pArr; // iterator가 가리킬 데이터를 관리하는 가변 배열 주소
T* m_pData; // 데이터들의 시작 주소 => 이 역시 벡터가 알고 있다.
int m_iIdx; // 가리키는 데이터의 인덱스 // 만약 iterator가 다음이나 이전을 가리키라고 하면 인덱스를 ++이나 --시키면 된다.
// 벡터 안에 데이터를 넣다가 꽉 채우게 되면 새로운 곳으로 메모리가 재할당된다.
// iterator가 첫 번째 데이터를 가리키고 있는데 인덱스 0인 것이 의미가 없어질 수도 있다.
// iterator가 첫 번째 데이터가 있는 줄 알고 접근하거나 iterator를 통해서 데이터를 덮어쓰려는 경우
// 이미 삭제된 곳에 데이터를 쓰려고 할 것이다.
// iterator 입장에서는 올바른건지 아닌지 알 수가 없다.
// 새로 할당된 곳의 주소를 알려면 iterator는 벡터를 알아야 한다.
// 벡터가 원래 꽉 찬 메모리와 새로 할당된 메모리의 주소도 갖고 있기 때문이다.
// 공간이 다른 곳으로 바꼈는지는 벡터의 멤버와 iterator가 알고 있던 주소가 같은지 비교하면 알 수 있다.
// 처음부터 CArr 객체 자체를 알고 있으면 된다.
// 만약 iterator가 벡터의 인덱스 3을 항상 가리킨다는 것이 보장되어있다면 아무리 메모리 공간이 바껴도 상관이 없기 때문에 데이터의 시작 주소(T* m_pData;)가 필요가 없게 된다.
// 그런데 iterator가 원래 주소랑 벡터가 관리하고 있는 주소가 다른 것을 따질거라면(문제로 삼을 거라면) iterator가 따로 관리해줘야 된다.
// 내가 만들어보고 있는 벡터는 이렇지만 실제 벡터는 바로 윗줄을 문제로 삼고 있다.
public:
// ----- 역참조 operator ----- //
T& operator * ()
// 반환 타입이 그냥 T라면 수정할 수 없다.
// T&여야 수정이 가능하다.
{
// 데이터의 시작 주소와 배열 쪽의 시작 주소가 같은지
// 만약 같지 않다면 경고
// 1. iterator가 알고 있는 주소와 가변 배열이 알고 있는 주소가 달라진 경우
// 공간 확장으로 주소가 달라진 경우
// 또는
// 2. iterator가 end iterator인 경우
if (m_pArr->m_pData != m_pData || -1 == m_iIdx)
{
assert(nullptr);
}
// 시작 주소(포인터)로부터 인덱스(위치)
return m_pData[m_iIdx]; // 여기서 []는 operator가 아니라 기본 문법이다. (포인터 연산)
}
// 연달아서 ++을 할 수 있다.
//int k = 0;
//++(++k); // k를 하나 증가시키고 또 하나를 증가시킨다.
// 이 부분 생각하면서 전위 연산자 구현하기
// ----- 전위 연산자 ----- //
iterator& operator ++() // 반환 타입이 iterator 본인이여야 증가시킨 iterator 본인을 다시 증가시키는 등의 작업을 할 수 있다.
{
// 예외 처리
// 2. end iterator인 경우
// 이 경우 더 이상 증가할 수 없다.
// 3. iterator가 알고 있는 주소와 가변 배열이 알고 있는 주소가 달라진 경우
// 공간 확장으로 주소가 달라진 경우
if (m_pArr->m_pData != m_pData || -1 == m_iIdx)
{
assert(nullptr);
}
// 1. iterator가 마지막 데이터를 가리키고 있는 경우
// --> end iterator가 된다.
// 예외 처리는 맞지만 오류가 발생하는 상황은 아니다.
// 어떻게 작성해야 될지 모를 때 iterator의 멤버 확인해보기
// iterator 클래스의 근본적인 역할 : 가변 배열(m_pArr)이 관리하고 있는 데이터들을 가리킨다.
if (m_pArr->size() - 1 == m_iIdx)
{
m_iIdx = -1;
}
else
{
++m_iIdx;
}
//++m_iIdx; // 주소로부터 떨어져있는 곳을 지칭하는 인덱스를 증가시킨다.
return *this;
}
iterator& operator --()
{
if (m_pArr->m_pData != m_pData || -1 == m_iIdx)
{
assert(nullptr);
}
if (m_pArr->size() - 1 == m_iIdx)
{
m_iIdx = -1;
}
else
{
--m_iIdx;
}
return *this;
}
// ----- 후위 연산자 ----- //
// 전위 연산자와 호출 시점은 동일하기 때문에 구현을 다르게 해줘야 한다.
// 가장 마지막에 호출되기 때문에 호출된 후의 반환타입이 중요하다.
// ※ 실제로 호출 시점은 가장 나중은 아니고 다른 멤버 함수처럼 해당 기호가 보이면 즉시 호출된다.
// 전위 연산자처럼 본인을 반환시키면(iterator&) 다음 대상을 가리키게 증가해버린다.
// 때문에 절대 후위 연산자처럼 동작할 수 없다.
// 래퍼런스가 아니라 ***복사본***을 줘야 한다.
// 참조가 아니라 또 다른 복사본이기 때문에 반환 타입은 사라진다.
iterator operator ++(int) // 인자로 적은 int는 아무 의미가 없고 받아올 변수명도 적을 필요 없다.
// int를 기반으로 컴파일러는 해당 함수를 후위 연산자로 인식한다.
// 일종의 fake를 준 것이다.
{
// 복사 생성자가 호출된다.
iterator copy_iter = *this; // 지역 변수로 iterator를 만든다.
// 지역 변수로 만들어진 iterator인 copy_iter는 후위 연산자를 호출한 객체와 값이 똑같아진다.
// 구현한 전위 연산자 활용
++(*this); // 후위 연산자를 호출시킨 객체는 다음 요소를 가리킨다.
return copy_iter; // 다음 대상으로 증가하지 않은 객체를 반환한다.
// 증가하기 이전에 복사받은 것을 반환한다.
}
iterator operator --(int)
{
iterator copy_iter = *this;
--(*this);
return copy_iter;
}
// ----- 비교 연산자 ==, != ----- //
// 반환 타입이 true 또는 false이기 때문에 bool 타입
bool operator ==(const iterator& _otheriter)
{
if (m_pData == _otheriter.m_pData && m_iIdx == _otheriter.m_iIdx)
{
return true;
}
return false;
}
bool operator !=(const iterator& _otheriter)
{
return !(*this == _otheriter);
}
public:
// 생성자
iterator()
: m_pArr(nullptr)
, m_pData(nullptr)
, m_iIdx(-1) // 아무것도 가리키지 않는다는 의미로 -1을 사용했다.
{
}
// 생성자 오버로딩
iterator(CArr* _pArr, T* _pData, int _iIdx)
: m_pArr(_pArr)
, m_pData(_pData)
, m_iIdx(_iIdx)
{
}
// 소멸자
~iterator()
{
}
};
};
// 생성자
template<typename T>
CArr<T>::CArr() // CArr 클래스의 T 버전 안에 구현되어있는 생성자를 지칭한다.
: m_pData(nullptr)
, m_iCount(0)
, m_iMaxCount(2)
{
m_pData = new T[2];
}
// 소멸자
template<typename T>
CArr<T>::~CArr()
{
delete[] m_pData;
}
template<typename T>
void CArr<T>::push_back(const T& _Data)
{
int i = 0;
if (m_iMaxCount <= m_iCount)
{
// 재할당
resize(m_iMaxCount * 2);
}
// 데이터 추가
m_pData[m_iCount++] = _Data;
}
template<typename T>
void CArr<T>::resize(int _iResizeCount)
{
if (m_iMaxCount >= _iResizeCount)
{
assert(nullptr);
}
// 1. resize시킬 개수만큼 동적 할당한다.
T* pNew = new T[_iResizeCount];
// 2. 기존 공간에 있던 데이터들을 새로 할당한 공간으로 복사시킨다.
for (int i = 0; i < m_iCount; ++i)
{
pNew[i] = m_pData[i];
}
// 3. 기존 공간은 메모리 해제
delete[] m_pData;
// 4. 배열이 새로 할당된 공간을 가리키게 한다.
m_pData = pNew;
// 5. iMaxCount 변경점 적용
m_iMaxCount = _iResizeCount;
}
template<typename T>
T& CArr<T>::operator[](int idx)
{
// m_pInt가 가리키고 있는 인덱스 첫 번째 데이터를 반환한다.
return m_pData[idx];
}
template<typename T>
typename CArr<T>::iterator CArr<T>::begin()
{
// 벡터 안에 데이터를 한 번도 넣은 적이 없다면?
// 시작(begin())이 곧 end()이다.
// 지칭하고 있는 주소(m_pData)로부터 인덱스 값이 -1(내 맘대로 정함.)이면 iterator를 end iterator 상태로 볼 것이다.
if (0 == m_iCount)
return iterator(this, m_pData, -1);
// 데이터가 없는 경우
// begin() == end()
else
return iterator(this, m_pData, 0);
}
template<typename T>
typename CArr<T>::iterator CArr<T>::end()
{
// 끝의 다음을 가리키는 iterator를 만들어서 반환해준다.
return iterator(this, m_pData, -1);
}
main.cpp
#include <iostream>
#include "CArr.h"
#include <vector>
#include <list>
using std::cout;
using std::endl;
using std::vector;
using std::list;
class CTest
{
// 임시로 private에서 public으로 변경했다.
//private:
public:
int m_i;
public:
CTest& operator =(const CTest& _other)
{
m_i = _other.m_i;
return *this;
}
public:
// 기본 생성자
CTest()
: m_i(0)
{
}
// 복사 생성자
// 직접 만들어주지 않아도 자동으로 만들어진다.
// 생성자이니까 반환 타입 없고 클래스명이랑 동일하다.
// 이니셜라이저로 초기화 문법도 사용할 수 있다.
CTest(const CTest& _other)
: m_i(_other.m_i) // 복사의 원형(_other)이 되는 것을 받아와서 그 값을 초기값으로 사용한다.
{
}
};
int main()
{
// 내가 만든 vector
CArr<int> myvector;
myvector.push_back(10);
myvector.push_back(20);
myvector.push_back(30);
CArr<int>::iterator myiter = myvector.begin();
// ++, -- (전위, 후위), --, ==, !=
for (; myiter != myvector.end(); ++myiter)
{
cout << *myiter << endl;
}
myiter = myvector.begin();
//int iData = *(++myiter); // 20
int iData = *(myiter++); // 10이 나온 다음에 myiter가 20으로 증가한다.
// 실제로 ++는 전위 연산자처럼 먼저 호출된다.
// myiter는 다음 요소를 이미 가리키고 있다.
// ++(*this);로 전위 연산자를 호출시켰기 때문이다.
// 후위 연산자 함수가 종료되고 리턴된 것은 증가하기 이전 상태의 복사본이다.
// 특별한 이유가 아니라면 후위보단 전위 연산자를 이용하기
// 후위 연산자 => 쓸데없는 객체 생성 및 삭제 비용이 발생한다.
// 지역 변수 객체 만든다. => 복사 (생성 1) -> 이를 리턴한다.
// 원본(copy_iter)을 지역 변수로 받는다.
// 반환 타입이 래퍼런스가 아닌 일반 타입이기 때문에 copy_iter는 함수가 종료되면 삭제된다. (삭제 1)
// 반환 타입으로써 존재하는 전달자 객체가 또 만들어진다. (생성 2)
// copy_iter를 다시 전달해주는 전달용 객체가 myiter로 돌려준다.
// 이 객체도 결국은 사라진다. (삭제 2)
// 즉, 무의미한 객체 생성 및 삭제가 각각 2번씩 발생하여 비효율적이다.
// <기본 생성자 만들기 전> 복사 생성자를 만들었는데 아래에서 오류가 발생한 이유? (면접 단골 질문)
//CTest t1, t2;
//t1 = t2;
// 복사 생성자는 어차피 자동으로 구현되는데 직접 구현했다.
// 이는 생성자 오버로딩이므로 생성자가 한 종류라도 생긴 것이다.
// 그럼 컴파일러는 기본 생성자를 만들어주지 않는다.
// 즉, 생성자를 하나라도 만들면 C++ 컴파일러는 기본 생성자를 만들어주지 않는다.
// 위의 t1, t2의 경우 아무것도 적지 않았기 때문에 기본 생성자를 호출하는데 컴파일러가기본 생성자를 만들어주지 않았기 때문에 오류가 발생한 것이다.
// 때문에 문제가 발생하지 않게 하려면 기본 생성자도 직접 만들어야 한다.
// 그럼 다른 생성자를 만들고 기본 생성자를 만들지 않았어도 기본 생성자를 만들어주면 되는거 아닌가?
// 나중에 기본 생성자를 만들고 싶지 않은 경우도 있다. (기본 생성자를 호출하고 싶지 않은 경우)
// t2 객체를 만들 때 t1을 기반으로 만들고 싶다.
// 객체 생성하면 기본 생성자로 t2.m_i에 0가 들어간다.
// 다시 대입 연산자를 이용해서 t1에 있는 m_i값을 t2로 옮긴다.
// 즉, 2번 나눠서 작업하는 꼴이다.
CTest t1;
t1.m_i = 100;
//CTest t2;
//t2 = t1;
// 이럴 때 복사 생성자를 이용하면 된다.
CTest t2(t1); // t2 생성자를 호출하는데 t1 인자를 받는다.
// **객체 생성과 대입을 동시에 한다.** (복사 생성자 => 면접 단골 내용)
//CTest t3 = t1;
// 겉으로는 대입 연산자를 호출한 것처럼 보인다.
// 대입 연산자가 발생한다. => t3 객체가 이미 만들어질 때 기본 생성자를 호출했다.
// t3 객체가 이미 만들어져있어야 대입을 받을 수 있다.
// 객체가 만들어질 때 무조건 생성자가 호출된다.
// CTest 객체인 t3가 만들어졌다는 것이 생성자가 호출됐다는 의미이다.
// 그리고나서 t1을 t3에 대입한다.
// 구문 자체는 객체를 생성하는 동시에 대입한다.
// 즉, 복사 생성자로 한 번에 처리할 수 있다.
// 객체 생성과 대입이 동시에 일어나므로 컴파일러가 알아서 복사 생성자로 바꿔준다.
CTest t3(t1);
return 0;
'Programming Language > C++' 카테고리의 다른 글
[C++] BST(이진 탐색 트리) 구현 (0) | 2024.04.03 |
---|---|
[C++] friend 선언(클래스, 함수) (0) | 2024.04.03 |
[C++] printf(), scanf()로 cin, cout 구현 (0) | 2024.04.02 |
[C++] 템플릿 (함수/클래스) (0) | 2024.04.02 |
[C++] 접근 지정자 (0) | 2024.04.01 |