티스토리 뷰

  • 주의 사항!
  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

new와 delete도 연산자이기 때문에 오버 로딩이 가능합니다. 그래서 이 두 연산자의 오버 로딩에 대해서 예를 보이겠습니다. 또한 포인터 연산자를 오버 로딩하면서 개념적으로 어렵다고 이야기하는 '스마트 포인터''펑터(functor)'에 대해서도 간단히 설명하겠습니다.

 

new 연산자 오버 로딩

new 연산자의 오버 로딩은 앞서 보였던 연산자 오버 로딩과는 조금 다릅니다. 간단한 예를 통해서 new 연산자는 어떻게 오버 로딩되는지 알아보겠습니다. 연산자를 오버 로딩할 대상 클래스는 다음과 같습니다.

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	friend ostream& operator<<(ostream& os, const Point& pos);
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << '[' << pos.xpos << ", " << pos.y << ']';
	return os;
}

 

이제 위의 클래스를 대상으로 new 연산문을 하나 작성해 보겠습니다.

Point* ptr = new Point(3, 4);

 

만약 이때 new 연산자가 오버 로딩되어 있다면, 위의 문장은 어떻게 해석이 될지 한 번 생각해 봅니다. 하지만 아쉽게도 마땅한 답은 찾지 못했을 것입니다. 만약 그럴싸하게 해석했다고 해도 그것은 답이 될 수 없습니다.

 

이유는 오버 로딩된 new 연산자는 기본적으로 제공되는 new 연산자를 완벽히 대체하는 연산자가 아니기 때문입니다. 지금껏 사용해온, 기본적으로 제공되는 new 연산자가 하는 일은 다음과 같습니다.

  1. 메모리 공간의 할당
  2. 생성자의 호출
  3. 할당하고자 하는 자료형에 맞게 반환된 주소 값의 형 변환

 

이 중에서 세 번째 내용은 C언어에서 사용하던 malloc 함수와 달리, new 연산자가 반환하는 주소 값을 형 변환할 필요가 없음을 의미합니다. 컴파일러가 해당 과정을 대신 수행해주기 때문입니다.

 

객체의 생성 과정은 위에서 보았듯이 다소 복잡한 과정을 거칩니다. 따라서 개발자의 실수가 빈번하게 발생할 수 있고, 이런 실수는 다른 실수와는 다르게 그 영향이 프로그램에 크게 미칩니다. 따라서 위 세 과정 중 생성자의 호출과 반환된 주소 값의 형 변환은 컴파일러만 관리하게 두고, 메모리 공간 할당 과정에 대해서만 오버 로딩을 허락합니다.

 

new 연산자를 오버 로딩하는 방법은 다음과 같이 약속되어 있습니다.

void* operator new (size_t size) {......}

 

반환형은 반드시 void 포인터 형이어야 하고, 매개 변수형은 size_t이어야 합니다. Point 클래스를 대상으로 new 연산자가 오버 로딩되어 있는 상태에서 컴파일러가 다음의 문장을 만나면,

Point* ptr = new Point(3, 4);

 

먼저 필요한 메모리 공간을 계산하고, operator new 함수를 호출하면서 계산된 크기의 값을 인자로 전달합니다. 여기서 중요한 점은 크기의 정보는 바이트 단위로 계산되어 전달되는 점입니다. 따라서 대략 다음의 형태로 operator new 함수를 정의해야 합니다.

void* operator new (size_t size)
{
	void* adr = new char[size];
	return adr;
}

 

컴파일러에 의해서 필요한 메모리 공간의 크기가 바이트 단위로 계산되어서 인자로 전달되니, 크기가 1바이트인 char 단위로 메모리 공간을 할당해서 반환했습니다. 물론 이것이 operator new 함수의 전부라면 굳이 new 연산자를 오버 로딩할 필요도 없을 것입니다. 더 자세한 설명은 나중에 다시 나옵니다.

 

size_t 자료형이 무엇인지 계속 궁금했을 것입니다. size_t는 일반적으로 다음과 같이 정의되어 있습니다.

typedef unsigned int size_t;

 

그래서 0 이상의 값을 정수 값을 표현할 목적으로 정의된 자료형입니다.

 

delete 연산자 오버 로딩

이번엔 delete 연산자의 오버 로딩입니다. 다음과 같이 객체 생성 이후에,

Point* ptr = new Point(3, 4);

 

다음의 문장으로 객체의 소멸을 명령하면,

delete ptr;

 

컴파일러는 먼저 ptr이 가리키는 객체의 소멸자를 호출합니다. 그리고는 다음의 형태로 정의된 함수에 ptr에 저장된 주소 값을 전달합니다.

void operator delete (void* adr) {......}

 

따라서 delete 함수는 다음의 형태로 정의해야 합니다.

void operator delete (void* adr)
{
	delete[] adr;
}

 

즉, 소멸자는 오버 로딩된 함수가 호출되기 전에 호출이 되니, 오버 로딩된 함수에서는 메모리 공간의 소멸을 책임져야 합니다. 물론, 그 이외에 필요한 내용은 얼마든지 추가로 담을 수 있습니다.

 

만약 사용하는 컴파일러에서 void 포인터형 대상의 delete 연산을 허용하지 않는다면, 위의 delete문을 다음과 같이 작성하면 됩니다.

void operator delete (void* adr)
{
	delete[] ((char*)adr);
}

 

즉, char 포인터 형으로 변환해서 delete 연산을 진행하면 됩니다.

 

static 함수로 간주되는 new와 delete 연산자 오버 로딩

이제 다음의 예제를 보면서 new와 delete 연산자의 오버 로딩에 대해 살펴보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstdlib>
using namespace std;

class Point
{
private:
	int x, y;
public:
	Point(int x = 0, int y = 0) : x(x), y(y) {}
	friend ostream& operator<< (ostream& os, Point& pos);

	void* operator new (size_t size)
	{
		cout << "operator new : " << size << endl;
		void* adr = new char[size];
		return adr;
	}
	void operator delete (void* adr)
	{
		cout << "operator delete()" << endl;
		delete[] adr;
	}
};

ostream& operator<<(ostream& os, Point& pos)
{
	os << '[' << pos.x << ", " << pos.y << ']';
	return os;
}

int main(void)
{
	Point* ptr = new Point(3, 4);
	cout << *ptr << endl;
	delete ptr;

	return 0;
}

/*
실행결과

operator new : 8
[3, 4]
operator delete()

*/

 

앞서 설명했던 내용을 잘 이해하고 있다면 위 예제를 이해하기가 어렵진 않을 것입니다. 다만, 조금 이해가 가지 않는 부분이 있을 것입니다. operator new 함수와 operator delete 함수는 Point 객체의 멤버 함수로서 선언이 되었습니다. 그런데 main 함수의 다음 문장을 보면,

Point* ptr = new Point(3, 4);

 

Point 객체를 이제 막 생성하던 참인데 operator new 함수를 호출할 수 있음을 확인할 수 있습니다. 일반적으로 객체가 생성되기도 전에 그 객체의 멤버 함수를 사용할 수 있게 되는 것은 불가능한 일입니다. 하지만 해당 클래스에 static으로 선언되어 있는 멤버 함수라면 이것이 가능합니다.

 

그렇습니다. operator new 함수와 operator delete 함수는 별도로 static 선언을 하지 않아도 static 함수로 간주합니다. 그래서 객체가 생성되기 전에도 operator new 함수를 호출하는 것이 가능했습니다.

 

new와 delete 연산자 오버 로딩의 두 가지 형태

new 연산자는 다음 두 가지 형태로 오버 로딩이 가능합니다.

void* operator new (size_t size) {......}
void* operator new[] (size_t size) {......}

 

첫 번째 함수는 앞서 설명한 것이고, 두 번째 함수는 new 연산자를 이용한 배열 할당 시 호출되는 함수입니다. 즉, 다음의 문장을 만나면,

Point* ptr = new Point[3];

 

컴파일러는 세 개의 Point 객체에 필요한 메모리 공간을 바이트 단위로 계산해서, 이를 인자로 전달하면서 다음 함수를 호출합니다.

void* operator new[] (size_t size) {......}

 

즉, 배열 할당 시 호출되는 함수라는 사실을 제외하고는 operator new 함수와 차이점이 없습니다.

 

마찬가지로 delete 연산자도 다음 두 가지의 형태로 오버 로딩이 가능합니다.

void operator delete (void* adr) {......}
void operator delete[] (void* adr) {......}

 

두 번째 함수는 delete 연산자를 이용한 배열 소멸 시 호출되는 함수입니다.

 

다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	Point(int x = 0, int y = 0) : x(x), y(y) {}
	friend ostream& operator<< (ostream& os, const Point& pos);

	void* operator new (size_t size)
	{
		cout << "operator new : " << size << endl;
		void* adr = new char[size];
		return adr;
	}
	void* operator new[] (size_t size)
	{
		cout << "operator new[] : " << size << endl;
		void* adr = new char[size];
		return adr;
	}

	void operator delete (void* adr)
	{
		cout << "operator delete()" << endl;
		delete[] adr;
	}
	void operator delete[] (void* adr)
	{
		cout << "operator delete[] ()" << endl;
		delete[] adr;
	}
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << '[' << pos.x << ", " << pos.y << ']';
	return os;
}

int main(void)
{
	Point* ptr = new Point(3, 4);
	Point* arr = new Point[3];
	cout << *ptr << endl;
	delete ptr;
	delete[] arr;

	return 0;
}

/*
실행결과

operator new : 8
operator new[] : 24
[3, 4]
operator delete()
operator delete[] ()

*/

 

포인터 연산자 오버 로딩

 

이제 포인터 연산자의 오버 로딩에 대해 배워보겠습니다. 포인터를 기반으로 하는 연산자 모두를 포인터 연산자라고 합니다. 그런데 그중에서도 대표적인 포인터 연산자 둘은 다음과 같습니다.

  • ->
  • *

 

이 두 연산자는 일반적인 연산자의 오버 로딩과 큰 차이는 없습니다. 다만 둘 다 피연산자가 하나인 단항 연산자의 형태로 오버 로딩됩니다.

 

다음의 예제를 보겠습니다. 이 예제에서는 포인터 연산자의 오버 로딩 방식을 보여주고 있습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Number
{
private:
	int num;
public:
	Number(int n) : num(n) {}
	void ShowData() { cout << num << endl; }

	Number* operator->()
	{
		//cout << "operator->()" << endl;
		return this;
	}

	Number& operator*()
	{
		//cout << "operator*()" << endl;
		return *this;
	}
};
int main(void)
{
	Number num(20);
	num.ShowData();

	(*num) = 30;    
	num->ShowData();
	(*num).ShowData();

	return 0;
}

/*
실행결과

20
30
30

*/

 

(위 예제를 보면 이해하기 힘든 부분이 있습니다. 아래의 코드를 보겠습니다.

num->ShowData();

 

 

여기서 num은 포인터가 아님에도 ->연산자를 사용해서 ShowData() 함수에 접근합니다. 물론 이는 Number 클래스 안에서 -> 연산자가 오버 로딩되었기 때문에 가능한 일입니다. 하지만 해당 오버 로딩된 연산을 수행하면 반환하는 것은 객체 자신을 가리키는 포인터가 됩니다. 그 포인터에 추가적인 ->연산을 수행하지 않고도 어떻게 ShowData 함수에 접근할 수 있었는지가 의문입니다.)

 

num->ShowData();

 

위 코드에 일반적인 해석 방법을 적용하면 다음과 같이 됩니다.

num.operator->()ShowData();

 

그런데 멤버 함수 operator->가 반환하는 것은 주소 값이니, ShowData 함수의 호출은 문법적으로 성립하지 않습니다. 때문에 반환되는 주소 값을 대상으로 적절한 연산이 가능하도록 ->연산자를 하나 더 추가하여 다음과 같이 해석을 진행합니다. 즉, ->연산자를 하나 더 추가하는 것까지가 operator-> 함수를 해석하는 방법인 것입니다.

num.operator->() -> ShowData();

 

스마트 포인터

스마트 포인터는 사실 객체입니다. 즉, 포인터의 역할을 하는 객체를 똣합니다. (배열을 흉내 내는 배열 클래스와 유사하다고 생각합니다.) 간단히 스마트 포인터를 하나 정의한 다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) 
	{
		cout << "Point 객체 생성" << endl;
	}
	~Point()
	{
		cout << "Point 객체 소멸" << endl;
	}
	void SetPos(int x, int y)
	{
		xpos = x;
		ypos = y;
	}

	friend ostream& operator<<(ostream& os, const Point& pos);
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << '[' << pos.xpos << ", " << pos.ypos << ']';
	return os;
}

class SmartPtr
{
private:
	Point* posptr;
public:
	SmartPtr(Point* ptr) : posptr(ptr) {}
	Point& operator*() const
	{
		return *(posptr);
	}
	Point* operator->() const
	{
		return posptr;
	}
	~SmartPtr()
	{
		delete posptr;
	}
};

int main(void)
{
	SmartPtr sptr1(new Point(1, 2));
	SmartPtr sptr2(new Point(3, 4));
	SmartPtr sptr3(new Point(5, 6));
	
	cout << *sptr1 << endl;
	cout << *sptr2 << endl;
	cout << *sptr3 << endl;

	sptr1->SetPos(10, 20);
	sptr2->SetPos(30, 40);
	sptr3->SetPos(50, 60);

	cout << *sptr1 << endl;
	cout << *sptr2 << endl;
	cout << *sptr3 << endl;

	return 0;
}

/*
실행결과

Point 객체 생성
Point 객체 생성
Point 객체 생성
[1, 2]
[3, 4]
[5, 6]
[10, 20]
[30, 40]
[50, 60]
Point 객체 소멸
Point 객체 소멸
Point 객체 소멸

*/

 

위의 예제에서 가장 중요한 사실은, Point 객체의 소멸을 위한 delete 연산이 자동으로 이뤄졌다는 사실입니다. 그리고 이것이 바로 스마트 포인터의 똑똑함입니다. 스마트 포인터는 전문 개발자들이 개발한 이후에도 오랜 시간 실무에 사용하면서 다듬어 가는 클래스입니다. 그래서 보통은 스마트 포인터를 개인적으로 구현해서 사용하는 경우는 드물고, 오랜 시간 다듬어진, 그래서 라이브러리의 일부로 포함되어 있는 스마트 포인터를 활용하는 경우가 대부분입니다. 또한 스마트 포인터도 그 성격에 따라서 종류가 여러 가지가 있으니, 라이브러리에 포함된 스마트 포인터를 사용하기 위해서도 공부가 필요합니다.

 

펑터(functor)

함수의 호출에 사용되는 ( )도 연산자입니다. 때문에 이 역시 오버 로딩이 가능합니다. 그리고 이 연산자를 오버 로딩하면 객체를 함수처럼 사용하는 것이 가능해집니다.

 

객체의 이름이 adder이고 이 객체에 ( ) 연산자가 멤버 함수로 오버 로딩되어 있는 상태라면, 다음의 문장은 어떻게 해석될지 생각해 봅니다.

adder(2, 4);

 

우선 함수의 이름은 operator( )일 것입니다. 그리고 인자로 2와 4가 전달되고 있으니 다음과 같이 해석될 것입니다.

adder.operator()(2, 4);

 

이 내용만 알면 ( )연산자의 오버 로딩에 대해서는 더 설명할 게 없습니다. 다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	Point operator+(const Point& pos) const
	{
		return Point(xpos + pos.xpos, ypos + pos.ypos);
	}
	friend ostream& operator<<(ostream& os, const Point& pos);
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << '[' << pos.xpos << ", " << pos.ypos << ']';
	return os;
}

class Adder
{
public:
	int operator()(const int& n1, const int& n2)
	{
		return n1 + n2;
	}
	double operator()(const double& n1, const double& n2)
	{
		return n1 + n2;
	}
	Point operator()(const Point& pos1, const Point& pos2)
	{
		return pos1 + pos2;
	}
};

int main(void)
{
	Adder adder;
	cout << adder(1, 3) << endl;
	cout << adder(1.5, 3.7) << endl;
	cout << adder(Point(3, 4), Point(7, 9)) << endl;

	return 0;
}

/*
실행결과

4
5.2
[10, 13]

*/

 

위 예제에서 정의한 Adder 클래스와 같이 함수처럼 동작하는 클래스를 가리켜 '펑터'라고 합니다. 그리고 '함수 오브젝트'라고도 합니다.

 

펑터가 어떠한 경우에 유용하게 사용되는지 알아보겠습니다. 펑터는 함수 또는 객체의 동작 방식에 유연함을 제공할 때 주로 사용됩니다. (Adder 객체만 해도, 같은 함수를 호출하면서 int형이나 double 형, 심지어는 Point 객체의 덧셈까지 유연하게 적용할 수 있었습니다.) 

 

펑터가 어떻게 사용될 수 있는지 예제를 통해 보겠습니다. 이 예제에는 '버블 정렬'이라는 정렬 알고리즘이 사용되었습니다. 이 알고리즘을 몰라도 예제를 통해 펑터를 이해하는 데에는 지장이 없습니다. 예제가 긴 관계로 예제를 보기 전에 먼저 펑터로 정의된 세 클래스를 보겠습니다.

class SortRule
{
public:
	virtual bool operator()(int num1, int num2) const = 0;
};

 

위 클래스는 추상 클래스로 정의되었습니다. 그리고 operator() 함수도 순수 가상 함수로 정의되어 있습니다. 이는 이 함수의 기능을 유도 클래스에서 확정 짓겠다는 의미입니다.

 

이후 이 클래스를 상속받는 두 개의 유도 클래스를 보겠습니다.

class AscendingSort : public SortRule    //오름차순
{
public:
	bool operator()(int num1, int num2) const
	{
		return (num1 > num2) ? true : false;
	}
};
class DescendingSort : public SortRule    //내림차순
{
public:
	bool operator()(int num1, int num2) const
	{
		return (num1 < num2) ? true : false;
	}
};

 

위 두 클래스를 이해하는데 어려움은 없을 것입니다. 이제 위에 정의된 펑터가 사용된 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class SortRule
{
public:
	virtual bool operator()(int num1, int num2) const = 0;
};

class AscendingSort : public SortRule    //오름차순
{
public:
	bool operator()(int num1, int num2) const
	{
		return (num1 > num2) ? true : false;
	}
};

class DescendingSort : public SortRule    //내림차순
{
public:
	bool operator()(int num1, int num2) const
	{
		return (num1 < num2) ? true : false;
	}
};

class DataStorage    //int 형 정수 데이터 저장소
{
private:
	int* arr;
	int idx;
	const int MAX_LEN;
public:
	DataStorage(int arrlen) : idx(0), MAX_LEN(arrlen)
	{
		arr = new int[MAX_LEN];
	}
	void AddData(int num)
	{
		if (MAX_LEN <= idx)
		{
			cout << "더 이상 저장이 불가능합니다." << endl;
			return;
		}
		arr[idx++] = num;
	}
	void ShowAllData()
	{
		for (int i = 0; i < idx; i++)
		{
			cout << arr[i] << ' ';
		}
		cout << endl;
	}
	void SortData(const SortRule& functor)    //버블정렬 알고리즘
	{
		for (int i = 0; i < (idx - 1); i++)
		{
			for (int j = 0; j < (idx - 1); j++)
			{
				if (functor(arr[j], arr[j + 1]))
				{
					int temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}
	}
};
int main(void)
{
	DataStorage storage(6);

	storage.AddData(40);
	storage.AddData(25);
	storage.AddData(12);
	storage.AddData(8);
	storage.AddData(25);
	storage.AddData(38);

	storage.SortData(AscendingSort());
	storage.ShowAllData();

	storage.SortData(DescendingSort());
	storage.ShowAllData();

	return 0;
}

/*
실행결과

8 12 25 25 38 40
40 38 25 25 12 8

*/

 

storage 객체의 멤버 함수 SortData는 매개 변수형이 SortRule의 참조형이므로, SortRule을 직접 및 간접적으로 상속받는 모든 클래스의 객체를 인자로 전달할 수 있습니다. 따라서 SortData 함수에 임시 객체를 생성하여 인자로 전달해 주는 모습입니다.

 

형 변환 연산자의 오버 로딩

이제 마지막으로 형 변환 연산자의 오버 로딩에 대해서 배워 보겠습니다. 다음과 같은 객체와 정수 간의 대입 연산이 왜 가능한지

int main(void)
{
	Number num;
	num = 30;
	num.ShowNumber();
	reutrn 0;
}

 

다음의 예제를 통해서 확인해보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Number
{
private:
	int num;
public:
	Number(int n = 0) : num(n)
	{
		cout << "Number constructor!" << endl;
	}
	Number& operator=(const Number& ref)
	{
		cout << "Number.operator=()" << endl;
		num = ref.num;
		return *this;
	}
	void ShowNumber() { cout << num << endl; }
};

int main(void)
{
	Number num;
	cout << "대입연산 전..." << endl;
	num = 30;
	cout << "대입연산 후..." << endl;
	num.ShowNumber();

	return 0;
}

/*
실행결과

Number constructor!
대입연산 전...
Number constructor!
Number.operator=()
대입연산 후...
30

*/

 

위 예제를 보면 30을 객체인 num에 단순 대입만 시켰을 뿐인데 객체의 생성자가 호출되었음을 확인할 수 있습니다. 여기서 생성자가 호출된 이유는 대입 연산의 과정이 다음과 같기 때문입니다.

num = Number(30);

 

우선 30을 인자로 주면서 임시 객체를 생성합니다. 따라서 이때 생성자가 한 번 호출됩니다.

num.operator=(Number(30));

 

이후 임시 객체와 객체 num 간 대입 연산을 수행합니다. 이때 오버 로딩된 대입 연산자가 호출됩니다.

 

여기서 핵심은 임시 객체의 생성입니다. 그리고 이러한 임시 객체의 생성을 통해서 대입 연산이 진행되는 데에는 다음과 같은 문법적 기준이 존재합니다.

  • A형 객체가 와야 할 위치에 B형 데이터(또는 객체)가 왔을 경우, B형 데이터(또는 객체)를 인자로 전달받는 A형 클래스의 생성자 호출을 통해서 A형 임시 객체를 생성한다.

때문에 위의 예제에서는 'Number형 객체가 와야 할 위치에 int형 데이터가 와서, int형 데이터를 인자로 전달받는 Number 클래스의 생성자 호출을 통해서 임시 객체를 생성한 것입니다.

 

이렇듯, 기본 자료형 데이터를 객체로 형 변환하는 것은 적절한 생성자의 정의를 통해서 얼마든지 가능합니다. 물론 반대로 객체를 기본 자료형 데이터로 형 변환하는 것도 가능합니다. 다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

class Number
{
private:
	int num;
public:
	Number(int n = 0) : num(n)
	{
		cout << "Number constructor!" << endl;
	}
	Number& operator=(const Number& ref)
	{
		cout << "Number.operator=()" << endl;
		num = ref.num;
		return *this;
	}
	operator int()    //형 변환 연산자의 오버로딩
	{
		cout << "Number.operator int()" << endl;
		return num;
	}
	void ShowNumber() { cout << num << endl; }
};

int main(void)
{
	Number num1(30);
	cout << "대입연산 전..." << endl;
	Number num2 = num1 + 20;
	cout << "대입연산 후..." << endl;
	num2.ShowNumber();

	return 0;
}

/*
실행결과

Number constructor!
대입연산 전...
Number.operator int()
Number constructor!
대입연산 후...
50

*/

 

위 예제의 Number 클래스를 보면 operator int 함수가 새로 정의되었습니다. 이 함수는 반환형이 없지만 반환하는 값은 존재합니다. operator 뒤에 int 말고 다른 자료형이 올 수 있습니다. int형 이 operator 뒤에 오면 해당 클래스의 객체를 int형으로 형 변환해야 할 때 이 함수를 호출합니다.

 

이어서 다음 문장을 보겠습니다.

Number num2 = num1 + 20;

 

위 문장이 제대로 수행되려면 숫자 20을 인자로 받는 생성자를 호출하면서 다음과 같이 임시 객체를 생성해야 합니다.

Number num2 = num1 + Number(20);

 

하지만 다른 방법도 있습니다. num1 객체를 int형으로 형 변환하는 것입니다. 이를 위해 operator int 함수를 오버 로딩하였습니다. 이 함수가 호출되면 멤버인 num을 반환하므로 다음과 같은 문장이 됩니다.

Number num2 = 30 + 20;

 

그러면 30 + 20이 연산되어 다음과 같은 문장이 되고,

Number num2 = 50;

 

50을 인자로 받는 생성자를 호출하면서 임시 객체를 생성합니다. 

Number num2 = Number(50);

 

그런데 여기서 실행결과를 보면 operator= 함수는 호출되지 않았습니다. 왜냐하면 위 문장은 대입 연산자를 호출하는 문장이 아닌, 복사 생성자를 호출하는 문장이기 때문입니다. 복사 생성자와 대입 연산자는 매우 유사하지만 호출되는 시점이 다르다는 것을 앞서 배웠습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
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 31
글 보관함