연산자 다중정의
연산자 다중 정의란?
- C++의 연산자들은 C++에서 제공되는 기본 자료형에 대해서만 정의되어 있기 때문에 사용자가 정의한 클래스에 대해서는 적용할 수 없다.
- 예를 들어 + 연산자는 int나 double 등의 자료형에 대해서는 정의되어 있지만 클래스 객체에 대해선느 정의돼있지 않다.
Complex1 c1(10, 20);
Complex1 c2(-5, 15);
Complex1 c3 = c1 + c2; // Error - Complex1을 위한 + 연산자 없음
- 복소수라는 것은 실수를 포함하는 수의 표현으로, 이에 대해서도 double형에서와 마찬가지로 사칙연산을 포함한 여러 가지 연산이 수학적으로 정의돼 있다.
- 그러므로 Complex1 객체에 대해서도 이러한 연산을 사용할 수 있도록 하면 보다 자연스러운 문장으로 프로그램을 작성할 수 있을 것이다.
- 이와 같이 연산자 다중 정의는 C++에서 제공하는 연산자를 사용자가 선언한 클래스의 객체에 대하여 사용할 수 있도록 다중정의하는 것이다.
- 연산자를 다중정의하는 구문은 함수와 같은 형식을 취하고 있으며, 임의의 문장을 사용하여 임의의 동작을 하도록 정의할 수 있다.
- 연산자의 우선순위를 바꾼다든지, 연산방법을 바꾸는 것 등은 연산자 다중정의를 통해서는 할 수 없다. 예를 들어 이항 연산자인 && 연산자를 단항 연산자로 정의할 수 없다.
- 의미가 비슷하다 할지라도 기존 연산자가 갖는 고유한 특성이 유지되게 하는 것도 중요.
- 예를 들어 ++ 연산자를 정의하는 경우 ++a와 a++라는 표현의 차이를 이해하고 다중정의도 이러한 특성이 반영되도록 할 필요가 있다.
- 연산자를 다중정의하는 위치는 클래스의 멤버로 정의할 수도 있고 별도의 일반 함수와 같이 정의할 수도 있다.
- 기본적으로는 클래스의 멤버로 선언하는 것으로 하고 불가피한 경우는 클래스 외부의 일반 함수 형식으로 정의하게 될 것이다.
단항 연산자의 다중정의
- 단항 연산자는 연산자가 피연산자 앞에 있는 것과 뒤에 있는 것이 있다.
- 연산자를 피연산자 앞에 사용하는 것을 전위 표기법이라 하고, 연산자가 뒤에 위치하도록 사용하는 것을 후위 표기법이라고 한다.
- 단항 연산자의 다중정의는 이와 같은 단항 연산자를 우리가 정의하는 자료형이나 클래스에서 사용할 수 있게 한다.
- 단항 연산자는 전위 표기를 하였는가 후위 표기를 하였는가에 따라 그 결과가 다르므로 이를 구분하여 연산자 다중정의를 한다.
전위 표기법
ReturnClass ClassName::operator opSymbol()
{
...
}
// opSymbol: ++, -- 등의 단항 연산자 기호
- 연산자를 다중정의하는 것은 함수를 정의하는 것과 크게 다르지 않다.
- 다만 함수 이름 대신 키워드 operator와 연산자 기호를 사용하는 점이 다르다.
- 또한 전위 표기법에 대한 단항 연산자를 정의할 때에는 인수가 없다는 점을 주의해야 한다.
- 단항 연산자에는 피연산자가 하나(해당 객체)이므로 다른 인수를 기입하지 않는다.
class IntClass1 {
int a;
public:
IntClass1(int n=0): a{n} {} // 생성자
IntClass1& operator ++ (){ // 전위 표기 ++ 연산자 다중 정의
++a; // 전위 증가에 맞게 증가 시키는 것을 제일 먼저
return *this; // * 연산의 이유: 반환 타입을 참조 타입으로 함
}
int getValue() const return a;
}
- operator ++는 객체의 상태(
int a)를 변경하므로 const 키워드를 사용하지 않는다.
후위 표기법
- 후위 표기법은 전위 표기법과 반대로 연산자를 뒤에 기입하는 방법이다.
- 후위 표기 단항 연산자를 다중정의하는 구문
ReturnClass ClassName::operator opSymbol(int)
{
...
}
// opSymbol: ++, -- 등의 단항 연산자 기호
- 함수의 인수 기입 위치에 형식매개변수의 이름 없이 int라고 기입한 것은 int형 인수를 전달한다는 의미가 절대 아니다.
- 이 연산자가 후위 표기법을 사용하는 단항 연산자임을 알려 주는 것이다.
- 이것은 C++에서 사용하는 특수한 표현 방법 중 하나라고 알아두어야 한다.
class IntClass2 {
int a;
public:
IntClass2(intn=0): a{n} {} // 생성자
IntClass2 operator ++ (int) { // 후위 표기 연산자 다중정의
// 복사 생성자의 매개변수 전달위한 * 연산
// 새로운 객체에 후위 연산 전의 원본의 상태를 복사해둠
IntClass2 tmp(*this)
++a;
return tmp;
}
int getValue() const { return a; }
}
// Pencils.h
class Pencils {
int dozens; // 타
int np; // 낱개
public:
Pencils(): dozens{0}, np{0} {};
Pencils(int n)
{dozens = n / 12; np = n % 12; }
Pencils(int d, int n): dozens{d}, np{n} {}
Pencils& operator++(); // 전위 연산자 다중정의
Pencils operator++(int); // 후위 연산자 다중정의
void display() const;
}
// Pencils.cpp
#include <iostream>
#include "Pencils.h"
using namespace std;
Pencils& Pencils::operator++() {
if(++np >= 12)
++dozens, np = 0;
return *this
}
Pencils& Pencils::operator++(int) {
Pencils tmp(*this) // 현재 객체를 보존
if(++np >= 12)
++dozens, np = 0;
return tmp; // 보존된 객체를 반환
}
void Pencils::display() const
{
if(dozens) {
cout << dozens << "타";
if(np) cout << np << "자루";
cout << endl;
}
else
cout << np << "자루" << endl;
}
// PnclMain.cpp
#include <iostream>
#include "Pencils.h"
using namespace std;
int main() {
Pencils p1(5, 7);
Pencils p2(23);
p1.display();
(++p1).display();
p1.display();
cout << endl;
p2.display();
p1 = p2++;
p1.display();
p2.display();
return 0;
}
5타 7자루
5타 8자루
5타 8자루
1타 11자루
1타 11자루
2타
이항 산술 연산자 및 관계 연산자의 다중정의
// Complex2.h
#include <ostream>
using namespace std;
class Class2 {
double rPart, iPart; // 실수부 및 허수부
public:
// 생성자
Complex2(double r=0, double i=0): rPart(r), iPart(i) {}
Complex2 conj() const {
return Complex1(rPart, -iPart);
}
Complex2 operator+(const Complex2& c) const;
...
};
ReturnClass ClassName::operator opSymbol(ArgClass arg)
{
...
}
산술 연산자의 다중정의
덧셈 연산자의 다중정의
-
- 복소수 객체와 복소수 객체의 덧셈 연산자
- 덧셈 연산을 수행한 후 두 피연산자의 내용이 바뀌면 안 된다. 따라서 operator+와 매개변수 c를 const로 선언하여 *this와 c의 내용이 변경되지 않도록 한 것을 볼 수 있다.
- tmp는 덧셈 결과를 저장할 임시 객체로서 *this의 값으로 초기화되도록 복사 생성자를 사용하고 있다.
- 임시로 사용되는 객체 대신 보다 간편하게 임시 객체를 사용하는 방법이 있다.
Complex2 Complex2::operator+(const Complex2& c) const
{
return Complex2(rPart + c.rPart, iPart + c.iPart);
}
Complex2 Complex2::operator+(const Complex2& c) const
{
Complex2 tmp(*this);
tmp.rPart += c.rPart;
tmp iPart += c.iPart;
return tmp;
}
-
- 복소수 객체와 실수의 덧셈 연산자
Complex2 Complex2::operator+(double r) const
{
return Complex2(rPart + r, iPart);
}
클래스에 속하지 않는 연산자 다중정의
- 연산자를 다중정의할 때 고려해야할 것은 피연산자 순서이다.
- 연산자 다중정의는 기본적으로 교환 법칙이 성립하지 않는다.
- a가 Complex2 클래스의 객체의 참조자일 때, a + 10.0 형태의 수식을 위한 연산자는 Complex2 클래스의 멤버로 정의할 수 있다.
- 하지만 10.0 + a라는 수식은 사용할 수 없다.
- a + 10.0의 operator+는 정의되어 있는 반면 10.0 + a의 operator+는 정의되어 있지 않다.
- 좌측 피연산자인 10.0은 double 형이고 이를 Complex2 객체와 더하는 연산자는 정의되어 있지 않다.
- 10.0 + a 형태의 수식을 사용하기 위해서는 이를 위한 연산자를 별도로 선언해야 한다.
- 그런데 이 수식의 좌측 피연산자는 Complex2 클래스의 객체가 아니라 기본 자료형 중의 하나인 double형 값이므로이 수식을 위한 연산자는 Complex2 클래스 내가 아닌 클래스와는 별개로 외부에서 선언하고 다중정의해야 한다.
- double + Complex2 형태의 수식을 사용하기 위한 연산자 다중정의
class Complex2 {
...
}
Complex2 operator+(double r, const Complex2& c) const {
return Complex2(r+c.rPart, c.iPart);
}
- 그런데 여기에는 문제가 있는데, 매개변수 c의 rPart 및 iPart는 c의 private 데이터 멤버이므로 Complex2 클래스 외부 영역인 operator+의 구현부에서는 c의 private 멤버인 rPart 및 iPart에 접근할 수 없다.
- 문제를 해결할 수 있는 2가지 방법이 있는데 하나는 Complex2 클래스 내에 데이터 멤버 값을 읽을 수 있는 public 멤버함수를 정의하는 것이다.
class Complex2
...
public:
...
double real() const { return rPart; }
double imag() const { return iPart; }
class Complex2 {
...
}
Complex2 operator+(double r, const Complex2& c) const {
return Complex2(r+c.real(), c.imag());
}
- 다른 한 가지 방법은 friend 키워드를 이용하여 이 함수를 Complex2 클래스의 친구 함수로 지정하는 것이다.
class Complex2 {
...
public:
...
friend Complex2 operator+(double r, const Complex2& c) const;
}
Complex2 operator+(double r, const Complex2& c) const {
return Complex2(r+c.rPart, c.iPart);
}
- freind 키워드를 사용하면 클래스 내부에 구현을 기입해줄 수도 있다.
class Complex2 {
...
public:
...
friend Complex2 operator+(double r, const Complex2& c) const {
return Complex2(r+c.rPart, c.iPart);
}
}
+= 연산자의 다중정의
- += 연산은 호출 객체의 값이 변경되어야 하므로 const 함수로 지정되지 않는다.
Complex2& Complex2::operator+=(const Complex2& c) {
rPart += c.rPart; iPart += c.iPart;
return *this;
}
관계 연산자의 다중정의
bool Complex2::operator==(const Complex2& c) const
{
return rPart == c.rPart && iPart == c.iPart;
}
스트림 입출력 연산자의 다중정의
<< 연산자를 정의할 위치
- 표준 출력 스트림에 정수형 변수 a에 저장된 값을 출력하려면
cout << a;
- Complex2 클래스의 객체 c를 출력할 때에도
cout << c;
- 이와 같이 표현하고자 할 때, << 연산자의 좌측 피연산자인 cout은 Complex2 클래스의 객체가 아니라 표준 출력 스트림을 나타내는 객체이다.
- 따라서 << 연산자를 Complex2 클래스에서 직접 다중정의할 수 없음을 알 수 있다.
- 이 때는 객체의 private 데이터 멤버를 반환하는 함수를 정의하든지 friend 키워드를 이용하는 방법을 사용할 수 있다.
- << 연산자를 연쇄적으로 사용(메서드 체이닝)하기 위해선 operator<< 함수의 반환 타입은 첫 번째 매개변수 타입과 동일한 ostream&이여야만 한다.
class Complex2 {
...
public:
...
friend ostream& operator<<(ostream& os, const Complex2& c);
}
ostream& operator<<(ostream& os, const Complex2& c)
{
os << "(" << c.rPart;
if(c.iPart > 0)
os << "+j" << c.iPart;
else if(c.iPart < 0)
os << "-j" << -c.iPart;
cout << ")";
return os;
}
대입 및 이동 대입 연산자의 다중정의
- 객체에 대한 기본적인 대입 연산은 객체의 데이터 멤버를 그대로 복사하는 방식
- 만약 객체에 동적으로 할당된 메모리를 가리키는 포인터가 포함되어 있을 경우
- 포인터의 값만 복사하므로 값을 받을 객체와 값을 제공하는 객체가 동일한 메모리를 가리키는 공유된 상태를 만들게 되어 문제를 일으킨다.
- 이는 묵시적 복사 생성자 문제와 유사하다.
- 이러한 유형의 클래스는 복사 생성자를 정의하는 것과 같은 이유로 대입 연산자를 정의할 필요가 있다.
- 또한 어떠한 객체에 임시 객체를 대입할 경우에는 임시 객체의 내용을 이동함으로써 대입을 효율적으로 처리할 수 있도록 하는 것이 바람직하다.
// VecF.h
#ifndef VEC_F_H_INCLUDED
#define VEC_F_H_INCLUDED
#include <iostream>
#include <cstring>
using namespace std;
class VecF{
int n;
float* arr;
public:
VecF(int d, float* a=nullptr): n {d} { // 생성자
arr = new floatp[d];
if(a){
memcpy(arr, a, sizeof(float) * n);
}
}
VecF(const VecF& fv): n {fv.n} { // 복사 생성자
arr = new float[n];
memcpy(arr, fv.arr, sizeof(float) * n);
}
VecF(VecF&& fv): n {fv.n}, arr {fv.arr} { // 이동 생성자
fv.arr = nullptr;
fv.n = 0;
}
~VecF(){
deletep[] arr;
}
VecF add(const VecF& fv) const {
VecF tmp(n);
for(int i = 0; i < n; i++){
tmp.arr[i] = arr[i] + fv.arr[i];
}
return tmp;
}
void print() const {
cout << "[ ";
for(int i = 0; i < n; i++){
cout << arr[i] << " ";
}
cout << "]";
}
}
대입 연산자의 다중 정의
VecF& VecF::operator=(const VecF& fv)
{
if(n != fv.n) // 벡터의 크기가 다르면
{
deletep[] arr; // 기존 배열 메모리를 반환
arr = new float[n=fv.n]; // 새로 메모리 할당
}
memcpy(arr, fv.arr, sizeof(float)*n) // 데이터 복사
return *this
}
- 대입 연산의 좌측 피연산자는 *this이며, 우측 피연산자는 형식 매개변수인 fv이다. fv는 VecF의 상수 참조로 선언한다.(const 참조)
- VecF 클래스의 이동 대입 연산자 다중 정의
VecF& VecF::operator=(VecF&& fv){
delete[] arr;
n = fv.n;
arr = fv.arr;
fv.arr = nullptr;
return *this;
}
- 이동 대입 연산의 좌측 피연산자는 *this이며, 우측 피연산자는 형식 매개변수 fv이다.
- fv는 임시 객체를 받을 수 있도록 r-value 참조로 선언하였다.(VecF&&)
- fv의 내용을 *this에 얕은 복사를 한 후 fv의 arr에 nullptr을 대입함으로써 fv의 내용이 *this에 이동되도록 하였다.
int main()
{
float a[3] = {1, 2, 3};
float b[3] = {2, 4, 6};
VecF v1(3, a);
VecF v2(3, b);
VecF v3(3);
VecF v4(v1); // 복사 생정자
VecF v5(v1.add(v2)) // 이동 생성자
v3 = v1; // 대입 연산자
cout << v3 << endl;
v3 = v1.add(v2); // 이동 대입 연산자
}
- 두 변수의 값을 교환해야 하는 경우가 많이 있는데, 이 경우 이동 대입을 활용하면 효율을 높일 수 있다.
- 대입하는 대상이 l-value 참조이면 복사 생성자 혹은 대입 연산자가 선택되어 사용되는데, 이 경엔 새로운 메모리를 할당하고 값을 복사하게 된다.
- 복사해야 하는 요소가 많아질수록 비효율적이다.
void swapVecF(VecF& v1, VecF& v2)
{
VecF tmp = v1; // 복사 생성자 사용
v1 = v2; // 대입 연산자 사용
v2 = tmp // 대입 연산자 사용
}
- 이러한 대입에 이동 대입 연산자를 사용할 수 있다면 효율을 크게 높일 수 있는데 이를 위해 표준 라이브러리의 move 함수를 이용할 수 있다.
- move 함수는 인수로 전달되는 객체의 r-value 참조를 반환하는 함수이다.
void swapVecF(VecF& v1, VecF& v2){
VecF tmp = move(v1); // 이동 생성자 사용
v1 = move(v2); // 이동 대입 연산자 사용
v2 = move(tmp); // 이동 대입 연산자 사용
}
[] 연산자의 다중정의
- [] 연산자는 배열의 첨자를 저장하는 연산자이다. [] 연산자를 다중 정의하면 보다 명료한 프로그램을 작성할 수 있다.
class SafeIntArray {
int limit; // 원소의 개수
int* arr; // 데이터 저장공간
public:
SafeIntArray(int n): limit(n) {
arr = new int[n]; // 공간 할당
}
~SafeIntArray() {
delete[] arr;
}
int size() const { return limit; }
// i번째 원소를 반환하는 멤버함수
int& operator[](int i) { // 반환 타입을 l-value 참조로
if(i < 0 || i >= limit) {
std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
exit(EXIT_FAILURE);
}
return arr[i]; // i번째 원소 값 반환
}
int operator[](int i) const { // const 함수 선언: const 객체에서 호출 가능
if(i < 0 || i >= limit)
std::cout << "첨자가 범위를 벗어나 프로그램을 종료합니다.";
exit(EXIT_FAILURE);
}
return arr[i]; // i번째 원소 값 반환
}
}
int main()
{
SafeIntArray a[10];
for(int i = 0; i < 10; i++)
a[i] = i; // int& operator[](int i) 시그니처의 함수 호출
cout << a[5] << endl; // int operator[](int i) const 시그니처의 함수 호출
}
- SafeIntArray 클래스에 정의된 [] 연산자는 2개이다.
- 반환 타입이 l-value 참조 타입 int&로 선언된 [] 연산자는 [] 연산자로 SafeIntArray 객체 안의 배열 요소에 값을 대입할 수 있게 해준다.
- const로 선언된 [] 연산자는 const로 선언된 SafeIntArray 객체에 대해 [] 연산자로 객체 안 배열 요소의 값을 읽을 수 있게 해준다.
- [] 연산자가 const로 선언되지 않았다면 다음과 같은 함수는 컴파일 에러가 발생한다.
void f(const SafeIntArray& arr)
{
for(int i = 0; i < arr.size(); i++)
cout << arr[i] << endl; // Error
}
자료형의 변환 연산 다중정의
- 서로 다른 클래스의 객체들 사이에서도 묵시적 형 변환이 일어나도록 할 수 있다.
class Meter{
int m; // 미터
int cm; // 센티미터
public:
// 생성자
Meter(): m(0), cm(0) {}
Meter(int meter, int cmeter): m(meter), cm(cmeter){}
void display() const {
if(m)
cout << m << "m";
if(cm || !m)
cout << cm << "cm";
cout <<endl;
}
int getM() const { return m;}
int getCm() const { return cm; }
}
class Feet{
int ft; // 피트
int in; // 인치
public:
Feet(): ft(0), in(0) {}
Feet(int f, int i): ft(f), in(i) {}
Feet(const Meter &m) // Meter -> Feet 형 변환 연산자
{
int cmeter = m.getM() * 100 + m.getCm();
in = static_cast<int>(cmeter/2.54 + 0.5);
ft = in / 12;
in %= 12;
}
operator Meter() const { // Feet -> Meter 형 변환 연산자
int m = static_cast<int>((ft * 12 + in) * 2.54 + 0.5);
return Meter(m / 100, m % 100);
}
void display() const {
if(ft)
cout << ft << "m";
if(in || !in)
cout << in << "cm";
cout <<endl;
}
}
int main()
{
Meter mLen;
Feet fLen(10, 5);
mLen = fLen; // operator Meter() const 연산자 호출
fLen = mLen; // Feet(const Meter &m) 연산자 호출
}
연산자 다중정의의 주의 사항
- 연산자 중에는 다중정의를 할 수 없는 것도 있다.
- 멤버 선택 연산자(.)
- 멤버에 대한 포인터 연산자(*)
- 유효범위 결정 연산자(: :)
- 조건 연산자(?:)