Implementing a Custom String Class in C++

A custom string class can be developed in C++ to mirror the functionality of the standard library's std::string. This implementation organizes data using a dynamically allocated character array, tracking length and capacity separately from the null terminator.

Core Data Structure

namespace custom {
    class String {
    public:
        static const size_t npos = static_cast<size_t>(-1);
    private:
        char* data;
        size_t len;
        size_t cap;
    };
}

The npos constant serves as a sentinel value for operations like find when no match is found. Member variables include a pointer to the character array, the current string length, and the allocated capacity.

Iterators

Iterators are implemented as pointers to characters, providing range-based access.

typedef char* iterator;
typedef const char* const_iterator;

iterator begin() { return data; }
iterator end() { return data + len; }
const_iterator begin() const { return data; }
const_iterator end() const { return data + len; }

Capacity Management

Basic functions return the current state of the string.

size_t size() const { return len; }
size_t capacity() const { return cap; }
void clear() { data[0] = '\0'; len = 0; }
bool empty() const { return len == 0; }

The reserve method increases capacity without modifying content, ensuring it never reduces capacity.

void reserve(size_t new_cap) {
    if (new_cap > cap) {
        char* new_data = new char[new_cap + 1];
        std::strcpy(new_data, data);
        delete[] data;
        data = new_data;
        cap = new_cap;
    }
}

resize adjusts the string length, truncating or extending with a specified character, and may increase capacity if needed.

void resize(size_t new_len, char fill = '\0') {
    if (new_len < len) {
        data[new_len] = '\0';
        len = new_len;
    } else {
        reserve(new_len);
        std::memset(data + len, fill, new_len - len);
        len = new_len;
        data[len] = '\0';
    }
}

Element Access

Subscript operators provide direct access to characters with bounds checking.

char& operator[](size_t idx) {
    assert(idx < len);
    return data[idx];
}
const char& operator[](size_t idx) const {
    assert(idx < len);
    return data[idx];
}

Modifications

Appending characters or strings handles reallocation automatically.

void push_back(char ch) {
    if (len == cap) {
        reserve(cap == 0 ? 5 : cap * 2);
    }
    data[len++] = ch;
    data[len] = '\0';
}

String& append(const char* src) {
    size_t src_len = std::strlen(src);
    if (len + src_len > cap) {
        reserve(len + src_len);
    }
    std::strcat(data, src);
    len += src_len;
    return *this;
}

Compound assignment operators leverage existing methods for simplicity.

String& operator+=(const char* src) {
    return append(src);
}
String& operator+=(char ch) {
    push_back(ch);
    return *this;
}

Insertion at a position shifts existing characters to make room.

String& insert(size_t pos, const char* src) {
    assert(pos <= len);
    size_t src_len = std::strlen(src);
    if (len + src_len > cap) {
        reserve(len + src_len);
    }
    for (size_t i = len; i >= pos && i != npos; --i) {
        data[i + src_len] = data[i];
    }
    std::memcpy(data + pos, src, src_len);
    len += src_len;
    return *this;
}

Deletion methods adjust the string length and null terminator placement.

void pop_back() {
    if (len > 0) {
        data[--len] = '\0';
    }
}

String& erase(size_t pos, size_t count = npos) {
    assert(pos < len);
    if (count == npos || pos + count >= len) {
        len = pos;
        data[len] = '\0';
    } else {
        std::strcpy(data + pos, data + pos + count);
        len -= count;
    }
    return *this;
}

Swapping exchanges internal pointers and metadata efficiently.

void swap(String& other) {
    std::swap(data, other.data);
    std::swap(len, other.len);
    std::swap(cap, other.cap);
}

String Operations

Conversion to a C-style string returns the internal buffer.

const char* c_str() const {
    return data;
}

Search functions locate characters or substrings, returning npos on failure.

size_t find(char ch, size_t start = 0) const {
    for (size_t i = start; i < len; ++i) {
        if (data[i] == ch) return i;
    }
    return npos;
}

size_t find(const char* substr, size_t start = 0) const {
    const char* result = std::strstr(data + start, substr);
    return result ? result - data : npos;
}

Substring extraction creates a new string from a portion of the original.

String substr(size_t start, size_t count = npos) const {
    assert(start < len);
    String result;
    size_t copy_len = (count == npos || start + count > len) ? len - start : count;
    result.reserve(copy_len);
    for (size_t i = start; i < start + copy_len; ++i) {
        result += data[i];
    }
    return result;
}

Constructors and Destructor

Constructors initialize the string from various sources, with a modern copy constructor using a temporary object.

String() : data(new char[1]), len(0), cap(0) {
    data[0] = '\0';
}

String(const char* src) : len(std::strlen(src)), cap(len) {
    data = new char[cap + 1];
    std::strcpy(data, src);
}

String(const String& src) : data(nullptr), len(0), cap(0) {
    String temp(src.data);
    swap(temp);
}

The destructor releases allocated memory.

~String() {
    delete[] data;
}

Assignment Operator

Assignment uses the copy-and-swap idiom for exception safety.

String& operator=(const String& src) {
    if (this != &src) {
        String temp(src);
        swap(temp);
    }
    return *this;
}

Non-Member Functions

Stream operators enable input and output integration.

std::ostream& operator<<(std::ostream& out, const custom::String& str) {
    for (char ch : str) out << ch;
    return out;
}

std::istream& operator>>(std::istream& in, custom::String& str) {
    str.clear();
    char buffer[128];
    size_t idx = 0;
    char ch = in.get();
    while (ch == ' ' || ch == '\n') ch = in.get();
    while (ch != ' ' && ch != '\n') {
        buffer[idx++] = ch;
        if (idx == 127) {
            buffer[idx] = '\0';
            str += buffer;
            idx = 0;
        }
        ch = in.get();
    }
    if (idx) {
        buffer[idx] = '\0';
        str += buffer;
    }
    return in;
}

A getline function reads entire lines, similar to the stream operator but ignoring space delimiters.

std::istream& getline(std::istream& in, custom::String& str) {
    str.clear();
    char buffer[128];
    size_t idx = 0;
    char ch = in.get();
    while (ch != '\n') {
        buffer[idx++] = ch;
        if (idx == 127) {
            buffer[idx] = '\0';
            str += buffer;
            idx = 0;
        }
        ch = in.get();
    }
    if (idx) {
        buffer[idx] = '\0';
        str += buffer;
    }
    return in;
}

Comparison operators define lexicographic ordering and equality.

bool operator==(const String& other) const {
    return len == other.len && std::strcmp(data, other.data) == 0;
}
bool operator<(const String& other) const {
    return std::strcmp(data, other.data) < 0;
}

Tags: C++ string Class implementation Standard Template Library

Posted on Tue, 12 May 2026 15:04:12 +0000 by ExpendableDecoy