[C++] 연산자 오버로딩

아현·2021년 10월 19일
0

C++

목록 보기
5/5

출처



1. 멤버 함수 연산자 오버로딩(Operator Overloading)

에러

  • result에 객체 nb1과 nb2를 더한 값을 result로 대입하고 있는데, 에러를 보시면 "이러한 피연산자와 일치하는 "+" 연산자가 없습니다. 피연산자 형식이 NUMBOX + NUMBOX입니다."라는 에러가 뜨시는것을 보실 수 있습니다.

    • 연산자 오버로딩을 활용하여 기존에 있던 + 연산자가 아닌, NUMBOX 객체 끼리의 덧셈이 가능한 연산자를 추가해보도록 합시다.

#include <iostream>
using namespace std;
class NUMBOX
{
private:
  int num1, num2;

public:
  NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowNumber(){
      cout << "num1: " << num1 << ", num2: " << num2 << endl;
  }
};

int main()
{
  NUMBOX nb1(10, 20);
  NUMBOX nb2(5, 2);
  NUMBOX result = nb1 + nb2; // 에러 발생
  nb1.ShowNumber();
  nb2.ShowNumber();
}



1) 객체 끼리의 덧셈



#include <iostream>
using namespace std;

class NUMBOX
{
private:
  int num1, num2;
public:
  NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowNumber()
  {
    cout << "num1: " << num1 << ", num2: " << num2 << endl;
  }
  NUMBOX operator+(NUMBOX &ref)
  {
    return NUMBOX(num1+ref.num1, num2+ref.num2);
  }
};

int main()
{
  NUMBOX nb1(10, 20);
  NUMBOX nb2(5, 2);
  NUMBOX result = nb1 + nb2;
  // NUMBOX result = nb1.operator+(nb2);

  nb1.ShowNumber();
  nb2.ShowNumber();
  result.ShowNumber();
}




num1: 10, num2: 20

num1: 5, num2: 2

num1: 15, num2: 22

계속하려면 아무 키나 누르십시오 . . .


  • nb1 + nb2nb1.operator+(nb2)로 해석되어 컴파일 됩니다.

  • 물론, operator+가 아니여도 operator<연산자>(operator+, operator-, operator*..)와 같은 형식으로 사용이 가능합니다.



2) 자료형 다른 덧셈


#include <iostream>
using namespace std;

class NUMBOX
{
private:
  int num1, num2;
public:
  NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowNumber()
  {
    cout << "num1: " << num1 << ", num2: " << num2 << endl;
  }
  NUMBOX operator+(int num)
  {
    return NUMBOX(num1+num, num2+num);
  }
};
int main()
{
  NUMBOX nb1(10, 20);
  NUMBOX result = nb1 + 10;
  nb1.ShowNumber();
  result.ShowNumber();
}






num1: 10, num2: 20

num1: 20, num2: 30

계속하려면 아무 키나 누르십시오 . . .


  • 멤버 함수를 통한 오버로딩은 객체.operator+(피연산자), 객체 + 피연산자식으로 이루어져, 자료형이 다른 두 피연산자를 대상으로 하는 연산시, 반드시 객체가 왼쪽에 위치해야 연산이 가능



2. 전역 함수 오버로딩


  • 전역 함수를 통한 오버로딩은 operator+(피연산자, 피연산자), 피연산자 + 피연산자 의 식으로 객체가 뒤에 위치해도 정상적인 결과를 출력합니다.


#include <iostream>
using namespace std;

class NUMBOX
{
private:
  int num1, num2;
public:
  NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowNumber()
  {
    cout << "num1: " << num1 << ", num2: " << num2 << endl;
  }

  NUMBOX operator+(int num)
  {
    return NUMBOX(num1+num, num2+num);
  }

  friend NUMBOX operator+(int num, NUMBOX ref);
};

NUMBOX operator+(int num, NUMBOX ref)
{
  ref.num1 += num;
  ref.num2 += num;
  return ref;
}

int main()
{
  NUMBOX nb1(10, 20);
  NUMBOX result = 10 + nb1 + 40;
  
  nb1.ShowNumber();
  result.ShowNumber();
}




  • friend 키워드가 붙은 이유는, 이 함수가 클래스의 멤버 함수가 아니기 때문에 멤버 변수에 접근할 수 없으므로 붙여준 것입니다.

  • operator+ 함수를 통해 operator+(10, nb1)으로 인식됩니다.

  • 이어서 main 함수를 다시 한번 보시면 10nb1을 더하고 그 결과에서 40을 더해 result에 대입하고 있습니다.



3. 단항 연산자 오버로딩



#include <iostream>
 
using namespace std;
 
class NUMBOX
{
private:
  int num1, num2;
public:
  NUMBOX() { }
  NUMBOX(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowNumber() {
    cout << "num1: " << num1 << ", num2: " << num2 << endl;
  }
  
  NUMBOX operator++(){
    num1+=1;
    num2+=1;
    return *this;
  }

  NUMBOX operator++(int)
  {
    NUMBOX temp(*this);
    num1+=1;
    num2+=1;
    return temp;
  }
};
 
int main()
{
  NUMBOX nb1(10, 20);
  NUMBOX nb2;

  nb2 = nb1++;
  nb2.ShowNumber();
  nb1.ShowNumber();

  nb2 = ++nb1;
  nb2.ShowNumber();
  nb1.ShowNumber();
}

num1: 10, num2: 20

num1: 11, num2: 21

num1: 12, num2: 22

num1: 12, num2: 22

계속하려면 아무 키나 누르십시오 . . .



  • 6~21행에서 ++ 연산자가 멤버 함수의 형태로 오버로딩 되었음을 보실 수 있습니다.

    • 안을 살펴보면, num1에 1을 더하고, num2에 1을 더하고, this가 아닌 *this를 반환합니다.

      • this는 객체의 주소를, *thisthis 포인터가 가리키는 객체, 실질적인 데이터를 의미합니다.
    • 이런 형식은 전위 증가 연산입니다.

  • 22~28행을 살펴보면 ++ 연산자가 멤버 함수의 형태로 오버로딩 되었으나, 위와는 달리 인수 목록에 int 타입이 등장합니다.

    • 이는, C++에서 전위 또는 후위 연산에 대한 구분 규칙에 의한 것이며, 전위와 후위는 아래와 같이 구분합니다.

++nb = nb.operator++(); // 전위 증가 연산
nb++ = nb.operator++(int); // 후위 증가 연산

🛑int는 그저 전위 증가 연산과 후위 증가 연산을 구분하는 기준일 뿐, int 타입의 데이터를 인자로 전달한다는 뜻으로 오해하지 마시기 바랍니다.


  • 24행을 보시면 NUMBOX 객체를 만들어두고 인자로 *this를 넘깁니다. 이 문장은 NUMBOX temp(num1, num2)와 같습니다.

  • 25~26행에선 num1, num2의 값을 1씩 증가시키고 27행에선 기존의 값을 담은 temp를 반환합니다.



4. 대입 연산자 오버로딩



#include <iostream>
 
using namespace std;
 
class A
{
private:
  int num1, num2;
public:
  A() { } // 디폴트 생성자
  A(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowData() { cout << num1 << ", " << num2 << endl; }
};
 
class B
{
private:
  int num1, num2;
public:
  B() { }
  B(int num1, int num2) : num1(num1), num2(num2) { }
  void ShowData() { cout << num1 << ", " << num2 << endl; }
};
 
int main()
{
  A a1(10, 50);
  A a2;
  B b1(10, 20);
  B b2;
 
  a2 = a1;
  b2 = b1;
 
  a2.ShowData();
  b2.ShowData();
  return 0;
  
}
10, 50

10, 20

계속하려면 아무 키나 누르십시오 . . .


  • 27행에서 a1 객체가 만들어짐과 동시에 생성자에게 10과 50을 각각 전달하고, 멤버 변수 num1, num2를 초기화 합니다.

  • 28행에서는 a2 객체가 만들어지고 디폴트 생성자가 호출됩니다. (초기화되지 않음)

  • 29~30행도 마찬가지로 생성과 동시에 멤버 변수가 초기화된 b1 객체와, 그렇지 않은 b2 객체로 나뉩니다.

  • 32~33행을 보시면 대입 연산자가 쓰였는데, 이상하게도, 우리가 대입 연산자를 정의하지 않았음에도 이런 문장은 정상적으로 멤버 대 멤버 복사가 이루어집니다.

    • 사실은, 디폴트 복사 생성자와 같이, 대입 연산자가 정의되지 않으면 디폴트 대입 연산자가 삽입이 되는 것입니다.

    • 멤버 대 멤버 복사를 수행할때 깊은 복사가 아닌 얕은 복사를 진행합니다.



얕은 복사




#include <iostream>
using namespace std;
 
class Student
{
private:
  char * name;
  int age;
public:
  Student(char * name, int age) : age(age) 
  {
    this->name = new char[10];
    strcpy(this->name, name);
  }
  void ShowInfo() {
    cout << "이름: " << name << endl;
    cout << "나이: " << age << endl;
  }
  ~Student()
  {
    delete []name;
    cout << "~Student 소멸자 호출!" << endl;
  }
};
 
int main()
{
  Student st1("김철수", 14);
  Student st2("홍길동", 15);
 
  st2 = st1;
 
  st1.ShowInfo();
  st2.ShowInfo();
  return 0;
}


이름: 김철수

나이: 14

이름: 김철수

나이: 14

~Student 소멸자 호출!

계속하려면 아무 키나 누르십시오 . . .




  • 5~25행에 Student 클래스가 정의되었습니다.

    • 이름을 나타내는 name, 나이를 나타내는 age 멤버 변수가 존재합니다.

    • 생성자를 보시면, 멤버 이니셜라이저를 통해 age를 초기화 하고, this->name에 길이가 10char형 공간을 할당해주고, 인자로 받은 namethis->name로 복사합니다.

  • 20~24행은 소멸자가 정의되었는데, 소멸자 안을 살펴보시면 따로 할당한 name을 메모리 공간에서 해제하고, 소멸자가 호출되었음을 알리기 위해 "~Student 소멸자 호출!"을 화면에 출력하게 했습니다.

  • 29~30행에서 st1, st2 객체가 생성됨과 동시에 멤버 변수 초기화를 했습니다.

  • 32행에서 디폴트 대입 연산자에 의해 멤버 대 멤버 복사가 이루어지는데, 여기서 문제가 발생합니다.

    • 복사가 이루어지면서 st2는 "홍길동"이 아닌 "김철수"란 문자열이 담긴 주소를 가리키고, "홍길동"이란 문자열은 접근도, 소멸도 불가능 해지는 상황이 벌어집니다.

    • 또한, 두 객체의 소멸자가 호출될 때 st1, st2 객체 모두 "김철수"란 문자열이 담긴 주소를 가리키고 delete를 통해 소멸할 때 중복 소멸하는 문제가 일어납니다.


✔ 이것을 해결하기 위해선 어떻게 해야할까요? 얕은 복사가 아닌 깊은 복사를 정의하면 됩니다. 아래와 같이 말이죠.



깊은 복사



#include <iostream>
using namespace std;
 
class Student
{
private:
  char * name;
  int age;
public:
  Student(char * name, int age) : age(age) 
  {
    this->name = new char[10];
    strcpy(this->name, name);
  }
  void ShowInfo() {
    cout << "이름: " << name << endl;
    cout << "나이: " << age << endl;
  }
  Student& operator=(Student& ref)
  {
    delete []name;
    name = new char[10];
    strcpy(name, ref.name);
    age = ref.age;
    return *this;
  }
  ~Student()
  {
    delete []name;
    cout << "~Student 소멸자 호출!" << endl;
  }
};
 
int main()
{
  Student st1("김철수", 14);
  Student st2("홍길동", 15);
 
  st2 = st1;
 
  st1.ShowInfo();
  st2.ShowInfo();
  return 0;
}

이름: 김철수

나이: 14

이름: 김철수

나이: 14

~Student 소멸자 호출!

계속하려면 아무 키나 누르십시오 . . .



  • 20~27행을 보시면 대입 연산자를 정의(연산자 오버로딩)하고 있습니다.

    • 메모리 누수를 막기 위해 name을 메모리 공간에서 해제시키고, 23행에서 새로 공간을 할당합니다. 24행에서 strcpy 함수를 통해 ref.namename에 복사시킵니다. 25행에서는, ageref.age를 대입하고, 객체가 담고있는 값을 반환합니다.
  • 이러게 되면, "홍길동"란 문자열이 해제되지 않고 메모리 공간에 남아있는 문제를 해결할 수 있고(22행의 delete 연산), 정의된 대입 연산자에 의해 복사가 이루어지고 st1의 "김철수"와 st2의 "김철수"는 서로 다른곳을 가리키게 됩니다.



profile
Studying Computer Science

0개의 댓글