본문 바로가기

Programming Study/C++

연산자 오버로딩_2

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

대입 연산자의 오버로딩은 그 성격이 복사 생성자와 매우 유사하다.

 

객체간 대입연산의 비밀: 디폴트 대입 연산자

잠시 복사 생성자에 대해서 복습해보자.

다음은 이전에 설명한 복사 생성자의 대표적인 특성이다.

  - 정의하지 않으면 디폴트 복사 생성자가 삽입된다.

  - 디폴트 복사 생성자는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.

  - 생성자 내에서 동적할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야 한다.

 

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

  - 정의하지 않으면 디폴트 대입 연산자가 삽입된다.

  - 디폴트 대입 연산자는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.

  - 연산자 내에서 동적 할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야 한다. 

 

살펴본 복사생성자와 대입 연산자는 정말 유사해 보인다.

하지만 호출되는 시점에는 차이가 있다.

복사생성자가 호출되는 대표적인 상황은 다음과 같다.

int main(void)

{

    Point pos1(5,7);

    Point pos2 = pos1;

    .....

}

여기서 중요한 사실은 새로 생성하는 객체 pos2의 초기화에 기존에 생성된 객체 pos1이 사용되었다는 점이다.

 

다음은 대입 연산자가 호출되는 대표적인 상황이다.

int main(void)

{

    Point pos1(5,7);

    Point pos2(9,10);

    pos2 = pos1;

    .....

여기서 중요한 사실은 pos2도, 그리고 pos1도 이미 생성 및 초기화가 진행된 객체라는 사실이다.

즉, 기존에 생성된 두 객체간의 대입연산 시에는 대입 연산자가 호출된다.

그런데 이 문장을 보면 다음과 같이 생각하기 쉽다.

"동일한 자료형의 두 객체간에 댕비연산이 허용되네!"

그리고 멤버 대 멤버의 복사가 이뤄지는 것을 보면서, C언어의 구조체 변수간 대입연산의 결과와 비슷하다고 생각하기 쉽다.

그러나 객체간의 대입연산은 C언어의 구조체 변수간의 대입연산과 본질적으로 다르다.

이는 단순한 대입연산이 아닌, 대입 연산자를 오버로딩 한 함수의 호출이기 때문이다.

 

생성자 내에서 동적 할당을 하는 경우,

디폴트 대입 연산자는 두 가지 문제를 일으키므로 다음의 형태로 직접 대입 연산자를 정의해야 한다.

  - 깊은 복사를 진행하도록 정의한다.

  - 메모리 누수가 발생하지 않도록, 깊은 복사에 앞서 메모리 해제의 과정을 거친다.

 

상속 구조에서의 대입 연산자 호출

대입 연산자는 생성자가 아니다!

이 이야기를 하는 이유는,

유도 클래스의 생성자에는 아무런 명시를 하지 않아도 기초 클래스의 생성자가 호출되지만,

'유도 클래스의 대입 연산자에는 아무런 명시를 하지 않으면, 기초 클래스의 대입 연산자가 호출되지 않는다'

사실을 말하기 위해서이다.

 

유도 클래스의 대입 연산자 정의에서, 명시적으로 기초 클래스의 대입 연산자 호출문을 삽입하지 않으면,

기초 클래스의 대입 연산자는 호출되지 않아서, 기초 클래스의 멤버변수는 멤버 대 멤버의 복사 대상에서 제외된다.

때문에 유도 클래스의 대입 연산자를 정의해야 하는 상황에 놓이게 되면, 기초 클래스의 대입 연산자를 명시적으로 호출해야 한다.

 

 

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

C, C++의 기본 배열은 경계검사를 하지 않는다는 특성을 지니고 있다.

이 특성의 부정적 측면만을 고려해 이 단점을 해결해 보도록 하자.

경계검사를 하지 않는 단점을 해결하기 위해서 '배열 클래스'라는 것을 디자인해 보자.

배열 클래스라는 것은 '배열의 역할을 하는 클래스'를 뜻하는 것이다.

우선 배열요소의 접근에 사용되는 [ ]연산자의 오버로딩에 대해 정리할 필요가 있다.

다음 문장을 보자.

arrObject[2];

여기서 arrObject가 객체의 이름이라고 가정할 때,

  - 객체 arrObject의 멤버함수 호출로 이어진다.

  - 연산자가 [ ] 이므로 멤버함수의 이름은 'operator[]'이다.

  - 함수호출 시 전달되는 인자의 값은 정수 2이다.

이렇게 정리할 수 있다.

따라서 위의 문장은  다음과 같이 해석됨을 알 수 있다.

arrOjbect.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 idx)
    {
        if(idx<0 || idx>=arrlen)

        {
            cout<<"Arrary index out of bound exception"<<endl;
            exit(1);
        }
        return arr[idx];
    }
    ~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<6; i++)
        cout<<arr[i]<<endl;

    return 0;

실행시키면

결과에서 보이듯이, 잘못된 배열 접근이 있음을 확인할 수 있다.

때문에 위 유형의 클래스 정의를 통해서 배열접근의 안전성을 보장받을 수 있다.

그리고 만약에 안전성을 더 높이기 위해서는 복사 생성자와 대입 연산자를 private으로 선언해서,

복사 또는 대입을 원천적으로 막을 수 있다.

위에서 정의한 BoundCheckIntArray 클래스 객체의 복사 또는 대입은 얕은 복사로 이어지기 때문에,

단순히 코드만 놓고 보면, 깊은 복사가 진행되도록 복사 생성자와 대입 연산자를 별도로 정의해야 한다고 생각할 수 있다.

그러나 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 '유일성'이 보장되어야 하기 때문에,

대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주된다.

따라서 깊은 복사가 진행되도록 클래스를 정의할 것이 아니라,

빈 상태로 정의된 복사생성자와 대입연산자를 private 멤버로 둠으로 복사와 대입을 원천적으로 막는 것이 좋은 선택이 되기도 한다.

class BoundCheckIntArray
{
private:
    int *arr;
    int arrlen;

    BoundCheckIntArray(const BoundCheckIntArray& arr){}

    BoundCheckIntArray& operator=(const BoundCheckIntArray& arr) {}
public: 

    ......

 

 

3. 그 이외의 연산자 오버로딩

 

스마트포인터

포인터의 역할을 하는 객체를 말한다.

 

펑터(Functor)

함수처럼 동작하는 클래스를 말한다.

'함수 오브젝트(Function Object)라고도 불린다.

펑터는 함수 또는 객체의 동작방식에 유연함을 제공할 때 주로 사용된다.

 

 

 

- 윤성우 저, 열혈강의 C++ 프로그래밍 中 -

 

 

'Programming Study > C++' 카테고리의 다른 글

템플릿_1  (0) 2014.10.07
String 클래스의 디자인  (0) 2014.10.06
연산자 오버로딩_1  (0) 2014.10.03
가상(Virtual)의 원리와 다중상속(Multiple Inheritance)  (0) 2014.09.30
상속과 다형성(Polymorphism)  (0) 2014.09.29