Part2::Ch 02. 연산자 오버로딩- 01&02. 산술 연산자 오버로딩
산술 연산자 오버로딩은 C++의 핵심 기능 중 하나로, 사용자 정의 클래스에 대해 +, -, *, / 등의 연산자를 직접 정의해서 객체 간의 연산을 가능하게 만드는 기능이다
먼저 하나의 클래스를 만들어보자.
class Vector {
public:
int x, y;
};
여기서 우리가 연산자 오버로딩을 통해서 수행하고 싶은 것은
int main() {
Vector v{ 1, 2 };
Vector v1{ 3, 4 };
Vector v2 = v + v1;
}
이런 것들에 대한 것이다.
기존의 클래스의 경우는 객체를 생성하려면 멤버 변수가 public이건 private이건
class Vector {
public:
int x, y;
};
int main() {
Vector v{ 1, 2 };
}
이런 선언을 하려면 생성자가 있어야만 가능하다고 알았었는데 이런 선언을 하면 문제 없이 초기화가 되고 객체가 생성된다
그 이유는 뭘까?
중괄호 초기화({})가 되려면 다음 두가지의 경우여야한다.
class MyClass {
public:
int a, b;
MyClass(int x, int y) : a(x), b(y) {} // 명시적으로 생성자 생성
};
이렇게 명시적으로 생성자가 있어 초기화를 하는 경우와 C++11 이상에서 aggregate (집합체) 타입인 경우엔 중괄호 초기화가 가능하다.
aggregate 초기화란
C++11부터는 다음 조건을 만족하면 자동으로 중괄호 초기화 가능하다.
클래스에 1. 생성자가 없고, 2. 모든 멤버가 public이며, 3. 상속도 없고, 4. virtual 함수도 없는 경우는 중괄호 초기화가 가능하고 이런 초기화를 aggregate (집합체) 초기화라고 부른다
struct MyClass {
int a;
int b;
// 생성자 없음
};
int main() {
MyClass m1{1, 2}; // aggregate 초기화됨
}
이는 구조체처럼 동작하는 클래스라고 보면 된다.
이건 앞으로 우리가 만들 함수를 호출해서 사용하는 것과 같은것으로
Vector v2 = v + v1;
↓
Vector v2 = v.operator+(v1)
와 같이 된다고 생각하면 된다.
이 함수를 이제 우리가 만들어보도록 하자.
클래스에서 멤버 함수로
class Vector {
public:
int x, y;
Vector operator+(const Vector& v) {
}
};
이렇게 선언해주고 이제 함수 내부에서 Vector의 각각의 멤버 함수끼리 더해준 객체를 반환한다고 작성해주면 된다.
class Vector {
public:
int x, y;
Vector operator+(const Vector& v) {
return Vector{ x + v.x, y + v.y };
}
};
이렇게 사용하면
int main() {
Vector v{ 1, 2 };
Vector v1{ 3, 4 };
Vector v2 = v + v1;
}
이 부분에 빨간 줄이 사라지는 것을 볼 수 있다.
이걸 한번 출력해보면
class Vector {
public:
int x, y;
Vector operator+(const Vector& v) {
return Vector{ x + v.x, y + v.y };
}
void print() {
std::cout << "x = " << x << ", y = " << y << std::endl;
}
};
int main() {
Vector v{ 1, 2 };
Vector v1{ 3, 4 };
Vector v2 = v + v1;
v2.print();
}
이렇게 잘 출력되는 것을 볼 수 있다.
그런데 이 함수들은 사실 완벽한 형태가 아닌데 그 이유는
int main() {
const Vector v{ 1, 2 };
const Vector v1{ 3, 4 };
Vector v2 = v + v1;
v2.print();
}
이렇게 const 객체를 선언할 경우에는 this를 전달해주는데 this 자체가 변경 가능한 형태이기에 컴파일에 문제가 있다
그렇기 때문에 어차피 연산자 오버로딩 된 함수는 자기 자신을 바꾸지 않기 때문에 const를 붙여서 사용해줘야 문제가 없다.
class Vector {
public:
int x, y;
Vector operator+(const Vector& v) const {
return Vector{ x + v.x, y + v.y };
}
void print() {
std::cout << "x = " << x << ", y = " << y << std::endl;
}
};
int main() {
const Vector v{ 1, 2 };
const Vector v1{ 3, 4 };
Vector v2 = v + v1;
v2.print();
}
물론 이렇게 만들면 const가 안붙은 객체끼리의 연산 또한 문제 없이 수행할 수 있다.
이 또한 const를 비 const에 넣는 것은 불가능하지만 비 const를 const에 넣는것은 가능하기에
int num = 10;
const int * const ptr = # // 가능
const int num1 = 10;
int * const ptr = &num1; // 불가능
와 동일하다고 보면 된다.
여기서 다양한 연산자의 사용에 대해서 한번 확인해보자.
1. 전위 연산자 ++ / --
전위 연산자 ++와 --의 경우에는 조금 다른점이 새로운 객체를 반환하는게 아니라 기존의 객체를 반환해줘야 한다.
또한 이게 복사가 되어서는 안되고 지금 본인 그 자체를 바라봐야만 한다는 다른 점이 존재한다.
그렇기에 우선 반환타입은 그냥 Vector 타입이 아니라 참조 타입이여야만 하고
Vector& operator++() {
}
그리고 내부에선 자신 자체의 값을 변경하기에 const 함수로 선언하는 것도 불가능하다.
Vector& operator++() {
++x;
++y;
}
그리고 마지막에 *this를 반환해주면 된다.
Vector& operator++() {
++x;
++y;
return *this;
}
2. 후위 연산자 ++/--
후위 연산자의 경우는 조금 다르다.
전위 연산자의 경우는 call할 경우 값자체가 변경되면서 반환되나 후위의 경우는 사용할때는 값의 변화를 보여서는 안된다.
그래서 먼저 반환타입은 참조 타입이 아니다.
Vector operator++() {
}
또한 함수명과 매개변수의 수, 타입이 동일하기에 중복되는 상황이 생긴다.
이를 방지하기 위해 매개변수에 int 타입을 하나 받아준다.
이는 실제 사용하는 것은 아니나 오버로딩을 위해서 넣어주는 매개변수이다.
Vector operator++(int) {
}
그리고 후위 연산자의 경우는 반환은 원본을 반환하고 자신의 값은 변경시키는 연산을 진행했었다.
그래서
Vector operator++(int) {
Vector temp = *this;
}
이렇게 반환을 위한 원본 변수를 하나 저장해주고
Vector operator++(int) {
Vector temp = *this;
++(*this); // 전위연산을 활용한것 or ++x ; ++y;
}
기존의 값을 변화시켜주고
원본 변수를 반환시켜주자.
Vector operator++(int) {
Vector temp = *this;
++(*this);
return temp;
}
3. 상수 * 객체
기존에 객체 * 상수를 수행하면 이를 객체의 연산자 오버로딩된 함수를 호출해서 연산한다고 했었다.
int main(){
Vector v{1, 2};
Vector v1 = v * 3; // v.operator+(3)
}
그렇다면 순서를 바꿔서
int main(){
Vector v{1, 2};
Vector v1 = 3 * v; // ???
}
이렇게 수행하도록 하려면 연산자 오버로딩을 어떻게 해야할까
위와 같이 수행하려면 연산자 오버로딩을 클래스 내에 만드는게 아니라 전역으로 만들어줘야 한다.
Vector operator*() {
}
int main(){
Vector v{1, 2};
Vector v1 = 3 * v;
}
그리고 매개변수를 두개 받는데 첫번째로는 전달될 상수의 타입, 두번째로는 전달될 객체의 타입이다.
Vector operator*(int num, const Vector& v) {
}
int main(){
Vector v{1, 2};
Vector v1 = 3 * v;
}
그리고 이제 return 부는 기존과 동일하게 연산을 적용시킨 백터를 반환하면 된다.
Vector operator*(int num, const Vecotr& v) {
return Vector{num*x, num*y};
}
int main(){
Vector v{1, 2};
Vector v1 = 3 * v;
}
그러면 아래처럼
Vector v1 = 3 * v; // operator(3, v);
와 같이 호출되어 연산이 수행된다.
그런데 만약에 이 상황에서 Vector의 멤버 변수들이 만약 public이 아니라 private라면
이렇게 전역 함수에서 멤버 변수에 접근할 수 없는 문제가 발생한다
이럴때 사용하는것이 friend라는 키워드이다.
friend
friend는 클래스 외부에 있는 함수나 다른 클래스가 해당 클래스의 private / protected 멤버에 접근할 수 있게 허용하는 키워드이다.
다시 말하면 접근제한자를 무시하고 너는 특별히 접근해도 돼 라고 예외를 부여하는 용도로 사용된다.
사용법은 단순하게 외부에서 접근하려는 함수의 프로토타입을 friend를 붙여서 클래스 내부에 선언해주면 된다.
이러면 접근 제한자와는 상관없이 내부 멤버 변수에 접근하는 것이 가능하다.
추가로 함수에 friend를 사용하는게 아니라 클래스에도 friend의 사용이 가능하다.
만약 멤버함수를 int로 받는 VectorI와 멤버 함수를 float로 받는 VectorF가 있다고 해보자.
#include <iostream>
#include <functional>
class VectorF {
private:
float x, y;
public:
VectorF(float x, float y)
:x(x), y(y)
{
}
};
class VectorI {
private:
int x, y;
public:
VectorI(int x, int y)
: x(x), y(y)
{
}
};
여기서 우리가 하기를 원하는 것은 VectorI와 VectorF사이의 연산인데
이걸 수행하려면 우선 기준이되는 객체(VectorI + VectorF 라면 VectorI, VectorF + VectorI 라면 VectorF)에서 함수를 하나 생성해주자
float형과 int형이 더해지면 float형이 반환되니 반환타입은 VectorF로 생성해주고
class VectorF {
private:
float x, y;
public:
VectorF(float x, float y)
:x(x), y(y)
{
}
VectorF operator+() {
}
};
VectorI를 매개변수로 받아 연산을 수행해주자
class VectorF {
private:
float x, y;
public:
VectorF(float x, float y)
:x(x), y(y)
{
}
VectorF operator+(const VectorI& vi) {
return VectorF{x+vi.x, y+vi.y}
}
};
근데 이렇게 수행하면 VectorI의 멤버변수가 private이기에 접근이 불가능하다고 나오게 될 것이다.
이럴때 함수 내부에 friend로 클래스 자체를 넣어줄 수 있는 것이다.
이 friend을 넣는 곳은 멤버 변수를 가져올 쪽에서 넣어줘야 아 이 클래스에서 접근하는 경우는 다 열어줘~ 라고 알려주는 것이다.
class VectorF {
private:
float x, y;
public:
VectorF(float x, float y)
:x(x), y(y)
{
}
VectorF operator+(const VectorI& vi) {
return VectorF{x+vi.x, y+vi.y}
}
};
class VectorI {
private:
int x, y;
public:
friend class VectorF;
VectorI(int x, int y)
: x(x), y(y)
{
}
};
근데 사실 이렇게 하나의 파일에서 이렇게 사용하는것은 사실 불가능한게 VectorF에서 VectorI를 사용하려면 VectorI에 대한 프로토 타입이 제공되어야 하고 그래서 전방선언으로
넣어줘야만 하는데 보면 operator+는 구현코드가 작성되어 있기 때문에 전방선언으로는 사용이 불가능하다.
그렇기에 사실은 이런 경우는 파일을 다 나눠줘야만 한다.
파일을 모두 나누는 경우
###VectorF.cpp
#include "VectorF.h"
#include "VectorI.h" // 구체적인 정의가 있기에 include 필요
VectorF::VectorF(float x, float y)
:x(x), y(y)
{
}
// 클래스 끼리의 연산이 추가된 부분
VectorF VectorF::operator+(const VectorI& v) {
return VectorF{ x + v.x, y + v.y };
};
===========================================================
###VectorF.h
#pragma once
class VectorI; // 선언만 있기에 전방선언 가능
class VectorF
{
private:
float x, y;
public:
VectorF(float x, float y);
VectorF operator+(const VectorI& v);
};
===========================================================
###VectorI.cpp
#include "VectorI.h"
VectorI::VectorI(int x, int y)
: x(x), y(y)
{
}
===========================================================
###VectorI.h
#pragma once
class VectorI
{
private:
int x, y;
public:
friend class VectorF; // VectorF에 대한 friend 선언 - VectorF에서 해달라는거 다해줘
VectorI(int x, int y)
: x(x), y(y)
{
}
};
이렇게 각 타입이 다른 객체끼리의 연산을 하려면 다 파일을 나눠주거나 단순하게 외부에 전역 함수를 선언해 준 다음에
각 클래스에 firend로 함수의 프로토타입을 넣어주는 것으로도 해결은 가능하다(물론 이때는 클래스에 대한 전방선언도 필요, 가능하다.)
이렇게 결과가 잘 나오는 것을 볼 수 있다.