[STL]STL에 필요한 주요 연산자 오버로딩
in STL
함수 호출 연산자 오버로딩( () 연산자)
함수 호출 연산자 오버로딩은 객체를 함수처럼 동작하게 하는 연산자다. c++에서 Print(10)이라는 함수 호출 문장은 3가지로 해석이 가능하다.
- 함수 호출: Print가 함수 이름이다.
- 함수 포인터: Print가 함수 포인터다.
- 함수 객체: Print가 함수 객체다.
여기서 함수 호출 연산자를 오버로딩한 객체를 함수 객체라고 한다.
예시 코드를 봐보자.
#include<iostream>
using namespace std;
class Func
{
public:
void operator () (int num) const
{
cout << "정수 : " << num << endl;
}
};
void Print1(int num)
{
cout << "정수 : " << num << endl;
}
void main()
{
void (*Print2)(int) = Print1;
Func Print3;
Print1(10); //1.함수를 이용한 정수 출력
Print2(10); //2.함수 포인터를 이용한 정수 출력
Print3(10); //3.함수 객체를 이용한 정수 출력
}
출력 결과
위에서 보이는 것처럼 Func라는 클래스 안에 함수 호출 연사자를 오버로딩하면 Print3은 Func클래스의 객체이며 함수처럼 작동한다.
이번에는 여러 인자를 받는 함수 호출 연산자를 함수 호출 연산자를 중복한 예시다.
#include<iostream>
using namespace std;
class Func
{
public:
void operator () (int num1) const
{
cout << "정수 : " << num1 << endl;
}
void operator () (int num1,int num2) const
{
cout << "정수 : " << num1 <<", "<<num2<< endl;
}
void operator () (int num1,int num2,int num3) const
{
cout << "정수 : " << num1<<", "<<num2<<", "<<num3 << endl;
}
};
void main()
{
Func print;
//함수객체를 이용한 암시적 호출
print(10);
print(10, 20);
print(10, 20, 30);
cout << endl;
//함수객체를 이용한 명시적 호출
print.operator()(10);
print.operator()(10,20);
print.operator()(10,20,30);
cout << endl;
//임시객체를 이용한 암시적 호출
Func()(10);
Func()(10,20);
Func()(10,20,30);
cout << endl;
//임시객체를 이용한 명시적 호출
Func().operator()(10);
Func().operator()(10,20);
Func().operator()(10,20,30);
}
출력 결과
위의 코드는 Func클래스안에 함수 호출 연산자를 오버로딩하는데 매개변수의 갯수를 달리하여 오버로딩하고 있다. 그리고 main함수에서 암시적 호출과 명시적 호출을 보여주는데 암시적 호출은 컴파일러가 ()를 만나고 함수객체라면 그 객체의 클래스 안에 있는 operator()함수를 찾아가 자동으로 호출 해준다. 명시적 호출은 직접 객체에 접근하여 객체안에 있는 operator()함수를 호출한다.
그리고 위에 코드에서 임시객체라는 것을 사용하는데 컴파일러가 Func()를 만나면 임시객체를 생성하고 그 임시객체 안에 있는 oeprator()함수를 호출한다.
임시 객체
여기서 임시객체는 클래스 이름을 통해 만든다. 하지만 임시객체는 그 문장에서 생성되고 그 문장을 벗어나면 소멸된다. 즉, 한 문장에서만 임시로 필요한 객체에 사용한다.
배열 인덱스 연산자 오버로딩( [] 연산자 )
[]연산자 오버로딩은 주로 컨테이너 객체에 사용 된다. 예시 코드를 봐보자.
#include<iostream>
using namespace std;
class Array {
private:
int* arr;
int size;
int capacity;
public:
Array(int cap=100):arr(0),size(0),capacity(cap)
{
arr = new int[capacity];
}
~Array()
{
delete[] arr;
}
void Add(int num)
{
if (size < capacity) {
arr[size++] = num;
}
}
int Size() const
{
return size;
}
int operator[](int idx) const //읽기만 가능함
{
return arr[idx];
}
};
void main()
{
Array ar(10);
ar.Add(10);
ar.Add(20);
ar.Add(30);
for (int i = 0; i < ar.Size(); i++)
{
cout << ar[i] << ", ";
}
}
코드에 별다른 어려움이 있지는 않아서 설명은 생략한다.
하지만 보통 배열은 읽기만 가능한게 아니라 쓰기도 가능하다. 하지만 위의 코드는 읽기만 가능하다 위의 코드에서 쓰기까지 가능하게 하려면 operator[]함수를 비 const 함수 둘 다 제공해야한다. 예시를 보자
#include<iostream>
using namespace std;
class Array {
private:
int* arr;
int size;
int capacity;
public:
Array(int cap=100):arr(0),size(0),capacity(cap)
{
arr = new int[capacity];
}
~Array()
{
delete[] arr;
}
void Add(int num)
{
if (size < capacity) {
arr[size++] = num;
}
}
int Size() const
{
return size;
}
int operator[](int idx) const //읽기만 가능함
{
return arr[idx];
}
int& operator[](int idx) //쓰기, 읽기 모두 가능
{
return arr[idx];
}
};
void main()
{
Array ar(10);
ar.Add(10);
ar.Add(20);
ar.Add(30);
cout << ar[0] << endl; //operator[](int idx) 호출
cout << endl;
const Array& ar2 = ar;
cout << ar2[0] << endl; //operator[](idx) const 호출
cout << endl;
ar[0] = 100; //operator[](int idx) 호출
//ar2[0]=100; //ar2가 const객체여서 불가능
}
출력 결과
위에서 비 const함수는 읽기, 쓰기 모두 가능한 ar객체에 사용되며 const함수는 읽기만 가능한 ar2객체에 사용된다.
메모리 접근, 클래스 멤버 접근 연산자 오버로딩( *, -> 연산자 )
*, -> 연산자는 스마트 포인터나 반복자(iterator)등의 특수한 객체에 사용된다. 반복자가 STL의 핵심요소이기에 *,-> 연산자 오버로딩은 아주 중요하다.
이 글에서는 스마트 포인터 클래스를 만들고 이 스마트 포인터로 *,->연사자 오버로딩을 설명하겠다. 스마트 포인터는 일반 포인터 기능에 몇 가지 유용한 기능을 추가한 포인터처럼 동작하는 객체 이다.
우리가 먼저 생각해 봐야 하는건 일반 포인터의 단점이다. 우선 일반 포인터의 사용 에시를 코드로 봐보자.
#include<iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int _x, int _y) :x(_x), y(_y) { }
void Print() const
{
cout << x << ", " << y << endl;
}
};
void main()
{
Point* p1 = new Point(2, 3); //메모리 할당
Point* p2 = new Point(4, 5); //메모리 할당
p1->Print();
p2->Print();
delete p1; //메모리 제거
delete p2; //메모리 제거
}
위와 같은 코드가 있다고 치면 main함수에서 메모리를 동적으로 할당 했으니 메모리 제거도 수동으로 해주는게 맞다. 하지만 메모리 제거 해주는게 귀찮은걸 떠나서 사람이다 보니 제거하는걸 까먹을 수 있고, 혹은 함수 사용중에 함수가 종료되거나 예외가 발생하여 메모리 제거를 해주지 못하면 메모리 누수가 일어난다. 이러한 것들을 방지하기 위한 클래스를 만들고 사용해보자. 우선, 예시를 보자
#include<iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int _x, int _y) :x(_x), y(_y) { }
void Print() const
{
cout << x << ", " << y << endl;
}
};
class PointPtr
{
private:
Point* ptr;
public:
PointPtr(Point* p) :ptr(p) { }
~PointPtr()
{
delete ptr;
}
};
void main()
{
PointPtr p1 = new Point(2, 3); //메모리 할당
PointPtr p2 = new Point(4, 5); //메모리 할당
//p1->Print(); //아직 사용 못함
//p2->Print(); //아직 사용 못함
//p1의 소멸자에서 Point 동적 객체를 자동으로 메모리에서 제거
//p2의 소멸자에서 Point 동적 객체를 자동으로 메모리에서 제거
}
위의 코드를 보면 PointPtr이라는 클래스에서 자동으로 메모리를 제거해주는 것을 볼 수 있다.
위의 코드의 메모리 구조를 보면 다음과 같다.
p1,p2는 스택 객체니까 main함수 블럭에서 제거되고, p1,p2의 소멸자에 의해 ptr이 가리키는 동적 메모리 객체를 제거한다. 하지만 PointPtr클래스는 완성이 아니다. 일반 포인터처럼 작동 할 수 있게 -> 연산자를 오버로딩 해야 한다. 다음 코드를 봐보자.
#include<iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int _x, int _y) :x(_x), y(_y) { }
void Print() const
{
cout << x << ", " << y << endl;
}
};
class PointPtr
{
private:
Point* ptr;
public:
PointPtr(Point* p) :ptr(p) { }
~PointPtr()
{
delete ptr;
}
Point* operator ->() const //현재 가리키고 있는 주소를 넘겨서 ->연산이 가능하게 만듬
{
return ptr;
}
};
void main()
{
PointPtr p1 = new Point(2, 3); //메모리 할당
PointPtr p2 = new Point(4, 5); //메모리 할당
p1->Print(); //p1.operator->()->Print() 호출
p2->Print(); //p2.operator->()->Print() 호출
//p1의 소멸자에서 Point 동적 객체를 자동으로 메모리에서 제거
//p2의 소멸자에서 Point 동적 객체를 자동으로 메모리에서 제거
}
여기서 p1->print()를 자세히 보면 p1.operator->()함수에서 실제 보관중인 주소를 가져오고 그 주소를 이용하여 실제 Point의 멤버 함수인 Print를 호출 하는걸 볼 수 있다. 하지만 여기서 끝이 아니다. 스마트 포인터도 일반 포인터처럼 똑같이 작동하게 하려면 *연산자도 오버로딩 해야한다. 다음은 스마트 포인터 클래스의 완성본이다.
#include<iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int _x, int _y) :x(_x), y(_y) { }
void Print() const
{
cout << x << ", " << y << endl;
}
};
class PointPtr
{
private:
Point* ptr;
public:
PointPtr(Point* p) :ptr(p) { }
~PointPtr()
{
delete ptr;
}
Point* operator ->() const //현재 가리키고 있는 주소를 넘겨서 ->연산이 가능하게 만듬
{
return ptr;
}
Point& operator*() const //현재 가리키고 있는 주소에 접급하여 가리키고 있는 객체 그 자체를 리턴한다.
{
return *ptr;
}
};
void main()
{
Point* p1 = new Point(2, 3); //일반 포인터
PointPtr p2 = new Point(4, 5); //스마트 포인터
p1->Print(); //p1.Print()호출
p2->Print(); //p2.operator->()->Print() 호출
cout << endl;
(*p1).Print(); //(*p1).Print() 호출
(*p2).Print(); //p2.operator*().Print() 호출
delete p1;
//p2의 소멸자에서 Point 동적 객체를 자동으로 메모리에서 제거
}
위와 같이 스마트 포인터인 p2는 자동으로 동적으로 할당한 메모리를 해제하지만 일반 포인터와 동일한 형태로 사용할 수 있다.