본문 바로가기

프로그램언어/C++

연산자1

연산자 오버로딩

연산자 오버로딩은 연산자가 가지고 있는 본래 기능을 새로운 데이터 타입에 적용할 수 있게 확장하기 위한 것이지, 연산자에 새로운 기능을 부여하기 위한 것이 아닙니다.

 

연산자 오버로딩의 제약

연산자 오버로딩을 통해 연산자의 우선순위나 결합성을 바꿀 수는 없습니다.

기본 데이터 타입끼리의 연산은 연산자 함수로 정의할 수 없습니다.

모든 연산자가 다 오버로딩을 할 수 있는 것은 아닙니다.(sizeof연산자, 삼항조건연산자는 오버로딩이 금지된 연산자)

 

단항연산자

Point 클래스에 대한  ++  연산자의 기능을 x, y 좌표를 각각 1씩 증가시키는 것으로 정의해보겠습니다. 연산자 함수는  operator 키워드 다음에 연산자 기호를 붙여서 정의합니다. 그래서 ++ 연산자의 기능을 정의하기 위한 연산자 함수는 operator++이 됩니다.

Point Point::operator++(void) {

  ++x, ++y;

  return *this;

}

이렇게 하고 나면, 다음과 같이 Point 클래스에 대한  ++ 연산자를 쓸 수  있습니다. pt++는 연산자 표현으로 씌어 있지만, 실제로는 pt.operator++() 함수를 호출하는 것입니다.

 

#include <iostream>

using namespace std;

class Point {

public:

    Point(void);

    Point(int x, int y);

    void Show(void);

    Point operator++(void);

private:

    int x, y;

};

Point::Point(void) {
    x=y=0;
}

Point::Point(int x, int y) {

    this->x = x;

    this->y = y;

}

void Point::Show() {

    cout << "(" << x << ", " << y <<")" << endl;

}

Point Point::operator++(void) {

    ++x, ++y;

    return *this;

}

int main() {

    Point pt(10, 20);

    ++pt;

    pt.Show();

}

실행 결과
(11, 21))

 

++ 연산자에는 다음과 같이 전위형과 후의형의 두 가지 종류가 있습니다.

++pt;    //전위형 연산자

pt++;    //후위형 연산자

Point Point::operator++(void) {

  ++x, ++y;

  return *this;

}

위 함수는 전위형 연산자 함수인데, 후위형 연산자는 쓰는 방법이나 기능이 약간 다르기 때문에 전위형과는 별도로 구현해야 합니다. 연산자 함수를 만드는 규칙은 operator 키워드 다음에 연산자 기호를 붙이는 것인데, 이렇게 하면 전위형과 후위형 함수를 구별할 방법이 없습니다. 그래서 궁여지책으로 후위형 함수는 다음과 같이 인자를 하나 받는 것처럼해서 전위형 함수와 구별되도록 했습니다.

Point Point::operator++(int dummy) {

  Point temp = *this;

  ++x, ++y;

  return temp;

}

실제로 dummy라는 매개변수로 의미 있는 값이 넘어오지는 않습니다. 이 값은 사용하면 안되기 때문에 아예 다음과 같이 매개변수 이름을 지정하지 않고 쓰는 것이 바람직합니다.

Point Point::operator++(int) {

  Point temp = *this;

  ++x, ++y;

  return temp;

}

 

#include <iostream>

using namespace std;

class Point {

public:

    Point(void);

    Point(int x, int y);

    void Show(void);

    Point operator++(void);

    Point operator++(int);

private:

    int x, y;

};

Point::Point(void) {
    x=y=0;
}

Point::Point(int x, int y) {

    this->x = x;

    this->y = y;

}

void Point::Show() {

    cout << "(" << x << ", " << y <<")" << endl;

}

Point Point::operator++(void) {

    ++x, ++y;

    return *this;

}

Point Point::operator++(int) {

    Point temp = *this;

    ++x, ++y;

    return temp;

}

int main() {

    Point p1(0, 0), p2, p3;

    p2 = ++p1;    //전위형 연사자, operator++()

    p1.Show();

    p2.Show();

 

    p3 = p1++;    //후위형 연사자, operator++(int)

    p1.Show();

    p3.Show();

    return 0;

}

 

이항 연산자

이항연산자는 연산을 하는데 항이 두 개 필요하기 때문에 이항연산자의 연산자 함수는 인자를 하나 받아야 합니다. 두 개의 항 중 하나는 this 포인터로 받고, 하나는 인자로 받는 것입니다. 다음과 같이 덧셈 연산자를 호출하는 예를 보겠습니다.

Point a, b, c;

a = b + c;

이와 같이 연산자 표현을 써서 표헌한 것을 함수 표현을 바꿔보면 다음과 같습니다.

a = b.operator+c;

 

, + 연산자의 앞에 있는 항은 b에 대해 멤버함수인 operator+를 호출하되, c 인자로 넘겨준 것입니다. 따라서, 이항연산자의 함수는 자기 자신과 인자로 넘어온 값을 이용하여 연산을 수행하도록 하면 됩니다.

Point 클래스에 대해  + 연산자의 기능을 x, y좌표를 각각 더하는 것으로 정의하면 다음과 같다.

Point Point::operator+(Point pt) {

    Point temp;

    temp.x = x + pt.x;

    temp.y = y + pt.y;

    return temp;

}

#include <iostream>

using namespace std;

class Point {

public:

    Point(void);

    Point(int x, int y);

    void Show(void);

    Point operator+(Point pt);

private:

    int x, y;

};

Point::Point(void) {
    x=y=0;
}

Point::Point(int x, int y) {

    this->x = x;

    this->y = y;

}

void Point::Show() {

    cout << "(" << x << ", " << y <<")" << endl;

}

Point Point::operator+(Point pt)

{

    Point temp;

    temp.x = x + pt.x;

    temp.y = y + pt.y;

    return temp;

}

int main() {

    Point p1(10, 20), p2(5, 7), p3;

    p3 = p1+ p2;

    p3.Show();

    return 0;

}

 

 

friend 함수

서로 다른 타입 간에도 연산을 할 수 있습니다. 예를 들어 점 좌표를 두 배로 늘리고 싶으면 다음과 같이 연산을 할 수 있습니다.

Point  p1(10, 20), p2;

p2 = p1 * 2;

, Point 클래스 형 변수에 int형 상수를 곱하겠다는 뜻입니다. 이를 함수 표현으로 바꾸면 p1.operator*(2)를 호출하는 것이니까, 다음과 같이 int형을 인자로 받는 operator* 함수를 구현하면 이와 같은 동작을 할 수 있게 됩니다.

Point Point::operator*(int mag)

{

  Point temp;

  temp.x = x * mag;

  temp.y = y * mag;

  return temp;

}

이와 같이 서로 다른 타입간에 연산을 하는 이항연산자를 만들 때는 연산의 순서도 매우 중요한 고려사항이 됩니다. 다음과 같이 곱셈 연산의 순서를 바꿔 보도록 하겠습니다.

Point p1(10, 20), p2;

p2 = 2 * p1;

 

곱셈은 교환법칙이 성립하는 연산자이므로 이렇게 순서를 바꿔도 같은 결과를 얻을 수 있어야 합니다. 하지만 이를 함수 표현으로 바꾸면 2.operator*(p1) 가 되어, int형 상수에 대해 Point형을 인자로 받는 operator* 함수를 호출하는 것이 됩니다. int형은 기본 데이터 타입이기 때문에, 이러한 연산자 함수를 정의할 수도 없습니다. 이는 연산자 함수를 클래스의 멤버함수로 정의할 때 첫 번째 항은 this 포인터로 전달하고 두 번째 항은 인자로 전달한다는 규칙 때문에 생기는 문제입니다. 해결책은 연산자 함수를 멤버함수로  정의하지  말고, 두 개의 항을 모두 인자로 전달 받는 전역함수로 만드는 것입니다.

Point operator*(int mag, Point pt)

{

  Point temp;

  temp.x = mag * pt.x;     //private 멤버변수를 외부에서 사용

  temp.y = mag * pt.y;     //private 멤버변수를 외부에서 사용

  return temp;

}

전역함수는 멤버함수와 달리 this 포인터를 사용하지 않기 때문에 필요한 인자를 순서대로 써 주고, 넘겨 받은 인자들을 이용하여 연산을 하도록 구현하면 됩니다.

그러나 이렇게 연산자 함수를 멤버함수로 만들지 않고 전역함수로 만들면 이 함수에서 Point 클래스의 private 멤버변수에 접근할 수 없게 되는 제약이 생깁니다. , 앞에서와 같이 pt.x, pt.y와 같은 접근을 할 수 없다는 것이지요. 이 문제의 해결책은 이 함수를 Point 클래스의 friend 함수로 선언하는 것입니다. friend 함수는 멤버함수는 아니지만, 멤버함수처럼  클래스의 모든 멤버를 참조할 수 있는 권한을 갖게 됩니다.

요컨대 클래스 안에 다음과 같이 * 연산자 함수를 선언합니다. 이 때 p1*2 형태의 연산을 처리하기 위한 멤버함수와 2*p1 형태의 연산을 처리하기 위한 friend 함수를 각각 선언합니다.

class Point {

public:

  Point(void);

  Point(int x, int y);

  void Show(void);

  Point operator*(int mag);

  friend Point operator*(int mag, Point pt);

private:

  int x, y;

};

다음과 같이  * 연산자 함수를 정의합니다. friend 함수는 멤버변수에 대한 접근 권한을 가질 뿐 멤버함수는 아니므로 함수로 정의할 때 함수이름 앞에 Point::를 쓰지 말아야 한다는 것도 주의하기 바랍니다.

Point Point::operator*(int mag)

{

  Point temp;

  temp.x = x * mag;

  temp.y = y * mag;

  return temp;

}

Point operator*(int mag, Point pt)

{

  Point temp;

  temp.x = mag * pt.x;

  temp.y = mag * pt.y;

  return temp;

}

#include <iostream>

using namespace std;

class Point {

public:

    Point(void);

    Point(int x, int y);

    void Show(void);

    Point operator*(int mag);

    friend Point operator*(int mag, Point pt);

private:

    int x, y;

};

Point::Point(void) {
    x=y=0;
}

Point::Point(int x, int y) {

    this->x = x;

    this->y = y;

}

void Point::Show() {

    cout << "(" << x << ", " << y <<")" << endl;

}

Point Point::operator*(int mag)

{

    Point temp;

    temp.x = x * mag;

    temp.y = y * mag;

    return temp;

}

Point operator*(int mag, Point pt)

{

    Point temp;

    temp.x = mag * pt.x;

    temp.y = mag * pt.y;

    return temp;

}

int main() {

   Point p1(10, 20), p2;
   //Point::operator*(int mag) 호출

    p2 = p1 * 2;    

    p2.Show();

    //operator*(int mag, Point pt) friend 함수호출

    p2 = 2 * p1;    

    p2.Show();


    return 0;

}

 

 

연산자 함수의 인자

연산자 함수의 인자는 크게 기본 데이터 타입 클래스 타입의 두 종류로 나눌 수 있습니다.

 

클래스 타입을 인자로 받는 경우

Point Point::operator+(Point pt) {

  Point temp;

  temp.x = x + pt.x;

  temp.y = y + pt.y;

  return temp;

}

클래스 타입을 인자로 받는 함수를 호출하면 인자로 넘겨진 변수와 별개의 매개변수가 생성되고, 복사 생성자가 호출되어 값이 통째로 복사됩니다. 크기가 큰 클래스를 인자로 넘겨주면 메모리도 많이 사용되고, 복사하는데 시간도 많이 걸릴 수 있습니다.

 

레퍼런스 타입을 인자로 받는 경우

Point Point::operator+(const Point &pt) {

  Point temp;

  temp.x = x + pt.x;

  temp.y = y + pt.y;

  return temp;

}

레퍼런스 타입을 인자로 받는 함수를 호출하면 연산자 함수 내에서 별개의 매개변수가 생성되지도 않고 복사도 일어나지 않기 때문에 훨씬 효율적입니다. 인자로 넘겨받은 변수의 값이 변경되지 않는다는 것을 보장하기 위해 const 키워드만 붙여주면 됩니다.

 

기본 데이터 타입을 인자로 받는 경우

Point Point::operator*(int mag) {

  Point temp;

  temp.x = x * mag;

  temp.y = y * mag;

  return temp;

}

기본 데이터 타입은 크기가 얼마 안되기 때문에 레퍼런스 타입을 사용하지 않아도 문제가 없습니다. 그래도 레퍼런스 타입을 사용하면 다만 몇 바이트라도 복사되는 것을 막을 수 있기 때문에 효율적일 거라고 생각할 수 있지만, 이로 인해 생기는 제약이 만만치 않습니다.

Point Point::operator*(const int mag) {

  Point temp;

  temp.x = x * mag;

  temp.y = y * mag;

  return temp;

}

예를 들어 기본 데이터 타입을 레퍼런스로 선언하면 다음과 같이 변수로 써서 연산자 함수를 호출하는 데는 문제가 없습니다.

Point a(10, 10), b;

int c = 2;

b = a * c;

여기서 변수 c가 레퍼런스 타입으로 operator*함수에 전달되는 것입니다. 그런데 기본 데이터 타입을 레퍼런스 타입으로 받으면 어이없게도 다음과 같이 쓸 수 없게 됩니다.

Point a(10, 10), b;

b = a * 2;

레퍼런스 타입은 변수에 새로운 이름을 붙이는 것이기 때문에 상수는 레퍼런스 타입으로 받을 수 없는 것입니다. 요컨대 연산자 함수의 인자로 클래스 타입을 받을 때는 레퍼런스 타입으로 받는 것이 좋고, 기본 데이터 타입을 받을 때는 그냥 변수 타입으로 받는 것이 좋습니다.

 

 

연산자 함수 상수화

연산자 함수를 호출한 결과로 멤버변수가 변경되는지 아닌지를 명확히 표시해 주는 것이 좋습니다. 예를 들어 대입 연산자는 주어진 값으로 현재 멤버변수의 값을 대치하는 것이므로 연산자 함수 호출결과 멤버변수의 값이 변경됩니다.

Point Point::operator=(const Point &pt) {

  x = pt.x;

  y = pt.y;

  return *this;

}

반면 덧셈 연산자를 수행하면 셈의 결과가 새롭게 만들어지는 것이지 자신의 값이 변경되는 것은 아닙니다. 다음과 같이 const 멤버함수를 써서 연산 결과로 자신이 변경되지 않는다는 것을 명시해 주는 것이 좋습니다.

Point Point::operator+(const Point &pt) const{

  Point temp;

  temp.x = x + pt.x;

  temp.y = y + pt.y;

  return temp;

}

요컨대 연산자의 기능에는 자신의 값을 변경시키는 것과 자신은 그대로 있고 새로운 값을 생성하는 두 종류가 있습니다. 자신의 값이 변경되지 않는 연산자를 구현할 때 cosnt 멤버함수를 사용하는 것이 좋습니다. 연산자 수행 결과 멤버변수의 값이 변경되지 않는다는 것을 명시하는 효과뿐만 아니라, 실수로 연산자의 기능을 구현할 가능성을 차단하는 효과도 얻을 수 있습니다.

'프로그램언어 > C++' 카테고리의 다른 글

클래스 예제2  (0) 2020.08.10
연산자2  (0) 2020.08.09
클래스5 문제  (0) 2020.08.08
클래스5  (0) 2020.08.08
클래스4 문제  (0) 2020.08.08