C++ string class의 재정의를 통한 연산자 오버로딩 알아보기

카테고리 없음

2019. 1. 29. 00:43

01. 기존 string class를 대체하는 커스텀 String class


C++ 프로그래머라면 한 번쯤 C++의 <string>을 사용해 본적이 있을것이다.

이번에는 기존의 string의 기본적인 동작을 흉내낸 String class를 직접 정의해 봄으로써


1) 객체 생성 문법에 대한 이해


2) 우리가 무심코 사용하던 객체에 대한 다양한 연산자들의 숨은 원리에 대한 이해


를 얻게될 것이다.

그럼 바로 String class를 정의해 보자. 이해에 필요한 주석은 코드상에 달아 놓았다.


02. 코드


* String.cpp

#include <iostream>
#include <cstring>
using namespace std;

class String
{
private:
    char* str;
    int len;
public:
    // 인자가 없는 생성자
    String() {
        this->len = 0;
        this->str = NULL;
    }

    // 문자열이 인자인 생성자
    String(const char* s) {
        this->len = strlen(s)+1;
        this->str = new char[this->len];
        strcpy(this->str, s);
    }

    // 복사 생성자
    String(const String& copy) {
        this->len = copy.len;
        this->str = new char[this->len];
        strcpy(this->str, copy.str);
    }

    // 소멸자
    ~String() {
        if(this->str != NULL)
            delete []str;
    }

    // 대입 연산자
    String& operator= (const String& s) {
        if(this->str != NULL)
            delete []this->str;
        this->len = s.len;
        this->str = new char[this->len];
        strcpy(this->str, s.str);
        return *this;
    }

    // + 연산자
    String operator+ (const String& s) {
        char* c = new char[this->len+s.len-1];
        strcpy(c, this->str);
        strcat(c, s.str);
        String temp(c);
        delete []c;
        return temp;
    }

    // += 연산자
    String& operator+= (const String& s) {
        this->len += (s.len-1);
        char* c = new char[this->len];
        strcpy(c, this->str);
        strcat(c, s.str);
        if(this->str != NULL)
            delete []this->str;
        this->str = c;
        return *this;
    }

    // == 연산자
    bool operator== (const String& s) {
        return strcmp(this->str, s.str) ? false : true;
    }

    // <<와 >>는 호출 순서상 멤버함수로 오버로딩 할수 없음
    friend ostream& operator<< (ostream& os, String& s);
    friend istream& operator>> (istream& is, String& s);
};

ostream& operator<< (ostream& os, String& s) {
    return os << s.str;
}

istream& operator>> (istream& is, String& s) {
    char str[100];
    is >> str;
    s = String(str);
    return is;
}

int main(void)
{
    // 인자없는 생성자 호출
    String plzinput;

    // 문자열을 전달받는 생성자의 호출
    String mystr1 = "Hello, My name is "; // -> String mystr1("Hello, My name is ");
    String mystr2 = "devsophia."; // -> String mystr2("devsophia.");

    // 오버로딩한 + 및 = 연산자 호출
    String mystr3 = mystr1 + mystr2; // -> String mystr3 = mystr1.operator+(mystr2);
                                     // -> mystr3.operator=(+ 연산자의 반환값);

    // 오버로딩한 >> 연산자 호출
    cin >> plzinput; // -> operator>>(cin, plzinput);

    // 오버로딩한 << 연산자 호출
    cout << plzinput << endl;
    
    cout << mystr1 << endl;
    cout << mystr2 << endl;
    cout << mystr3 << endl;

    // 오버로딩한 += 연산자 호출
    mystr1 += mystr2; // -> mystr1.operator+=(mystr2);

    cout << mystr1 << endl;

    // 오버로딩한 == 연산자 호출
    if(mystr1 == mystr3)
        cout << "same !" << endl;
    else
        cout << "different !" << endl;

    return 0;
}

인자가 없는 생성자를 보면, str에 NULL이 들어갈 수도 있기 때문에 코드 곳곳에 delete연산을 하기 앞서 NULL체크를 하였다.


복사 생성자와 대입 연산자를 정의한 이유는, 멤버 대 멤버의 얕은 복사를 수행하는 디폴트 복사 생성자와 디폴트 대입 연산자를 사용하지 않고 새로 정의하여 깊은 복사를 수행하기 위해서이다.


+연산자 오버로딩에서는 operator+를 호출하는 객체와 인자 모두 변경이 일어나지 않고, 새로운 값을 만들어 내 반환한다.


+= 연산자 오버로딩에서는 operator+=를 호출하는 객체의 변경이 일어난다. 기존에 할당된 배열의 확장은 불가능하기 때문에, 원래 str 포인터변수가 가리키던 배열을 메모리 할당 해제하고 새로 할당하여 문자열을 이은 다음 반환한다.


03. 실행 결과

$ ./a.out
hahahoho
hahahoho
Hello, My name is 
devsophia.
Hello, My name is devsophia.
Hello, My name is devsophia.
same !


04. 느낀 점


무심코 "일단은 이렇게 쓰는거니까 일단 쓰자" 라는 마음으로 사용하던 여러가지 연산자나 생성자를 다시 한번 바라보게 된 계기였다.

일단 사용법만 알고 쓰는것도 좋지만, 내부에서 어떤 원리로 동작하는지도 모르는 상태에서 사용하면 어떻게든 안좋은 결과를 초래할 수 있으니, 적절한 저수준에서의 이해는 필수적이다.


05. 참조


윤성우, 열혈 C++ 프로그래밍, 오렌지미디어, 2010년