KoreanFoodie's Study

C++ 기초 개념 5-3 : C++ 캐스팅, 디폴트 인자, 반복자(iterator) 본문

Tutorials/C++ : Beginner

C++ 기초 개념 5-3 : C++ 캐스팅, 디폴트 인자, 반복자(iterator)

GoldGiver 2021. 12. 22. 17:19

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

C++ 스타일의 캐스팅

기존의 C 언어에서는 컴파일러가 알아서 캐스팅하는 암시적(implicit) 캐스팅과, 프로그래머가 직접 캐스팅하라고 지정하는 명시적(explicit) 캐스팅이 있었다. 하지만 C++에서는 다음과 같은 4가지의 캐스팅을 제공하고 있다.

 

1. static_cast : 우리가 흔히 생각하는, 언어적 차원에서 지원하는 일반적인 타입 변환

2. const_cast : 객체의 상수성(const)를 없애는 타입 변환. 쉽게 말해 const int가 int로 바뀐다.

3. dynamic_cast : 파생 클래스 사이에서의 다운캐스팅

4. reinterpret_cast : 위험을 감수하고 하는 캐스팅으로, 서로 관련이 없는 포인터들 사이의 캐스팅 등

 

 

N 차원 배열 만들기

우리가 앞서 이야기했던 포인터들과 casting에 대한 지식을 바탕으로, N 차원 배열을 직접 구현해 보자. 먼저, Address라는 구조체를 이용한 Array 클래스를 생성한다.

class Array {

const int dim;	// 몇 차원 배열인지
int* size; // size[0] * size[1] * ... * size[dim - 1] 짜리 배열이다.

struct Address {
	int level; 
	// 맨 마지막 레벨(dim-1 레벨)은 데이터 배열
	// 그 위 상위 레벨에서는 다음 Address 배열
	void *next;
};

Address *top;

public:

	Array(int dim, int* array_size) : dim(dim) {
		size = new int[dim];
		for (int i = 0; i < dim; ++i) {
			size[i] = array_size[i];
		}
	}

};

 

Array 클래스를 만들었으면, N차원 배열을 실제로 초기화하고 삭제해주는 함수를 각각 만들어야 한다. 

void Array::initialize_address(Address *current) {

	if (!current) return;
	if (current->level == dim-1) {
		for (int i = 0; i < size[dim-1]; ++i) {
			current->next = new int[size[current->level]];
			return;
		}
	}
	current->next = new Address[size[current->level]];
	for (int i = 0; i < size[current->level]; ++i) {
		(static_cast<Address *>(current->next) + i)->level = current->level + 1;

		initialize_address(static_cast<Address *>(current->next) + i);
	}
}

void Array::delete_address(Address *current) {

	if (!current) return;

	for (int i = 0; current->level < dim - 1 && i < size[current->level]; ++i) {
		delete_address(static_cast<Address *>(current->next) + i);
	}
	
	if (current->level == dim-1)
		delete[] static_cast<int *>(current->next);

	delete[] static_cast<Address *>(current->next);
}

 

위의 initialize_address와 delete_address를 조합하여, 생성자와 소멸자를 재정의하면 다음과 같다.

	Array(int dim, int* array_size) : dim(dim) {
		size = new int[dim];
		for (int i = 0; i < dim; ++i) {
			size[i] = array_size[i];
		}

		top = new Address;
		top->level = 0;

		initialize_address(top);
	}

	Array(const Array &arr) : dim(arr.dim) {
		size = new int[dim];
		for (int i = 0; i < dim; ++i) size[i] = arr.size[i];

		top = new Address;
		top->level = 0;

		initialize_address(top);

		// 내용물 복사
		copy_address(top, arr.top);
	}

	void copy_address(Address *dst, Address *src) {
		if (dst->level == dim-1) {
			for (int i = 0; i < size[dst->level]; ++i) {
				static_cast<int *>(dst->next)[i] = static_cast<int *>(src->next)[i];
				return;
			}
		}
		for (int i = 0; i < size[dst->level]; ++i) {
			Address *new_dst = static_cast<Address *>(dst->next) + i;
			Address *new_src = static_cast<Address *>(src->next) + i;
			copy_address(new_dst, new_src);
		}
	}

	~Array() {
		delete_address(top);
		delete[] size;
	}

 

 

operator[] 문제

이제 [ ]를 이용해서 우리가 만든 N차원 배열에 접근해 보자. 만약 arr[1][2] 와 같은 방식으로 N차원 배열에 접근한다고 가정했을 때, [1]은 우리가 만든 Array 클래스를, [2]는 실제 데이터를 리턴해야 한다.

이를 위해서, 우리는 [ ]를 위한 Int wrapper 클래스를 정의해 볼 수 있다.

class Int {

	void* data;

	int level;
	Array *array;

public:
	Int(int index, int _level = 0, void *_data = NULL, Array *_array = NULL)
		: level(_level), data(_data), array(_array) {
			if (_level < 1 || index >= array->size[_level - 1]) {
				data = NULL;
				return;
			}
			if (level == array->dim) {
				// 이제 data에 우리의 int 자료형을 저장하도록 해야 한다.
				data = static_cast<void *>(
				(static_cast<int *>(static_cast<Array::Address *>(data)->next) + index));
			} else {
				// 그렇지 않을 경우 data에 그냥 다음 addr을 넣어준다.
				data = static_cast<void *>(
				static_cast<Array::Address *>(static_cast<Array::Address *>(data)->next) +
				index);
			}

		}
	Int(const Int& i) : data(i.data), level(i.level), array(i.array) {}

	operator int() {
		if (data) return *static_cast<int *>(data);
		return 0;
	}

	Int& operator=(const int &a) {
		if (data) *static_cast<int *>(data) = a;
		return *this;
	}	

	Int operator[](const int index) {
		if (!data) return 0;
		return Int(index, level + 1, data, array);
	}
};

Int Array::operator[](const int index) {
	return Int(index, 1, static_cast<void *>(top), this);
}

또한,  Array 클래스에서는 Int 클래스의 생성자를 호출해서 [ ] 연산자를 처리하도록 만들고 있다.

 

전체 코드는 다음과 같다.

// 대망의 Array 배열
#include <iostream>

namespace MyArray {
class Array;
class Int;

class Array {
  friend Int;

  const int dim;  // 몇 차원 배열 인지
  int* size;  // size[0] * size[1] * ... * size[dim - 1] 짜리 배열이다.

  struct Address {
    int level;
    // 맨 마지막 레벨(dim - 1 레벨) 은 데이터 배열을 가리키고, 그 위 상위
    // 레벨에서는 다음 Address 배열을 가리킨다.
    void* next;
  };

  Address* top;

 public:
  Array(int dim, int* array_size) : dim(dim) {
    size = new int[dim];
    for (int i = 0; i < dim; i++) size[i] = array_size[i];

    top = new Address;
    top->level = 0;

    initialize_address(top);
  }
  Array(const Array& arr) : dim(arr.dim) {
    size = new int[dim];
    for (int i = 0; i < dim; i++) size[i] = arr.size[i];

    top = new Address;
    top->level = 0;

    initialize_address(top);
    // 내용물 복사
    copy_address(top, arr.top);
  }

  void copy_address(Address* dst, Address* src) {
    if (dst->level == dim - 1) {
      for (int i = 0; i < size[dst->level]; ++i)
        static_cast<int*>(dst->next)[i] = static_cast<int*>(src->next)[i];
      return;
    }
    for (int i = 0; i != size[dst->level]; i++) {
      Address* new_dst = static_cast<Address*>(dst->next) + i;
      Address* new_src = static_cast<Address*>(src->next) + i;
      copy_address(new_dst, new_src);
    }
  }
  // address 를 초기화 하는 함수이다. 재귀 호출로 구성되어 있다.
  void initialize_address(Address* current) {
    if (!current) return;
    if (current->level == dim - 1) {
      current->next = new int[size[current->level]];
      return;
    }
    current->next = new Address[size[current->level]];
    for (int i = 0; i != size[current->level]; i++) {
      (static_cast<Address*>(current->next) + i)->level = current->level + 1;
      initialize_address(static_cast<Address*>(current->next) + i);
    }
  }
  void delete_address(Address* current) {
    if (!current) return;
    for (int i = 0; current->level < dim - 1 && i < size[current->level]; i++) {
      delete_address(static_cast<Address*>(current->next) + i);
    }

    if (current->level == dim - 1) {
      delete[] static_cast<int*>(current->next);
    }
    delete[] static_cast<Address*>(current->next);
  }
  Int operator[](const int index);
  ~Array() {
    delete_address(top);
    delete[] size;
  }
};
class Int {
  void* data;

  int level;
  Array* array;

 public:
  Int(int index, int _level = 0, void* _data = NULL, Array* _array = NULL)
      : level(_level), data(_data), array(_array) {
    if (_level < 1 || index >= array->size[_level - 1]) {
      data = NULL;
      return;
    }
    if (level == array->dim) {
      // 이제 data 에 우리의 int 자료형을 저장하도록 해야 한다.
      data = static_cast<void*>((
          static_cast<int*>(static_cast<Array::Address*>(data)->next) + index));
    } else {
      // 그렇지 않을 경우 data 에 그냥 다음 addr 을 넣어준다.
      data = static_cast<void*>(static_cast<Array::Address*>(
                                    static_cast<Array::Address*>(data)->next) +
                                index);
    }
  };

  Int(const Int& i) : data(i.data), level(i.level), array(i.array) {}

  operator int() {
    if (data) return *static_cast<int*>(data);
    return 0;
  }
  Int& operator=(const int& a) {
    if (data) *static_cast<int*>(data) = a;
    return *this;
  }

  Int operator[](const int index) {
    if (!data) return 0;
    return Int(index, level + 1, data, array);
  }
};
Int Array::operator[](const int index) {
  return Int(index, 1, static_cast<void*>(top), this);
}
}  // namespace MyArray

int main() {
  int size[] = {2, 3, 4};
  MyArray::Array arr(3, size);

  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 4; k++) {
        arr[i][j][k] = (i + 1) * (j + 1) * (k + 1);
      }
    }
  }
  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 4; k++) {
        std::cout << i << " " << j << " " << k << " " << arr[i][j][k]
                  << std::endl;
      }
    }
  }
}

 

또한 순회를 위해, Array 클래스 내부에 Iterator 클래스를 넣어 준다.

class Iterator {
    int* location;
    Array* arr;

    friend Int;

   public:
    Iterator(Array* arr, int* loc = NULL) : arr(arr) {
      location = new int[arr->dim];
      for (int i = 0; i != arr->dim; i++)
        location[i] = (loc != NULL ? loc[i] : 0);
    }
    Iterator(const Iterator& itr) : arr(itr.arr) {
      location = new int[arr->dim];
      for (int i = 0; i != arr->dim; i++) location[i] = itr.location[i];
    }
    ~Iterator() { delete[] location; }
    // 다음 원소를 가리키게 된다.
    Iterator& operator++() {
      if (location[0] >= arr->size[0]) return (*this);

      bool carry = false;  // 받아 올림이 있는지
      int i = arr->dim - 1;
      do {
        // 어차피 다시 돌아온다는 것은 carry 가 true
        // 라는 의미 이므로 ++ 을 해야 한다.
        location[i]++;
        if (location[i] >= arr->size[i] && i >= 1) {
          // i 가 0 일 경우 0 으로 만들지 않는다 (이러면 begin 과 중복됨)
          location[i] -= arr->size[i];
          carry = true;
          i--;
        } else
          carry = false;

      } while (i >= 0 && carry);

      return (*this);
    }
    Iterator& operator=(const Iterator& itr) {
      arr = itr.arr;
      location = new int[itr.arr->dim];
      for (int i = 0; i != arr->dim; i++) location[i] = itr.location[i];

      return (*this);
    }
    Iterator operator++(int) {
      Iterator itr(*this);
      ++(*this);
      return itr;
    }
    bool operator!=(const Iterator& itr) {
      if (itr.arr->dim != arr->dim) return true;

      for (int i = 0; i != arr->dim; i++) {
        if (itr.location[i] != location[i]) return true;
      }

      return false;
    }
    Int operator*();
  };

 

Iterator는 다음과 같이 활용한다.

 Int Array::Iterator::operator*() {
  Int start = arr->operator[](location[0]);
  for (int i = 1; i <= arr->dim - 1; i++) {
    start = start.operator[](location[i]);
  }
  return start;
}
 
 int main() {
  MyArray::Array::Iterator itr = arr.begin();
  for (int i = 0; itr != arr.end(); itr++, i++) (*itr) = i;
  for (itr = arr.begin(); itr != arr.end(); itr++)
    std::cout << *itr << std::endl;
}

*itr 를 어떻게 이해하면 좋을까? Array::Iterator::operator*() 를 통해, 우리는 Iterator에서 * 연산자를 취하면 Int 타입이 리턴된다는 것을 알 수 있다.

리턴된  Int 타입은, 다시 Int wrapper 클래스 내부의 int( )연산자를 통해 int 타입으로 변환되므로, ostream에서 이를 문제없이 오버로딩할 수 있는 것이다.

Comments