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

사진처럼 Point클래스 타입의 p1 포인터를 사용하여 Heap영역에 동적으로 메모리를 할당한다
p1 포인터는 동적으로 생성된 객체의 시작주소를 가리킨다
포인터를 통해 객체의 함수에 접근할 수 있다 ( 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( )함수를 호출할 수 있다


PointPtr 클래스에서는 Point타입 객체를 가리키는 포인터가 멤버변수로 정의되어있다
생성자에서 주소값을 인자로 받아온 뒤 ptr포인터가 해당 주소값을 가리키게된다
메인함수에서 동적으로 메모리를 할당하게 되면 p1은 PointPtr 클래스 타입인 스마트 포인터 객체이다
p1은 Point객체의 주소값을 가리키고 있고 인자로 주소값이 넘어가게 되면 해당 ptr 포인터도 그 주소값을 가리키게 된다
(쉽게 말하자면 p1(스마트포인터 객체) 안에 ptr이라는 일반 포인터가 생성된 것)
ptr이 가리키는 주소를 delete하게되면 메모리가 해제된다

[ Point * operator -> ( ) const ]
[ Point & operator * ( ) const ]

#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;
}

하지만, 클래스에 만약 정수로 인자를 받는 생성자가 있다고 한다면, 메인함수에서 객체에 정수값을 대입해서 생성자를 호출해도 오류없이 실행이 된다
-> 의도치 않게 생성자를 통해 형변환이 될 수 있다는 것
이럴경우 사용하는 것이 explicit 키워드
예시 코드)
B타입 클래스의 생성자 앞에 explicit 키워드를 붙였다
이제부터 메인함수에서 생성자를 호출할때 명시적으로 밖에 호출할 수 없다!! (객체에 대입을 통해 할 수 없음)
객체에 바로 정수값을 대입하면 에러
객체에 매개변수로 값을 넣어주면 명시적으로 생성자 호출 가능!
암시적 생성자 형변환을 의도하지 않는다면 인자를 같는 생성자는 explicit 생성자로 만들어주는 것도 방법이다


#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이 된다