* 본 글을 읽기 전에 미리 구조체와 C++의 Class 개념에 대해 미리 알고 학습을 하는 것을 권장드립니다.
일반적으로 C언어에서 사용하는 +, -, ==, [] 와 같은 기본 연산자들은 모두 C언어에서 기본적으로 정의 되어 있는 데이터 타입 (int, double, char 등) 에서만 사용 가능한 연산이였기에, 사용자가 정의해서 만든 타입인 구조체로 만든 구조체 변수의 경우 이러한 연산자를 적용할 수 없었습니다.
#include <stdio.h>
struct point{
int x;
int y;
};
int main(){
struct point pos1 = {10,20};
struct point pos2 = {20,40};
if(pos1 == pos2){ //오류 발생
printf("same");
}
return 0;
}
예를 들어서 struct point 라는 구조체 변수 pos1, pos2를 2개 만들고 pos1 과 pos2가 같은지 확인하기 위해 pos1 == pos2 와 같은 연산을 수행하는 것은 C언어에서는 불가능하다는 것입니다.
왜냐하면 연산자로 비교를 할때 == 는 단순히 값이 같은지 비교하는 연산자기 때문에 구조체 변수의 경우 여러 멤버 변수가 존재하여 == 연산자로 같음 동작을 어떻게 처리해야 할지 모르기 때문입니다. 한마디로 point라는 구조체의 "==" 는 멤버 변수가 자신의 int x 가 상대방의 int x 랑만 같아야 하는지, 아니면 모든 멤버 변수가 각각 같아야 하는지 이런 내용들이 정의되어 있지 않기 때문이죠.
하지만 놀랍게도 C++ 에서는 사용자 정의 연산자를 사용할 수 있으며, 이러한 것은 연산자 오버로딩(Operator Overloading) 에 의해 실현됩니다. 기본 연산자들을 직접 사용자가 정의하는 것을 연산자를 오버로딩(Overloading) 한다고 부릅니다.
일반적으로 함수의 오버로딩이라고 하면 이름은 같지만 인자를 다르게 한 함수를 여러개 만들어서 사용하는 것을 '함수를 오버로딩' 이라고 했다고 표현했습니다.
연산자 오버로딩도 동일하게, 기본적으로 함수의 오버로딩과 같이 연산자도 하나의 함수라는 개념을 이용해서 중복 정의 하는 것을 의미합니다.
=> 기본 연산자를 우리가 설계한 클래스(또는 구조체)에 맞게 직접 사용하는 것을 '연산자를 오버로딩 했다' 라고 표현합니다.
아래는 재정의가 가능한 연산자입니다. :: (범위 지정) , . (멤버 지정) , .* (멤버 포인터로 멤버 지정) 연산자를 제외한 우리가 생각하는 모든 연산자가 재정의 가능하다고 보시면 됩니다. (사실 말한 3가지 연산자 이외에도 sizeof 연산자나 static_cast 같은 연산자 등등 예외가 일부 존재하나 우리가 사용하고 싶어하는 연산자들은 대부분 됩니다.)
대표적으로 아래와 같습니다.
- +, -, * 등 산술 연산자
- +=, -= 와 같은 축약형 연산자
- >= , == 와 같은 비교 연산자
- && , || 와 같은 논리 연산자
- -> , * 과 같은 멤버 선택 연산자 (여기서 * 는 곱하기가 아닌, 역참조 연산자. 포인터에서 사용하는 *p 를 의미)
- ++, -- 증감 연산자 (전위, 후위에 따라 모두 오버로딩 가능)
- [] 배열 연산자 (첨자 연산자, Subscript 연산자라고도 함) , () 연산자까지 (함수 호출 연산자)
보시면 아시겠지만 배열 접근할때 쓰는 a[] 연산자도 재정의 되고, ++, -- 도 전위에 따라 모두 재정의 가능합니다. C++의 높은 자유도를 보여주는 기능 중 하나가 연산자 오버로딩이라고 보시면 됩니다!
예제를 보면서 익힐 수 있도록, 본 예제에서는 수학에서 2차원 좌표를 Class로 설계해서 알아보도록 하겠습니다.
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
};
간단하게 설계해온 2차원 좌표(x,y) 의 한 점을 저장하는 Class 타입인 Point를 한번 가져와봤습니다 ㅎㅎ.
물론 구조체로 하면 더 간단하긴 한데 캡슐화를 잘 지키도록 설계했고, 나중에 기능 추가를 많이 한다고 가정하고 우선 Class로 사용해 봅시다.
위 Point Class 의 경우 간단하게 기본 생성자는 (x,y) 를 (0, 0) 으로 수행하는 동작, 인자로 x,y 를 제공해 Point(x, y) 와 같은 형태로 호출하면 멤버 변수의 x, y를 들어온 인자값으로 초기화 하는 행위를 하며,
print_point() 멤버 함수(메서드)는 자신의 x , y 값을 출력해주는 역할을 하게 됩니다.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
};
int main()
{
Point p1;
Point p2(1, 2);
Point result = p1 + p2; //error!
result.print_point(); //expected (1, 2)
}
만약에 Point 객체를 두개 더하면 각각 자신의 x,y 멤버 변수의 값을 더해서 합쳐진 x , y 값을 가진 Point 객체를 반환하는 형태의 연산을 진행하고 싶다고 가정해 봅시다.
즉 p1 에 (0,0) 이 저장되어 있고, p2 에 (1,2) 가 저장되어 있는데 result = p1 + p2 를 하면 result에는 p1 과 p2의 각 x , y 값을 더한 (0+1, 0+2) 를 가지는 Point 객체가 저장되도록 하고 싶다는 겁니다.
Point result = p1 + p2; //error!
/*
이러한 피연산자와 일치하는 "+" 연산자가 없습니다.C/C++(349)
main.cpp(23, 23): 피연산자 형식이 Point + Point입니다.
*/
당연히 기본 + 연산자의 경우 이러한 동작은 기본적으로 정의되어 있지 않기 때문에 위 코드라인에서 오류가 발생하겠죠. 이제 연산자 오버로딩을 통해 사용자 연산자를 정의함으로써 p1 과 p2를 더할 수 있게 해봅시다!!
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
Point operator+(const Point &p2)
{
// 자신의 값과 합치려는 들어온 p2의 값을 더해서
// 새로운 Point 객체를 만들어서 리턴
return Point(x + p2.x, y + p2.y);
}
};
int main()
{
Point p1;
Point p2(1, 2);
Point result = p1 + p2;
result.print_point();
}
>>> x: 1, y: 2
코드를 조금 추가해줬더니 이제 p1 과 p2 가 정상적으로 합쳐져서, 터미널에 x: 1, y: 2 가 나온걸 알 수 있습니다.
Point operator+(const Point &p2)
{
// 자신의 값과 합치려는 들어온 p2의 값을 더해서
// 새로운 Point 객체를 만들어서 리턴
return Point(x + p2.x, y + p2.y);
}
우리가 보아야 할 부분은 추가한 이 부분입니다. 보시면 operator+ 라는 이름이 매우 특이한 함수가 정의되어 있습니다. 이 함수는 p2라는 객체 변수를 const 레퍼런스로 받아서 읽고, 자신의 멤버 변수 x , y 와 p2 객체의 x , y 값을 읽어서 합친 다음 그 합친 값으로 새로운 Point 객체를 만들어서 반환하고 있습니다.
Point result = p1 + p2;
result.print_point();
이제는 p1과 p2를 합치는 행위가 컴파일 오류 없이 정상적으로 수행되며, result로 point 값을 찍어보면 1,2 라는 값이 제대로 찍히는걸 볼 수 있습니다.
이제 Point result = p1 + p2 를 아래와 같은 코드로 조금 수정해봅니다.
// Point result = p1 + p2; 주석처리
Point result = p1.operator+(p2);
위 처럼 코드를 실행해도 아무 문제 없이 제대로 동작한다는걸 알 수 있습니다.
위 코드는 p2 객체를 p1 객체의 operator+ 함수로 인자값으로 넘겨 호출 합니다. operator+ 함수를 호출하는 방식을 통하던, + 만 써서 처리하던 결과는 100% 같습니다.
즉, + 연산자를 사용해 Point 객체를 대상으로 연산을 진행시 , 알아서 그 객체의 operator+ 함수를 호출해서 수행한다는 뜻입니다.
p1 + p2 == p1.operator+(p2)
결론적으로, p1 + p2 는 p1.operator+(p2) 로 해석되어 컴파일 됩니다. 컴파일러가 연산자를 함수로 변경하여 호출하게 됩니다! 그리고 그 연산자 함수는 결과값을 반환(return) 하게 되겠죠.
Point result = p1 + p2;
이 코드의 흐름을 살펴보자면 p1 + p2는 p1.operator+(p2) 로 해석되어 함수로써 호출되고, operator+ 함수는 p1 과 p2의 각각 x, y 값을 더한 Point 임시 객체를 반환할 것이며, 이것은 result에 그대로 전달되게 됩니다.
당연하지만 operator+가 아니여도 뺄셈, 곱하기 등등.. (operator-, operator* ..) 와 같은 형식으로 정의만 했다면 모두 사용이 가능합니다.
Point operator+(const Point &p2)
{
return Point(x - p2.x, y - p2.y);
}
물론 이렇게 Point 끼리 + 연산을 하면 멤버 변수를 서로 빼도록 하여 반환할 수도 있습니다. 이러면 Point 변수 2개를 더했더니 좌표가 서로 빠져서 나온 Point 객체가 반환되는 괴이한 연산을 만들 수도 있습니다 (...)
괴이하다곤 했지만 자유도가 높다는 것을 보여드린 것이죠.
(리턴 타입) operator(연산자) (연산자가 받는 인자)
즉 연산자 오버로딩을 하고 싶으면 다음과 같은 형태로 오버로딩을 원하는 연산자 함수를 제작하면 됩니다.
연산자가 받은 인자 부분에 타입을 잘만 정의하면, int 타입과 Point 타입을 더하는 오버로딩 구현도 가능하고, 다양한 타입을 연산하는 오버로딩 구현이 가능합니다.
참고로, 위 방법 이외에 함수 이름으로 절대 연산자를 넣을 수 없습니다.
그리고 추가적으로 말씀드리지 않았지만, 현재 구현한 오버로딩 방식은 멤버 함수를 이용한 오버로딩 방식입니다. 즉 p1 + p2를 만나면 p1.operator+(p2) 로 해석되기 때문에, p1 입장에선 p2의 값이 들어와서 호출되는 형태로, 인자가 하나인 것입니다.
연산자 오버로딩을 구현하는 방법은 실제로 2가지가 있습니다.
1. 멤버 함수에 의한 연산자 오버로딩
2. 전역 함수에 의한 연산자 오버로딩
우선 멤버 함수에 의한 연산자 오버로딩을 조금 더 알아보고, 전역 함수에 의한 연산자 오버로딩을 알아보겠습니다.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
Point operator+(const Point &p2)
{
// 자신의 값과 합치려는 들어온 p2의 값을 더해서
// 새로운 Point 객체를 만들어서 리턴
return Point(x + p2.x, y + p2.y);
}
Point operator+(int add)
{
// 자신의 값과 합치려는 들어온 p2의 값을 더해서
// 새로운 Point 객체를 만들어서 리턴
return Point(x + add, y + add);
}
};
int main()
{
Point p1;
Point p2(1, 2);
Point result = 10 + p1; //error
result.print_point();
}
이번에는 조금 다른 의도로 Point 객체와 int 형 값을 더하는 오버로딩을 구현하도록 해봅시다.
우리의 의도는 Point 객체에 정수값을 더하면 해당 객체의 x , y 값에 모두 그 값 정수값만큼 더해서 새로운 Point 객체를 만들어서 반환한다고 가정해봅시다.
Point result = 10 + p1; //error
/*
이러한 피연산자와 일치하는 "+" 연산자가 없습니다.C/C++(349)
main.cpp(36, 23): 피연산자 형식이 int + Point입니다.
*/
기쁜 마음으로 컴파일 하였지만 해당 라인에서 오류가 발생합니다.
왜 여기서 오류가 발생할까요?
아까 전에도 봤듯이 p1 + p2는 p1.operator+(p2) 와 같은 형태로 해석이 된다고 했습니다.
그러면 10 + p1 은 실제로 10.operator+(p1) 으로 해석이 되겠죠. 10 이라는 정수형 숫자에 operator+ 라는 멤버 변수도 없을 뿐더러 p1이라는 객체를 넘기는 형태가 되었습니다. 솔직히 사람 입장에선 p1 + 10 이나, 10 + p1이나 덧셈이라 어짜피 똑같은데 안되는것도 좀 짜증나지만, 순서도 신경써야 하는게 더 햇갈립니다.
p1 + p2 가 p1.operator+(p2) 로 해석되기 때문에 p1의 입장에서 생각해야 되기도 하고요.. 피연산자는 2개인데 인자값은 1개라 햇갈리죠?
이러한 문제를 이제 연산자 오버로딩의 2번째 방법 전역 함수에 의한 연산자 오버로딩으로 해결해봅시다.
전역 함수(비 멤버 함수)에 의한 연산자 오버로딩
앞에서 구현한 멤버 함수를 통한 오버로딩은, "객체.operator+(피연산자), 객체 + 피연산자" 와 같은 형태로 이루어져 자료형이 다른 두 피연산자를 대상으로 연산시, 반드시 객체가 왼쪽에 위치해야 했습니다. (객체가 자신의 연산자 오버로딩 함수를 호출하기 위해)
피연산자 + 피연산자 = operator+(피연산자, 피연산자)
그러나 전역 함수를 통한 오버로딩은 "operator+(피연산자, 피연산자), 피연산자 + 피연산자" 와 같은 형태로 객체의 순서가 뒤에 위치해도 정상적인 결과를 출력합니다. 또 인간의 입장에선 a + b 와 같은 연산 수행 시 a의 입장에서 b가 들어와 호출되는게 아니라 operator+(a, b) 와 같은 형태로 호출된다고 이해하는게 더 편리합니다. 피연산자의 갯수와 , 인자의 갯수가 일치하는 형태인것이죠!
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
// friend 선언 필수
friend Point operator+(int add, const Point &p);
};
Point operator+(int add, const Point &p)
{
return Point(p.x + add, p.y + add);
}
int main()
{
Point p1(1, 2);
Point result1 = 10 + p1;
result1.print_point();
}
>>> x: 11, y: 12
위에서 본 멤버 함수를 통한 오버로딩과 코드가 살짝 달라졌습니다.
이번에도 변경된 부분에 주목하면서 확인해봅시다.
friend Point operator+(int add, const Point &p);
우선 Point Class 안에 friend 키워드로 operator+ 함수를 선언하고 있습니다.
friend 키워드를 붙여준 이유는, operator+가 클래스의 멤버 함수가 아니기 때문에, 자신의 멤버 변수(private 변수, protected) 변수에 접근할 수 있도록 권한을 부여해주는 것입니다.
Point operator+(int add, const Point &p)
{
return Point(p.x + add, p.y + add);
}
이제 멤버 함수가 아닌 전역 함수로써 operator+ 함수가 선언되어 있습니다.
이 함수는 아까전에 Point class가 friend로 선언해줘서, private 로 되어있는 Point 클래스의 int x , int y 멤버 변수에 직접 접근이 가능합니다.
Point result1 = 10 + p1;
이제 main 함수에서 다음과 같은 코드를 수행하면
operator+(10, p1)
실제로 다음과 같이 해석됩니다. operator+ 함수에 정수 10과, p1 객체가 전달되었고, 이제 operator+ 함수는 들어온 값을 통해 임시 Point 객체를 반환하며, 이것은 result1에 전달되게 됩니다.
이제 전역변수로 오버로딩 함으로써 객체가 오른쪽에 위치해도 정상적으로 덧셈이 진행되는 겁니다!
Point result2 = p2 + 10;
물론 전역함수 오버로딩 예제에서는 기존에 객체가 왼쪽에 위치해서 더하는 멤버 함수 오버로딩 코드를 지워서 다음과 같이 호출하면 오류가 발생할 겁니다. 왜냐면 현재 + 연산자 오버로딩은 Point operator+(int add, const Point &p) 와 같이 int, Point 순서로 오버로딩 되어있기 때문입니다.
10 + p2 , p2 + 10 은 실제로 동일한 작업을 수행해야 합니다. p2 + 10 과 같은 수행을 원하시면 앞전에서 배운 멤버 함수 오버로딩으로 코드를 다시 추가해주시거나 Point operator+(const Point &p, int add) 처럼 Point, int 순서의 오버로딩 함수를 하나 더 추가해주시면 됩니다.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
// friend 선언 필수
friend Point operator+(int add, const Point &p);
};
Point operator+(int add, const Point &p)
{
return Point(p.x + add, p.y + add);
}
Point operator+(const Point &p, int add)
{
operator+(add, p);
}
int main()
{
Point p1(1, 2);
Point p2(3, 4);
Point result1 = 10 + p1;
Point result2 = p2 + 10;
result1.print_point();
result2.print_point();
}
즉 전역변수 오버로딩으로 p1 + 10 이나 10 + p1 모두 작동하게 하고 싶으면 위와 같이 구현하면 됩니다.
이미 10 + p1 에 대해 구현을 해두었으므로, p1 + 10 과 같은 순서로 오버로딩 하는건
Point operator+(const Point &p, int add)
{
operator+(add, p);
}
그냥 위처럼 방금 구현해둔 전역 operator+ 함수를 인자 순서를 바꿔서 호출해주시면 됩니다.
인자 순서만 바꿔서 호출하면 되므로 추가로 뭔가 구현할 필요도 없습니다.
나머지는 C++의 함수 오버로딩 동작에 의해 알아서 타입이 맞게 잘 호출이 될 겁니다.
비슷하게 어떤 사용자 타입에 대해 + 연산자와 = 연산자를 미리 구현해뒀으면 += 연산자는 기존에 + 연산자, = 연산자를 잘 조합해서 구현하면 됩니다. (a+=b 는 어짜피 a = a + b 와 동일하기 때문에) 즉, 처음에 기초적인 연산자만 잘 구현해두면 순서를 바꾸거나, 축약형에 불과한 연산자들은 알아서 쉽게 구현이 따라오게 됩니다.
증감 연산자 오버로딩 (전위, 후위)
아까 소개했듯이 i++, ++i, i--, --i 와 같이 일반적으로 변수의 증가나 증감에 사용하는 증감 연산자(++, --) 의 경우에도 당연히 오버로딩이 가능합니다.
그런데 증감 연산자의 경우엔 i++ , ++i 와 같이 후위와 전위에 따라 동작이 조금 다르잖아요?
이것도 어떻게 하는지 한번 알아봅시다.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
// 전위 증감 연산자
Point operator++()
{
x += 1;
y += 1;
return *this; //증가한 자기 자신 (객체)를 반환
}
// 후위 증감 연산자
Point operator++(int)
{
Point temp(*this); //현재 객체를 이용해 임시 객체를 생성 (실제로 반환할 객체)
// 자신의 값은 증가
x += 1;
y += 1;
return temp; //증가 이전의 임시 객체를 반환
}
};
int main()
{
Point p1(1, 2);
p1++.print_point(); // (1, 2)
p1.print_point(); // (2, 3)
p1 = Point(1, 2);
(++p1).print_point(); // (2, 3)
p1.print_point(); // (2, 3)
}
>>>
x: 1, y: 2
x: 2, y: 3
x: 2, y: 3
x: 2, y: 3
// 전위 증감 연산자
Point operator++()
// 후위 증감 연산자
Point operator++(int)
전위 증감 연산자와, 후위 증감 연산자를 오버로딩 하는 방법은 다음과 같습니다.
전위는 ++operator , 후위는 ++operator 이라고 나타낼 거 같지만 그렇지 않습니다.
전위 증감 연산자는 operator++() 로, 후위 증감 연산자는 operator++(int) 로 나타냅니다.
++p1 = p1.operator++(); // 전위 증가 연산
p1++ = p1.operator++(int); // 후위 증가 연산
즉 다음과 같이 해석될 수 있다는 뜻입니다.
증감 연산자는 기본적으로 단항 연산자로 피연산자가 1개입니다. 그러므로 멤버 함수로 받는 인자는 없어야 겠죠.
어? 그런데 p1.operator++(int); 에는 int처럼 뭔가 인자로 받는게 있잖아요?
여기서 오해하시면 안되는건, 요 int는 단순히 전위와 후위를 구분하기 위해 C++ 에서 구분하기 위한 구분자일뿐, int 타입의 데이터를 인자로 받는다는건 아닙니다.
그냥 전위와 후위, 같은 함수 이름으로 쓰는데 구분하기 위해 int라고 붙여준다고 보시면 됩니다.
// 전위 증감 연산자
Point operator++()
{
x += 1;
y += 1;
return *this; //증가한 자기 자신 (객체)를 반환
}
// 후위 증감 연산자
Point operator++(int)
{
Point temp(*this); //현재 객체를 이용해 임시 객체를 생성 (실제로 반환할 객체)
// 자신의 값은 증가
x += 1;
y += 1;
return temp; //증가 이전의 임시 객체를 반환
}
기본적으로 ++a 와 같이 전위 증감 연산자는 증가한 자기 자신을 바로 반환합니다.
그렇기에 전위 증감 연산자 오버로딩 코드의 경우엔 자기 자신의 값 x , y 를 1씩 증가시키고 증가한 자기 자신 객체 (*this)를 반환합니다. 여기서 this는 자기 객체 자신을 가리키는 포인터로 *을 이용해 값을 참조하면 객체 자기 자신을 던지는 형태가 됩니다.
그러나 a++ 와 같은 후위 증감 연산자의 경우 결과값이 우선 현재 값을 가지는 임시 객체를 생성해 반환하고, 나중에 값을 찍어보면 값이 증가되있는 형태이기에, 우선 Point temp(*this); 와 같은 코드로 자기 자신을 이용해 임시객체를 생성합니다. 이 코드는 사실 Point temp(x, y); 와 같습니다. 이후 자신의 x , y 값을 증가시키고, 연산자의 결과값으론 만들어둔 임시 객체를 반환합니다.
프로그래밍을 조금 깊게 공부해보신 분들이라면 for문에서 ++i 가 i++ 보다 더 빠르다는 소리를 한번쯤 들어본적이 있을겁니다. 그 이유가 바로 이러한 코드에 있는데 ++i 와 같은 전위 증감 연산자는 증가시킨 자기 자신을 바로 반환하지만, i++ 와 같은 경우 임시 객체를 한번 더 만드는 과정이 추가되어, 값이 증가되기 때문에 조금더 느린 것입니다. 물론 현대 컴파일러는 최적화가 매우 잘되있어서 for문안에서 반복 변수 i를 ++i로 쓰나 i++로 쓰나 거의 차이가 없다고 합니다.
https://mapadubak.tistory.com/115
대입 연산자 오버로딩(=)
사칙연산 오버로딩 만큼 많이 사용될 수 있는게 바로 대입 연산자 (=) 오버로딩입니다.
일반적으로 객체 생성시 한 줄로 사용하는 = 의 경우 기본 생성자나 복사 생성자에서 처리가 되므로, 보통 대입 연산자 오버로딩에 사용하는 경우는 복사 대입 연산자나, 이동 대입 연산자에 사용된다고 보시면 됩니다.
* 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자 의 차이들에 관해선 본 글에선 자세하게 다루지 않습니다. 인터넷의 좋은 글들을 참고해보세요. 나중에 관련글을 포스팅하게 되면 링크를 걸어두도록 하겠습니다.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
};
int main()
{
Point p1;
Point p2(3, 4);
p1 = p2; //복사 대입 연산자
p1.print_point();
}
이번에도 예제로 익혀봅시다. p1은 기본 생성자를 호출해서, (0, 0)으로 초기화 된 객체를, p2는 (3,4) 로 초기화 합니다.
p1 = p2; 를 이용해서 p2의 내용을 p1에 복사하는 복사 대입 연산자를 호출하고 있습니다
(만약에 Point p1 = p2; 와 같은 형태로 p1을 p2 이후에 새로 생성한 경우 기존의 객체 p2를 이용해 p1을 새로 생성하는 경우이므로 p1의 복사 생성자가 호출됩니다. 복사 대입 연산자가 호출되지 않죠. 이 차이에 대해선 다음 글을 참고해보세요.)
따로 p1 과 p2의 대입 연산을 정의해주지 않았는데도, p1의 x , y 값을 출력해보면 p2의 값이 그대로 복사되 있음을 확인할 수 있습니다.
기본적으로 디폴트 복사 생성자처럼, 대입 연산자도 정의되어 있지 않으면 디폴트 대입 연산자가 자동 삽입되며, 역시 멤버 대 멤버로 얕은 복사가 이루어 집니다. (깊은 복사를 원하면 지금 배우는 연산자 오버로딩을 활용해서 따로 코드를 작성해줘야 합니다.)
p1 = p2 == p1.operator=(p2)
앞에서 배운 내용에 따르면 p1 에 p2를 대입하게 되면 (복사 대입이라고 가정하게 되면) 다음과 같이 해석 될 것입니다.
만약에 p1 = p2 의 기본 동작을 재정의 하고 싶다면 p1이 Point 객체이므로 멤버 함수로써 오버로딩을 구현하면 되겠네요.
#include <iostream>
using namespace std;
class Point
{
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
void print_point()
{
cout << "x: " << x << ", y: " << y << endl;
}
Point& operator=(const Point &p)
{
x = p.x;
y = p.y;
return *this;
}
};
int main()
{
Point p1;
Point p2(3, 4);
Point p3;
p1 = p2;
p1.print_point();
}
즉 다음과 같이 대입 연산자 오버로딩을 구현하면 될 것 입니다.
컴파일러가 어짜피 자동 추가해주지만, 동일한 기능을 직접 명시적으로 코드로써 구현해보았습니다.
p1 의 입장에선 p2를 인자로 operator=(p2) 를 호출했기 때문에, p2의 값이 p1에 복사되어야 하므로, 들어온 p2 객체의 값을 그대로 자신의 멤버 변수에 대입해주면 될 것 입니다.
그리고 p2 = p1 과 같이 순서를 바꾸어도 p2.operator=(p1) 과 같은 형태로, 호출이되어 p2 역시 Point 객체로써 operator= 함수를 멤버 함수로 가지기 때문에 정상적으로 대입이 이루어 집니다.
앞으로 연산자 오버로딩을 볼때는, 연산자를 기준으로 왼쪽에 있는놈은 함수를 호출하는놈, 오른쪽은 그 함수에 인자로 전달되는 놈. 으로 생각을하고 오버로딩을 구현해주시면 되겠습니다.
사실 글 쓰는 지금까지도 멤버 오버로딩으로 하면 순서가 좀 햇갈려서.. 저는 전역 함수 오버로딩을 선호하고 있습니다.
위에서 대입 연산자 오버로딩을 할 때 왜 Point 객체(*this) 를 반환하는 건가요?
위에 대입 연산자 오버로딩 코드를 보면서 한 가지 의아한 점이라면, 대입 연산자의 결과값으로 *this 를 통해 자기 자신 객체를 Point 레퍼런스 타입으로 반환하고 있음을 알아볼 수 있습니다. 사실 a = b ; 와 같은 대입 연산에서 어짜피 대입만 할건데 결과값이 무슨 소용이 있나 싶을 수 있습니다.
그런데 실제로 대입 연산자는 연산에 대한 반환값이 있습니다.
예를 들어서 a = b = c = 10; 과 같은 코드를 실행한다고 하면 대입 연산자의 결합 방향은 오른쪽에서 왼쪽으로, c = 10 을 먼저 수행합니다. c = 10을 수행하면 c에 10이 복사되고 결과값으로 10을 반환합니다. 그러면 다시 a = b = 10 이 되고, 다시 b = 10 을 수행하고 다시 10을 반환해서 마지막으로 a = 10 을 수행하고 a에 10이 대입되서 종료되게 됩니다. 즉 실제로 순서는 괄호를 쳐보자면 (a = (b = (c = 10))) 이런식으로 수행된다는 겁니다.
즉 위처럼 연쇄적으로 대입하는 경우 (Chaining Assignment) 를 염두에 두고 대입 연산자는 복사 받은 값을 결과값으로 반환해야 합니다. Point 객체의 경우에도 p1 = p2 = p3 와 같은 대입을 염두에 두면, p2 = p3 수행 시 값을 p3에서 p2로 복사받은 다음, p2가 자기 자신 객체를 반환함으로써 p1에 다시 전달해서 연쇄적인 대입이 이루어질 수 있는겁니다.
출처
https://blog.hexabrain.net/177
http://www.parkjonghyuk.net/lecture/2019-2nd-lecture/programming2/ch/chap08.pdf
https://welikecse.tistory.com/37
'프로그래밍 > C++' 카테고리의 다른 글
[Arduino/C++] 시리얼 모니터 데이터 공백구분으로 입력받기, 명령어 처리 (0) | 2022.11.12 |
---|---|
[C++] 모든 인자값을 레퍼런스로 넘겨야 성능상 유리할까? (0) | 2022.10.27 |
[C/C++] 선언(Declaration)과 정의(Definition)의 차이 (0) | 2022.06.21 |
[C++] Natural Sort 사용하기 (0) | 2022.06.14 |
[C++] Visual Studio <std::filesystem> 사용하기 (2) | 2022.06.11 |