티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

C++에서는 함수뿐만 아니라 연산자도 오버 로딩이 가능합니다. 그런데 연산자의 오버 로딩은 좀 생소하게 느껴질 수 있습니다. 하지만 기본 개념은 매우 단순하니 어렵지 않을 것입니다.

 

함수가 오버 로딩되면, 오버 로딩된 수만큼 다양한 기능을 제공하게 됩니다. 즉, 이름은 하나지만 기능은 여러 가지가 되는 셈입니다. 마찬가지로 연산자의 오버 로딩을 통해서 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있습니다.

 

다음 예제를 살펴보겠습니다.

#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point operator+(const Point& ref)
	{
		Point pos(xpos + ref.xpos, ypos + ref.ypos);
		return pos;
	}
};

int main(void)
{
	Point pos1(3, 4);
	Point pos2(10, 20);
	Point pos3 = pos1.operator+(pos2);

	pos1.ShowPosition();
	pos2.ShowPosition();
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[3, 4]
[10, 20]
[13, 24]

*/

 

x축, y축 좌표 공간 상에 점의 위치를 표현할 수 있는 Point 클래스를 정의했습니다. 해당 클래스는 멤버 변수로서 x값과 y값을 가지며, 현재 좌표를 출력하는 ShowPosition 멤버 함수를 가지고 있습니다. 그리고 처음 보는 operator+라고 하는 함수가 보입니다. 나중에 자세히 설명하겠지만 이를 +연산자 오버 로딩이라고 합니다. 이것이 무슨 의미를 가지는지 알아보겠습니다.

 

먼저 다음 코드를 보겠습니다.

Point pos1(3, 4);
Point pos2(10, 20);
Point pos3 = pos1 + pos2;

 

위 코드를 보면 Point객체 pos3를 선언하는데 pos1 + pos2를 대입하고 있습니다. 기본 자료형 변수를 덧셈하는 연산은 많이 봐왔지만 객체를 이렇게 덧셈 연산하는 것은 처음 볼 것입니다. 해당 코드 줄의 실행 결과를 예상해보면 pos3의 멤버 변수는 (13, 24)을 가질 것으로 생각이 됩니다. 정말로 그러할지 다음 예제를 통해 확인해 보겠습니다.

#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point operator+(const Point& ref)
	{
		Point pos(xpos + ref.xpos, ypos + ref.ypos);
		return pos;
	}
};

int main(void)
{
	Point pos1(3, 4);
	Point pos2(10, 20);
	Point pos3 = pos1 + pos2;

	pos1.ShowPosition();
	pos2.ShowPosition();
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[3, 4]
[10, 20]
[13, 24]

*/

 

위 예제를 보면 정말로 예상한 것과 같은 연산이 수행됨을 확인할 수 있습니다. 그런데 다음과 같이 함수 operator+를 주석 처리하면 컴파일 에러가 발생합니다.

#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	/*
	Point operator+(const Point& ref)
	{
		Point pos(xpos + ref.xpos, ypos + ref.ypos);
		return pos;
	}
	*/
};

int main(void)
{
	Point pos1(3, 4);
	Point pos2(10, 20);
	Point pos3 = pos1 + pos2;    //컴파일 에러 발생!

	pos1.ShowPosition();
	pos2.ShowPosition();
	pos3.ShowPosition();

	return 0;
}

 

이로써 pos1 + pos2와 operator+ 함수 사이에 어떠한 연관이 있다는 것을 유추할 수 있습니다.

 

연산자 오버 로딩의 문법적 이해

다음 두 코드를 비교해 보겠습니다.

pos3 = pos1.operator+(pos2);
pos3 = pos1 + pos2;    //pos1.oeratro+(pos2)를 호출하는 다른 방법

 

사실 위 코드에서 pos1 + pos2는 pos1.operator+(pos2)를 호출하는 또 다른 표현 방법입니다. 그리고 이것이 가능하게 된 이유는 앞서 operator+ 함수를 정의했기 때문입니다.

 

C++에서는 다음과 같은 약속을 했습니다.

  • C++에서는 기본 자료형과 마찬가지로 객체 역시 덧셈, 뺄셈, 곱셈, 나눗셈 등의 연산을 가능하게 하고 싶습니다.
  • 그래서 다음과 같은 방법을 생각해냈습니다.
  • 'operator'키워드와 '연산자'를 묶어서 함수의 이름을 정의하면, 함수의 이름을 이용한 함수의 호출뿐만 아니라, 연산자를 이용한 함수의 호출도 가능하게 하는 방법입니다.
  • 예를 들어서 'pos1 + pos2'라는 문장이 있습니다.
  • pos1과 pos2가 기본 자료형이라면 덧셈 연산을 수행하겠지만, 객체라면 이 문장을 'pos1.operator+(pos)'라는 문장으로 바꿔서 해석합니다.

 

이해가 됐는지 모르겠습니다. 쉬운 개념인데 설명하기가 조금 까다롭습니다. 만약 제대로 이해가 됐다면 다음의 문장이 어떻게 바뀌어 해석되는지도 쉽게 예상할 수 있을 겁니다.

pos1 - pos2;
pos1 * pos2;
pos1 / pos2;

 

위 세 문장은 다음과 같이 바뀌어 해석됩니다.

pos1.operator-(pos2);
pos1.operator*(pos2);
pos1.operator/(pos2);

 

물론 위와 같은 operator-, operator*, operator/ 함수가 정의되어 있어야 합니다.

 

전역 함수로서의 연산자 오버 로딩

연산자를 오버 로딩하는 방법, 즉, operator+와 같은 함수를 선언하는 방법은 두 가지가 있습니다. 앞선 예에서 사용한 방법은 멤버 함수로서 선언하는 방법이었습니다. 다른 방법으로는 전역 함수로서 선언하는 방법이 있습니다.

 

다음 예제를 보면서 전역 함수로서 연산자를 오버 로딩하는 방법을 살펴보겠습니다.

#include <iostream>
using namespace std;

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const;
	friend Point operator+(const Point& pos1, const Point& pos2);
};

void Point::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

Point operator+(const Point& pos1, const Point& pos2)
{
	Point pos(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);
	return pos;
}

int main(void)
{
	Point pos1(3, 4);
	Point pos2(10, 20);
	Point pos3 = pos1 + pos2;

	pos1.ShowPosition();
	pos2.ShowPosition();
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[3, 4]
[10, 20]
[13, 24]

*/

 

위 예제에서 operator+함수는 전역 함수로서 선언되었습니다. 다만 해당 함수가 Point 클래스에 friend 선언되어 있어 멤버 함수로서 선언된 것으로 착각하기 쉽습니다. 하지만 operator+ 함수는 전역 함수로서 정의된 것이 맞습니다. 만약 해당 함수가 Point 클래스의 멤버 함수로서 선언이 되었다면 ShowPosition 함수처럼 함수명 앞에 Point:: 가 삽입되어 있어야 합니다.

 

전역 함수로 선언된 operator+ 함수가 Point 클래스에 friend 선언이 된 이유는 Point 클래스의 xpos, ypos와 같은 private 멤버에 접근할 수 있게 하기 위함입니다.

 

이처럼 연산자 오버 로딩이 전역 함수로서 선언되면, pos1 + pos2 문장을 다음과 같이 해석하게 됩니다.

operator+(pos1, pos2)

 

만약 똑같은 연산자 오버 로딩이 클래스의 멤버 함수로서, 그리고 전역 함수로서 두 번 선언이 되면, 컴파일러는 멤버 함수로서 선언이 된 것을 호출하게 됩니다. 다만, 컴파일러에 따라서 에러를 발생할 수도 있으니 이런 경우가 생기지 않도록 하는 것이 중요합니다.

 

그리고 연산자의 오버 로딩은 전역 함수로서 하기보다, 멤버 함수로서 선언하는 것이 좋습니다. 왜냐하면 객체지향에서는 '전역'에 대한 개념이 존재하지 않기 때문입니다. C++은 C 스타일의 코드 구현도 가능한 언어이기 때문에 예외적으로 '전역'에 대한 개념이 존재할 뿐입니다.

 

오버 로딩이 불가능한 연산자들

C++의 모든 연산자들을 오버 로딩할 수는 없습니다. 다음과 같이 오버 로딩이 불가능한 연산자들도 있습니다.

  • .                       멤버 접근 연산자
  • .*                      멤버 포인터 연산자
  • ::                       범위 지정 연산자
  • ? :                     조건 연산자(3항 연산자)
  • sizeof                 바이트 단위 크기 계산
  • typeid                RTTI 관련 연산자
  • static_cast           형 변환 연산자
  • dynamic_cast       형 변환 연산자
  • const_cast           형 변환 연산자
  • reinterpret_cast    형 변환 연산자

 

뭔가 많이 나열되어 있어서 다 기억해둬야 할 것만 같습니다. 하지만 그럴 필요는 없습니다. 어차피 위의 연산자들을 오버 로딩해야만 하는 상황이 딱히 존재하지 않기 때문입니다. 즉, 위 연산자들을 오버 로딩하게 될 일은 없다고 보면 됩니다.

 

위 연산자들의 오버 로딩이 불가능한 이유는 C++의 문법 규칙을 보존하기 위함입니다. 만약 위 연산자들까지 오버 로딩이 가능해지면, C++의 문법 규칙에 어긋나는 문장의 구성이 가능해지고, C++은 보다 혼란스러운 언어가 될 것입니다.

 

멤버 함수로서만 오버 로딩이 가능한 연산자

이어서 멤버 함수 기반으로만 오버 로딩이 가능한 연산자를 소개하겠습니다.

  • =      대입 연산자
  • ( )     함수 호출 연산자
  • [ ]     배열 접근 연산자(인덱스 연산자)
  • ->    멤버 접근을 위한 포인터 연산자

이들은 객체를 대상으로 진행해야 의미가 통하는 연산자들이기 때문에, 멤버 함수 기반으로만 연산자의 오버 로딩을 허용합니다.

 

이런 까다로운 조건을 기억해야 할 것만 같습니다. 하지만 그럴 필요 없습니다. 앞서 얘기했듯이 연산자의 오버 로딩은 멤버 함수를 기반으로 하는 것을 습관을 들이면 됩니다. 

 

단항 연산자 오버 로딩

대표적인 단항 연산자로는 다음 두 가지가 있습니다.

  • ++ (1 증가 연산자)
  • --  (1 감소 연산자)

 

다음 예제를 보면서 ++연산자와 --연산자를 오버 로딩하는 방법을 알아보겠습니다.

//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) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point& operator++()
	{
		xpos += 1;
		ypos += 1;
		return *this;
	}
	friend Point& operator--(Point& ref);
};

Point& operator--(Point& ref)
{
	ref.xpos -= 1;
	ref.ypos -= 1;
	return ref;
}

int main(void)
{
	Point pos(1, 2);

	++pos;
	pos.ShowPosition();
	--pos;
	pos.ShowPosition();

	++(++pos);
	pos.ShowPosition();
	--(--pos);
	pos.ShowPosition();

	return 0;
}

/*
실행결과

[2, 3]
[1, 2]
[3, 4]
[1, 2]

*/

 

위 예제를 보면 ++연산자는 멤버 함수 기반으로 오버 로딩했고, --연산자는 전역 함수 기반으로 오버 로딩했습니다. 코드를 이해하기에 어려운 부분은 없습니다.

 

그런데 ++연산자와 --연산자는 피연산자의 위치에 따라서 그 의미가 달라진다는 것을 알고 있을 것입니다.

++pos    //전위증가
pos++    //후위증가

 

앞선 예제에서 오버 로딩한 ++연산자와 --연산자는 각각 전위 증가, 전위 감소에 해당합니다. 후위 증가와 후위 감소에 해당하는 연산자 오버 로딩을 하기 위해서는 다음과 같이 매개 변수 자리에 int를 삽입합니다.

pos.operator++()       //++pos
pos.operator++(int)    //pos++

 

삽입한 int는 매개 변수로 int형 변수를 받겠다는 의미가 전혀 아닙니다. 후위 증가의 의미를 가지도록 삽입한 형식 상의 문구에 불과합니다. 

 

그런데 int만 삽입했다고 해서 후위 증가 연산자로 오버 로딩된 것은 아닙니다. 그에 맞게 함수의 내용도 수정해 주어야 합니다. 다음 예제를 보겠습니다.

//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) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point& operator++()
	{
		xpos += 1;
		ypos += 1;
		return *this;
	}
	const Point operator++(int)
	{
		const Point pos(xpos, ypos);
		xpos++;
		ypos++;
		return pos;
	}
	friend Point& operator--(Point& ref);
	friend const Point operator--(Point& ref, int);
};

Point& operator--(Point& ref)
{
	ref.xpos -= 1;
	ref.ypos -= 1;
	return ref;
}

const Point operator--(Point& ref, int)
{
	const Point pos(ref.xpos, ref.ypos);
	ref.xpos--;
	ref.ypos--;
	return pos;
}


int main(void)
{
	Point pos(3, 5);
	Point cpy;
	cpy = pos--;
	cpy.ShowPosition();
	pos.ShowPosition();

	cpy = pos++;
	cpy.ShowPosition();
	pos.ShowPosition();

	return 0;
}

/*
실행결과

[3, 5]
[2, 4]
[2, 4]
[3, 5]

*/

 

위 예제에서 후위 증가 ++연산자는 멤버 함수를 기반으로, 후위 감소 --연산자는 전역 함수를 기반으로 선언했습니다. 후위 증감 연산자의 오버 로딩의 특징은 cons Point를 반환한다는 것입니다. 1씩 증가 혹은 감소하기 전에 멤버 변수의 값을 반환해야 하기 때문에 연산 이후 자기 자신을 참조형으로 반환할 수는 없습니다. 그러면 연산 전에 함수 내에서 Point 객체를 생성하고 이를 반환해야 하는데, 함수 내 지역변수를 참조형으로 반환할 수도 없으므로 const Point 형으로 반환하게 된 것입니다.

 

반환형에 const가 붙은 이유는 이 함수의 호출로 인해서 임시 Point객체를 생성하게 되는데 이때 생성되는 임시 객체의 데이터 변경을 허용하지 않겠다는 의미입니다. 이는 다음과 같은 연산을 불가능하게 합니다.

(pos++)++;

 

얼핏 보면 pos객체의 멤버들을 총 2씩 증가시키는 것으로 보입니다. 하지만 위 코드는 컴파일 에러를 발생시킵니다. 소괄호 안의 pos++연산을 수행하면 반환되는 값은 const로 선언된 임시 객체입니다. 즉, pos와는 전혀 상관이 없는 객체가 반환됩니다(멤버는 모두 같지만). 그런데 그다음 const 선언된 객체의 데이터를 변경하려고 하니 컴파일 에러가 발생하게 되는 것입니다.

 

'그러면 const 선언을 하지 않으면 되는 것 아니냐'하고 생각할 수 있습니다. 만약 const 선언을 하지 않으면 컴파일 에러는 사라집니다. 하지만 pos의 멤버들은 2씩 값이 증가하지 않고 1씩 증가한 채 있습니다. 프로그래머가 위 코드를 작성한 의도는 2씩 증가하도록 하기 위함이었을 것입니다. 하지만 의도대로 되지 않고 1씩 증가에 그쳤으므로 이는 분명한 오류이자 프로그래머의 실수입니다.

 

그럼 왜 1씩 더 증가하지 않았을까요? 바로 처음 소괄호 안의 pos++연산을 통해 반환되는 것이 pos객체가 아닌 그와 완전히 동일한 멤버를 가졌을 뿐인 전혀 다른 임시 객체이기 때문입니다. 따라서 그다음 ++연산은 pos가 아닌 이 임시 객체의 멤버들을 1씩 증가시키게 됩니다.

 

따라서 프로그래머의 실수를 방지하기 위해서라도 const Point로 반환하는 것은 꼭 필요합니다.

 

교환 법칙 문제의 해결

교환 법칙이란 ' a + b = b + a'입니다. 즉 연산자를 중심으로 피연산자의 위치가 서로 바뀌어도 연산 결과는 같다는 법칙입니다. 대표적으로 교환 법칙이 성립되는 연산은 덧셈과 곱셈이 있습니다.

 

다음 예제는 자료형이 다른 두 데이터 간의 곱셈 연산이 가능하도록 *연산자를 오버 로딩하고 있습니다.

//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) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point operator*(int time)
	{
		Point pos(xpos * time, ypos * time);
		return pos;
	}
};

int main(void)
{
	Point pos(3, 5);
	Point cpy;

	cpy = pos * 3;
	cpy.ShowPosition();

	cpy = pos * 3 * 2;
	cpy.ShowPosition();

	return 0;
}

/*
실행결과

[9, 15]
[18, 30]

*/

 

*연산자 오버 로딩에 대해서는 이해하기 어렵지 않습니다. 그런데 곱셈 연산은 앞서 말했다시피 교환 법칙이 성립해야 합니다. 따라서 다음과 같은 코드도 같은 결과를 내야 합니다.

pos * 3;
3 * pos;    //컴파일 에러 발생

 

그런데 위 예제에서 오버 로딩된 *연산자는 이러한 교환 법칙이 성립하지 않습니다.

 

3 * pos는 컴파일러가 3.operator*(pos)로 해석해야 합니다. 그런데 이러한 해석은 불가능합니다. 왜냐하면 operator* 함수의 매개 변수는 int형으로 선언되어 있어 객체인 pos는 인자로 받을 수 없으며, 애초에 3은 Point객체가 아니기 때문에 operator* 함수에 접근하는 것조차 불가능합니다. 

 

멤버 함수 기반의 오버 로딩으로는 이 문제를 해결할 수 없습니다. 따라서 이 문제를 해결하기 위해서는 전역 함수 기반의 오버 로딩이 필요합니다. 다음 예제를 보겠습니다.

//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) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	Point operator*(int time)
	{
		Point pos(xpos * time, ypos * time);
		return pos;
	}
	friend Point operator*(int, Point&);
};

Point operator*(int time, Point& ref)
{
	return ref * time;
}


int main(void)
{
	Point pos(3, 5);
	Point cpy;

	cpy = pos * 3;
	cpy.ShowPosition();

	cpy = 6 * pos;
	cpy.ShowPosition();

	return 0;
}

/*
실행결과

[9, 15]
[18, 30]

*/

 

위 예제에서는 전역 함수를 기반으로 operator* 함수를 오버 로딩하고 있습니다. 대신 매개 변수의 위치를 보면 int형이 앞에, Point&형이 뒤에 위치해 있습니다. 따라서 main 함수에서 '6 * pos' 문장은 전역 함수로 오버 로딩된 operator* 함수를 호출하게 되고, 이 함수는 'ref * time'을 반환함으로써 두 인자의 위치를 바꾸고, 멤버 함수를 기반으로 오버 로딩된 operator* 함수를 호출하는 효과를 냅니다.

 

cout, cin 그리고 endl 흉내 내기

앞서 연산자 오버 로딩에 대해 배웠습니다. 이에 제대로 이해했다면 C++의 콘솔 입출력에 사용되는 cout, cin, endl의 개념도 쉽게 이해할 수 있을 것입니다.

 

다음 예제를 보겠습니다. 예제는 cout과 endl에 대한 이해를 돕기 위해 cout과 endl을 흉내 내고 있습니다.

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

namespace mystd
{
	class ostream
	{
	public:
		ostream& operator<< (const char* str)
		{
			printf("%s", str);
			return *this;
		}
		ostream& operator<< (const char word)
		{
			printf("%c", word);
			return *this;
		}
		ostream& operator<< (int num)
		{
			printf("%d", num);
			return *this;
		}
		ostream& operator<< (double e)
		{
			printf("%lf", e);
			return *this;
		}
		ostream& operator<< (ostream& (*func)(ostream&))
		{
			func(*this);
			return *this;
		}
	};

	ostream& myendl(ostream& ref)
	{
		ref << '\n';
		fflush(stdout);
		return ref;
	}

	ostream mycout;
}

int main(void)
{
	using mystd::mycout;
	using mystd::myendl;

	mycout << "Simple string" << myendl;
	mycout << 3.14;
	myendl(mycout);
	mycout << 123 << myendl;

	return 0;
}

/*
실행결과

Simple string
3.140000
123

*/

 

예제를 따로 설명하지 않아도 이해를 할 수 있을 것입니다. 이 예제에서 콘솔 입출력을 위해 std 네임스페이스를 사용하지도, cout이나 endl도 사용하지 않았습니다. 다만 이들을 흉내 내는 mystd 네임스페이스와 mycout, myendl을 사용했습니다.

 

클래스 ostream을 선언하고, 이 클래스를 바탕으로 객체 mycout을 선언했습니다. 해당 클래스에는 <<연산자가 여러 가지 경우에 대해 오버 로딩되어 있습니다.

 

myendl는 함수입니다. 이 함수에 대한 <<연산자 오버 로딩도 역시 되어 있습니다. myendl이 함수이기 때문에 main 함수에 보면 myendl(mycout); 을 통해 myendl 함수를 직접 호출할 수도 있었습니다.

 

실제로 cout과 cin, endl은 위와 같이 동작합니다. endl도 함수이며 위와 같은 방식으로 endl함수를 직접 호출할 수도 있습니다. 아래는 그 예를 보인 것입니다.

#include <iostream>
using namespace std;

int main(void)
{
	
	cout << "KOEY is";
	endl(cout);
	cout << "Free" << endl;

	return 0;
}

/*
실행결과

KOEY is
Free

*/

 

이번에는 Point 클래스를 대상으로 <<연산자를 오버 로딩해 보고자 합니다. <<연산자를 오버 로딩하지 않으면 다음과 같은 코드는 당연하게도 에러를 발생시킵니다.

Point pos(11, 16);
cout << pos << endl;    //에러 발생

 

위 코드에서 'cout << pos'라는 문장으로 pos의 멤버들의 정보를 출력하려면, 컴파일러에서 'cout.operator<<(pos)'로 바꿔서 해석할 수 있어야 합니다. 하지만 pos는 프로그래머가 직접 정의한 Point 객체이며, 따라서 이 객체를 매개 변수로 받을 수 있는 operator<< 함수가 표준 라이브러리에 정의되어 있을 리가 없습니다.

 

따라서 다음과 같이 Point 객체를 매개변수로 받을 수 있는 <<연산자 오버 로딩을 직접 해주어야 합니다.

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

class Point
{
private:
	int x, y;
public:
	Point(const int x = 0, const int y = 0) : x(x), y(y) {}
	void ShowPosition()
	{
		cout << '[' << x << ", " << y << ']' << endl;
	}
	friend ostream& operator<<(ostream&, Point&);
};

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

int main(void)
{
	Point pos(11, 16);
	cout << pos << endl;

	return 0;
}

/*
실행결과

[11, 16]

*/

 

그런데 <<연산자나 >>연산자는 오버 로딩하는 방법이 조금 까다롭습니다. 앞서 연산자 오버 로딩은 멤버 함수를 통한 오버 로딩과 전역 함수를 통한 오버 로딩, 두 가지 방법이 있다고 배웠습니다. 그런데 <<, >>연산자는 멤버 함수를 통한 오버 로딩이 불가능합니다.

 

만약 멤버 함수를 통해 오버 로딩을 한다면, 'cout << pos' 문장은 'cout.operator<<(pos)'로 해석되어야 합니다. 그러려면 cout객체의 클래스인 ostream 클래스 안에 오버 로딩을 해야 하는데, ostream 클래스는 표준 라이브러리에 정의되어 있으므로 이러한 오버 로딩은 불가능합니다.

 

그러면 남은 방법은 전역 함수를 통한 오버 로딩입니다. 위 예제가 전역 함수를 통한 오버 로딩을 한 모습입니다. 해당 함수는 ostream 객체와 Point 객체를 매개 변수로 받습니다. 그런데 해당 함수에서는 ostream 객체의 멤버 변수에 접근할 일이 없기 때문에(어쩌면 접근할 멤버 변수가 하나도 없을지도 모릅니다) ostream에 friend 선언할 필요가 없습니다. 하지만 Point 객체의 멤버 변수에는 접근하기 때문에 Point 클래스에 friend선언을 해주었습니다.

 

반드시 해야 하는 대입 연산자 오버 로딩

대입 연산자의 오버 로딩은 반드시 공부해야 합니다. 왜냐하면 대입 연산자의 오버 로딩은 클래스 정의에 있어서 생성자, 복사 생성자와 함께 빠질 수 없는 요소이기 때문입니다.

 

대입 연산자의 오버 로딩은 그 성격이 복사 생성자와 매우 유사합니다. 따라서 복사 생성자에 대한 이해를 바탕으로 대입 연산자를 이해하면 수월합니다.

 

다음은 복사 생성자의 대표적인 특성입니다.

  • 정의하지 않으면 '디폴트 복사 생성자'가 삽입된다.
  • '디폴트 복사 생성자'는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.
  • 생성자 내에서 동적 할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야 한다.

 

그리고 다음은 대입 연산자의 대표적인 특성입니다.

  • 정의하지 않으면 '디폴트 대입 연산자'가 삽입된다.
  • '디폴트 대입 연산자'는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.
  • 연산자 내에서 동적 할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야 한다.

 

특성을 보면 복사 생성자와 대입 연산자는 정말 유사합니다. 하지만 호출되는 시점에는 차이가 있습니다. 다음은 복사 생성자가 호출되는 대표적인 상황입니다.

int main(void)
{
	Point pos1(5, 7);
	Point pos2 = pos1;
	......
)

 

그리고 다음은 대입 연산자가 호출되는 대표적인 상황입니다.

int main(void)
{
	Point pos1(5, 7);
	Point pos2(9, 10);
	pos2 = pos1;
	......
)

 

두 상황의 차이점은 객체의 생성자가 호출되는 시점에 대입 연산자가 사용되느냐에 있습니다.

 

지금까지 언급한 대입 연산자의 특성을 확인하기 위해 다음 예제를 보겠습니다.

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

class First
{
private:
	int num1, num2;
public:
	First(int n1 = 0, int n2 = 0) : num1(n1), num2(n2) {}
	void ShowData() { cout << num1 << ", " << num2 << endl; }
};

class Second
{
private:
	int num3, num4;
public:
	Second(int n3 = 0, int n4 = 0) : num3(n3), num4(n4) {}
	void ShowData() { cout << num3 << ", " << num4 << endl; }
	Second& operator=(const Second& ref)
	{
		cout << "Second& operator=()" << endl;
		num3 = ref.num3;
		num4 = ref.num4;

		return *this;
	}
};

int main(void)
{
	First fsrc(111, 222);
	First fcpy;
	Second ssrc(333, 444);
	Second scpy;

	fcpy = fsrc;
	scpy = ssrc;
	fcpy.ShowData();
	scpy.ShowData();

	First fob1, fob2;
	Second sob1, sob2;

	fob1 = fob2 = fsrc;
	sob1 = sob2 = ssrc;
	
	fob1.ShowData();
	fob2.ShowData();
	sob1.ShowData();
	sob2.ShowData();

	return 0;
}

/*
실행결과

Second& operator=()
111, 222
333, 444
Second& operator=()
Second& operator=()
111, 222
111, 222
333, 444
333, 444

*/

 

위 예제에서 First 클래스에는 대입 연산자를 정의하지 않았고, Second 클래스에만 대입 연산자를 정의했습니다. 그런데 대입 연산자를 정의하지 않은 First 클래스도 Second 클래스와 똑같이 대입 연산이 일어나고 있음을 확인할 수 있습니다. 대입 연산자를 정의하지 않으면 디폴트 대입 연산자가 삽입되기 때문에 가능한 일입니다.

 

그리고 다음과 같이 대입 연산을 여러 번 수행할 경우,

fob1 = fob2 = fsrc;
sob1 = sob2 = ssrc;

가장 오른쪽에서부터 대입 연산을 수행합니다.

 

디폴트 대입 연산자는 디폴트 복사 생성자와 같은 문제점을 가지고 있습니다. 바로 얕은 복사를 수행한다는 것입니다. 다음 예제를 보겠습니다.

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

class Person
{
private:
	char* name;
	int age;
public:
	Person(const char* name, const int age) : age(age)
	{
		name = new char[strlen(name) + 1];
		strcpy(this->name, name);
	}
	void ShowPersonInfo() const
	{
		cout << "이름 : " << name << endl;
		cout << "나이 : " << age << endl;
	}
	~Person()
	{
		delete[] name;
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("KOEY", 29);
	Person man2("Lee", 22);
	man2 = man1;

	man1.ShowPersonInfo();
	man2.ShowPersonInfo();

	return 0;
}

/*
실행결과

이름 : KOEY
나이 : 29
이름 : KOEY
나이 : 29
called destructor!

*/

 

위 예제는 컴파일은 정상적으로 되지만 프로그래밍 실행 단계에서 에러를 발생시킵니다. 컴파일러에 따라서 에러를 발생시키지 않을 수도 있고, 프로그램 실행을 아예 하지 않는 경우도 있습니다.

 

위 예제가 에러를 일으키는 이유는 디폴트 복사 생성자의 문제를 지적했던 것과 같습니다. man2에 man1을 대입하는 과정에서 얕은 복사가 일어나고 man2의 name에는 man1의 name에 저장된 것과 같은 주소가 저장됩니다. 따라서 man1의 name과 man2의 name은 같은 문자열을 가리키게 되므로, 나중에 소멸자가 호출될 때 같은 메모리 공간을 두 번 소멸하는 연산이 수행되어 에러를 일으키게 됩니다.

 

이를 해결하는 방법은 복사 생성자와 마찬가지로 대입 연산자를 깊은 복사가 일어나도록 직접 정의해 주는 것입니다. 그런데 대입 연산자의 경우 복사 생성자와 달리 딱 하나 다른 것이 있습니다. 아래의 코드는 깊은 복사가 일어나도록 대입 연산자를 직접 정의해준 예입니다.

Person& operator=(const Person& ref)
{
	delete[] name;    //딱 하나 다른 것
	name = new char[strlen(ref.name) + 1];
	strcpy(this->name, ref.name);
	age = ref.age;

	return *this;
}

 

깊은 복사가 일어나도록 대입 연산자를 정의할 때는 가장 먼저 delete 연산을 통해서 동적 할당된 name을 소멸 처리합니다. 이 과정을 거치지 않고 새로 동적 할당한 메모리 공간의 주소를 name에 저장하게 되면, 다시는 기존의 name이 가리키고 있던 동적 할당된 공간에 접근할 수 없게 되며, 이는 메모리 공간의 누수로 이어지게 됩니다.

 

상속 관계에서 유도 클래스의 생성자는 기초 클래스의 생성자를 호출하기 위해 별도의 명시를 하고 있습니다. 다음의 예를 보겠습니다.

class First
{
private:
	int num1, num2;
public:
	First(int n1, int n2) : num1(n1), num2(n2) {}
}

class Second : public First
{
private:
	int num3, num4;
public:
	First(int n1, int n2, int n3, int n4)
		: First(n1, n2), num3(n3), num4(n4) {}    //First(n1, n2)를 통해 기초 클래스의 생성자 호출을 명시하고 있다.
}

 

위 예의 Second 클래스의 생성자에서는 기초 클래스의 생성자인 First를 호출하고 있습니다. 물론 대입 연산자의 호출에서도 이와 같은 일이 일어나야 합니다. 다음의 예제를 보겠습니다.

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

class First
{
private:
	int num1, num2;
public:
	First(int n1, int n2) : num1(n1), num2(n2) {}
	void ShowData()
	{
		cout << num1 << ", " << num2 << endl;
	}
	First& operator=(const First& ref)
	{
		cout << "First& operator=()" << endl;
		num1 = ref.num1;
		num2 = ref.num2;

		return *this;
	}
};

class Second : public First
{
private:
	int num3, num4;
public:
	Second(int n1, int n2, int n3, int n4) : First(n1, n2), num3(n3), num4(n4) {}
	void ShowData()
	{
		First::ShowData();
		cout << num3 << ", " << num4 << endl;
	}
	Second& operator=(const Second& ref)
	{
		cout << "Second& operator=()" << endl;
		num3 = ref.num3;
		num4 = ref.num4;

		return *this;
	}
};

int main(void)
{
	Second ssrc(111, 222, 333, 444);
	Second scpy(0, 0, 0, 0);
	scpy = ssrc;
	scpy.ShowData();

	return 0;
}

/*
실행결과

Second& operator=()
0, 0
333, 444

*/

 

위 예제를 보면 scpy에 ssrc 객체를 대입하고 있습니다. 그러면 상식적으로는 scpy의 멤버들의 값들이 111, 222, 333, 444가 되어야 할 것 같은데, 실제 결과를 보면 기초 클래스의 멤버 num1과 num2는 0 그대로 바뀌지 않은 것을 확인할 수 있습니다.

 

이러한 문제가 발생하는 이유는 Second 클래스에 정의된 대입 연산자 오버 로딩에서 기초 클래스의 대입 연산자를 호출하지 않았기 때문입니다. 해당 연산자 내부를 보면 num3와 num4에 대해서만 값이 대입되고 있음을 볼 수 있습니다. 따라서 다음과 같이 Second 클래스의 대입 연산자에서 First 클래스의 대입 연산자를 호출해주어야 이 문제를 해결할 수 있습니다.

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

class First
{
private:
	int num1, num2;
public:
	First(int n1, int n2) : num1(n1), num2(n2) {}
	void ShowData()
	{
		cout << num1 << ", " << num2 << endl;
	}
	First& operator=(const First& ref)
	{
		cout << "First& operator=()" << endl;
		num1 = ref.num1;
		num2 = ref.num2;

		return *this;
	}
};

class Second : public First
{
private:
	int num3, num4;
public:
	Second(int n1, int n2, int n3, int n4) : First(n1, n2), num3(n3), num4(n4) {}
	void ShowData()
	{
		First::ShowData();
		cout << num3 << ", " << num4 << endl;
	}
	Second& operator=(const Second& ref)
	{
		cout << "Second& operator=()" << endl;
		First::operator=(ref);    //기초 클래스의 대입 연산자 호출
		num3 = ref.num3;
		num4 = ref.num4;

		return *this;
	}
};

int main(void)
{
	Second ssrc(111, 222, 333, 444);
	Second scpy(0, 0, 0, 0);
	scpy = ssrc;
	scpy.ShowData();

	return 0;
}

/*
실행결과

Second& operator=()
First& operator=()
111, 222
333, 444

*/

 

그렇다면 유도 클래스에서 대입 연산자를 정의하지 않는 경우는 어떻게 될까요? 유도 클래스에 자동으로 삽입되는 디폴트 대입 연산자는 이러한 문제를 일으키지 않을까요? 다음 예제를 살펴보겠습니다.

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

class First
{
private:
	int num1, num2;
public:
	First(int n1, int n2) : num1(n1), num2(n2) {}
	void ShowData()
	{
		cout << num1 << ", " << num2 << endl;
	}
	First& operator=(const First& ref)
	{
		cout << "First& operator=()" << endl;
		num1 = ref.num1;
		num2 = ref.num2;

		return *this;
	}
};

class Second : public First
{
private:
	int num3, num4;
public:
	Second(int n1, int n2, int n3, int n4) : First(n1, n2), num3(n3), num4(n4) {}
	void ShowData()
	{
		First::ShowData();
		cout << num3 << ", " << num4 << endl;
	}
	/*
	Second& operator=(const Second& ref)
	{
		cout << "Second& operator=()" << endl;
		First::operator=(ref);
		num3 = ref.num3;
		num4 = ref.num4;

		return *this;
	}
	*/
};

int main(void)
{
	Second ssrc(111, 222, 333, 444);
	Second scpy(0, 0, 0, 0);
	scpy = ssrc;
	scpy.ShowData();

	return 0;
}

/*
실행결과

First& operator=()
111, 222
333, 444

*/

 

위 예제에서는 Second 클래스의 대입 연산자를 주석 처리하였습니다. 따라서 Second 클래스에는 디폴트 대입 연산자가 삽입됩니다. 그리고 실행 결과를 보면, 기초 클래스의 대입 연산자가 호출되었으며, 기초 클래스의 멤버까지 정상적으로 대입 연산이 일어난 것을 확인할 수 있습니다.

 

즉, 디폴트 대입 연산자는 알아서 기초 클래스의 대입 연산자를 호출함을 알 수 있습니다. 다만 디폴트 대입 연산자는 얕은 복사가 일어나니 사용에 주의해야 합니다.

 

배열의 인덱스 연산자 오버 로딩

C와 C++의 기본 배열은 경계 검사를 하지 않는 단점을 가지고 있습니다. 따라서 다음과 같이 엉뚱한 코드가 만들어져도 컴파일도 되고, 실행도 무리 없이 진행됩니다.

int main(void)
{
	int arr[3] = {1, 2, 3};
	cout << arr[-2] << endl;
	cout << arr[-1] << endl;
	cout << arr[3] << endl;
	cout << arr[4] << endl;

	return 0;
}

 

이러한 문제를 해결하기 위해 '배열 클래스'를 만들어 사용할 수 있습니다. 배열 클래스는 말 그대로 배열의 역할을 하는 클래스를 말합니다. 그리고 배열 클래스를 사용하기 위해서는 배열의 인덱스 연산자[ ]를 오버 로딩해야 합니다.

 

다음의 문장을 보겠습니다.

arrObject[2];

 

위 문장에서 arrObject가 객체의 이름이고, [ ] 연산자가 오버 로딩되어 있다면, 위 문장은 아래와 같은 문장으로 해석될 것으로 쉽게 생각할 수 있습니다.

arrObject.operator[](2);

 

이 부분만 이해할 수 있으면 배열 클래스는 쉽게 만들 수 있습니다. 다음의 예제를 보겠습니다.

#include <iostream>
#include <cstdlib>
using namespace std;

class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new int[len];
	}
	int& operator[] (int index)
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}
	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

int main(void)
{
	BoundCheckIntArray arr(5);
	for (int i = 0; i < 5; i++)
	{
		arr[i] = (i + 1) * 11;
	}

	for (int i = 0; i < 7; i++)
	{
		cout << arr[i] << endl;
	}

	return 0;
}

/*
실행결과

11
22
33
44
55
Array index out of bound exception

*/

 

위 예제에서 배열 클래스 BoundCheckIntArray가 정의되었습니다. 그리고 [ ] 연산자가 오버 로딩되었습니다. 그리고 해당 오버 로딩 내용을 보면 선언된 배열의 범위를 벗어나 접근하는 것을 차단하는 코드가 작성되어 있습니다. 만약 정상적인 배열 범위를 벗어나는 접근을 시도하게 되면 exit(1)을 통해 프로그램을 즉시 종료합니다.

 

위 예제처럼 배열 클래스를 만들면서 배열에 대한 경계 검사를 할 수 있게 되었고, 배열 접근의 안전성을 보장받을 수 있었습니다. 그런데 경계 검사 외에도 배열 접근의 안전성을 해칠 수 있는 경우가 또 있습니다. 다음의 예를 보겠습니다.

int main(void)
{
	BoundCheckIntArray arr(5);
	for (int i = 0; i < 5; i++)
	{
		arr[i] = (1 + 1) * 11;
	}

	BoundCheckIntArray cpy1(5);
	cpy1 = arr;                       //안전하지 않은 코드
	BoundCheckIntArray cpy2 = arr;    //안전하지 않은 코드

	return 0;
}

 

위 코드가 왜 안전하지 않은지 쉽게 짐작할 것입니다. arr와 cpy1, cpy2는 배열 클래스의 객체입니다. 그리고 위 코드에서는 객체를 객체에 대입하고 있습니다(대입 연산자가 오버 로딩되어 있지는 않습니다). 따라서 클래스의 복사 생성자가 호출될 것이고, 복사 생성자를 따로 정의하지 않았기 때문에 얕은 복사가 일어날 것입니다. arr객체의 멤버 중에는 배열의 주소를 저장하는 int형 포인터가 있기 때문에 얕은 복사가 수행되면 문제가 발생합니다. 어떤 문제인지는 앞서 배웠습니다. 

 

그런데 사실 얕은 복사가 일어난다는 것이 문제가 아닙니다. 깊은 복사가 일어나도록 복사 생성자를 직접 정의하면 문제가 해결될 것이라고 생각했을 것입니다. 그런데 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 '유일성'이 보장되어야 하기 때문에, 대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주됩니다.

 

따라서 깊은 복사가 진행되도록 복사 생성자를 정의할 것이 아니라, 아래의 코드와 같이 빈 상태로 정의된 복사 생성자와 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 원천적으로 막는 것이 좋은 선택입니다.

class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
	BoundCheckIntArray (BoundCheckIntArray& copy) {}
	BoundCheckIntArray& operator= (BoundCheckIntArray& ref) {}
public:
	......
}

 

앞서 정의한 BoundCheckIntArray 클래스에는 제약이 존재합니다. 어떠한 제약이 존재하는지 다음 예제의 컴파일 결과를 통해서 확인해 보겠습니다.

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

class BoundCheckIntArray
{
private:
	int* arr;
	int arrlen;
	BoundCheckIntArray(BoundCheckIntArray& copy) {}
	BoundCheckIntArray& operator=(BoundCheckIntArray& ref) {}
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new int[len];
	}

	int& operator[] (int index)
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}

	int GetArrLen() const
	{
		return arrlen;
	}

	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

void ShowAllData(const BoundCheckIntArray& ref)
{
	int len = ref.GetArrLen();
	for (int i = 0; i < len; i++)
	{
		cout << ref[i] << endl;    //컴파일 에러 발생
	}
}

int main(void)
{
	BoundCheckIntArray arr(5);
	for (int i = 0; i < 5; i++)
	{
		arr[i] = (i + 1) * 11;
	}

	ShowAllData(arr);

	return 0;
}

 

위 예제에서 ShowAllData 함수의 매개 변수가 const로 선언되어 있습니다. 이는 ref의 데이터 변경을 허용하지 않겠다는 의미입니다. 그런데 ref[i]에서 에러가 발생했습니다. [ ] 연산자가 오버 로딩되어 있기 때문에 해당 문장은 다음과 같이 해석됩니다.

ref.operator[](i)

즉, ref의 멤버 함수인 operator[] 함수를 호출하고 있습니다. 그런데 이 함수는 const가 선언되지 않은 함수입니다. 앞서 ref에 const 선언을 하여 ref의 데이터 변경을 불가능하게 하였는데, ref의 멤버 변수에 접근할 수 있고, 변수 값을 변경까지도 할 수 있는 함수를 호출하려고 하니 에러가 발생하게 됩니다.

 

그럼 operator[] 함수를 const 선언하면 이 문제가 해결될까요? 물론 해당 함수를 const 선언하게 되면 컴파일 에러는 발생하지 않습니다. 하지만 이 방법도 제약을 가지고 있습니다.

 

지금 BoundCheckIntArray 클래스는 배열의 주소를 받는 멤버 arr을 가지고 있지만, 대신 배열 그 자체를 멤버로서 가지고 있는 배열 클래스라고 가정해보겠습니다. 그럼 [ ] 연산자와 객체 이름을 통해 해당 배열에 데이터를 저장해야 할 텐데 operator[] 함수는 const 선언되었으므로 이 자체가 불가능하게 됩니다.

 

따라서 이러한 제약을 갖지 않게 하기 위해 다음과 같이 const 키워드를 이용한 함수 오버 로딩을 합니다.

int& operator[] (int index)
{
	if (index < 0 || index >= arrlen)
	{
		cout << "Array index out of bound exception" << endl;
		exit(1);
	}

	return arr[index];
}

int& operator[] (int index) const
{
	if (index < 0 || index >= arrlen)
	{
		cout << "Array index out of bound exception" << endl;
		exit(1);
	}

	return arr[index];
}

 

함수명, 매개 변수의 자료형과 개수가 모두 같아도 const 선언의 유무만으로도 함수의 오버 로딩이 가능합니다. 따라서 operator[] 함수를 위와 같이 const 선언 유무에 따라 오버 로딩하게 되면 효과적으로 컴파일 에러를 발생시키지 않고 배열 클래스에 다른 제약이 발생하지도 않습니다.

 

이번에는 객체를 대상으로 하는 배열 클래스를 정의해 보겠습니다. 저장의 대상은 다음과 같은 Point 객체입니다.

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);
};

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

 

위 클래스의 객체를 정의할 수 있는 배열 클래스를 정의하되, 다음의 두 가지 형태로 각각 정의해 보겠습니다.

  • Point 객체를 저장하는 배열 기반의 클래스
  • Point 객체의 주소 값을 저장하는 배열 기반의 클래스

 

먼저, Point 객체를 저장하는 배열 기반의 클래스입니다.

#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, const Point& pos);
};

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

class BoundCheckIntArray
{
private:
	Point* arr;
	int arrlen;
	BoundCheckIntArray(const BoundCheckIntArray& copy) {}
	BoundCheckIntArray& operator=(const BoundCheckIntArray& ref) {}
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new Point[len];
	}

	Point& operator[] (int index)
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}

	Point& operator[] (int index) const
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}

	int GetArrLen() const
	{
		return arrlen;
	}

	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

int main(void)
{
	BoundCheckIntArray arr(3);
	arr[0] = Point(3, 4);
	arr[1] = Point(5, 6);
	arr[2] = Point(7, 8);

	for (int i = 0; i < arr.GetArrLen(); i++)
	{
		cout << arr[i] << endl;
	}

	return 0;
}

/*
실행결과

[3, 4]
[5, 6]
[7, 8]

*/

 

객체를 저장하는 배열 클래스는 객체 간 대입 연산을 기반으로 객체를 저장합니다. 따라서 다음 예제에서 보이는 주소 값을 저장하는 방식이 보다 많이 사용됩니다.

//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, const Point& pos);
};

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

class BoundCheckIntArray
{
private:
	Point** arr;
	int arrlen;
	BoundCheckIntArray(const BoundCheckIntArray& copy) {}
	BoundCheckIntArray& operator=(const BoundCheckIntArray& ref) {}
public:
	BoundCheckIntArray(int len) : arrlen(len)
	{
		arr = new Point*[len];
	}

	Point*& operator[] (int index)
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}

	Point*& operator[] (int index) const
	{
		if (index < 0 || index >= arrlen)
		{
			cout << "Array index out of bound exception" << endl;
			exit(1);
		}

		return arr[index];
	}

	int GetArrLen() const
	{
		return arrlen;
	}

	~BoundCheckIntArray()
	{
		delete[] arr;
	}
};

int main(void)
{
	BoundCheckIntArray arr(3);
	arr[0] = new Point(3, 4);
	arr[1] = new Point(5, 6);
	arr[2] = new Point(7, 8);

	for (int i = 0; i < arr.GetArrLen(); i++)
	{
		cout << *(arr[i]) << endl;
	}

	delete arr[0];
	delete arr[1];
	delete arr[2];

	return 0;
}

/*
실행결과

[3, 4]
[5, 6]
[7, 8]

*/

 

객체의 주소를 저장하는 배열 클래스의 경우 이중 포인터가 사용됐습니다. 그리고 main 함수에서도 Point 객체를 저장하고 소멸하기 위해 new와 delete 키워드가 사용되었습니다. 얼핏 보면 객체의 주소를 저장하는 방법이 더 복잡해 보이지만 객체의 주소를 저장하는 배열 클래스는 얕은 복사를 걱정하지 않아도 되는 장점이 있기 때문에 이 방법이 더 많이 사용됩니다.

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