-
연산자 오버로딩플밍/C++ (overview) 2012. 1. 3. 23:162006/08/21 21:31
연산자 오버로딩
... operator연산자기호(...) 와 같이 선언한다.
예를 들어, void operator+(int i) 이 선언되었을때,
x+10 는 x.operator+(10)과 같은 문장으로 해석된다.
========================================================================================
이항연산자 오버로딩 - 연산자 오버로딩의 두가지 방법
연산자는 단항연산자, 이항연산자, 삼항연산자가 있는데, 이항연산자가 대부분이므로
그에대한 이야기를 먼저 해보자.
첫번째. 멤버 함수에 의한 오버로딩
class AAA{
AAA operator+(const AAA& _a) const{
....
}
};
main(){
AAA a;
AAA b;
AAA c = a+b;
}
멤버함수에 의한 오버로딩일 경우에는 a+b는 다음과 같이 해석된다. a.operator+(b)
AAA클래스의 operator+의 선언부분을 보면, 성능향상을 위해 parameter를 레퍼런스로 받고있고,
parameter를 건드리는걸 방지하기위해 const로 받고 있다. 그리고 때에 따라 그 함수내에서
멤버변수를 조작할 필요가 없을땐 함수자체를 const시킨다.
두번째. 전역 함수에 의한 오버로딩
class AAA{
int a;
int b;
friend AAA operator+(const AAA& _a1, const AAA& _a2);
};
AAA operator+(const AAA& _a1, const AAA& _a2){
....
AAA객체의 멤버변수에의 접근이 필요한 상황;
}
main(){
AAA a;
AAA b;
AAA c = a+b;
}
전역함수에 의한 오버로딩일 경우에는 a+b는 다음과 같이 해석된다. operator+(a,b)
operator+가 전역에 선언되었다는것만 다르다.
그리고, 또 한가지. operator+내에서 AAA내부에 접근해야 할 필요가 있을때, 그냥 접근을
시도하면 당연히 컴파일 오류가 난다. 멤버함수는 전역에선 볼수 없으니까!
그래서 불가피하게 AAA클래스 내에서 operator+를 friend로 선언해주어야 한다.
그림으로 정리해보자.
이렇게 두가지 경우를 봤는데, 뭐가 더 좋은걸까?"객체지향에는 전역이라는 개념이 존재하지 않는다."
이말은 즉, 두가지 모두로 해결가능한 경우라면, 당연히 멤버함수에 의한 방법이 좋다.
(그래야 프로그램이 좀더 간결해진다.)
부득이한경우, 즉 전역함수에 의한 방법으로만 해결가능한 경우라면 어쩔수 없지만..
기타
[오버로딩이 불가능한 연산자]
. .* :: ?: sizeof
(모든 연산자를 오버로딩 가능하게 해버린다면, C++고유의 문법규칙자체가 무너져버릴수있다..)
[연산자 오버로딩에 있어서의 주의사항]
a. 본 의도를 벗어난 오버로딩은 좆치 안타.
좆치 않은 예 : 철수는 + 기호가 - 기능을 하도록 만들었다. (붕신 -_-;)
b. 연산자 우선순위와 결합성을 바꿀순 없다.
c. 디폴트 매개 변수 설정이 불가능하다.
d. 기본 기능까지 뺏을수는 없다.
int형 변수의 + 연산은 이미 그 법칙이 정해져있다.
좆치 않은 예 : 영수는 a+b 가 a+b+3의 결과를 생산하도록 만들었다. (...-_-;)
[왜 오버로딩이라고 부르는가?]
3 + 4
(A객체)p1 + (A객체)p2
이 둘은 같은 + 연산자를 쓰고 있지만, 피연산자의 종류에 따라 각각 다른 함수가 호출되므로!
========================================================================================
단항연산자 오버로딩
1. ++, -- 연산자 오버로딩
대표적인 단항연산자인 ++ 와 -- 이다.
마찬가지로 멤버함수, 전역함수 두가지 방법이 모두 가능하다.
(이항연산자일때와 비교해서 잘 살펴보자.)
Point& Point::operator++(){
x++;
y++;
return *this;
}
++연산자는 또다른 결과물을 내놓는게 아니라, 자기 자신이 값이 변하게 하는 연산이기 때문에,
연속된 형태의 연산(예: ++(x++))을 위해서는,
Point&형태로 리턴을 해야 한다. 그래서 *this 를 리턴하고 있는것이다.
(*연산을 하면, 포인터가 가리키는 대상을 참조하겠다는 뜻이다. 즉, 자기자신을 리턴한다.)
Point&가 아닌, Point형태로 리턴을 하면, 원하는 답이 안 나온다.
연속된 형태의 연산을 하려면, 자기자신을 리턴해서 자기자신을 ++연산시켜야 하는데,
Point형태로 리턴을 하면, 복사본이 리턴되는것이므로 자기자신은 변화가 일어나지 않는다.
2. 선연산과 후연산의 구분
컴파일러는 ++p나 p++를 똑같이 operator++(p)/p.operator++()로 인식한다.
선연산(선연산 후증가/감소)과 후연산(선증가/감소 후연산)의 기능은 엄연히 다른데 말이다.
이를 구분해서 프로그램하기 위해서 규칙을 만들었다.
operator++()는 ++x ,즉, 선증가 후연산
operator++(int)는 x++, 즉, 선연산 후증가
(--도 마찬가지!)
구분하기 위해서 parameter로 int를 써줬다.
Point& operator++(){
x++;
y++; <선증가, 후연산>
return *this;
}Point operator++(int){
Point tmp(x,y); // Point tmp(*this);x++; } 두줄 대신, ++(*this);
y++; } <선연산, 후증가>
return tmp;
}밑의 경우, 현재값으로 연산후, 증가를 해야 하기때문에 현재의 값을 보존해둘 필요가 있다.
그래서 복사본으로 임시객체를 만들어서 리턴한다. 그리고 내부적으로는 자신의 값을 증가시킨다.
리턴타입이 그냥 Point라는것도 잘 살펴봐두자. (값만 필요한것이므로 복사본으로 리턴.)
========================================================================================
교환 법칙 해결하기
1. 잘못된 형태의 오버로딩 바로잡기
a+b 는 뭘하는 수식일까?
a와 b를 합한 결과를 낳는 수식이다. a나 b를 변경시키는 수식이 아니다.
연산자 오버로딩을 할때 실수할수도 있으니 무엇을 하는 연산인지를 잘 생각해보고 코딩하자.
2. 교환법칙
일반적인(기본형) 덧셈 연산에서는 교환 법칙이 성립한다.
a+3이나 3+a나 같은 의미를 가진 수식이다.
그러나 만약 a가 기본형이 아닌 사용자정의형이라면? 예를 들어, a를 Point형이라고 가정하자.
(멤버 함수에 의해서 정의했을 경우)
a+3은 a.operator+(3)이 되고,
3+a는 3.operator+(a)이 된다.
첫번째 경우는 문제가 없지만, 두번째 경우는 문제가 생긴다.
즉, 이 경우에는 전역함수에 의한 오버로딩이 불가피한 상황이다.
그러면,
a+3은 operator+(a,3)이 되고,
3+a는 operator+(3,a)가 된다.
아직까지는 별 문제가 없어보인다.
그렇다면 교환법칙이 성립할수 있도록 하기 위해서 매개변수 순서가 다른 두개의 함수를
오버로딩해서 만들어야 하는가? 그렇다!
우선 하나를 만들어보자.
Point operator+(const Point& p, int x){
.....
}
이번엔 매개변수 순서가 바뀐 함수를 만들어보자.
Point operator+(int x, const Point& p){
return (p+x);
}
이해가 가는가? 이해해라!
그리고 이렇듯 매개변수 순서가 바꼈을때 해결하는 방법을 알게 됐으니, 한번 생각해보자.
굳이 두 함수를 모두 전역으로 정의할 필요가 있을까?
기본이 되는 함수 하나는 멤버변수로, 교환법칙을 위한 함수는 전역으로 선언해보자.
class Point{
Point operator+(int x);
friend Point operator+(int x, const Point& p);
};
Point operator+(int x, const Point& p){
return (p+x);
}
이렇게 해주면 훨씬 좋다! ^-^
리턴 타입이 Point형 인것을 눈여겨 보자. 이것은 결합 법칙을 가능하게 한다!!
Point x = 3 + p2 + p1;
a. 먼저 3+p2의 결과로 어떤 Point형이 리턴 된다.
b. a의 결과인, 어떤 Point형을 p1과 더해서 그 결과로 또다른 Point형이 리턴된다.
c. b의 결과인, 또다른 Point형이 x에 복사된다.
(리턴 타입이 Point형이 아니라면, 연속적인 연산이 불가능해진다.)
3. 임시 객체의 생성
임시 객체란 어떤 줄에서 생성된 후 그 다음줄로 넘어가자마자 자동 소멸되는 객체이다.
즉, 임시 객체가 존재할수 있는 범위는 자신이 정의된 그 한줄 동안 뿐인것이다!! (한줄살이? -_-;)
정의하는 방법은 간단하다. 일반적인 객체 생성방법과 같지만, 거기서 객체의 이름만 없애면된다.
보통 객체 : Point pnt1(....);
임시 객체 : Point(....);
어디에 쓸까?
하나의 예로..
Point fn(int val){
Point temp(val + 10);
return temp;
}
위의 함수는 다음과 같이 쓸수 있다.
Point fn(int val){
return Point(val + 10);
}
임시 객체는 컴파일러에 dependent한 경우가 많은데, 대체적으로 좀더 빠르고, 메모리를
효율적으로 사용할수 있다는 장점이 있다.
========================================================================================
[특집]
1. cout, cin, endl를 파헤쳐보자!
cout 은 ostream 클래스의 객체, cin 은 istream 클래스의 객체이다!
둘다 표준 이름공간 std 안에 선언되어 있는 클래스들이다.
cout과 cin은 결합성이 오른쪽으로 흘러가기 때문에,
cout << xxx << xxxxxx << xx
이런식의 문장에서, 한번의 cout << ( ) 연산이 끝나고 나면 cout을 반환해야 한다.
(cin도 마찬가지)
예제
ostream& operator<<(ostream& os, const xxx& p){
os<<"[" <<......<<endl;
return os;
}
2. 배열의 인덱스 연산자 오버로딩
(기본자료형의) 배열 역할을 하는 클래스를 하나 정의해보자.
xxx.getitem(idx) 이런식으로 index를 통해서 배열 원소에 접근할수 있도록 만들어진 클래스인데,
실제 배열처럼 xxx[idx] 이렇게 써도 접근 가능하도록 하고 싶다!!
[] 도 역시 operator이다. 그래서 오버로딩이 가능하다.
xxx& operator[](int s) 와 같이 정의해주면 된다.
이번엔 객체를 저장할수 있는 배열 클래스를 정의해보자.
방식은 똑같다.
다만 고려해야 할 점이 있다.
객체의 포인터를 저장할것인가? 객체 자체를 저장할것인가?
포인터로 저장한다면, 객체 복사가 이루어질때 얕은 복사를 할것인가, 깊은 복사를 할것인가?
예제
class Point{
...
};
class PointArr{
...
};
main(){
PointArr arr;
arr.AddElem(Point(1,1));
arr.AddElem(Point(2,2));
arr.AddElem(Point(3,3));
}
main에서 원소를 저장할때 임시객체 형태로 넘겨주는것을 잘 봐두자.
3. 반드시 해야 하는 대입연산자의 오버로딩
1) 대입연산자
p1 = p2; (p1, p2는 모두 사용자정의형)
위와 같은 객체간의 대입을 할때는, 대입연산자가 호출된다. '=' 이 오버로딩 된것이다.
즉, p1.operator=(p2); 이 표현과 같다. (복사생성자와는 다르다. 복사생성자는 객체가 처음
초기화될때 호출되는 것이다.)
대입연산자 역시 사용자가 정의해주지 않으면 디폴트로 생성된다.
기능은, 복사생성자와 거의 흡사하다. 멤버변수 대 멤버변수의 복사를 한다.
기능 뿐만아니라, 여러면에서 복사생성자와 유사한데,
또다른 예로, 생성자내에서 동적할당을 했을 경우가 그렇다. 대입연산자에서 깊은 복사를 해주지 않으면 에러가 난다.
예제
Person 클래스는 char * name를 갖고 있다.
Person형 p1, p2를 만들고, p1=p2라고 하면, p1의 name에 p2의 name이 대입 된다.
그러면, p1이 원래 가리키고 있던 name은 접근할 방법이 없다. 메모리 유출이 생긴다.
그리고, p1,p2가 같은 name을 가리키고 있으므로, 각 객체가 소멸할때 같은 name을 두번
삭제하려고 하기때문에 오류가 난다.
해결 방법은, 다음과 같다.
Person& operator=(const Person& p){
delete name[];
멤버 변수 복사;
return *this;
}
다시한번 포괄적으로 정리를 하자면, 생성자 내에서 동적할당을 했을때는, 반드시
이를 해제하는 소멸자를 정의하고, 복사 생성자 뿐만 아니라, 대입연산자도 깊은 복사를 하도록
정의해야 한다.
'플밍 > C++ (overview)' 카테고리의 다른 글
템플릿(template) (0) 2012.01.03 string 클래스 디자인 (0) 2012.01.03 virtual 응용(원리, 다중상속) (0) 2012.01.03 상속(심화), 두종류binding, virtual (0) 2012.01.03 상속 (0) 2012.01.03