본문 바로가기

문과 코린이의, [C. C++] 기록/C++ 이론

[문과 코린이의 IT 기록장] C,C++ - 연산자 오버로딩 5 : 배열의 인덱스 연산자 오버로딩 (배열 클래스, const 함수를 이용한 오버로딩의 활용, 객체의 저장을 위한, 배열 클래스의 정의)

반응형

[문과 코린이의 IT 기록장] C,C++ - 연산자 오버로딩 4 : 배열의 인덱스 연산자 오버로딩 

(배열 클래스,  const 함수를 이용한 오버로딩의 활용, 객체의 저장을 위한, 배열 클래스의 정의)

 


객체배열에 대해 좀 더 알고 싶다면?

-  이번 포스팅에서는, 배열요소에 접근할 때 사용하는, [ ] 연산자를 오버로딩 하고자 한다.


 

 1. 배열 클래스

- C, C++의 기본 배열은 경계검사를 하지 않는다는 단점을 가지고 있다. 따라서 컴파일, 실행 모두 무리없이 진행되는, 엉뚱한 코드가 만들어질 수 있다.

 

int main(){

  int arr[3] = {1,2,3};

  cout<<arr[-1]<<endl; // arr의 주소 + sizeof(int) x -1의 위치에 접근한다.

  cout<<arr[-2]<<endl; // arr의 주소 + sizeof(int) x -2의 위치에 접근한다.

  cout<<arr[3]<<endl;

  cout<<arr[4]<<endl;

  ....

}

 

- 이러한 단점을 해결하기 위해서, 배열 클래스라는 것을 디자인해보고자 한다.

 

1) 배열 클래스

: 배열의 역할을 하는 클래스

 

- 만약, arrObject가 객체의 이름이라고 가정하면, 'arrObject[2];' 이 문장은 어떻게 해석되는가? 

 a. 객체 arrObject의 맴버함수 호출로 이어진다.
 b. 연산자가 [ ]이므로, 맴버함수의 이름은 'operator[ ]'이다.
 c. 함수호출시 전달되는 인자의 값은, 정수 2이다.

 즉, arrObject.operator[ ] (2); 로 해석된다.


ex )

#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 << "Array index out of bound exception" << endl;

   exit(1); // 에러시 강제 종료 (ps. exit(0) : 정상종료때는 0을 반환하고, 에러가 나서 종료되면 1을 반환)

  }

  return arr[idx]; // 배열요소의 참조값이 반환되고, 이 값을 이용해 배열요소에 저장된 값의 참조뿐만 아니라, 변경도 가능.

 }

 

 ~BoundCheckIntArray() { // 소멸자

  delete[]arr;

 }

};

 

 

int main() {

BoundCheckIntArray arr(5); // arr 객체 배열 생성

 

for (int i = 0; i < 5; i++) 

  arr[i] = (i + 1) * 11; // 배열에 직접 접근하는 느낌을 준다. 이렇듯 실제로 배열처럼 느끼고 사용할 수 있다.

arr[i]의 해석
 a. 객체 arr의 맴버함수 호출로 이어진다.

 b. 연산자가 [ ]이므로, 맴버함수의 이름은 operator[ ]이다.
 c. 함수호출 시, 전달되는 인자의 값은, 정수 i이다.

/*

    copy = arr; // 안전하지 않은 코드

    BoundCheckIntArray copy = arr; // 안전하지 않은 코드

*/

 

for (int i = 0; i < 6; i++) //벗어난 범위의 배열접근 결과의 확인을 위해, 반복범위를 0~5로 지정했다.

  cout << arr[i] << endl;

/*

   Array index out of bound exception의 결과로, 실행결과를 통해 잘못된 배열접근이 있었음을 확인할 수 있다. 

   이러한 유형의 클래스 정의를 통해서 배열접근의 안정성을 보장받을 수 있다.

*/

 

return 0;

}

 


이 코드에서 정의한 BoundCheckIntArray 클래스 객체의 복사 또는 대입은, 얕은 복사로 이어지기 때문에, 단순히 코드만 놓고 본다면, 깊은 복사가 이루어지도록 복사생성자와 대입연산자를 별도로정의해야 한다고 생각할 수 있다.

 

그러나, 배열은 저장소의 일종이며, 저장소에 저장된 데이터는 '유일성'이 보장되어야 하기때문에, 대부분의 경우 배열에서의 복사는 불필요하거나 잘못된 일로 간주되곤 한다.

 

따라서, 깊은 복사가 진행되도록 클래스를 정의하는 것이 아닌, 아래의 코드와 같이, 사생성자와 대입연산자를 private맴버로 둠으로써 복사와 대입을 원천적으로 막는 것이 좋은 선택이 되기도 한다.

 

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

 


 

 

 2. const 함수를 이용한, 오버로딩의 활용

- 위의 BoundCheckIntArray 클래스에는 제약이 존재한다.

 

ex 1 ) 코드의 오류 이해하기

이 코드는 '컴파일 오류'가 발생한다.

#include <iostream>

#include<cstdlib>

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; } // 해당 객체의 arrlen값 반환

 ~BoundCheckIntArray() { delete[]arr; } // 소멸자

};

 

void ShowAllData(const BoundCheckIntArray &ref) {

  int len = ref.GetArrLen();

  for (int idx = 0; idx < len; idx++) {

   cout << ref[idx] << endl; // 컴파일 에러 발생. ref.operator[ ](idx);가 호출할 때, 호출되는 함수는 operator[ ]함수이지, const함수가 아니기 때문이다. 따라서 operator[ ]함수에, const 선언을 추가해줄 필요가 있다.

 }

}

 

int main() {

BoundCheckIntArray arr(5);

for (int i = 0; i < 5; i++) {

  arr[i] = (i + 1) * 11;

 }

ShowAllData(arr);

return 0;

}

 

 


 

ex 2 ) ex 1의 문제 해결

.

#include <iostream>

#include<cstdlib>

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 { // const 맴버함수가 추가되었다.

  if (idx < 0 || idx >= arrlen) {

   cout << "Array index out of bound exception" << endl;

   exit(1);

  }

 return arr[idx];

 }

 

 int GetArrLen() const { return arrlen; } // 해당 객체의 arrlen값 반환

 ~BoundCheckIntArray() { delete[]arr; } // 소멸자

};

 

void ShowAllData(const BoundCheckIntArray &ref) {

  int len = ref.GetArrLen();

  for (int idx = 0; idx < len; idx++) {

   cout << ref[idx] << endl; // const 참조자를 이용한 연산이니, const함수(int operator[](int idx) const) 함수가 호출된다.

 }

}

 

int main() {

BoundCheckIntArray arr(5);

for (int i = 0; i < 5; i++) {

  arr[i] = (i + 1) * 11; // const로 선언되지 않은, arr을 사용한 연산이기 때문에, (int operator[](int idx))함수가 호출된다.

 }

ShowAllData(arr);

return 0;

}

 

 


 

 

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

- 앞서 보인 예제기본자료형 대상의 배열 클래스였기 때문에, 이번에는 객체 대상의 배열 클래스를 제시하고자 한다. 저장의 대상은 다음과 같다.

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){
     os<<'['<<pos.xpos<<", "<<pos.ypos<<']'<<endl;
     return os;
  }

 

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

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

 


ex 1 ) 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);

};

연산자 오버로딩 4 : cout, endl 오버로딩에 대해 좀 더 알고싶다면?

ostream& operator<<(ostream& os, const Point& pos) { // cout 연산을 위해

  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 객체로 이루어진 배열을 생성하고 있다. (모든 맴버가 Point 생성자 디폴트값에 따라, 0으로 초기화된다.)

 }

 

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

// 임시객체를 생성해서, 배열요소를 초기화하고 있다. 초기화 과정에서 디폴트 대입 연산자가 호출되어 맴버대 맴버의 복사가 진행된다. (저장의 대상이 객체일 경우, 대입연산자를 통해 객체에 저장된 값을 복사해야 한다.)

// 즉, 깊은 복사, 얕은 복사에 대한 고민을 할 필요가 있다.

임시객체에 대해 좀 더 알고싶다면?

 

연산자 오버로딩 3 : 대입연산자를 좀 더 알고싶다면?

for (int i = 0; i < arr.GetArrLen(); i++) {

  cout << arr[i];

 }

 

return 0;

}

 


 

ex 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); // cout 연산을 위해

};

 

ostream& operator<<(ostream& os, const Point &pos) {

  os << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;

  return os;

}

 

typedef Point* POINT_PTR; // 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 객체의 주소값이기 때문에, POINT_PTR배열을 생성했다.

 }

 

 POINT_PTR& operator[](int idx) {

   if (idx < 0 || idx >= arrlen) {

     cout << "Array index out of boundd 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);

// Point 객체의 주소 값을 저장하고 있다. (깊은복사, 얕은복사에 대한 문제를 신경쓰지 않아도 됨)

 

for (int i = 0; i < arr.GetArrLen(); i++) {

  cout << *(arr[i]);

}

delete arr[0];

delete arr[1];

delete arr[2];

return 0;

}

 

- 이 예제처럼 주소 값을 저장하는 경우, 객체의 생성과 소멸을 위한 new, delete연산 때문에 더 신경쓸 것이 많아 보인다.

- 그러나, 깊은복사냐 얕은복사냐 하는 문제를 신경쓰지 않아도 되기 때문에, 이 방법이 더 많이 사용된다.

 

 

 

 


* 유의사항
- 아직 공부하고 있는 문과생 코린이가, 정리해서 남겨놓은 정리 및 필기노트입니다.
- 정확하지 않거나, 틀린 점이 있을 수 있으니, 유의해서 봐주시면 감사하겠습니다.
- 혹시 잘못된 점을 발견하셨다면, 댓글로 친절하게 남겨주시면 감사하겠습니다 :)
반응형