TIL/2025

나만의 C++ 문자열 클래스 만들기, GuString

고무 오리 2025. 4. 24. 18:34
728x90
📢 언어를 효과적으로 학습하는 방법은 무언가를 만들어보는 것이죠. C++의 std::string을 구현해보며 C++을 익혀봤어요. 이름하여 GuString클래스!

 

 

전체 여정은 아래와 같아요

namespace Gu {...} 안에 모든 것들을 구현했죠

 

기본적으로 Class 구현 시 Class 내부에는 함수 선언만 존재하고 Class 외부에 함수 구현을 해요

  • 컴파일 시간 단축 (h, cc 파일 구분 시)
  • 정보 은닉
  • 코드 관리 편리 (협업 및 유지보수)
  • 재사용성 및 의존성 최소화

※ 단, 템플릿 클래스는 함수 구현도 class 내에 존재

 

 

Step 1. 문자열 길이와 복사 함수 직접 만들기

먼저 C표준 라이브러리를 대체 할 strlen, strcpy를 구현했어요

namespace Gu {
    ...
    static int gu_strlen(const char* str) {
        int length = 0;
        while (str[length] != '\0') {
            length++;
        }
        return length;
    }

    static char* gu_strcpy(char *dest, const char *src) {
        char *original_dest = dest;
        while ((*dest++ = *src++) != '\0');
        return original_dest;
    }
    ...
}

 

 

Step 2. 기본 생성자 만들기

const char*로부터 문자열을 복사하는 기본 생성자를 작성했어요

  • nullptr 처리도 해주고 내부적으로 len_data_를 관리해요
  • capacity_ 개념을 도입해서 메모리 재할당을 줄이고 성능을 향상시켰어요
namespace Gu {
    ...
    class GuString {
    public:
        GuString(const char* data);      // constructor
        ...

    private:
        char* data_;
        int len_;
        int capacity_;
    };

    GuString::GuString(const char* data){
        if (data == nullptr) {
            len_ = 0;
            capacity_ = len_ + 1;
            data_ = new char[capacity_];
            data_[0] = '\0';
        }
        else {
            len_ = gu_strlen(data);
            capacity_ = len_ + 1;
            data_ = new char[capacity_];
            gu_strcpy(data_, data);
        }
    }
    ...
}

 

 

Step 3. 소멸자 작성

동적으로 할당한 문자열 메모리를 delete[]로 해제해요

RAII 원칙의 기본이라고 하네요 :)

namespace Gu {
    ...
    class GuString {
    public:
        ~GuString() {
            delete[] data_;
        }
        ...
    };

    ...
}

 

 

Step 4. 복사 생성자 구현

깊은 복사가 되도록 메모리를 새로 할당한 뒤, 문자열을 복사해요

shallow copy가 되면 큰일나요

  • 객체 간 같은 문자열 배열의 주소를 공유하여 Memory leak이 발생 가능하죠
namespace Gu {
    ...
    class GuString {
    public:
        ...
        GuString(const GuString& other); // copy constructor
        ...
    };

    GuString::GuString(const GuString& other) {
        len_ = other.len_;
        capacity_ = len_ + 1;
        data_ = new char[capacity_];
        gu_strcpy(data_, other.data_);
    }
    ...
}

 

복사 생성자는 아래의 경우들에서 호출돼요

// 복사 초기화 (copy initialization)
// s1을 사용해 새로운 객체 s2를 초기화 → 복사 생성자 호출
GuString s2 = s1;

// 직접 초기화 (direct initialization)
// s1을 사용해 s2 생성 → 복사 생성자 호출
GuString s2(s1);

// 함수 인자로 값 전달	
// 함수 호출 시 복사본 생성 필요 → 복사 생성자 호출
void foo(GuString s); → foo(s1);

// 함수 반환 시 값으로 받기
// 리턴된 객체를 받으면서 복사 생성자 호출 (혹은 이동 생성자, RVO 적용 시 생략)
GuString s = make();

 

 

Step 5. 복사 대입 연산자 구현

operator=를 오버로딩해서 이미 생성된 객체에 대입할 수 있게 했어요

  • 자기 자신에 대한 대입 방지, capacity_에 따라 재할당 여부도 조건부 처리했어요
namespace Gu {
    ...
    class GuString {
    public:
        ...
        GuString& operator=(const GuString& other); // 복사 대입
        ...
    };

    GuString& GuString::operator=(const GuString& other) {
        if (this == &other) {
            return *this;
        }
        
        if (capacity_ >= other.len_ + 1) {
            len_ = other.len_;
            gu_strcpy(data_, other.data_);
        }
        else {
            delete[] data_;
            len_ = other.len_;
            capacity_ = len_ + 1;
            data_ = new char[capacity_];
            gu_strcpy(data_, other.data_);
        }
        return *this;
    }
    ...
}

 

아래와 같은 상황에서 복사 대입 연산자가 호출 돼요

GuString a("hello");
GuString b("world");

a = b;  // 여기서 "복사 대입 연산자"가 호출됨!

 

 

Step 6. 이동 생성자 & 이동 대입 연산자

C++11부터 중요한 move semantics도 적용해봤어요

  • 복사 생성자는 객체 복사 시 메모리 새로 할당 후 내용을 복사하죠
  • 이동 생성자는 기존 객체의 자원을 옮겨오고(훔치고) 원래 객체는 안전하게 초기화하는 개념이죠
namespace Gu {
    ...
    class GuString {
    public:
        ...
        GuString(GuString&& other);      // move constructor
        ...
    };

    GuString::GuString(GuString&& other) {
        len_ = other.len_;
        capacity_ = other.capacity_;
        data_ = other.data_;
        other.data_ = nullptr;
        other.len_ = 0;
    }
    ...
}

 

그냥 소유권만 넘겨주는 방식이라 복사 생성자보다 빠르고 효율적이에요

GuString a = GuString("hello");     // rvalue로 객체 초기화: 이동 생성자
GuString b = makeTemp();            // 함수 리턴값 받기: 이동 생성자 or RVO

GuString c("test");
GuString d = std::move(c);          // std::move를 사용한 명시적 이동: 이동 생성자

std::vector<GuString> vec;
vec.push_back(std::move(a));        // 컨테이너에 객체 이동: 이동 생성자

 

 

Step 7. reserve() 함수 구현

capacity를 미리 확보해두는 reserve()를 구현했어요

  • 문자열 내용이 변경 될 때 길이가 모자라면 메모리 재할당을 해야하는데 이게 빈번하면 성능에 문제가 생기죠
  • 사전에 문자열 max 길이를 알고 있다면 reserve()로 개발자가 미리 공간을 확보하고 진행 할 수도 있어요

이는 append()와 같은 함수에서 큰 역할을 해요

namespace Gu {
    ...
    class GuString {
    public:
        ...
        void reserve(int new_capacity);
        ...
    };

    void GuString::reserve(int new_capacity) {
        if (capacity_ >= new_capacity) {
            return;
        }

        char* new_data = new char[new_capacity];
        gu_strcpy(new_data, data_);
        capacity_ = new_capacity;
        delete[] data_;
        data_ = new_data;
    }
    ...
}

 

 

Step 8. append() 함수 구현

다른 문자열을 덧붙이는 함수에요
reserve()를 활용해 capacity가 부족할 경우 자동 확장되도록 했어요

namespace Gu {
    ...
    class GuString {
    public:
        ...
        void append(const char* str);
        ...
    };

    void GuString::append(const char* str) {
        int append_len = gu_strlen(str);

        if(capacity_ < len_ + append_len + 1) {
            reserve((len_ + append_len + 1)*2);
        }

        gu_strcpy(data_ + len_, str);
        len_ = len_ + append_len;
    }
    ...
}

 

 

Step 9. 연산자 오버로딩 – +

GuString끼리 concat하는 operator+를 구현했어요

  • 새로운 문자열을 만들어 반환
  • 깊은 복사, 메모리 할당 주의
namespace Gu {
    ...
    class GuString {
    public:
        ...
        GuString operator+(const GuString& rhs) const;
        ...
    };

    GuString GuString::operator+(const GuString& rhs) const {
        int total_len = len_ + rhs.len_;
        
        GuString ret("");
        ret.reserve(total_len + 1);
        gu_strcpy(ret.data_, data_);
        gu_strcpy(ret.data_ + len_, rhs.data_);
        ret.len_ = total_len;
        
        return ret;
    }
    ...
}

 

 

Step 10. 연산자 오버로딩 – ==, !=

문자열 내용을 비교하는 연산자들을 구현했어요

  • 길이부터 먼저 비교한 후
  • 문자 하나하나 비교해요
namespace Gu {
    ...
    class GuString {
    public:
        ...
        bool operator==(const GuString& rhs) const;
        bool operator!=(const GuString& rhs) const;
        ...
    };

    bool GuString::operator==(const GuString& rhs) const {
        if(len_ != rhs.len_) {
            return false;
        }

        for (int i = 0; i < len_; i++) {
            if(data_[i] != rhs.data_[i]) return false;
        }

        return true;
    }
    
    bool GuString::operator!=(const GuString& rhs) const {
        return !(*this == rhs);
    }
    ...
}

 

 

Step 11. 연산자 오버로딩 – []

문자 인덱스 접근용 operator[] 구현도 빠질 수 없죠

  • 수정 가능한 버전
  • 읽기 전용(const) 버전 둘 다 구현했어요 ← const char a = s[2];

이걸로 str[1] = 'a'; 도 완벽 지원돼요

namespace Gu {
    ...
    class GuString {
    public:
        ...
        char& operator[](int index);
        ...
    };

    char& GuString::operator[](int index) {
        return data_[index];
    }

    const char& GuString::operator[](int index) const {
        return data_[index];
    }
    ...
}

 

 

Step 12. clear() & shrink_to_fit() 구현

  • clear()는 문자열 비우기
  • shrink_to_fit()는 필요 없는 capacity를 줄이는 기능이에요

작지만 성능에 민감한 환경에서는 유용합니다.

namespace Gu {
    ...
    class GuString {
    public:
        ...
        void clear();
        void shrink_to_fit();
        ...
    };

    void GuString::clear() {
        len_ = 0;
        data_[0] = '\0';
    }

    void GuString::shrink_to_fit() {
        if (len_ + 1 == capacity_) {
            return;
        }

        int new_capacity = len_ + 1;
        char* new_data = new char[new_capacity];
        gu_strcpy(new_data, data_);
        delete[] data_;
        data_ = new_data;
        capacity_ = new_capacity;
    }
    ...
}

 

 

Step 13. print(), c_str(), length() 함수 구현

  • print()로 콘솔에 문자열 출력
  • c_str()은 C 스타일 문자열 반환
  • length()는 현재 길이 리턴
namespace Gu {
    ...
    class GuString {
    public:
        ...
        const char* c_str() const;
        ...
    };

    const char* GuString::c_str() const {
        return data_;
    }
    ...
}

 

 

Step 14. operator<< 오버로딩

마지막으로 std::cout << GuString 스타일 출력을 위해 operator<<을 구현

  • std::ostream을 받아 내부 문자열 출력
  • namespace Gu 내부에 정의하여 MISRA 규칙도 만족!
namespace Gu {
    ...
    std::ostream& operator<<(std::ostream& os, const GuString& str) {
        return os << str.c_str();
    }
    ...
}

 

 

 

GuString Class를 만들며 아래의 것들에 대해 익혔어요

 

  • RAII 원칙과 메모리 안전 설계
  • 복사 / 이동 생성자의 개념과 차이
  • 연산자 오버로딩의 유용성과 설계 철학
  • capacity를 이용한 효율적인 동적 버퍼 관리

 

전체 소스 코드는 아래 github에서 확인 가능해요

https://github.com/geongupark/cpp-GuString

 

GitHub - geongupark/cpp-GuString: cpp project for String library

cpp project for String library. Contribute to geongupark/cpp-GuString development by creating an account on GitHub.

github.com

 

 

C++을 익히는데 큰 도움이 됐어요

"문자열을 구현했지만, 사실은 C++을 구현해봤다."라는 생각이 들어요

728x90