CPP/씹어먹는 c++

[cpp 개념공부]연산자 오버로딩 (사용자 지정 연산자, 대입연산자, 입출력 오버로딩 ,첨자 연산자 Wrapper class)

뜨거운 개발자 2023. 2. 6. 10:31

사용자 지정 연산자

💡
기본 연산자를 사용자가 직접 정의해서 사용하는것 :

사용불가 사용자 지정 연산자

💡
::(범위지정), .(멤버지정), .*(멤버포인터로 멤버지정)을 제외한 모든 연산자 가능

사용가능 사용자 지정 연산자

  • +, -, * 와 같은 산술 연산자
  • +=, -= 와 같은 축약형 연산자
  • >=, == 와 같은 비교 연산자
  • &&|| 와 같은 논리 연산자
  • ->  나  *  와 같은 멤버 선택 연산자 (여기서 * 는 역참조 연산자 입니다. 포인터에서 p 할 때 처럼)
  • ++, -- 증감 연산자
  • [] (배열 연산자) 와 심지어 () 까지 (함수 호출 연산자)

연산자 오버로딩

기본형 : (리턴 타입) operator (연산자) (연산자가 받는 인자)

이 방법 외에는 함수 이름으로 연산자를 넣을 수 없다.

  Complex operator+(const Complex& c) const;
  Complex operator-(const Complex& c) const;
  Complex operator*(const Complex& c) const;
  Complex operator/(const Complex& c) const;
Complex Complex::operator+(const Complex& c) const {
  Complex temp(real + c.real, img + c.img);
  return temp;
}


Complex Complex::operator+(const char* str) const {
  Complex temp(str);
  return (*this) + temp;
}

Complex(const char *str)생성자만 남겨놓고, operator+(const char *str)계열들을 모두 지워보시고 컴파일시 정상작동한다 왜냐면 놀랍게도 컴파일러는 생성자가 있다면 그 생성자를 찾아서 자동으로 암시적 변환을 수행하기 때문이다. 여기서 생기는 문제는 앞뒤가 바뀌면 안된다는점 즉

a = a + "-1.1 + i3.923"; // ① 이건 되지만

a = "-1.1 + i3.923" + a; // ② 이건 안된다는 사실

중요! 사칙연산에서는 레퍼런스를 리턴하는게 아닌 값을 리턴해야만 한다.

이유는 Complex a = b + c + b; 이런 식이 있을때 사용자는 아마 2 * b + c를 의도했을 텐데, 사실은 (b.plus(c)).plus(b) 이렇게 처리가 된다. 만약 레퍼런스 타입을 넣게 되면 b객체에 b+c 가 들어가게 되서 실제로는 반드시 값을 리턴해야만 합니다.

인자 값이 함수 내부에서 바뀌지 않는게 확실 할때 const키워드를 붙혀주는게 바람직하다.

상수함수란

객체 내부의 함수가 함수안에서 어떤 변수도 바꿀 수 없다. 이는 클래스에서만 존재하는 개념이고, const함수에서는 const가 붙은 함수만을 호출 할 수가 있다.

대입 연산자 함수

Complex& operator=(const Complex& c);

대입연산자 함수에는 레퍼런스를 리턴해야만 불필요한 복사를 막을 수 있다.

이유는 간단하게 설명해서 a=b=c; 인 경우에 b=c에서 b를 리턴해야지 a=b가 성공적으로 수행이 가능하기 떄문이다.

이때 타입을 리턴하지 않고 레퍼런스를 리턴하는 이유는 불필요한 복사를 막기 위해서이다.

디폴트 대입연산자

디폴트 복사 생성자가 있던 것 처럼 대입연산자 역시 디폴트로 존재한다. 그러나 디폴트 대입연산자 역시 얕은 복사를 수행한다.

Complex& Complex::operator+=(const Complex& c) {
  (*this) = (*this) + c;
  return *this;
}//객체의 내부상태를 변경하기 때문에 const 함수 아니다!!!

some_class a = b;

a의 복사 생성자가 호출되는 것

some_class a;
a = b; 

기본 연산자 호출 후 대입연산자 함수가 실행 되는 것.

주의사항

💡
연산자 오버로딩을 사용하게 되면 a+=b 가 a=a+b와 같다고 보장되지 않는다. operator+ 와 operator=을 정의했다고 해서 a+=b를 사용할 수 있는 게 아니다.
💡
이는++가 +=1, --가 -=1과 로 바뀌지 않는다는 말이다.
💡
즉 연산자 오버로딩을 하게 된다면 생각하는 모든 연산자를 개별적 정의가 필요하다.
💡
컴파일러는 연산자 오버로딩 뿐만 아니라 오버로딩이 불가하다면 생성자 오버로딩 후 실행을 할수도 있다.

friend 키워드

💡
friend로 정의된 클래스나 함수들은 원래 클래스의 private 으로 정의된 변수나 함수들에 접근할 수 있음
class A {
 private:
  void private_func() {}
  int private_num;

  // B 는 A 의 친구!
  friend class B;

  // func 은 A 의 친구!
  friend void func();
};

단 여기서 B에서는 A에 접근 할 수 있지만 A에서 B는 접근이 안 된다.

이항 연산자

  • a = a + "-1.1 + i3.923"; // ① 이건 아무 문제 없이 a = a.operator+(”-1.1 + i3.923) 가 돼서 문제가 없다.
  • a = "-1.1 + i3.923" + a; // ② 이 경우는 문제가 발생한다.
💡
다행스럽게도, 사실 컴파일러는 이항 연산자 (피연산자를 두 개를 취하는 연산자들; 예를 들어서 +, -, *, /, ->, =등) 를 다음과 같은 두 개의 방식으로 해석한다.
//@를 연산자라고 가정하고 a@b의 경우 두개중에 선택함.
*a.operator@(b); //a클래스의 멤버함수로 정의된 연산자
*operator@(a, b); // 클래스 외부에 일반적으로 정의되어있는 함수                       
💡
a.operator@(b)에서의 operator@는 a의 클래스의 멤버 함수로써 사용되는 것
💡
operator@(a,b)에서의 operator@는 클래스 외부에 정의되어 있는 일반적인 함수를 의미

이는 일부 연산자들에 대해서는 해당되지 않는데 대표적으로 [] 연산자 (첨자), -> 연산자 (멤버 접근), 대입 연산자 (=), () 함수 호출 연산자 들의 경우 멤버 함수로만 존재할 수 있다.

  • 외부 함수로 정의 할 때 우리는 private 에 접근해야만 하기 때문에 그 함수를 friend로 선언해주어야 한다.
class complex{
private : //이것저것
public: 
// 이제 이 함수는 Complex 의 private 멤버 변수들에 접근할 수 있습니다.
  friend Complex operator+(const Complex& a, const Complex& b);//이건 멤버함수가 아닌 외부 함수이다.
};

Complex operator+(const Complex& a, const Complex& b) {
  Complex temp(a.real + b.real, a.img + b.img);
  return temp;
}
💡
통상적으로 자기 자신을 리턴하지 않는 이항 연산자들, 예를 들어 위와 같은 +-*/들은 모두 외부 함수로 선언하는 것이 원칙
💡
반대로 자기 자신을 리턴하는 이항 연산자들, 예를 들어 +=-=같은 애들은 모두 멤버 함수로 선언하는 것이 원칙

입출력 연산자 오버로딩 하기

std::cout << a;std::cout.operator<<(a) 와 같다

💡
std::cout이 int나 double변수, 심지어 문자열 까지 자유 자재로 operator<<하나로 출력할 수 있었던 이유는 그 많은 수의 operator<<함수들이 오버로딩 돼있기 때문

ostream클래스에 다른 객체를 오버로딩하는 operator<<연산자 함수를 추가할 수는 없다. (표준헤더 파일은 수정 불가)

해결 방법 : ostream클래스 객체와 Complex객체 두 개를 인자로 받는 전역 operator<<함수를 정의

// 아마 Complex -- 다른 클래스에서 입출력 연산자를 friend 로 지정해줬다.
friend ostream& operator<<(ostream& os, const Complex& c);`
};
std::ostream& operator<<(std::ostream& os, const Complex& c) 
{ // 연산자 오버로딩 할 새로운 함수 os 는 cout이 될것이며 자동으로 암시적 형변환이 된다고 생각하자.
  os << "( " << c.real << " , " << c.img << " ) ";
  return os;
}
friend키워드를 남발하는 것은 썩 권장하지 않는다. 왜냐하면 friend키워드는 해당 함수나 클래스에게 자기 자신의 모든 private멤버 함수와 변수들을 공개하기 때문. 따라서 구현 디테일은 최대한 숨기라는 원칙을 지키기가 힘들어진다.

첨자 연산자 (operator[])

💡
배열에서 원소를 지정할 때 사용되는 첨자 연산자 []를 오버로딩

char& operator[](const int index);

char& 를 리턴하는 이유는 str[10] = 'c'; 같은 명령을 수행하기 때문이다.

char& MyString::operator[](const int index) { return string_content[index]; }

Wrapper 클래스 - 타입 변환 연산자

💡
Wrapper클래스는 무언가를 포장하는 클래스라는 의미인데, C++ 에서 프로그래밍을 할 때 어떤 경우에 기본 자료형들을 객체로써 다루어야 할 때가 있다. 이럴 때, 기본 자료형들 (int, float등등) 을 클래스로 포장해서 각각의 자료형을 객체로 사용하는 것

만약 wrapper클래스에서 형을 감싼경우 모든 연산자를 오버로딩을 해야하겠지만 그것을 아주 쉽게 해결하고 하는 방법이 있다.

그 wrapper 클래스 내부에 만약 wrapper class 를 int를 wrapping한 클래스라면 operator int() {return data;} 이렇게 해주면된다.

operator (변환 하고자 하는 타입) ()

operator int()

operator int() { return data; }

#include <iostream>

class Int {
  int data;
  // some other data

 public:
  Int(int data) : data(data) {}
  Int(const Int& i) : data(i.data) {}

  operator int() { return data; }//이걸 해줌으로써 우리는 연산자 오버로딩을 하나하나 해주지 않아도 되는 것이다!!!!!
};

전위/후위 증감 연산자

💡
전위 증감 연산의 경우 값이 바뀐 자기 자신을 리턴해야 하고, 후위 증감의 경우 값이 바뀌기 이전의 객체를 리턴해야 된다는 점이 다르다.

전위 증감 연산자

operator++();
operator--();
A& operator++() {
  // A ++ 을 수행한다.
  return *this;
}

후위 증감 연산자

operator++(int x);
operator--(int x);
//인자 x 는 아무런 의미가 없습니다. 단순히 컴파일러 상에서 전위와 후위를 구별하기 위해 int 인자를 넣어주는 것이지요.
//실제로 ++ 을 구현하면서 인자로 들어가는 값을 사용하는 경우는 없습니다
//operator++(int);
//operator--(int); 사실 이렇게 해도 똑같다!
A operator++(int) {
  A temp(A);
  // A++ 을 수행한다.
  return temp;
}//temp 객체를 만들어서 이전 상태를 기록한 후에, ++ 을 수행한 뒤에 temp 객체를 반환하게 됩니다.

후위 증감 연산의 경우 추가적으로 복사 생성자를 호출하기 때문에 전위 증감 연산보다 더 느리다!


Uploaded by N2T

728x90