[STL]STL에 필요한 주요 연산자 오버로딩



함수 호출 연산자 오버로딩( () 연산자)

함수 호출 연산자 오버로딩은 객체를 함수처럼 동작하게 하는 연산자다. c++에서 Print(10)이라는 함수 호출 문장은 3가지로 해석이 가능하다.

  1. 함수 호출: Print가 함수 이름이다.
  2. 함수 포인터: Print가 함수 포인터다.
  3. 함수 객체: 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.함수 객체를 이용한 정수 출력
}

출력 결과

image

위에서 보이는 것처럼 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);
}

출력 결과

image

위의 코드는 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객체여서 불가능
}

출력 결과

image

위에서 비 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이라는 클래스에서 자동으로 메모리를 제거해주는 것을 볼 수 있다.

위의 코드의 메모리 구조를 보면 다음과 같다.

image

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는 자동으로 동적으로 할당한 메모리를 해제하지만 일반 포인터와 동일한 형태로 사용할 수 있다.




© 2022. by KSC

Powered by sora