.. Cover Letter

ㅇ 공부#언어/(C++) 자료구조 및 STL

(C++) 초급 1장. 연산자 오버로딩(연산자 중복) 복습하기

BrainKimDu 2023. 7. 10. 17:26

주의 : 이 글은 C++의 기초문법에 대해 상세하게 다루지 않습니다. 

※ 뇌를 자극하는 C++ STL 의 책을 참고하여 개인적으로 정리한 글입니다. 
이 글의 목적은 해당 책의 내용을 인용하여 더 쉽게 이해하고자 정리하고, 더 쉬운 예제를 통해 이해하는 것을 목표로 하고 있습니다. 깊게 공부하고 싶다면 책을 구매하는 것을 추천드립니다.
책의 저작권 등등 각종 권한은 출판사와 지은이/옮긴이에 있습니다.

- 출판사: (한빛 미디어)
- 지음: 공동환

※ 이 블로그는 포토폴리오 목적으로 별도의 수익을 창출하지 않습니다.

뇌를 자극하는 C++ STL - YES24

 

뇌를 자극하는 C++ STL - YES24

코드 중심으로 설명했다. 코드를 실습하면서 한 단계씩 실력을 쌓을 수 있게 했다. 단계별로 난이도를 조금씩 올리고 예제를 점진적으로 개선하는 방법을 택해 독자가 STL의 동작 원리와 구현 원

www.yes24.com


연산자 오버로딩

연산자 오버로딩 (C++ 기초에서 연산자 중복 : Operator Overloading)이란 정말 간단한 예제로 보면 이해가 가능하리라 생각됩니다.

1 + 1 = 2

1 + 1은 2입니다. +에 대한 연산의 정의는 컴파일러에 정의되어 있기 때문에 컴파일러는 손쉽게 계산할 수 있습니다. 그러나

class water
{
	....
}

int main(){
	water A, B;
    std::cout << A + B;
}

다음의 연산은 도대체 무엇일까요? water 객체 A와 B를 더하는건 컴파일러가 어떤 일을 하라는 건지 이해를 못합니다. 그러므로 오류가 발생합니다.

그래서 우리는 water의 객체 A와 B를 더하는 것이 무엇인지 정의를 해주어야합니다. 이 과정을 연산자 오버로딩 (연산자 중복)이라고 이야기 합니다.

※ 참고 삼아서 언급합니다. 코드를 짤 때 다음을 따라주는 것이 좋습니다.

이는 ROS의 코딩 방식을 따릅니다. 그러므로 다른 개발환경에서는 다를 수 있습니다.
http://wiki.ros.org/CppStyleGuide 

class water
{

};

class water{  

};
"아래의 방법 말고 C++에서는 위의 방법으로 코드를 작성해주어야 하는 것이 좋습니다."
"ChatGPT 말로는 아래의 방법은 주로 Java 개발자들이 사용하는 방식이라 설명합니다."

"또한 C++에서는 Tab을 사용하지 않습니다. 대신 띄어쓰기 2번을 사용합니다."
"이 부분은 파이썬과 다릅니다. 파이썬은 Tab이 띄어쓰기 4번이며, 강제로 지켜주어야합니다"
class water
{
  int A; "A가 올바른 방법입니다."
	int B; "B는 올바르지 않습니다."
public:
}

제 글의 모든 코드가 이를 따를지는 모르겠지만, 최대한 지키면서 진행하겠습니다.

이에 대한 설정은 블로그 글로 정리 해놓았습니다. C++ VS community에서 탭의 크기 변경하기 (tistory.com)

 

※ 또한 다음의 코드 한 줄에 대한 이야기입니다.

Using namespace std;

저는 해당 코드를 사용하지 않습니다. ROS 코드를 작성하거나 Embeded 코드를 작성할 때 조금 불편함이 있었던 거로 기억합니다. (이건 제 개인 취향이라 정답은 아닙니다.) ChatGPT도 일반적으로 권장을 하는 방법은 아니라고 합니다. 보통 알고리즘 문제를 풀때 주로 사용하는 것으로 알고 있습니다.

 

 

연산자 오버로딩 활용

그러면 연산자 오버로딩을 한 번 구현해봅시다. water 클래스에는 물과 소금이 존재합니다. 이 물의 농도를 구해주는 코드를 작성해봅시다. (소금의 양 / 소금물의 양)

#include<iostream>

class SaltWater
{
  int salt;
  int water;
public:
  SaltWater(){}
  SaltWater(int salt, int water)
  {
    this->salt = salt;
    this->water = water;
  }
  void printSaltWater()
  {
	std::cout << "소금물의 농도는 :" << (double)salt / (water+salt);
  }
};

다음과 같은 클래스가 있습니다. int main에서는 소금물 객체 3개를 만들어 보도록 합시다.

int main() {
  SaltWater A(5, 100), B(1, 50), C;
  C = A + B;
  C.printSaltWater();
}

A와 B 객체가 만들어졌으나, A+B에 대한 연산을 모르기 때문에 C = A + B 행에서 오류가 발생합니다. 오류의 이름은 다음과 같습니다.

이럴 때에는 클래스에서 직접 +역할을 하는 연산자를 정의해주면 됩니다.

SaltWater operator+(const SaltWater &arg)
{
  SaltWater temp;
  temp.salt = this->salt + arg.salt;
  temp.water = this->water + arg.water;

  return temp;
}

다음의 코드를 추가하는 경우 +연산이 정상적으로 실행됩니다.

그러면 이제 operator+ 함수를 조금 들어가보도록 하겠습니다. 

 

SaltWater operator+(const SaltWater &arg)

우리는 C 객체에 A와 B 객체의 +연산을 한 후에 저장을 하길 원했습니다. 그러니 operator+는 SaltWater 객체 그 자체를 반환해주어야 합니다. 

그 다음에 SaltWater &arg 의 매개변수 부분을 보면 arg 객체를 참조를 통해 들어오게 됩니다. 참조는 간단하게 A 객체를 들고 올때 A객체를 가리키는 친구입니다. A객체를 가리키는 친구들 들고오니 메모리면에서 효율적입니다. 

그러나 A객체를 가리키는 친구를 수정하면 A객체가 바뀌는 문제가 발생합니다. 이를 막기위해서는 참조변수를 매개변수로 사용할 때는 const를 통해서 "나는 A객체를 수정하지 않겠다" 라는 것을 컴파일러에게 알리게 됩니다. 그래서 A객체의 요소가 변경되는 경우 오류를 발생시킵니다.

또한 operator+(매개변수)가 어떤 일을 하게되는지 이해를 해보면 다음과 같습니다.  A+B 라는 연산을 이해해보면 A+ 와  B의 구조를 가지고 있습니다.  그래서 A객체에서 + 연산자 오버로딩 함수를 불러온 것입니다. 

{
  SaltWater temp;
  temp.salt = this->salt + arg.salt;
  temp.water = this->water + arg.water;

  return temp;
}

그래서 this->salt 는 A객체의 salt이고, arg.salt는 매개변수 B객체의 salt 입니다.  그 외 SaltWater temp를 왜 생성하는지에 대해서는 생략합니다.

※ 추가로 다음과 같은 경우가 있습니다.

SaltWater operator+(const SaltWater &arg) const
  {
    SaltWater temp;
    temp.salt = this->salt + arg.salt;
    temp.water = this->water + arg.water;

    return temp;
  }

함수의 뒤에 const를 붙인 경우를 const 맴버 함수라고 합니다. 이는 자신의 맴버함수를 변경하지 않는다는 것을 나타냅니다.

그래서 다시 이해를 해보면 const SaltWater &arg 를 통해서 arg 참조변수가 가리키는 객체를 보호합니다. 함수의 뒤에 const를 넣음으로서 A+B 에서  A객체를 보호합니다.

그러면 다음의 예제가 있습니다.

const class operator+(const class &A) const

각 const들의 역할입니다. 

맨앞의 const는 반환되는 class 객체가 수정되지 않는다는 것을 의미합니다.

매개변수의 cosnt는 참조로 받은 A를 수정하지 않는 다는 것을 의미합니다.

맨 뒤의 const는 해당 함수가 호출된 객체의 상태를 변경하지 않음을 의미합니다. 

그래서 구조만 이해된다면 굳이 C = A+ B 가 아니여도 A + B 와 같이 코드를 작성해도 됩니다.

 

연산자 오버로딩의 다양한 모습

C++에는 다양한 연산 방법이 존재합니다.

a++
a--
++a
--a
!a
&a 

등등

 

1. 단항 연산자 오버로딩

단항 연산자는 ! & ~ * + - ++ -- 가 해당됩니다. 우선은 ++에 다해서 보도록 하겠습니다. 위에서 소금물에 대한 예제를 더 들어가 봅시다.

객체++ 를 하는 경우 물이 50g 넣어진다고 생각합시다. ++객체를 하는 경우 소금이 10g 넣어진다고 생각합니다. 

(코드는 추가된 부분과 main에 대해서만 언급합니다.)

int main() {
  SaltWater A(5, 100), B(1, 50);
  A.printSaltWater();
  A++;
  A.printSaltWater();
  ++A;
  A.printSaltWater();
}

다음의 main을 만족하기 위해서는 A++와 ++A에 대한 정의가 필요합니다. 그래서 class에 연산자 멤버함수(메소드)를 추가합시다.

그리고 printSaltWater() 함수를 조금 손봐줍니다.

void printSaltWater()
  {
	std::cout << "물은 : " << water << "소금은 :" << salt << "소금물의 농도는 : " << (double)salt / (water+salt) << "\n";
  }
  ※ 참고로 줄바꿈에서 "\n" 과 std::endl이 있으며, 속도면에서는 "\n" 이 유리합니다. (코테팁)

 

 

다음의 연산은 후위 연산이 아닙니다. 선행연산으로 취급됩니다. 그래서 소금을 추가하는 코드는 다음과 같습니다.

void operator++()
  {
    this->water += 100;
  }

 

후위연산을 하는 경우에는 매개변수에 int를 넣습니다. 이는 의미없는 dummy를 보내는 것으로 왜 int인가에 대해 깊게 고민할 필요는 없습니다. 

  void operator++(int)
  {
    this->water += 50;
  }

그래서 결과를 보면 다음과 같습니다.

++에 대해 공부를 했음으로 --의 경우는 ++와 같습니다. 그래서 --는 다음처럼 작성될 수 있을 겁니다.

const void operator--()
  {
    this->salt -= 50;
  }

  void operator--(int)
  {
    this->water -= 50;
  }

사실 소금물에서 물만 빼는거나 소금을 빼는게 조금 어렵긴 한데, 그냥 받아들입시다.

 

이항 연산자 오버로딩

이항 연산자는 +, -, *, /, ==, !=, <, <= 등이 있습니다. 가장 처음에 예제로 보았던 예시가 이항 연산자입니다. 여기서는 ==에 대해서 살펴보도록 하겠습니다. 반환되는 것은 true와 false 입니다.

다음과 같은 int main 함수를 만족하는 함수를 작성해봅시다.

int main() {
  SaltWater A(5, 100), B(1, 50);
  if (A == B)
  {
    std::cout << "같아요";
  }
  if (A != B)
  {
    std::cout << "같지 않아요";
  }
}

반환 타입은 bool이므로 다음처럼 코드 작성이 가능할 것입니다. 

bool operator==(const SaltWater &arg) const
  {
    if (this->salt == arg.salt && this->water == arg.water)
      return true;
    else
      return false;
  }
bool operator!=(const SaltWater& arg) const
  {
    if (this->salt == arg.salt && this->water == arg.water)
      return false;
    else
      return true;
  }

 

전역 함수를 이용한 연산자 오버로딩

지금까지는 클래스 내에서 멤버 함수(메소드) 형식으로 연산자를 오버로딩 했습니다. 연산자 오버로딩에는 멤버 함수로만 하는 방법이 존재하는 것이 아니라 전역함수를 이용한 연산자 오버로딩도 가능합니다. 

예를 들어서 이런 겁니다.

class A
{
}

int main()
{
  A a;
  k + a;
}

위와 같은 경우입니다. k는 A의 객체가 아니기 때문에 operatior+(a)를 호출할 수 없습니다. 그래서 전역함수로 이를 선언합니다.

간단하게 위에서 진행했던 예제의 +를 전역함수로 선언해보겠습니다.

SaltWater operator+(const SaltWater &arg) const
  {
    SaltWater temp;
    temp.salt = this->salt + arg.salt;
    temp.water = this->water + arg.water;

    return temp;
  }

다음의 코드입니다.  해당 코드를 전역함수로 취해보겠습니다.

우선 클래스 밖으로 위의 함수를 빼줍니다.

const SaltWater operator+(const SaltWater &argL , const SaltWater& argR)
{
  SaltWater temp;
  temp.salt = argL.salt + argR.salt;
  temp.water = argL->water + argR.water;

  return temp;
}

다음의 방식이 있겠지만 코드는 맞습니다만 한가지 문제가 있습니다. salt와 water는 private으로 선언되어 있기 때문에 외부에서 접근이 차단되어 있습니다. 그래서 private를 public으로 바꿔주거나 getSalt와 getWater 멤버함수를 만들어주는 방법이 있습니다.

여기서는 후자의 방법을 사용하며, 후자의 방법이 정석적인 방법입니다.

 int getSalt()
  {
    return salt;
  }
  int getWater()
  {
    return water;
  }
  void setSalt(int salt)
  {
    this->salt = salt;
  }
  void setWater(int water)
  {
    this->water = water;
  }

 

const SaltWater operator+(const SaltWater &argL , const SaltWater& argR)
{
  SaltWater temp;
  temp.setSalt(argL.getSalt() + argR.getSalt());
  temp.setWater(argL.getWater() + argR.getWater());

  return temp;
}

그러나 문제가 발생합니다. const에 걸려있어 getSalt의 접근이 차단되었습니다. 이 경우에는 getSalt와 getWater 함수를 const로 선언하여 읽기만 가능한 상태로 만들어주어야합니다. 그래서 클래스 내의 멤버함수를 다음처럼 변경합니다.

 int getSalt() const
  {
    return salt;
  }
  int getWater() const
  {
    return water;
  }
  void setSalt(int salt)
  {
    this->salt = salt;
  }
  void setWater(int water)
  {
    this->water = water;
  }

정상적으로 실행이 됩니다.  get을 사용하지않는 방법은 하나 더 있습니다. get 함수를 사용해서 접근하는 수고를 안하고 friend로 선언을 하는 경우입니다. friend는 클래스내의 private와 protect 멤버변수에 접근하기 위한 방법 중 하나입니다.

이것 또한 정석적인 방법입니다. 그래서 함수를 다음과 같이 수정합니다.

다음처럼 클래스 내에서 friend로 선언해주면 연산자 오버로딩 전역함수는 각 멤버변수에 접근이 가능해집니다.

여기까지는 복습의 개념이 강했지만, 다음부터는 조금 복잡한 이야기에 들어갑니다.