연산자 오버로딩
1. 연산자 오버로딩이란
c++의 문자열 연산은 어떻게 구현한 것일 까? 연산자는 실제로는 클래스 안에서 정의된 함수이다. 이들 함수는 operator 키워드 다음에 실제 연산자를 붙인 이름으로 지정한다. 아래의 분수를 연산하는 예제를 보며 구현 방식을 공부해보자.
2. Rational 실수 연산자 오버로딩
#ifndef RATIONAL_H
#define RATIONAL_H
#include <string>
#include <sstream>
using namespace std;
//분수 계산 + - * / 비교
//최대공약수를 활용해 약분할 것
class Rational {
private:
int numerator; //분자
int denominator; //분모
static int gcd(int n, int d) { //Rational 클래스에서 공통으로 사용하는 최대공약수 찾기 함수
int n1 = abs(n);
int n2 = abs(d);
int gcd = 1;
for (int k = 1; k <= n1 && k <= n2; k++) {
if (n1 % k == 0 && n2 % k == 0)
gcd = k;
}
return gcd;
}
public:
Rational() {
numerator = 0;
denominator = 1;
}
Rational(int numerator, int denominator) {
int factor = gcd(numerator, denominator);
this->numerator = ((denominator > 0) ? 1 : -1) * numerator / factor;
this->denominator = abs(denominator) / factor; //abs : 절대값 반환
}
int const getNumerator() const { return numerator; }
int const getDenominator() const { return denominator; }
Rational add(const Rational& secondRational) const {
int n = numerator * secondRational.getDenominator() + denominator * secondRational.getNumerator();
int d = denominator * secondRational.getDenominator();
return Rational(n, d);
}
Rational subtract(const Rational& secondRational) const {
int n = numerator * secondRational.getDenominator() - denominator * secondRational.getNumerator();
int d = denominator * secondRational.getDenominator();
return Rational(n, d);
}
Rational multiply(const Rational& secondRational) const {
int n = numerator * secondRational.getDenominator();
int d = denominator * secondRational.getNumerator();
return Rational(n, d);
}
Rational divide(const Rational& secondRational) const {
int n = numerator * secondRational.getDenominator();
int d = denominator*secondRational.getNumerator();
return Rational(n, d);
}
int compareTo(const Rational& secondRational) const {
Rational temp = subtract(secondRational);
if (temp.getNumerator() < 0)
return -1;
else if (temp.getNumerator() == 0)
return 0;
else
return 1;
}
bool equals(const Rational& secondRational) const {
if (compareTo(secondRational) == 0)
return true;
else
return false;
}
int intValue() const {
return getNumerator() / getDenominator();
}
double doubleValue() const {
return 1.0 * getNumerator() / getDenominator();
}
string toString() const {
stringstream ss;
ss << numerator;
if (denominator > 1) {
ss << "/" << denominator;
}
return ss.str();
}
};
#endif
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(4, 2);
Rational r2(2, 3);
cout << r1.toString() << " + " << r2.toString() << " = " << r1.add(r2).toString() << endl;
cout << r1.toString() << " + " << r2.toString() << " = " << r1.subtract(r2).toString() << endl;
cout << r1.toString() << " + " << r2.toString() << " = " << r1.multiply(r2).toString() << endl;
cout << r1.toString() << " + " << r2.toString() << " = " << r1.divide(r2).toString() << endl;
cout << "r2.intValue() is " << r2.intValue() << endl;
cout << "r2.doubleValue() is " << r2.doubleValue() << endl;
cout << "r1.compareTo(r2) is " << r1.compareTo(r2) << endl;
cout << "r2.compareTo(r1) is " << r2.compareTo(r1) << endl;
cout << "r1.compareTo(r1) is " << r1.compareTo(r1) << endl;
cout << "r1.equals(r1) is " << (r1.equals(r1) ? "true" : "false") << endl;
cout << "r1.equals(r2) is " << (r1.equals(r2) ? "true" : "false") << endl;
}
3. 연산자 오버로딩 함수
위의 Rational 클래스를 문자열 연산처럼 직관적으로 사용할 수 있는 방법은 없을까? 클래스에서 연산자 함수(operator function)이라는 특별한 함수를 정의할 수 있다. 연산자 함수는 실제 연산자 앞에 operator키워드를 적는 것을 빼면 일반적인 함수와 똑같이 기능한다.
14.4 첨자 연산자 []의 오버로딩
[] 대괄호 쌍을 c++에서는 첨자 연산자(subscript operator)이라고 한다. 객체 내용에 접근하여 내용을 변경하거나 가져올 때 사용한다. 사용할 때에는 아래처럼 사용하는 데 변경자로도 사용하기 위해서는(r[0]=6;를 기능하게 하기 위해서는) 참조연산자&를 반드시 사용해주어야 한다.
int& operator[](int index) {
if (index == 0)
return numerator;
else
return denominator;
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r(2, 3);
cout << "r[0] is " << r[0] << endl;
cout << "r[1] is " << r[1] << endl;
r[0] = 5;
r[1] = 6;
cout << "change-----------" << endl;
cout << "r[0] is " << r[0] << endl;
cout << "r[1] is " << r[1] << endl;
cout << "r.doubleValue() is " << r.doubleValue() << endl;
}
14.5) 증강 대입 연산자의 오버로딩
+=, -=,*=,%= 를 증강 연산자라고 한다. 증강 연산자도 오버로딩을 할 때 반드시 & 참조연산자로 선언해야 한다.
Rational& operator+=(const Rational& secondRational) {
*this = operator+(secondRational);
return *this;
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(2,4);
Rational r2 = r1 += Rational(2, 3);
cout << "r1 is " << r1.toString() << endl;
cout << "r2 is " << r2.toString() << endl;
}
14.6) 단항 연산자 오버로딩
단항연산자 함수 (예를 들어 r3 = -r2) 를 오버로딩하려면 매개변수없이 선언하면 된다.
Rational operator-() {
return Rational(-numerator, denominator);
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(2,3);
Rational r2 = -r1;
cout << "r1 is " << r1.toString() << endl;
cout << "r2 is " << r2.toString() << endl;
}
14.7) ++ 와 -- 연산자 오버로딩
전위 연산자는 Lvalue 연산자이지만 후위 연산자는 아닙니다. 후위 연산자는 연산하기 전 값을 그 statement(문장 한 줄)에서 사용하기 때문에 & 참조연산자를 붙여주지 않습니다. 또 후위 연산자는 dummy 매개변수를 만들어주어야 합니다.
Rational& operator++() {
numerator += denominator;
return *this;
}
Rational operator++(int dummy) {
Rational temp(numerator, denominator);
numerator += denominator;
return temp;
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r2(2,3);
Rational r3 = ++r2;
cout << "r3 is " << r3.toString() << endl;
cout << "r2 is " << r2.toString() << endl;
Rational r1(2, 3);
Rational r4 = r1++;
cout << "r1 is " << r1.toString() << endl;
cout << "r4 is " << r4.toString() << endl;
}
14.8) friend 함수와 friend 클래스
경우에 따라 몇몇 신뢰할 수 있는 함수나 클래스가 다른 클래스의 전용멤버에 접근하도록 허용하는 것이 편리할 때가 있습니다. friend 키워드를 이용해 다른 클래스에 있는 전용 멤버에 접근할 수 있는 friend함수와 friend 클래스를 정의할 수 있습니다.
#include <iostream>
using namespace std;
class Date {
private:
int year;
int month;
int day;
public:
Date(int year, int month, int day) {
this->year = year;
this->month = month;
this->day = day;
}
friend class AccessDate;
};
class AccessDate {
public:
static void p() {
Date birthDate(2010, 3, 4);
birthDate.year = 2000;
cout << birthDate.year << endl;
}
};
int main() {
AccessDate::p();
return 0;
}
#include <iostream>
using namespace std;
class Date {
private:
int year;
int month;
int day;
public:
Date(int year, int month, int day) {
this->year = year;
this->month = month;
this->day = day;
}
friend void p();
};
void p() {
Date birthDate(2010, 3, 4);
birthDate.year = 2000;
cout << birthDate.year << endl;
}
int main() {
p();
return 0;
}
14.9) <<와 >> 연산자 오버로딩
ostream의 << 를 오버로딩해야 한다. 하지만 동시에 Rational클래스에 접근할 수 있어야 하므로 friend 키워드를 사용해 작성한다.
friend ostream& operator<<(ostream& out, const Rational& r) {
if (r.denominator == 1)
out << r.numerator;
else
out << r.numerator << "/" << r.denominator;
return out;
}
friend istream& operator>>(istream& in, Rational& rational) {
cout << "Enter numerator: ";
in >> rational.numerator;
cout << "Enter denominator: ";
in >> rational.denominator;
return in;
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(3, 6);
Rational r2(1, 4);
Rational r3 = r1 + r2; //3) r1+r2로 변경
cout << "r3 is " << r3 << endl;
Rational r4 = r1 - r2; //3) r1-r2로 변경
cout << "r4 is " << r4 << endl;
Rational r5(6, 12);
Rational r6 = -r5; //4) 단항 -
cout << "r5 is " << r5 << " r6 is " << r6 << endl;
return 0;
}
14.10) 자동 형변환
c++에서는 사용자 정의 클래스와 primitve 자료형 사이에도 형변환을 할 수 있습니다. Rational 객체와 primitive 타입을 더할 때도 이것이 일어나길 바란다면 어떤 식으로 구현해야 할까?
객체를 int나 double 형으로 변환하는 함수는 다음과 같이 구현합니다.
operator double() {
return doubleValue();
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(1, 4);
double d = r1 + 5.1;
cout << "r1 + 5.1 is " << d << endl;
return 0;
}
반대로 int형을 Rational 객체로 변환하는 것은 생성자를 이용해 해결할 수 있습니다.
Rational(int numerator) {
this->numerator = numerator;
this->denominator = 1;
}
14.11) 오버로딩 연산자를 위한 비멤버 함수 정의
4+ r1 을 하는 것을 구현하기 위해서는 primitive타입을 Rational 형으로 바꾸는 형변환 생성자를 작성하거나 operator+를 다음과 같이 멤버함수가 아닌 일반함수(비멤버함수)로 작성해야 한다.
Rational operator+(const Rational& r1, const Rational& r2){
return r1.add(r2);
}
14.13) =연산자 오버로딩
=연산자는 한 객체에서 다른 객체로 멤버 간 복사(memberwise copy)를 수행합니다. 이것이 주소를 복사하는 얕은 복사가 아닌 깊은 복사가 되도록 바꾸시오.
Rational operator=(const Rational r2) const{
if (this != &r2) { //스스로를 복사하지 않음
int n = r2.getNumerator();
int d= r2.getDenominator();
return Rational(n, d);
}
}
#include "Rational.h"
#include <iostream>
using namespace std;
int main() {
Rational r1(1, 2);
Rational r2(4, 5);
r1 = r2;
cout << "r1 is " << r1 << endl;
cout << "r2 is " << r2 << endl;
return 0;
}
checkpoint)
14.1) 연산자 오버로딩을 위한 연산자 함수는 어떻게 정의하는가?
operator 연산자와 연산자를 붙여 정의한다.
14.2) 오버로딩이 불가능한 연산자의 목록을 작성하여라.
?: . .* ::
4개가 있다.
14.3) 오버로딩으로 연산자 우선순위나 결합성을 변경할 수 있는가?
오버로딩에 의해 연산자 우선순위와 결합성을 변경할 수 없다.
14.4) Lvalue는 무엇인가? Rvalue는 무엇인가?
lvalue는 왼쪽에 놓이는 변수를 주로 지칭하며 이 변수에 값을 대입하고 싶을 때 사용한다.
rvalue는 오른쪽에 놓이는 수를 지칭하며 어떤 변수에 대입할 값을 말한다.
14.5) 참조에 의한 전달과 참조에 의한 반환을 설명하여라
참조에 의한 전달과 참조에 의한 반환은 서로 같은 개념이다. 참조에 의한 전달에서 형식 매개변수와 실 매개변수는 서로에게 별명이 된다. 참조에 의한 반환에서 함수는 변수로 별명을 반환한다.
14.6) [] 연산자에 의한 함수 서명은 어떻게 작성되어야 하는가?
참조형으로 작성되어야 한다.
14.7) +=과 같은 증강 연산자를 오버로딩할 때, 함수는 void이어야 하는가? 아니면 그 반대인가?
Rational& 같은 참조 연산자여야 한다. *this로 그 값 자체를 반환한다.
14.8) 증강 대입 연산자를 위한 함수는 참조를 반환해야 하는 이유는 무엇인가?
증강 대입연산자도 값을 변환시키는 Lvalue이기 때문이다.
14.9) + 단항 연산자에 대한 함수 서명은 어떻게 작성되어야 하는가?
매개변수 없이 선언하면 된다.
14.10) -단항 연산자에 대한 다음 구현이 잘못된 이유는 무엇인가?
Rational Rational::operator-(){
numerator *= -1;
return *this;
}
원래 객체의 numerator값도 변화한다. 따라서 따로 값을 정리해서 반환하거나 한번에 계산하여 반환해야 한다.
14.11) 전위 ++ 연산자와 후위 ++연산자에 함수 서명은 어떻게 작성되어야 하는가?
전위 ++연산자는 참조 연산이 꼭 필요하고 후위 ++ 연산자는 참조연산 없이 매개변수 하나를 가진다.
14.12) 다음과 같이 후위++를 구현했다고 가정하자.
Rational Rational::operator++(int dummy){
Rational temp(*this);
add(Rational(1,0));
return temp;
}
이는 올바른 구현인가? 만약 그렇다면 본문에서 구현한 것과 비교하여 어떤 것이 더 좋은가?
잘못된 구현이다.
14.13) 클래스의 전용 멤버에 접근할 수 있도록 friend 함수를 어떻게 정의하는가?
이용하고자 하는 클래스의 public 멤버함수로 friend 자료형 함수이름(){} 한다.
14.14) 클래스의 전용 멤버에 접근할 수 있도록 friend 클래스를 어떻게 정의하는가?
이용하고자 하는 클래스의 public 멤버함수로 friend class 클래스이름{} 한다.
14.15) <<연산자와 >>연산자의 함수 서명은 어떻게 작성되어야 하는가?
friend ofstream& operator<<(ofstream& out, const Raional& r)
friend ifstream& operator>>(ifstream& in, Rational& rational)
14.16) <<와 >>연산자가 비멤버 함수로 정의되어야 하는 이유는 무엇인가?
<<와 >>연산자는 ostream, istream의 객체이기 때문이다.
14.17) <<연산자를 다믕과 같이 오버로딩한다고 하자.
ostream& operator<<(ostream& stream, const Rational& rational){
stream <<rational.getNumerator() <<"/" <<rational.getDenominator();
return stream;
}
Rational 클래스에 다음을 정의할 필요가 있는가?
friend ostream& operator<<(ostream& stream, Rational& rational)
예
14.18) 객체를 int형으로 변환하기 위한 함수 서명은 어떻게 작성되어야 하는가?
operator int(){}
14.19) 원시 유형 값을 객체로 변환하는 방법은 무엇인가?
생성자를 이용한다.
14.20) 클래스에 객체를 원시 유형 값으로 변환하는 변환 함수를 정의하고 동시에 원시 유형 값을 객체로 변환하는 변환 생성자를 클래스 내에 정의할 수 있는가?
네
14.21) 연산자에 비멤버 함수를 작성하는 것이 더 좋은 이유는 무엇입니까?
대칭성이 지켜지기 때문에 자동형변환이 쉽게 일어나 편하다.
14.22) []연산자는 비멤버 함수로 정의될 수 있는가?
가능은 하지만 매우 불편하게 돌아간다.
14.23) 함수+를 다음과 같이 정의한다면, 잘못된 부분은 무엇인가?
Rational operator+(const Rational& r1, const Rational& r2) const
값을 변화시키기는 함수이기 때문에 const를 붙이면 안된다.
Rational operator+(const Rational& r1, const Rational& r2)
14.24) Rational(int numerator) 생성자를 삭제할 경우 1+ r4에 문제가 생기는가?
네
14.25) Rational 클래스에서 gcd함수를 상수 함수로 정의할 수 있는가?
gcd를 정적함수가 아니게 만들면 가능하다.
14.6) 어떤 상황에서 =연산자를 오버로딩해야 하는가?
깊은 복사를 해야할 때
'c, c++ > c++로 시작하는 객체지향 프로그래밍' 카테고리의 다른 글
16.1~16.4, 16.6~ 예외 처리 (0) | 2024.06.18 |
---|---|
13.7~)이진 입출력 (0) | 2024.05.24 |
13.1~ 13.5)파일 입력과 출력 (0) | 2024.05.24 |
15.5~15.9) 상속과 "다형성" (0) | 2024.05.09 |
15.1~15.4, 15.8) 상속 (0) | 2024.05.02 |