[STL] 연산자 오버로딩(2)

치치·2025년 1월 12일

STL

목록 보기
2/21
post-thumbnail

배열 인덱스 연산자 오버로딩 ([ ]연산자)

배열의 요소에 접근할 때 사용하는 [ ] 연산자 오버로딩
일반적으로 많은 객체를 저장하고 관리하는 객체에 사용된다
컨테이너 객체에 주로 사용 -> 컨테이너 객체가 관리하는 내부 원소에 접근할때 사용됨

[ ]연산자 예시

  • Array 클래스를 만들고 생성자 초기화 리스트를 사용해 매개변수로 아무값도 안받을 시 기본값을 100 으로 지정
  • 매개변수로 값을 받을 경우 그 값만큼 배열의 크기로 지정
  • 아래의 코드에서는 size값은 0부터 시작, capacity값은 10으로 시작
#include <iostream>
using namespace std;

class Array
{
	int* arr;
	int size;
	int capacity;

public:
	Array(int cap = 100) : arr(0), size(0), capacity(cap)
	{
		arr = new int[capacity];
	}

	~Array()
	{
		delete [] arr;
	}

	void Add(int data)
	{
		if (size < capacity)
		{
			arr[size++] = data;
		}
	}

	int Size() const
	{
		return size;
	}

	// 상수타입 -> 읽기 전용
	int operator[] (int idx) const
	{
		return arr[idx];
	}

	// 읽기 & 쓰기 전용
	int & operator[] (int idx)
	{
		return arr[idx];
	}
};


int main()
{
	Array ar(10);
	ar.Add(10);
	ar.Add(20);
	ar.Add(30);

	cout << ar[0] << endl;

	const Array & ar2 = ar;
	cout << ar2[0] << endl;

	ar[0] = 100;

	// ar2[0] = 100; // 에러 발생
}
  • [ ]연산자 오버로딩은 a[i] = 19 처럼 값을 변경하는 쓰기 연산도 가능해야 하기 때문에 const함수와 비const함수를 모두 제공해야한다

  • ar[0]의 의미 : ar.operator[ ].(0)

결과값: 배열의 크기는 10이고 [0][1][2] 인덱스에 값을 추가



메모리 접근, 클래스 멤버 접근 연산자 오버로딩(*, -> 연산자)

  • 연산자, -> 연산자는 스마트 포인터나 반복자(iterator) 등의 특수한 객체에 사용된다

일반포인터의 경우

  1. 사진처럼 Point클래스 타입의 p1 포인터를 사용하여 Heap영역에 동적으로 메모리를 할당한다

  2. p1 포인터는 동적으로 생성된 객체의 시작주소를 가리킨다

  3. 포인터를 통해 객체의 함수에 접근할 수 있다 ( p1 -> Print( ) )

4. 일반 포인터를 사용하면 new를 사용해서 동적할당
-> 직접 delete로 꼭 메모리를 해제를 해주어야함
-> 그렇지 않으면 메모리누수가 발생함!!

#include <iostream>
using namespace std;

class Point
{
	int x;
	int y;

public:
	Point(int _x = 0, int _y = 0) : x(_x), y(_y) {}
	void Print() const
	{
		cout << x << ',' << y << endl;
	}
};


int main()
{
	Point* p1 = new Point(2, 3);
	Point* p2 = new Point(5, 5);

	p1->Print();
	p2->Print();

	delete p1;
	delete p2;

	return 0;
}

스마트 포인터의 경우

[Point클래스]
1. Point클래스를 생성하고 생성자 초기화 리스트로 초기값을 설정한다
2. Point클래스 내에 Print( )함수 정의

[PointPtr클래스]
1. Point타입의 ptr포인터 멤버변수 생성
-> Point 타입을 가리키는 포인터
2. 생성자에서 인자로 주소값 받아온 뒤 ptr의 값으로 지정

#include <iostream>
using namespace std;

class Point
{
	int x;
	int y;

public:
	Point(int _x = 0, int _y = 0) : x(_x), y(_y) {}
	void Print() const
	{
		cout << x << ',' << y << endl;
	}
};

class PointPtr
{
	Point * ptr;

public:
	PointPtr(Point* p) : ptr(p) { }

	~PointPtr()
	{
		delete ptr;
	}

	Point* operator -> () const
	{
		return ptr;
	}
};


int main()
{
	PointPtr p1 = new Point(2, 3);
	PointPtr p2 = new Point(5, 5);

	p1->Print();
	p2->Print();

	return 0;
}
  • 메인함수에서 PointPtr 타입의 p1객체를 생성하고 동적으로 생성된 Point(2, 3)의 주소값을 가리킨다 (사진)

  • new : 메모리를 할당하고 해당 메모리의 주소값을 반환

  • Point의 주소값을 인자로 PointPtr 생성자에 전달한다
    -> p1객체는 Point를 가리키는 중

  • PointPtr 클래스의 ptr이 p1과 동일하게 동적할당된 메모리를 가리킨다

    스마트 포인터를 일반 포인터 처럼 사용하려면

    내가 가리키고 있는 동적할당된 메모리의 클래스 멤버에 접근할 수 있어야 하는데 그러기 위해서는 operator함수가 필요하다

  • -> 포인터가 가리키는 객체의 멤버에 접근하는 데 사용된다

  • 메인함수에서 p1 객체를 생성하고 p1->Print( )를 호출하면
    PointPtr 클래스에 정의해둔 p1.operator -> ( )호출된다

  • operator -> ( )함수는 Point타입인데 ptr을 반환하기 때문
    -> ptr은 동적으로 할당된 메모리를 가리키는 중
    -> 반환된 ptr을 통해서 할당된 메모리의 클래스에 접근이 가능하고 멤버변수와 멤버함수에도 접근이 가능해진다

  • 따라서 p1->Print( )를 호출하게되면 p1객체를 통해서 Point클래스 내부에 정의한 Print( )함수를 호출할 수 있다




스마트 포인터 다시!! 정리

  • 내가 이해를 너무 못해서 한단계식 차근차근 정리를 해보겠다
  1. Point클래스는 인자로 받은 값으로 멤버변수인 x와 y값을 지정
    Print( )는 해당 값을 출력하는 함수
  • PointPtr 클래스에서는 Point타입 객체를 가리키는 포인터가 멤버변수로 정의되어있다

  • 생성자에서 주소값을 인자로 받아온 뒤 ptr포인터가 해당 주소값을 가리키게된다

  • 메인함수에서 동적으로 메모리를 할당하게 되면 p1은 PointPtr 클래스 타입인 스마트 포인터 객체이다

  • p1은 Point객체의 주소값을 가리키고 있고 인자로 주소값이 넘어가게 되면 해당 ptr 포인터도 그 주소값을 가리키게 된다
    (쉽게 말하자면 p1(스마트포인터 객체) 안에 ptr이라는 일반 포인터가 생성된 것)

  • ptr이 가리키는 주소를 delete하게되면 메모리가 해제된다

  1. 메인함수에서 함수를 호출할때, 일반 포인터 처럼 작동하기 위해서 연산자 오버로딩을 작성한다

[ Point * operator -> ( ) const ]

  • 메인에서 p1 -> Print( );를 호출하게 되면 Pointptr클래스의 오버로딩으로 들어가 ptr을 반환하게 된다
  • p1 -> Print( ); == p1.operator -> ( ) Print( )호출
    -> 여기서 ptr은 해당객체를 가리키는 포인터임
    -> 결국 p1 -> Print( ); == ptr -> Print( ); 인것
    -> ptr이 가리키는 곳이 해당 객체이니 해당객체의 출력함수를 호출

[ Point & operator * ( ) const ]

  • 메인에서(*p1).Print( );를 호출하게 되면,(*p1) 은 p1이 가리키는 객체 자체를 의미한다
  • 다시말해, p1 스마트포인터 객체는 현재 동적할당된 메모리를 가리키고 있다 -> 즉, 해당객체를 바로 가리키고 있는 것
  • 그 p1 스마트 포인터를 가리키는곳을 접근하여 함수를 호출한다는 것
  • (*p1).Print( ); == p1.operator *( ).Print( ) 호출

스마트 포인터 역할

  1. delete를 호출하지 않아도 자동으로 메모리가 해제된다
  2. 위의 이유로 메모리 누수를 방지해준다
  3. 핵심은 스마트 포인터 내부의 ptr은 실제 객체의 주소를 담고 있는 일반 포인터로, 이 ptr을 통해 접근 & 객체를 관리한다

    -> ptr이 내부적으로 사용되기는 하지만 호출할 때 일반 포인터처럼 작동하기 위해 연산자 오버로딩을 작성하여 감춰둔 것!!
    -> 왜냐하면 연산자 오버로딩 안에 결국 반환값은 ptr이기 때문 -> ptr이 사용되고 있긴 하다는 것

스마트 포인터 전체코드

#include <iostream>
using namespace std;

class Point
{
	int x;
	int y;

public:
	Point(int _x = 0, int _y = 0) : x(_x), y(_y) {}
	void Print() const
	{
		cout << x << ',' << y << endl;
	}
};

class PointPtr
{
	Point * ptr;

public:
	PointPtr(Point* p) : ptr(p) { }

	~PointPtr()
	{
		delete ptr;
	}

	// 스마트 포인터
	Point* operator -> () const
	{
		return ptr;
	}

	Point& operator * () const
	{
		return *ptr;
	}

};


int main()
{
	PointPtr p1 = new Point(2, 3);
	Point * p2 = new Point(5, 5);

	p1->Print(); // p1.operator-> () Print() 호출
	p2->Print();

	(*p1).Print(); // p1.operator* () Print() 호출
	(*p2).Print();


	return 0;
}

p1객체는 스마트 포인터, p2 객체는 일반 포인터


타입 변환 연산자 오버로딩

사용자가 직접 정의해서 사용할 수 있는 타입 변환 2가지 방법
1. 생성자를 이용한 타입변환
2. 타입 변환 연산자 오버로딩을 이용한 타입 변환

타입 변환을 하기전에 우선 정의를 잘 알고 가보자
형변환 : 어떤 자료형으로 선언된 변수를 다른 자료형으로 변환하는 것

  • 명시적 형변환 : 코드에 직접 변환 될 자료형을 입력해야 하는것
  • 암시적 형변환 : 코드에 직접 변환 될 자료형을 입력하지 않아도 되는 것

생성자를 이용한 타입변환

클래스 내부에 특정 타입을 인자로 받는 생성자가 있다면 생성자 호출로 타입변환이 가능하다! (객체를 생성 후 대입)

#include <iostream>
using namespace std;

class A
{

};

class B
{
public:
	B() { cout << "B" << endl; }

	B(A& _a) { cout << "B(A& _a)" << endl; }

	B(int n) { cout << "B(int n)" << endl; }

	B(double d) { cout << "B(double d)" << endl; }

};

int main()
{
	A a;
	int n = 10;
	double d = 5.5f;

	B b;

	b = a;
	b = n;
	b = d;

	return 0;
}
  1. b = n; 으로 예시를 들면 b = n; 를 호출하여 B(int n) 생성자가 호출되면 B타입의 임시객체가 생성된다
  2. B타입 임시객체가 복사나 이동으로 b에 대입된다
  3. 대입되면 임시객체가 소멸한다
  4. 즉, b = n; 에서 int타입인 n을 B타입 객체인 b에 바로 대입할 수 없기 때문에, n이 대입연산되는 동안만 잠시 B타입처럼 동작하는 것 (생성자에 인자로 받게 해뒀기 때문에 가능)

하지만, 클래스에 만약 정수로 인자를 받는 생성자가 있다고 한다면, 메인함수에서 객체에 정수값을 대입해서 생성자를 호출해도 오류없이 실행이 된다
-> 의도치 않게 생성자를 통해 형변환이 될 수 있다는 것

이럴경우 사용하는 것이 explicit 키워드


생성자 타입변환X - explicit

  • 예시 코드)
    B타입 클래스의 생성자 앞에 explicit 키워드를 붙였다

  • 이제부터 메인함수에서 생성자를 호출할때 명시적으로 밖에 호출할 수 없다!! (객체에 대입을 통해 할 수 없음)

  • 객체에 바로 정수값을 대입하면 에러

  • 객체에 매개변수로 값을 넣어주면 명시적으로 생성자 호출 가능!

암시적 생성자 형변환을 의도하지 않는다면 인자를 같는 생성자는 explicit 생성자로 만들어주는 것도 방법이다



타입 변환 연산자 오버로딩을 이용한 타입변환

  • 원래의 연산자 오버로딩은 operator 키워드앞에 반환타입이 있지만 타입 변환 연산자는 반환타입을 지정하지 않는다
#include <iostream>
using namespace std;

class A
{

};


class B
{
public:
	operator A()
	{
		cout << "operator A()호출" << endl;
		return A();
	}
	operator int()
	{
		cout << "operator int()호출" << endl;
		return 10;
	}
	operator double()
	{
		cout << "operator double()호출" << endl;
		return 5.5;
	}
};

int main()
{
	A a;
	int n;
	double d;

	B b;
	a = b;
	n = b;
	d = b;
	
	cout << endl;

	a = b.operator A();
	n = b.operator int();
	d = b.operator double();

	return 0;
}
  • 타입 변환 연산자 오버로딩은 해당 클래스 내에 정의해두면 그 클래스 타입의 객체가 다른 형으로 변환될때 호출된다
    -> 여기서 n = b;로 예시를 들어보겠다

  • n = b;를 호출하면 B클래스 내부의 타입 변환 연산자 오버로딩이 호출된다
    -> 의미는 B타입 객체인 b를 int 타입으로 변환하겠어요!!
    -> 오버로딩 함수 내부의 실행구문을 지나 10을 반환한다
    -> n = 10이 된다



profile
뉴비 개발자

0개의 댓글