diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..20f3746 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: C++ CI + +on: + push: + branches: [main, feature/production-grade-mystring] + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure + + - name: Sanitizer build + run: | + cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" + cmake --build build-asan --parallel + ctest --test-dir build-asan --output-on-failure diff --git a/.gitignore b/.gitignore index 7f9497f..954e816 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,21 @@ -# Build files -*.exe -*.out +# Build artifacts +build/ +build-*/ +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile *.o *.obj +*.out +*.exe -# Visual Studio Code +# IDE files .vscode/ +.idea/ +*.user +*.suo -# Debug files +# Debug / compiler artifacts *.pdb *.ilk - -# CMake -CMakeFiles/ -CMakeCache.txt -cmake_install.cmake -Makefile diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ffa6d1a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) + +project(MyString VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_library(mystring src/MyString.cpp) +target_include_directories(mystring PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_options(mystring PRIVATE + $<$:-Wall;-Wextra;-Wpedantic> + $<$:/W4> +) + +add_executable(mystring_demo examples/main.cpp) +target_link_libraries(mystring_demo PRIVATE mystring) + +include(CTest) +if(BUILD_TESTING) + add_executable(mystring_tests tests/test_mystring.cpp) + target_link_libraries(mystring_tests PRIVATE mystring) + add_test(NAME MyStringTests COMMAND mystring_tests) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0d7187f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jyatin Kumar Singh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Main.cpp b/Main.cpp deleted file mode 100644 index 6baf13b..0000000 --- a/Main.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include "MyString.h" - -using namespace std; - -int main() -{ - cout << "========== MyString Class Demonstration ==========\n\n"; - - MyString s1; - MyString s2; - - cout << "Enter first string : "; - cin >> s1; - - cout << "Enter second string: "; - cin >> s2; - - cout << "\n----- Input Strings -----\n"; - cout << "s1 = " << s1 << endl; - cout << "s2 = " << s2 << endl; - - cout << "\n----- Concatenation -----\n"; - MyString s3 = s1 + s2; - cout << "s1 + s2 = " << s3 << endl; - - cout << "\n----- Comparison -----\n"; - if (s1 == s2) - cout << "s1 and s2 are equal.\n"; - else - cout << "s1 and s2 are not equal.\n"; - - if (s1 != s2) - cout << "operator!= also works correctly.\n"; - else - cout << "operator!= also works correctly.\n"; - - cout << "\n----- Copy Constructor -----\n"; - MyString copy(s1); - - cout << "Original : " << s1 << endl; - cout << "Copied : " << copy << endl; - - cout << "\nChanging first character of original string...\n"; - - if (!s1.empty()) - s1[0] = 'X'; - - cout << "Original after modification : " << s1 << endl; - cout << "Copied remains : " << copy << endl; - - cout << "\nDeep copy verified.\n"; - - cout << "\n----- Assignment Operator -----\n"; - - MyString assign; - - assign = s2; - - cout << "Assigned string = " << assign << endl; - - cout << "\n----- Index Operator -----\n"; - - if (!assign.empty()) - { - cout << "First character : " << assign[0] << endl; - - assign[0] = 'Z'; - - cout << "Modified string : " << assign << endl; - } - - cout << "\n----- Utility Functions -----\n"; - - cout << "Length of s1 : " << s1.size() << endl; - cout << "Length of s2 : " << s2.size() << endl; - - if (s1.empty()) - cout << "s1 is empty.\n"; - else - cout << "s1 is not empty.\n"; - - cout << "\n----- Clear Function -----\n"; - - s2.clear(); - - cout << "After clear(), s2 = \"" << s2 << "\"" << endl; - cout << "Length = " << s2.size() << endl; - - if (s2.empty()) - cout << "s2 is now empty.\n"; - - cout << "\n----- c_str() Function -----\n"; - - cout << "Character array of s1 : " << s1.c_str() << endl; - - cout << "\n========== Program Finished Successfully ==========\n"; - - return 0; -} \ No newline at end of file diff --git a/MyString.cpp b/MyString.cpp deleted file mode 100644 index da39e50..0000000 --- a/MyString.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "MyString.h" -#include -#include - -int MyString::stringLength(const char* s) const -{ - int len = 0; - - while (s[len] != '\0') - len++; - - return len; -} - -void MyString::copyString(char* dest, const char* src) const -{ - int i = 0; - - while (src[i] != '\0') - { - dest[i] = src[i]; - i++; - } - - dest[i] = '\0'; -} - -int MyString::compareString(const char* s1, const char* s2) const -{ - int i = 0; - - while (s1[i] != '\0' && s2[i] != '\0') - { - if (s1[i] != s2[i]) - return s1[i] - s2[i]; - - i++; - } - - return s1[i] - s2[i]; -} - -MyString::MyString() -{ - length = 0; - str = new char[1]; - str[0] = '\0'; -} - -MyString::MyString(const char* s) -{ - length = stringLength(s); - - str = new char[length + 1]; - - copyString(str, s); -} - -MyString::MyString(const MyString& other) -{ - length = other.length; - - str = new char[length + 1]; - - copyString(str, other.str); -} - -MyString::~MyString() -{ - delete[] str; -} - -MyString& MyString::operator=(const MyString& other) -{ - if (this != &other) - { - delete[] str; - - length = other.length; - - str = new char[length + 1]; - - copyString(str, other.str); - } - - return *this; -} - -MyString MyString::operator+(const MyString& other) const -{ - MyString temp; - - delete[] temp.str; - - temp.length = length + other.length; - - temp.str = new char[temp.length + 1]; - - int i; - - for (i = 0; i < length; i++) - temp.str[i] = str[i]; - - for (int j = 0; j < other.length; j++) - temp.str[i++] = other.str[j]; - - temp.str[i] = '\0'; - - return temp; -} - -bool MyString::operator==(const MyString& other) const -{ - return compareString(str, other.str) == 0; -} - -bool MyString::operator!=(const MyString& other) const -{ - return !(*this == other); -} - -char& MyString::operator[](int index) -{ - if (index < 0 || index >= length) - throw out_of_range("Index out of range"); - - return str[index]; -} - -const char& MyString::operator[](int index) const -{ - if (index < 0 || index >= length) - throw out_of_range("Index out of range"); - - return str[index]; -} - -int MyString::size() const -{ - return length; -} - -bool MyString::empty() const -{ - return length == 0; -} - -void MyString::clear() -{ - delete[] str; - - length = 0; - - str = new char[1]; - - str[0] = '\0'; -} - -const char* MyString::c_str() const -{ - return str; -} - -ostream& operator<<(ostream& out, const MyString& s) -{ - out << s.str; - - return out; -} - -istream& operator>>(istream& in, MyString& s) -{ - char buffer[1000]; - - in >> buffer; - - delete[] s.str; - - s.length = s.stringLength(buffer); - - s.str = new char[s.length + 1]; - - s.copyString(s.str, buffer); - - return in; -} \ No newline at end of file diff --git a/MyString.h b/MyString.h deleted file mode 100644 index 7ef2f48..0000000 --- a/MyString.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef MYSTRING_H -#define MYSTRING_H - -#include - -using namespace std; - -class MyString -{ -private: - char* str; - int length; - - int stringLength(const char* s) const; - void copyString(char* dest, const char* src) const; - int compareString(const char* s1, const char* s2) const; - -public: - MyString(); - MyString(const char* s); - MyString(const MyString& other); - ~MyString(); - - MyString& operator=(const MyString& other); - - MyString operator+(const MyString& other) const; - bool operator==(const MyString& other) const; - bool operator!=(const MyString& other) const; - - char& operator[](int index); - const char& operator[](int index) const; - - int size() const; - bool empty() const; - void clear(); - const char* c_str() const; - - friend ostream& operator<<(ostream& out, const MyString& s); - friend istream& operator>>(istream& in, MyString& s); -}; - -#endif \ No newline at end of file diff --git a/README.md b/README.md index d6ccfc5..9edcaeb 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,182 @@ - # Custom MyString Class in C++ +# MyString — a small `std::string`-style class built from scratch -A custom implementation of a `String` class in C++ built from scratch without using `std::string`. This project demonstrates object-oriented programming, dynamic memory management, deep copying, and operator overloading. +[![C++ CI](https://github.com/Jyatin/MyString/actions/workflows/ci.yml/badge.svg)](https://github.com/Jyatin/MyString/actions/workflows/ci.yml) +[![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C.svg)](https://en.cppreference.com/w/cpp/17) -## Features +A production-minded custom string implementation in modern C++17. The goal is not to replace `std::string`, but to make the mechanics behind a dynamic string explicit: ownership, heap allocation, deep copying, move semantics, capacity growth, bounds checking, searching, and operator overloading. -- Dynamic memory allocation -- Constructors and Destructor -- Copy Constructor -- Copy Assignment Operator -- Deep Copy Implementation -- String Length -- Character Access -- String Comparison -- Operator Overloading +> **Constraint:** the implementation does not use `std::string` for its internal storage. It owns a null-terminated `char` buffer and manages that buffer itself. -## Project Structure +## What changed + +The original project was a compact Rule-of-Five demonstration. It is now organised as a small library with a demo, automated tests, CMake build support, and GitHub Actions CI. + +### Core functionality + +- Rule of Five: destructor, copy constructor/assignment, move constructor/assignment +- Dynamic heap-backed storage with geometric capacity growth +- Stronger copy-assignment exception safety using copy-and-swap +- `size()`, `length()`, `capacity()`, `empty()`, `clear()`, `reserve()` +- `operator[]`, checked `at()`, `front()`, `back()` +- `c_str()` and mutable/const `data()` access +- `append()` and `operator+=` for strings and characters +- Concatenation with `operator+` +- Equality and lexicographical relational operators +- `find()` for characters and substrings +- `contains()`, `starts_with()`, `ends_with()` +- `substr()` and non-mutating `reversed()` +- Stream insertion/extraction (`<<`, `>>`) +- `std::string_view` construction, including strings containing embedded `\0` bytes +- Self-append protection and a reusable moved-from state + +## Project structure ```text -Custom-MyString-CPP/ -│ -├── Main.cpp # Driver program -├── MyString.h # Class declaration -└── MyString.cpp # Class implementation +MyString/ +├── include/ +│ └── MyString.h # Public class interface +├── src/ +│ └── MyString.cpp # Memory management + implementation +├── examples/ +│ └── main.cpp # Feature demonstration +├── tests/ +│ └── test_mystring.cpp # Assertion-based test suite +├── .github/ +│ └── workflows/ +│ └── ci.yml # Build, test and sanitizer CI +├── CMakeLists.txt # Portable build configuration +├── LICENSE # MIT license +└── README.md ``` -## Technologies Used +## Quick start -- C++ -- Git -- GitHub -- Visual Studio Code +### GCC / Clang -## Build & Run +```bash +g++ -std=c++17 -Wall -Wextra -Wpedantic \ + -Iinclude src/MyString.cpp examples/main.cpp \ + -o mystring_demo + +./mystring_demo +``` -Compile the project using: +### CMake ```bash -g++ Main.cpp MyString.cpp -o MyString +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure +./build/mystring_demo ``` -Run: +On Windows with a Visual Studio generator, build the same CMake targets from the generated solution or with `cmake --build build --config Release`. -**Windows** -```bash -MyString.exe +## Demo output + +The example exercises construction, concatenation, capacity, append, search, prefix/suffix checks, substring extraction, reversal, deep copy, move construction, and clearing. + +```text +=== MyString demo === +language : Modern C++17 +project : MyString +combined : Modern C++17 - MyString +size : 23 +capacity : 30 +append : Modern C++17 - MyString library +find : 17 +contains : true +prefix : true +suffix : true +substr : C++17 +reverse : yrarbil gnirtSyM - 71++C nredoM +copy : modern C++17 - MyString library +original : Modern C++17 - MyString library +moved : modern C++17 - MyString library +moved-from size: 0 +cleared : "" (empty=true) +=== demo complete === ``` -**Linux/macOS** +The CI workflow builds the demo and runs the test suite on pushes and pull requests. It also runs the tests under AddressSanitizer and UndefinedBehaviorSanitizer on Ubuntu. + +## API at a glance + +| API | Purpose | Complexity | +|---|---|---:| +| `size()` / `length()` | Current number of characters | O(1) | +| `capacity()` | Allocated character capacity | O(1) | +| `operator[]` | Unchecked indexed access | O(1) | +| `at()` | Bounds-checked indexed access | O(1) | +| `append()` / `+=` | Add characters or another string | Amortized O(n) | +| `reserve()` | Pre-allocate capacity | O(n) when reallocation occurs | +| `find()` | Find character/subsequence | O(n·m) worst case | +| `substr()` | Create a substring | O(k) | +| `reversed()` | Return a reversed copy | O(n) | +| comparisons | Lexicographical comparison | O(min(n,m)) | + +`append()` uses geometric capacity growth, so repeated appends avoid an allocation on every operation. + +## Memory-management design + +Each `MyString` owns one character buffer and maintains these invariants: + +1. `data_[size_]` is always the null terminator. +2. `size_ <= capacity_`. +3. The allocated buffer contains at least `capacity_ + 1` characters. + +Copying allocates independent storage, so modifying a copy cannot mutate the source. Moving transfers the owned buffer without copying its characters; the source becomes an empty, reusable object. + +Copy assignment uses **copy-and-swap**, which avoids destroying the existing state before the replacement allocation succeeds. + +## Testing + +The test suite covers: + +- default and normal construction +- indexed access and `at()` exceptions +- deep copy semantics +- copy and move assignment +- moved-from object reuse +- capacity preservation after `clear()` +- self-append +- searching and substring boundaries +- lexicographical comparisons +- stream input/output +- embedded null characters + +Run it with: + ```bash -./MyString +cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug +cmake --build build --parallel +ctest --test-dir build --output-on-failure ``` -## Concepts Covered +## Why build this instead of using `std::string`? + +Because it is a useful systems-programming exercise. Implementing a small string type exposes concepts that are easy to hide behind the standard library: + +- RAII and ownership +- Rule of Five +- shallow vs deep copy +- move semantics +- exception safety +- dynamic allocation and reallocation +- object lifetime +- operator overloading +- API design and invariants +- algorithmic complexity -- Object-Oriented Programming -- Dynamic Memory Management -- Rule of Three -- Operator Overloading -- Pointers and Memory Safety +For application development, prefer the battle-tested standard library. For learning C++ internals, this project makes those mechanisms visible. + +## License + +MIT — see [`LICENSE`](LICENSE). ## Author -**Jyatin Singh** \ No newline at end of file +**Jyatin Kumar Singh** +C++ / Full-Stack Developer · Open Source Contributor + +GitHub: https://github.com/Jyatin diff --git a/examples/main.cpp b/examples/main.cpp new file mode 100644 index 0000000..60f44aa --- /dev/null +++ b/examples/main.cpp @@ -0,0 +1,44 @@ +#include +#include + +#include "MyString.h" + +int main() { + MyString language("Modern C++17"); + MyString project("MyString"); + + std::cout << "=== MyString demo ===\n"; + std::cout << "language : " << language << '\n'; + std::cout << "project : " << project << '\n'; + + MyString combined = language + MyString(" - "); + combined += project; + std::cout << "combined : " << combined << '\n'; + std::cout << "size : " << combined.size() << '\n'; + std::cout << "capacity : " << combined.capacity() << '\n'; + + combined += " library"; + std::cout << "append : " << combined << '\n'; + std::cout << "find : " << combined.find(MyString("String")) << '\n'; + std::cout << "contains : " << std::boolalpha << combined.contains(MyString("C++")) << '\n'; + std::cout << "prefix : " << combined.starts_with(MyString("Modern")) << '\n'; + std::cout << "suffix : " << combined.ends_with(MyString("library")) << '\n'; + std::cout << "substr : " << combined.substr(7, 5) << '\n'; + std::cout << "reverse : " << combined.reversed() << '\n'; + + MyString copied(combined); + copied[0] = 'm'; + std::cout << "copy : " << copied << '\n'; + std::cout << "original : " << combined << '\n'; + + MyString moved(std::move(copied)); + std::cout << "moved : " << moved << '\n'; + std::cout << "moved-from size: " << copied.size() << '\n'; + + MyString cleared = combined; + cleared.clear(); + std::cout << "cleared : \"" << cleared << "\" (empty=" << cleared.empty() << ")\n"; + + std::cout << "=== demo complete ===\n"; + return 0; +} diff --git a/include/MyString.h b/include/MyString.h new file mode 100644 index 0000000..e148f69 --- /dev/null +++ b/include/MyString.h @@ -0,0 +1,81 @@ +#ifndef MYSTRING_H +#define MYSTRING_H + +#include +#include +#include + +class MyString { +public: + static constexpr std::size_t npos = static_cast(-1); + + MyString(); + explicit MyString(const char* text); + explicit MyString(std::string_view text); + MyString(const MyString& other); + MyString(MyString&& other) noexcept; + ~MyString(); + + MyString& operator=(const MyString& other); + MyString& operator=(MyString&& other) noexcept; + + std::size_t size() const noexcept; + std::size_t length() const noexcept; + std::size_t capacity() const noexcept; + bool empty() const noexcept; + const char* c_str() const noexcept; + char* data() noexcept; + const char* data() const noexcept; + + char& operator[](std::size_t index) noexcept; + const char& operator[](std::size_t index) const noexcept; + char& at(std::size_t index); + const char& at(std::size_t index) const; + char& front(); + const char& front() const; + char& back(); + const char& back() const; + + void clear() noexcept; + void reserve(std::size_t new_capacity); + void swap(MyString& other) noexcept; + + MyString& append(const MyString& other); + MyString& append(const char* text); + MyString& append(char ch); + MyString& operator+=(const MyString& other); + MyString& operator+=(const char* text); + MyString& operator+=(char ch); + + std::size_t find(char ch, std::size_t pos = 0) const noexcept; + std::size_t find(const MyString& needle, std::size_t pos = 0) const noexcept; + bool contains(const MyString& needle) const noexcept; + bool starts_with(const MyString& prefix) const noexcept; + bool ends_with(const MyString& suffix) const noexcept; + MyString substr(std::size_t pos, std::size_t count = npos) const; + MyString reversed() const; + + int compare(const MyString& other) const noexcept; + + friend MyString operator+(const MyString& lhs, const MyString& rhs); + friend bool operator==(const MyString& lhs, const MyString& rhs) noexcept; + friend bool operator!=(const MyString& lhs, const MyString& rhs) noexcept; + friend bool operator<(const MyString& lhs, const MyString& rhs) noexcept; + friend bool operator<=(const MyString& lhs, const MyString& rhs) noexcept; + friend bool operator>(const MyString& lhs, const MyString& rhs) noexcept; + friend bool operator>=(const MyString& lhs, const MyString& rhs) noexcept; + friend std::ostream& operator<<(std::ostream& out, const MyString& value); + friend std::istream& operator>>(std::istream& in, MyString& value); + +private: + char* data_; + std::size_t size_; + std::size_t capacity_; + static char empty_data_; + + static std::size_t text_length(const char* text) noexcept; + static void copy_n(char* destination, const char* source, std::size_t count) noexcept; + void ensure_capacity(std::size_t required); +}; + +#endif diff --git a/src/MyString.cpp b/src/MyString.cpp new file mode 100644 index 0000000..ebbe3aa --- /dev/null +++ b/src/MyString.cpp @@ -0,0 +1,259 @@ +#include "MyString.h" + +#include +#include +#include +#include +#include +#include + +char MyString::empty_data_ = '\0'; + +std::size_t MyString::text_length(const char* text) noexcept { + if (text == nullptr) return 0; + std::size_t length = 0; + while (text[length] != '\0') ++length; + return length; +} + +void MyString::copy_n(char* destination, const char* source, std::size_t count) noexcept { + for (std::size_t i = 0; i < count; ++i) destination[i] = source[i]; +} + +MyString::MyString() : data_(new char[1]{'\0'}), size_(0), capacity_(0) {} +MyString::MyString(const char* text) : MyString(std::string_view(text == nullptr ? "" : text)) {} + +MyString::MyString(std::string_view text) + : data_(new char[text.size() + 1]), size_(text.size()), capacity_(text.size()) { + copy_n(data_, text.data(), size_); + data_[size_] = '\0'; +} + +MyString::MyString(const MyString& other) + : data_(new char[other.size_ + 1]), size_(other.size_), capacity_(other.size_) { + copy_n(data_, other.data_, size_); + data_[size_] = '\0'; +} + +MyString::MyString(MyString&& other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = &empty_data_; + other.size_ = 0; + other.capacity_ = 0; +} + +MyString::~MyString() { + if (data_ != &empty_data_) delete[] data_; +} + +MyString& MyString::operator=(const MyString& other) { + if (this == &other) return *this; + MyString copy(other); + swap(copy); + return *this; +} + +MyString& MyString::operator=(MyString&& other) noexcept { + if (this == &other) return *this; + if (data_ != &empty_data_) delete[] data_; + data_ = other.data_; + size_ = other.size_; + capacity_ = other.capacity_; + other.data_ = &empty_data_; + other.size_ = 0; + other.capacity_ = 0; + return *this; +} + +std::size_t MyString::size() const noexcept { return size_; } +std::size_t MyString::length() const noexcept { return size_; } +std::size_t MyString::capacity() const noexcept { return capacity_; } +bool MyString::empty() const noexcept { return size_ == 0; } +const char* MyString::c_str() const noexcept { return data_; } +char* MyString::data() noexcept { return data_; } +const char* MyString::data() const noexcept { return data_; } + +char& MyString::operator[](std::size_t index) noexcept { return data_[index]; } +const char& MyString::operator[](std::size_t index) const noexcept { return data_[index]; } + +char& MyString::at(std::size_t index) { + if (index >= size_) throw std::out_of_range("MyString::at: index out of range"); + return data_[index]; +} +const char& MyString::at(std::size_t index) const { + if (index >= size_) throw std::out_of_range("MyString::at: index out of range"); + return data_[index]; +} +char& MyString::front() { + if (empty()) throw std::out_of_range("MyString::front: empty string"); + return data_[0]; +} +const char& MyString::front() const { + if (empty()) throw std::out_of_range("MyString::front: empty string"); + return data_[0]; +} +char& MyString::back() { + if (empty()) throw std::out_of_range("MyString::back: empty string"); + return data_[size_ - 1]; +} +const char& MyString::back() const { + if (empty()) throw std::out_of_range("MyString::back: empty string"); + return data_[size_ - 1]; +} + +void MyString::clear() noexcept { + if (data_ == &empty_data_) return; + size_ = 0; + data_[0] = '\0'; +} + +void MyString::ensure_capacity(std::size_t required) { + if (required <= capacity_) return; + const std::size_t new_capacity = std::max(required, capacity_ == 0 ? std::size_t{1} : capacity_ * 2); + char* replacement = new char[new_capacity + 1]; + copy_n(replacement, data_, size_); + replacement[size_] = '\0'; + if (data_ != &empty_data_) delete[] data_; + data_ = replacement; + capacity_ = new_capacity; +} + +void MyString::reserve(std::size_t new_capacity) { + if (new_capacity <= capacity_) return; + char* replacement = new char[new_capacity + 1]; + copy_n(replacement, data_, size_); + replacement[size_] = '\0'; + if (data_ != &empty_data_) delete[] data_; + data_ = replacement; + capacity_ = new_capacity; +} + +void MyString::swap(MyString& other) noexcept { + using std::swap; + swap(data_, other.data_); + swap(size_, other.size_); + swap(capacity_, other.capacity_); +} + +MyString& MyString::append(const MyString& other) { + if (this == &other) { + MyString copy(other); + return append(copy); + } + ensure_capacity(size_ + other.size_); + copy_n(data_ + size_, other.data_, other.size_); + size_ += other.size_; + data_[size_] = '\0'; + return *this; +} + +MyString& MyString::append(const char* text) { + if (text == nullptr) return *this; + const std::size_t count = text_length(text); + if (count == 0) return *this; + if (data_ != &empty_data_ && text >= data_ && text <= data_ + size_) { + MyString copy(text); + return append(copy); + } + ensure_capacity(size_ + count); + copy_n(data_ + size_, text, count); + size_ += count; + data_[size_] = '\0'; + return *this; +} + +MyString& MyString::append(char ch) { + ensure_capacity(size_ + 1); + data_[size_++] = ch; + data_[size_] = '\0'; + return *this; +} + +MyString& MyString::operator+=(const MyString& other) { return append(other); } +MyString& MyString::operator+=(const char* text) { return append(text); } +MyString& MyString::operator+=(char ch) { return append(ch); } + +std::size_t MyString::find(char ch, std::size_t pos) const noexcept { + for (std::size_t i = pos; i < size_; ++i) if (data_[i] == ch) return i; + return npos; +} + +std::size_t MyString::find(const MyString& needle, std::size_t pos) const noexcept { + if (needle.empty()) return pos <= size_ ? pos : npos; + if (needle.size_ > size_ || pos > size_ - needle.size_) return npos; + for (std::size_t i = pos; i <= size_ - needle.size_; ++i) { + std::size_t j = 0; + while (j < needle.size_ && data_[i + j] == needle.data_[j]) ++j; + if (j == needle.size_) return i; + } + return npos; +} + +bool MyString::contains(const MyString& needle) const noexcept { return find(needle) != npos; } + +bool MyString::starts_with(const MyString& prefix) const noexcept { + if (prefix.size_ > size_) return false; + for (std::size_t i = 0; i < prefix.size_; ++i) if (data_[i] != prefix.data_[i]) return false; + return true; +} + +bool MyString::ends_with(const MyString& suffix) const noexcept { + if (suffix.size_ > size_) return false; + const std::size_t offset = size_ - suffix.size_; + for (std::size_t i = 0; i < suffix.size_; ++i) if (data_[offset + i] != suffix.data_[i]) return false; + return true; +} + +MyString MyString::substr(std::size_t pos, std::size_t count) const { + if (pos > size_) throw std::out_of_range("MyString::substr: position out of range"); + const std::size_t actual = std::min(count, size_ - pos); + return MyString(std::string_view(data_, actual)); +} + +MyString MyString::reversed() const { + MyString result(*this); + for (std::size_t left = 0, right = size_ == 0 ? 0 : size_ - 1; left < right; ++left, --right) + std::swap(result.data_[left], result.data_[right]); + return result; +} + +int MyString::compare(const MyString& other) const noexcept { + const std::size_t common = std::min(size_, other.size_); + for (std::size_t i = 0; i < common; ++i) { + if (data_[i] < other.data_[i]) return -1; + if (data_[i] > other.data_[i]) return 1; + } + if (size_ < other.size_) return -1; + if (size_ > other.size_) return 1; + return 0; +} + +MyString operator+(const MyString& lhs, const MyString& rhs) { + MyString result; + result.reserve(lhs.size_ + rhs.size_); + result.append(lhs); + result.append(rhs); + return result; +} + +bool operator==(const MyString& lhs, const MyString& rhs) noexcept { return lhs.compare(rhs) == 0; } +bool operator!=(const MyString& lhs, const MyString& rhs) noexcept { return !(lhs == rhs); } +bool operator<(const MyString& lhs, const MyString& rhs) noexcept { return lhs.compare(rhs) < 0; } +bool operator<=(const MyString& lhs, const MyString& rhs) noexcept { return lhs.compare(rhs) <= 0; } +bool operator>(const MyString& lhs, const MyString& rhs) noexcept { return lhs.compare(rhs) > 0; } +bool operator>=(const MyString& lhs, const MyString& rhs) noexcept { return lhs.compare(rhs) >= 0; } + +std::ostream& operator<<(std::ostream& out, const MyString& value) { + return out.write(value.c_str(), static_cast(value.size_)); +} + +std::istream& operator>>(std::istream& in, MyString& value) { + value.clear(); + in >> std::ws; + while (in.good()) { + const int next = in.peek(); + if (next == std::char_traits::eof() || std::isspace(static_cast(next))) break; + value.append(static_cast(in.get())); + } + return in; +} diff --git a/tests/test_mystring.cpp b/tests/test_mystring.cpp new file mode 100644 index 0000000..254bf7e --- /dev/null +++ b/tests/test_mystring.cpp @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include + +#include "MyString.h" + +static void test_construction_and_access() { + MyString empty; + assert(empty.empty()); + assert(empty.size() == 0); + assert(empty.c_str()[0] == '\0'); + + MyString value("hello"); + assert(value.size() == 5); + assert(value.length() == 5); + assert(value.front() == 'h'); + assert(value.back() == 'o'); + value[0] = 'H'; + assert(value == MyString("Hello")); + assert(value.at(1) == 'e'); + + bool threw = false; + try { (void)value.at(99); } catch (const std::out_of_range&) { threw = true; } + assert(threw); +} + +static void test_rule_of_five() { + MyString original("resource"); + MyString copied(original); + copied[0] = 'R'; + assert(original == MyString("resource")); + assert(copied == MyString("Resource")); + + MyString assigned; + assigned = original; + assert(assigned == original); + + MyString moved(std::move(copied)); + assert(moved == MyString("Resource")); + assert(copied.empty()); + assert(copied.c_str()[0] == '\0'); + + MyString move_assigned; + move_assigned = std::move(moved); + assert(move_assigned == MyString("Resource")); + assert(moved.empty()); +} + +static void test_modification_and_capacity() { + MyString value("ab"); + value.reserve(32); + assert(value.capacity() >= 32); + value.append('c').append("def"); + assert(value == MyString("abcdef")); + value += MyString("!"); + assert(value == MyString("abcdef!")); + + value += value; + assert(value == MyString("abcdef!abcdef!")); + + const std::size_t old_capacity = value.capacity(); + value.clear(); + assert(value.empty()); + assert(value.capacity() == old_capacity); +} + +static void test_search_and_substrings() { + MyString value("banana bread"); + assert(value.find('b') == 0); + assert(value.find('x') == MyString::npos); + assert(value.find(MyString("bread")) == 7); + assert(value.contains(MyString("ana"))); + assert(!value.contains(MyString("cake"))); + assert(value.starts_with(MyString("ban"))); + assert(value.ends_with(MyString("bread"))); + assert(value.substr(7) == MyString("bread")); + assert(value.substr(0, 6) == MyString("banana")); + assert(value.reversed() == MyString("daerb ananab")); +} + +static void test_comparison_and_streams() { + assert(MyString("abc") < MyString("abd")); + assert(MyString("abc") <= MyString("abc")); + assert(MyString("abd") > MyString("abc")); + assert(MyString("abc") != MyString("abd")); + + std::stringstream input("hello world"); + MyString first; + MyString second; + input >> first >> second; + assert(first == MyString("hello")); + assert(second == MyString("world")); + + std::stringstream output; + output << first << ' ' << second; + assert(output.str() == "hello world"); +} + +static void test_embedded_nulls() { + const char raw[] = {'a', '\0', 'b'}; + MyString value(std::string_view(raw, 3)); + assert(value.size() == 3); + assert(value[1] == '\0'); + assert(value.substr(1, 2).size() == 2); +} + +int main() { + test_construction_and_access(); + test_rule_of_five(); + test_modification_and_capacity(); + test_search_and_substrings(); + test_comparison_and_streams(); + test_embedded_nulls(); + return 0; +}