+, -, / 등의 primitive operator들에 대해 사용자가 정의한 동작을 수행시킬 수 있다. 예를 들어 어떤 클래스 Food는 + 연산자가 기본으로 없지만 사용자 정의를 통해 + 연산을 하도록 만들 수 있다.
구현은 클래스의 멤버 또는 글로벌 함수로 가능하다. 글로벌 함수로 구현되었을 때는 연산자에 필요한만큼(+의 경우 2개)의 인자를 요구한다. 멤버 함수로 정의된 경우에 자기 자신은 this 포인터를 통해 접근할 수 있다. 따라서 단항 연산자의 경우 멤버 함수로 오버로딩 되었을 때는 인자가 필요없다.
class Integer {
public:
int value;
Integer() = default;
Integer(int value): value(value) {
}
Integer operator+(const Integer &i) const {
return Integer(this->value + i.value);
}
Integer & operator++() {
++this->value;
return *this;
}
};
Integer operator-(const Integer &i1, const Integer &i2) {
return Integer(i1.value - i2.value);
}
int main()
{
Integer i1(1), i2(2);
std::cout << (i1 + i2).value << std::endl;
std::cout << (i1 - i2).value << std::endl;
++i1;
std::cout << i1.value << std::endl;
return 0;
}
위 코드에서 + 연산자는 멤버 함수로 오버로딩 되었고, – 연산자는 전역 함수로 오버로딩 되었다. 연산자 수행 결과로 Integer 객체가 return by value 형식으로 리턴된다. 단항 연산자인 ++도 오버로딩 된것을 확인할 수 있다.
++i와 i++은 사용자 정의 객체에서 같을 수 없다. 전자의 경우 ++연산을 수행 한 후에 객체를 리턴하고, 후자의 경우 객체를 리턴 한 후에 ++연산을 수행한다. 따라서 i++의 경우 복사 생성자를 만들어내기 때문에 전자에 비해 속도가 떨어진다. Primitive Type인 int 같은 경우에는 컴파일러가 자동으로 최적화해주지만 사용자 객체는 그럴 수 없다.
이미 존재하는 객체에 다른 객체를 대입할 때가 있다.
class Integer {
public:
int value;
Integer() = default;
Integer(int value): value(value) {
}
Integer & operator=(const Integer &i) {
this->value = i.value;
return *this;
}
};
int main()
{
Integer i1(1), i2(2);
i1 = i2;
std::cout << i1.value << std::endl;
return 0;
}
다음과 같이 대입 연산자를 오버로딩 할 수 있다. 복사 생성자와 마찬가지로 멤버 변수에 포인터가 존재하는 경우에는 기존 멤버 변수에 할당된 공간을 제거하고 새로 할당해야한다. 이 때 원본이 자기 자신인지 체크할 필요가 있다. 원본이 자기자신일 때 멤버 변수에 할당된 공간을 지워버리면 맛이간다.
지난 챕터에서 다룬 것처럼, 복사 대입 연산자가 있으면 복사 생성자 / 소멸자 / 이동 생성자 / 이동 대입 연산자를 모두 구현해야한다.
class Integer {
public:
int value;
Integer() = default;
Integer(int value): value(value) {
}
Integer operator+(const Integer &i) {
return Integer(this->value + i.value);
}
Integer & operator=(const Integer &i) {
this->value = i.value;
return *this;
}
};
int main()
{
Integer i1(1), i2(2);
i1 = i2 + 3; // Works
i1 = 3 + i2; // Error
std::cout << i1.value << std::endl;
return 0;
}
위 코드에서 i1은 되고 i2는 되지 않는 이유는 무엇인가. i2 + 3에 대해서는 Integer의 오버로드 연산자 + 가 실행된다. Integer의 매개변수 생성자 Integer(int value)에 의해 3은 Integer로 변환되어 결국 Integer + Integer의 연산이 호출된다.
반면에 3 + i2에서는 Primitive Type의 연산자 + 가 호출된다. Integer를 int로 바꿀 수 없기 때문에 이 구문은 적합한 연산자를 찾지 못해 에러를 낸다. 이 때 전역 오버로딩을 사용해 다음과 같이 연산자를 정의할 수 있다.
Integer operator +(int x, const Integer &y) {
Integer temp(x + y.value);
return temp;
}
전역 오버로드 연산자는 객체의 Private 연산자에 접근할 수 없다. 물론 Public한 Access를 줄 수도 있지만 그렇게 하지 않을 때 friend 키워드를 통해 처리할 수 있다.
class Integer {
private:
int value;
public:
Integer() = default;
Integer(int value): value(value) {
}
Integer & operator=(const Integer &i) {
this->value = i.value;
return *this;
}
friend Integer operator +(int x, const Integer &y);
};
Integer operator +(int x, const Integer &y) {
return Integer(x + y.value);
}
int main()
{
Integer i1(1), i2(2);
i1 = 3 + i2;
return 0;
}
다음의 코드에서 클래스의 멤버 변수 value는 private으로 클래스 외부에서 접근할 수 없다. 하지만 friend 키워드가 있는 함수로는 접근할 수 있다. 연산자 뿐만 아니라 함수나 클래스도 friend로 지정해서 private 멤버에 접근할 수 있게 할 수 있다.
기본 타입의 포인터는 그 할당과 삭제를 수동으로 관리해야한다. 이 때문에 Memory leak과 같은 문제 발생 가능성이 높다. 스마트 포인터의 개념은 어떤 포인터가 더 이상 사용되지 않을 경우 즉시 할당된 공간을 반환하고, 자신을 nullptr로 초기화시켜 위 문제를 해결하도록 하는 것이다.
std::unique_ptr<T> 유니크 포인터는 해당 주소를 단독으로 가리키기 위해 사용된다. 따라서 복사와 대입이 불가능하며, Scope를 벗어나면 자동으로 포인터가 가리키는 주소의 객체도 지워버린다.
int main()
{
std::unique_ptr<Integer> i(new Integer(1));
return 0;
}
위와 같이 Integer 객체를 할당하고 그 주소를 unique_ptr로 참조시키면 Main 함수가 종료되면서 Scope가 끝이나고, 자동으로 소멸자가 호출되면서 Integer 객체를 삭제한다.
소유자가 단 하나인만큼 Move 연산은 가능하다.
std::shared_ptr<T> 공유 포인터의 경우 해당 객체를 여러 곳에서 참조할 수 있게 허용한다. 객체의 소멸 시기는 해당 객체의 참조 카운트가 0일 때이다.
void printValue(std::shared_ptr<Integer> i) {
std::cout << i->value << std::endl;
}
int main()
{
std::shared_ptr<Integer> i(new Integer(1)); // Reference count = 1
printValue(i); // Reference count = 2
return 0;
}
다음과 같이 객체의 참조 카운트가 변화한다. 여기서 참조 카운트는 객체의 원시 포인터를 가리키는 객체가 몇 개인지에 따라 결정된다. 만약 위의 printValue가 포인터가 아닌 레퍼런스를 인자로 받도록 했다면 객체의 참조 카운트가 바뀌지 않았을 것이다.
Explicit 키워드가 붙은 생성자는 컴파일러가 암시적으로 호출할 수 없다. 반드시 코드에 명시적으로 호출 구문이 있어야만 가능하다.
예를 들어 Integer i = 3과 같은 구문에서 매개변수 생성자인 Integer(int)가 존재하면, 컴파일러가 암시적으로 Integer 객체를 3을 가지고 생성해서 복사 생성자에 대입할 수 있다. 하지만 explicit 키워드를 매개변수 생성자에 붙이면 이런 암시적인 변환이 일어나지 못하므로 에러가 발생한다.
Integer 클래스 i1에 대해서 int x = i1과 같은 연산을 어떻게 수행할 수 있을까. Integer 클래스에 변환 연산자를 구현하면 가능하다.
class Integer {
public:
int value;
Integer(int value): value(value) {
}
operator int() {
return this->value;
}
};
int main()
{
Integer i1(1);
int x = i1;
}
변환 연산자의 형식은 operator <T>()이며 따로 인자를 받지 않는다.
합격왕 우여곡절 끝에 드디어 합격왕에 광고를 붙였다. 서비스를 시작한지 무려 4년이 지나서야 드디어 광고를 시작하게…
반복적인 일상 매일 아침 일어나 회사에 출근하고, 저녁을 먹고 돌아오는 일상의 반복이다. 주말은 가족을 보러…
Planning A well-structured plan has the following characteristics: First, the ultimate vision you aim to…
English The most common problem for English learners like myself is that we often use…
This website uses cookies.