read

스물여섯번째 열혈강의


treadmill

이번엔 배열의 인덱스 연산자 오버로딩에 대해 알아보는 시간을 가져보자.

배열에서 사용하는 [] 연산자를 오버로딩할 것인데, 이는 다른 연산자와 달리 연산자 기호 안으로 들어가기 때문에 조금 어색하고 생소할 수 있다.

그리고 연산의 기본 특성상 멤버함수 기반으로만 오버로딩 하도록 제한되어 있다.


배열 클래스


C, C++에서 기본 배열은 다음과 같은 단점이 있다.

경계검사를 하지 않는다.

따라서 다음처럼 만들어 질 수 있다.

int arr[3] = {1,2,3}; cout << arr[-1] << endl;

그러면 arr의 주소 + sizeof(int) * -1 의 위치에 접근을 한다.

물론 이런 특성이 유용할 수도 있지만 부정적인 측면만 보도록 하자. 따라서 배열 클래스 를 만들어 단점을 해결하려 한다.

여기서 배열 클래스 라 함은 배열의 역할을 하는 클래스이다.

그 전에 []연산자의 오버로딩에 대해 조금 정리하자.

arrObject[2];

여기서 arrObject가 객체의 이름이라고 가정할 때, 어떻게 해석이 되는지 생각해보자.

  1. 객체 arrObject의 멤버함수 호출로 이어진다.
  2. 연산자가 [] 이므로 멤버함수의 이름은 operator []이다.
  3. 함수호출 시 전달되는 인자의 값은 정수 2이다.

그럼 다음과 같이 멤버함수의 호출이 된다는 걸 유추할 수 있다.

type operator[ ] (int idx){...}

따라서 arrObject[2]는 다음처럼 해석이 된다.

arrObject.operator ;

[]연산자에 뭔가 넣어야 할 것 같은 느낌이 들지만, 단순히 함수의 이름으로 사용되었다는 것만 인식하면 어렵게 느낄 것도 없다.

그럼 한 번 클래스를 만들어 보자.


#include <iostream>

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 << "Array index out of bound exception" << endl;
            exit(1);
        }
        return arr[idx];
    }
    ~BoundCheckIntArray(){
        delete []arr;
    }
};

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

결론은 55 이후, exception 처리가 되는 걸 알 수 있다.

그리고 안전성을 더 높이기 위해 다음처럼 하는 걸 허용하지 않으려면


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

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

복사생성자와 대입연산자를 private으로 선언해서, 복사 또는 대입을 원천적으로 막을 수도 있다.


class BoundCheckIntArray{
private:
  int * arr;
  int arrlen;
  BoundCheckIntArray(const BoundCheckIntArray &arr){}
  BoundCheckIntArray& operator=(const BoundCheckIntArray& arr){}
}

위 처럼 객체의 복사 또는 대입은 얕은 복사로 이어지므로 단순히 코드만 놓고 보면 깊은 복사가 진행되도록 복사 생성자와 대입 연산자를 별도로 정의해야 한다고 생각할 수 있다. 그러나 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 유일성이 보장되어야 하기 때문에, 대부분의 경우 저장소의 복사는 불필요하거나 잘못된 일로 간주된다. 따라서 깊은 복사가 진행되도록 클래스를 정의할 것이 아니라, 빈 상태로 정의 된 복사 생성자와 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 원천적으로 막는것이 좋은 선택일 수 있다.


const 함수를 이용한 오버로딩 활용


앞서 정의한 BoundCheckIntArray 클래스에는 제약이 존재한다. 어떠한 제약이 존재하는지 다음 예제의 컴파일 결과를 통해 확인하자. (에러 발생) 여기엔 배열의 길이를 반환하는 함수가 추가되어 있다.


#include <iostream>

using namespace std;

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

void ShowAllData(const BoundCheckIntArray& ref){
    int len = ref.GetArrLen();
    for(int idx=0; idx<len; idx++){
        cout << ref[idx] << endl;
    }
}

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

여기서는 에러가 왜 발생할 것인가?

당연히 const 때문인데, 먼저 showAllData에서 const BoundCheckIntArray& ref 파라미터는 아주 좋은 선택이다.

다만 ref[idx]가 호출될 때, 연산자 오버로딩에 의해 ref.operator[ ](idx)가 호출된다.

여기서 operator[ ] 함수에 const 선언이 없기 때문이다.

그럼 해결방법은 const를 선언해 주는 것이다. 그런데 선언을 해버리면 배열을 멤버로 선언할 경우, 저장 자체가 불가능해 진다. 따라서 컴파일 에러가 발생하게 되므로 좋은 해결책이 되지 못한다. 그럼 한가지 떠올려보자.

const의 선언유무도 함수 오버로딩의 조건에 해당한다는 것을 배운적 있다.

따라서 다음과 같이 확장을 해보자.


#include <iostream>

using namespace std;

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

void ShowAllData(const BoundCheckIntArray& ref){
    int len = ref.GetArrLen();
    for(int idx=0; idx<len; idx++){
        cout << ref[idx] << endl;
    }
}

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

확인해 보면 좋을 것이다.


객체의 저장을 위한 배열 클래스의 정의


이제는 다양한 예시를 볼껀데, 앞선 예제가 기본자료형 대상의 배열 클래스였다면 이젠 객체 대상의 배열클래스를 보려고 한다.


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.ypos << ']' << endl;
    return os;
}

위 클래스의 객체를 저장할 수 있는 배열 클래스를 정의하되, 다음 두가지 형태로 정의해 보고자 한다.

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

그럼 예시를 보자.


#include <iostream>
#include <cstdlib>

using namespace std;

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.ypos << ']' << endl;
    return os;
}


class BoundCheckPointArray{
private:
    Point* arr;
    int arrlen;

    BoundCheckPointArray(const BoundCheckPointArray& arr){}
    BoundCheckPointArray& operator=(const BoundCheckPointArray& arr){}
public:
    BoundCheckPointArray(int len) : arrlen(len){arr = new Point[len];}
    Point& operator[](int idx){
        if(idx<0 || idx>=arrlen){
            cout << "Array index out of bound exception" << endl;
            exit(1);
        }
        return arr[idx];
    }
    Point& operator[](int idx) const{
        if(idx<0 || idx>=arrlen){
            cout << "Array index out of bound exception" << endl;
            exit(1);
        }
        return arr[idx];
    }
    int GetArrLen() const {return arrlen;}
    ~BoundCheckPointArray(){delete []arr;}
};

int main(){
    BoundCheckPointArray 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];
    }
    return 0;
}

위의 예시는 Point 객체를 저장하는 배열 기반의 클래스다.

객체의 저장은 객체간의 대입연산을 기반으로 한다. 따라서 주소 값을 저장하는 방식이 보다 많이 사용된다.


#include <iostream>
#include <cstdlib>

using namespace std;

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.ypos << ']' << endl;
    return os;
}

typedef Point * POINT_PTR;

class BoundCheckPointPtrArray{
private:
    POINT_PTR * arr;
    int arrlen;

    BoundCheckPointPtrArray(const BoundCheckPointPtrArray& arr){}
    BoundCheckPointPtrArray& operator=(const BoundCheckPointPtrArray& arr){}
public:
    BoundCheckPointPtrArray(int len) : arrlen(len){arr = new POINT_PTR[len];}
    POINT_PTR& operator[](int idx){
        if(idx<0 || idx>=arrlen){
            cout << "Array index out of bound exception" << endl;
            exit(1);
        }
        return arr[idx];
    }
    POINT_PTR& operator[](int idx) const{
        if(idx<0 || idx>=arrlen){
            cout << "Array index out of bound exception" << endl;
            exit(1);
        }
        return arr[idx];
    }
    int GetArrLen() const {return arrlen;}
    ~BoundCheckPointPtrArray(){delete []arr;}
};

int main(){
    BoundCheckPointPtrArray 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]);
    }
    delete arr[0];
    delete arr[1];
    delete arr[2];
    return 0;
}

다음 포스팅에서는 원래 그 이외의 연산자 오버로딩에 대해 알아 보려고 했으나 그 내용이 부록에 가깝다.

간단히 알아보면, newdelete도 연산자이기 때문에, 오버로딩이 가능하다. 따라서 이 두 연산자를 오버로딩을 할텐데, 포인터 연산자를 오버로딩하면서 개념적으로 어렵다고 하는 스마트 포인터(smart pointer)펑터(functor)에 대해서 조금 알아보는 것이다.

이것에 대해서는 마지막 혹은 필요한 상황에서 한번 다시 다루도록 하고, 단계별 프로젝트로 진행하도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview