연산자 오버로딩과 대입연산자

연산자 오버로딩

  • C++에는 + - / *>> <<와 같은 연산자를 오버로딩하는 기능이 존재한다.
  • 이를 통해 객체간의 연산을 직관적으로 표현할 수 있다.

멤버 함수를 통한 오버로딩

class Point{
private:
    int xpos, ypos;
public:
    explicit Point(int x = 0, int y = 0) : xpos(x), ypos(y){
        cout<<"Point()"<<endl;
    }
    void ShowValue(){
        cout<<"Value: ["<<xpos<<", "<<ypos<<"]"<<endl;
    }
    Point operator+(const Point& ref) const{
        return Point(xpos+ref.xpos, ypos+ref.ypos);
    }
};
int main(){
    Point p1(3,4);
    Point p2(5,12);
    Point p3 = p1+p2;
    return 0;
}

p1+p2p1.operator+(p2)와 같다.

  • 객체인 Point간의 + 연산을 가능하도록 한다.

전역 함수를 통한 오버로딩

class Point{
private:
    int xpos, ypos;
public:
    explicit Point(int x = 0, int y = 0) : xpos(x), ypos(y){
        cout<<"Point()"<<endl;
    }
    void ShowValue(){
        cout<<"Value: ["<<xpos<<", "<<ypos<<"]"<<endl;
    }
    friend Point operator+(const Point& p1, const Point& p2);
};
//전역 함수
Point operator+(const Point& p1, const Point& p2){
    Point result(p1.xpos + p2.xpos, p1.ypos + p2.ypos);
    return result;
}
int main(){
    Point p1(3,4);
    Point p2(5,12);
    Point p3 = p1+p2;
    return 0;
}

private 멤버인 xpos, ypos에 접근해야 하므로 friend함수로 정의한다.

  • 마찬가지로 객체인 Point간의 + 연산을 가능하도록 한다.
  • 전역함수에서는 피연산자 두 개를 모두 인자로 받아야 한다.

cout, cin, endl의 비밀

  • cout, cin, endl과 >>, <<에도 연산자 오버로딩이 사용된다.
  • cout는 ostream클래스이며, cin은 istream클래스이다.

ostream의 경우

class ostream{
public:
    ostream& operator<<(char * str){
        printf("%s", str);
        return *this;
    }
    ostream& operator<<(char str){
        printf("%c", str);
        return *this;
    }
    ostream& operator<<(int num){
        printf("%d", num);
        return *this;
    }
    ostream& operator<<(double e){
        printf("%g", e);
        return *this;
    }
    ostream& operator<<(ostream& (*fp)(ostream &ostm)){
        return fp(*this);
    }
};

Point 객체에 <<, >> 오버로딩하기

class Point{
private:
    int xpos, ypos;
public:
    explicit Point(int x = 0, int y = 0) : xpos(x), ypos(y){
        cout<<"Point()"<<endl;
    }
    void ShowValue(){
        cout<<"Value: ["<<xpos<<", "<<ypos<<"]"<<endl;
    }
    friend ostream& operator<<(ostream& os, Point& ref);
    friend istream& operator>>(istream& is, Point& ref);
};
//전역함수
ostream& operator<<(ostream& os, Point& ref){
    printf("[%d, %d]", ref.xpos, ref.ypos);
    return os;
}

istream& operator>>(istream& is, Point& ref){
    scanf("%d %d", &ref.xpos, &ref.ypos);
    return is;
}
int main(){
    Point p1(1,2);
    cout<<p1<<endl;
    Point p2;
    cout<<"두 수 차례로 입력: ";
    cin>>p2;
    cout<<p2<<endl;
    return 0;
}
<결과>
Point()
[1, 2]
Point()
두 수 차례로 입력: 2 3
[2, 3]

대입연산자

  • 대입 연산자는 복사 생성자와 매우 유사하다.
    • 정의하지 않으면 디폴트 대입 연산자가 삽입된다.
    • 디폴트 대입 연산자는 멤버 대 멤버의 얕은 복사를 진행한다.
    • 연산자 내에서 동적 할당을 한다면, 그리고 깊은 복사가 필요하다면 직접 정의해야 한다.
  • 객체에 대입 연산자를 오버로딩할때는 얉은 복사/깊은 복사를 신경써야 한다.
  • 메모리 누수를 막기 위해 힙에 저장된 데이터는 해제를 해주고 나서 깊은 복사를 시행한다.

예시

Person& operator=(const Person& ref){
    delete []name; //누수 방지를 위한 힙 해제
    int len = strlen(ref.name)+1;
    name = new char[len];
    strcpy(name, ref.name);
    age = ref.age;
    return *this;
}

생성자, 복사생성자, 대입연산자의 호출 시기

  • 어느 상황에서 뭐가 호출되는지 헷갈려서 정리함.
class Test {
private:
    int m_i;
public:
    // 기본 생성자(default constructor)
    Test(){
        this->m_i = 0;
        cout << "call the default constructor" << endl;
    }
    // 복사 생성자(copy constructor)
    Test(const Test &t) {
        this->m_i = t.m_i;
        cout << "call the copy constructor" << endl;
    }
    // 복사 대입 연산자(copy assignment operator)
    Test& operator=(const Test &t) {
        this->m_i = t.m_i;
        cout << "call the copy assignment operator" << endl;
        return *this;
    }
};
int main() {
    Test t1;      //기본 생성자
    Test t2(t1);  //복사 생성자
    Test t3 = t1; //복사 생성자
    t1 = t2;      //대입 연산자
    
    return 0;
}
<결과>
call the default constructor
call the copy constructor
call the copy constructor
call the copy assignment operator

대입연산자와 이니셜라이저

  • 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성된다.
  • 따라서 아래와 같은 코드에서 BBB는 복사 생성자만 호출이 된다.
  • CCC는 복사 생성자 + 대입 연산자까지 호출이 되어 성능이 더 안좋다.
//BBB의 복사 생성자
BBB(const AAA& ref) : mem(ref) {}

...

//CCC의 복사 생성자
CCC(const AAA& ref) { mem = ref; }

...

//main 함수
AAA obj1(12);
BBB obj2(onj1);   //복사 생성자 호출
CCC obj3(obj1);