ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 연산자 오버로딩
    플밍/C++ (overview) 2012. 1. 3. 23:16
    2006/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
Designed by Tistory.