각 연산자별로 주의해야할 점들이 많고 고유하게 적용되는 규칙들이 있다.
이번 글에서는 하나하나 알아보도록 하자.
관계 연산자는 동일한 타입의 두 객체에 대해 상등 및 대소를 비교하는 연산자이다.
다음 예시를 보도록 하자.
#include <iostream>
class Num
{
private:
int x;
public:
Num() {}
Num(int _x) : x(_x)
{}
bool operator==(const Num& right) const // ==
{
return x == right;
}
bool operator!=(const Num& right) const // !=
{
return !(*this == right);
}
bool operator>(const Num& right) const // >
{
return x > right;
}
bool operator>=(const Num& right) const // >=
{
return (*this == right || *this > right);
}
bool operator<(const Num& right) const // <
{
return !(*this >= right);
}
bool operator<=(const Num& right) const // <=
{
return !(*this > right);
}
};
증감 연산자는 값을 1 증가시키는데 이게 전위•후위 증감자에 따라 표현법이 조금 달라진다.
우선 코드부터 보도록 하겠다.
class Num
{
private:
int x;
public:
Num() {};
Num(int x) : (_x)
{}
Num& operator++() // 전위
{
x++;
return *this;
}
const Num operator++(int dummy) // 후위
{
Num ret = *this;
++(*this);
return ret;
}
};
결론부터 말하자면 전위 증감 연산자와 후위 증감 연산자를 이름이나 인수 목록으로 구분 할 수 없다.
둘 다 A.operator++() 형태를 띄우고 있는데 이를 구분하기 위해서 C++은 더미 인수를 쓰는 방법을 쓰기로 결정했다.
전위는 증가되는 객체 외에는 인수를 취하지 않으면 후위형은 연산 대상인 객체 외에도 정수형의 더미 인수를 하나 더 추가하여 오버로딩을 성립하기로 한 것이다.
이를 통해 컴파일러가 둘을 구분할 수 있게 된 것이다.
이건 일종의 약속이라 어쩔 수 없다.
또 주목해야 할 점은 둘의 리턴값이 다르다는 것이다.
전위형은 Num&를 리턴하고 후위형은 const Num을 리턴한다.
1. 전위형 - Num&
2. 후위형 - const Num
우선 전위형은 ++++A같은 식이 성립되어야 하기 때문이다. 연속적으로 실행되어야 하기 때문에 참조형으로 반환해야 한다.
만약 Num으로 반환한다면 첫 번째 ++에는 A가 증가할테지만 두 번째 ++에는 첫 번째의 *this의 사본을 증가 시키게 되며 최종적으로 A는 한번만 증가하게 될 것이다.
후위형의 경우는 값을 먼저 평가한 후 증가해야 하므로 객체 자체를 리턴할 수 없으며 값만 리턴할 수 있다..
또한 리턴된 임시 객체를 변경시키는 것은 의미가 없으며 A++++은 금지되어야 한다.
그러므로 아예 시도를 하지 못하게 막아버리는 것이다.
가급적이면 전위형의 연산자를 사용하는 것이 효율적이므로 더 유리하다.
대입 연산자는 초창기에도 한번 언급한 적이 있다.
우선 코드는 다음과 같다.
class Num {
private:
int x;
public:
Num()
{}
Num(int _x) : x(_x)
{}
Num& operator=(const Num& right)
{
this->x = right.x;
return *this;
}
Num& operator+=(const Num& right)
{
this->x += right.x;
return *this;
}
};
우선 리턴 값이 참조형인 이유는 리턴 후에도 연속적으로 대입이 되어야 하기 때문이다.
A = B = C;
그리고 함수가 상수형이 아닌 이유는 객체 자체가 값이 바뀌기 때문이다.
그리고 주의해야할 점이 있는데 이것은 이 글을 통해서 보도록 하자.
C++의 표준 스트림 출력 객체인 cout은 << 연산자를 오버로딩하여 이 연산자의 우변을 표준 출력으로 내보내는 기능을 한다.
이것은 자료형을 따지지 않고 대부분 출력되는데 그 이유는 cout 객체의 소속 클래스인 ostream에 이미 << 멤버 연산자 함수가 오버로딩되어 있기 때문이다.
그럼 객체는 어떻게 출력할 수있을까?
class Num
{
friend ostream& operator<<(ostream& c, const Num& N);
friend ostream& operator<<(ostream& c, const Num* pN);
private:
int x;
public:
Num() {}
Num(int _x) : x(_x)
{}
void OutNum()
{
std::cout << x;
}
};
ostream& operator<<(ostream& c, const Num& N)
{
c << N.x;
return c;
}
ostream& operator<<(ostream& c, const Num* pN)
{
c << *pN;
return c;
}
우선 생각할 수 있는 방법은 두 가지다.
1. ostream 클래스에 멤버 함수를 추가하는 것
2. 전역 함수로 추가하는 것
결론부터 말하자면 1번은 안된다. 왜냐하면 ostream 클래스는 이미 기본 자료형의 연산자처럼 컴파일러 단위에서 수정하는걸 막았기 때문이다.
그럼 전역 함수로 추가해야 하는 건데 그 방법은 위와 같다. 이 전에 전역 연산자 함수를 추가하는 것과 똑같은 방법이다.
레퍼런스로 리턴되는 이유는 이 전과 마찬가지로 연속적으로 연산을 해야 하는 상황이 있기 때문이다.
cout << x << y << z;
[] 연산자는 배열에서 첨자 번호로부터 요소를 찾는 연산자이다.
이는 반드시 멤버 함수로만 정의할 수 있으며 전역 함수로는 정의할 수 없다.
우선 코드를 먼저 보도록 하자.
class NArr
{
private:
int arr[3];
public:
NArr()
{
arr[0] = 1;
arr[1] = 5;
arr[2] = 10;
}
int& operator[](int index)
{
return arr[index];
}
const int& operator[](int index) const
{
return arr[index];
}
};
우선 똑같은 내용이 두 번 정의 된 것을 볼 수 있다.
1. 비상수
2. 상수
우선 배열의 동작은 대표적으로 읽고 쓰기가 가능해야 한다.
그렇기 때문에 비상수 객체에 대해서는 첫 번째 정의가 되지만 만약 상수 객체는 어떻게 하는가?
상수 객체는 읽기만 가능할 뿐 쓰기가 허용되어선 안된다.
하지만 비상수 버전만 정의해 놓는다면 상수 객체는 함수의 상수성이 지정되어 있지 않으면 일반 멤버 함수를 호출할 수 없다.
그렇기 때문에 상수 객체를 위해서 하나 더 정의한다고 생각하면 된다.
하지만 이것 하나만을 위해서 새로 정의하는건 불편하지 않을까?
사실 한가지 더 이유가 있다.
void func(const Narr *ptr) {...}
이렇게 포인터로 넘겼을 때 해당 포인터가 가리키고 있는 객체에 대한 수정을 불가능하게 하였을 경우 상수 객체를 위한 오버로딩도 따로 만들어줘야 하는 것이다.
그럼 만약 arr[0][1] = 1; 이런 식으로 호출하면 어떻게 될까?
하지만 이는 쉽게 구분할 수 있다. 앞쪽의 [0]는 전체 배열 arr에 대해 쓰여졌으므로 이 연산자는 본래의 배열 첨자 연산자이며 arr 배열에서 0번째 요소를 선택하고, 뒤쪽의 [1]은 arr[0]에 대해 쓰여졌으며 arr[0]은 NArr 타입의 객체이므로 이 연산자는 오버로딩된 연산자를 호출한다.
클래스나 구조체의 멤버를 참조하는 연산자에는 . 과 -> 두 가지가 있다.
이중 . 연산자는 기본적인 연산자라 오버로딩이 불가능하다.
그럼 -> 연산자는 가능한데 여기에 조금 특별한 규칙이 적용된다.
원래 이항 연산자인데 마치 오버로딩하면 단항 연산자가 되며 전역 함수로는 정의할 수 없고 클래스의 멤버 함수로만 정의할 수 있다.
멤버 함수이면서 단항이므로 결국 인수를 취하지 않는다.
이 연산자의 리턴 타입은 클래스나 구조체의 포인터로 고정되어 있다.
이를 사용하게 되면 객체의 멤버 객체를 마치 자기 자신처럼 사용할 수 있다.
#include <iostream>
#include <cstring>
struct Person
{
char Name[32];
int Age;
};
class Book
{
private:
Person Writer;
public:
Book(const char* _Name, int _age)
{
strcpy(Writer.Name, _Name);
Writer.Age = _age;
}
Person* operator->()
{
return &Writer;
}
};
int main(void)
{
Book bkr("jason", 24);
std::cout << bkr->Name << ' ' << bkr->Age << std::endl;
return 0;
}
-> 연산자 오버로딩의 본체를 보면 분명 멤버 객체의 주소를 반환하고 끝나는 것처럼 보이는데 어떻게 출력물은 Writer의 내부 멤버들의 값을 불러들일 수 있을까?
사실 bkr->Name 표현식은 컴파일러에 의해 다음과 같이 해석된다.
bkr->()->Name;
즉, -> 연산자는 숨겨진 멤버의 포인터를 읽어 줌과 동시에 이 멤버에 속한 멤버를 바로 액세스 할 수 있도록 중계하는 역할을 한다.
()도 함수를 호출하는 일종의 연산자다. 두둥
다른 연산자와 달리 매개 변수의 개수가 정해져 있지 않다는 것이 특징이다. 이항일수도 있고 단항일 수도 있고 세 개 이상의 인수를 요구할 수도 있다.
#include <iostream>
class Sum
{
public:
int operator()(int a, int b, int c, int d)
{
return a + b + c + d;
}
};
int main(void)
{
Sum S;
std::cout << S(1, 2, 3, 4) << std::endl;
return 0;
}
호출부를 다르게 표현하면 다음과 같다.
S()(1, 2, 3, 4);
매개 변수의 개수가 다양하게 쓸 수 있다는 것은 굉장히 큰 장점이 되며 () 연산자 오버로딩 한 객체를 함수 객체(Functor)라고 한다.
http://www.soen.kr/lecture/ccpp/cpplec.htm
C/C++ 강좌
www.soen.kr
C++ 오버라이딩 (0) | 2023.08.07 |
---|---|
C++ 상속 (0) | 2023.08.07 |
C++ 전역 연산자 함수 (0) | 2023.08.05 |
C++ 연산자 함수 (0) | 2023.08.05 |
C++ 상수 멤버, const (0) | 2023.08.05 |