Implement Your Own std::string (MyString)
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
Prior to C++11, c_str() was guaranteed to return a null-terminated string (const char*), whereas data() was only required to return a pointer to the character array without guaranteeing a trailing null terminator. Since C++11, std::string requires contiguous, null-terminated storage at all times (data_[size()] == '\0'), meaning c_str() and data() are completely identical and both return a valid null-terminated const char* in O(1) time. In C++17, a non-const overload char* data() was also added to allow direct buffer mutation.
A standard dynamic string requires 24 bytes on 64-bit systems (char* ptr, size_t size, and size_t capacity). In GCC's implementation, the string structure is a union of a heap layout and an inline buffer char _M_local_buf[16]. When size <= 15, ptr points directly to _M_local_buf, capacity is set to 15, and the characters are stored inside the stack object itself with 0 heap allocations. Only when the length reaches 16 characters does the string allocate dynamic memory on the heap.
std::string: An owning container that manages its own heap (or SSO) memory buffer. Copying a std::string performs a deep copy of the underlying characters.
std::string_view: A non-owning view / slice consisting of just a pointer and a length (const char* data_,size_t length_, 16 bytes total). Creating or slicing astring_viewis strictly O(1) with zero memory allocation. However,string_viewdoes not guarantee null termination (it can be an interior substring) and does not manage object lifetime (it dangles if the source string is destroyed).
Interviewer's lens
Interviewers frequently ask candidates to write a custom string class (MyString) because it is the quintessential test of dynamic resource ownership, manual C-string memory layout, and bulletproof Rule of 5 mechanics. Weak candidates make classic blunders: forgetting the + 1 null-terminator byte, crashing when a caller passes MyString(nullptr), writing vulnerable shallow-copy assignment operators that crash on self-assignment (s = s), or leaving moved-from strings in an invalid null-dereferencing state. Senior candidates immediately preserve the data_[size_] == '\0' invariant, employ the Copy-and-Swap idiom, guarantee O(1) non-throwing move transfers, and explain how Small String Optimization (SSO) eliminates heap allocation for small payloads.
Under the hood & memory layout
A basic dynamic string class occupies 24 bytes on the stack on 64-bit platforms: an 8-byte heap pointer char* data_, an 8-byte size_t size_, and an 8-byte size_t capacity_. The heap buffer allocated from the OS must always be at least capacity_ + 1 bytes long to store the mandatory null terminator \0. This guarantees that c_str() and data() operate in strictly O(1) time without runtime memory allocation. In modern production implementations (e.g. std::string in GCC and Clang), this 24-byte footprint is repurposed as an SSO union: when the string length is β€ 15 characters, the 24-byte stack struct stores the characters inline inside a char inlineBuf_[16] array, achieving zero heap allocations and optimal CPU cache locality.
char* data_ (8 bytes), size_t size_ (8 bytes), and size_t capacity_ (8 bytes). For empty strings, data_ points to a valid 1-byte heap slab containing \0 (or a static literal).
Allocated buffer size is always capacity_ + 1. The invariant data_[size_] == '\0' must hold after every constructor, mutation, append, and shrink operation.
Copy construction requires allocating an independent heap slab of other.capacity_ + 1 bytes. A compiler-generated shallow copy would duplicate the pointer, triggering a double-free crash upon destruction.
An SSO-enabled string overlays char inlineBuf_[16] onto the same memory as data_ and capacity_. Strings up to 15 characters require 0 heap allocations.
Core concepts & syntax traps
Managing Dynamic Null-Terminated Buffers & The +1 Byte Invariant
Managing Dynamic Null-Terminated Buffers & The +1 Byte Invariant
The single most pervasive bug in custom string implementations is failing to account for the null terminator \0 in memory allocations:
// FATAL BUG: Buffer overflow!
char* buf = new char[size_]; // Needs new char[size_ + 1]!
std::memcpy(buf, src, size_);
buf[size_] = '\0'; // WRITES PAST THE ALLOCATED HEAP BLOCK!
To ensure c_str() operates in O(1) time without dynamic allocation, data_[size_] must always be \0. When allocating memory for a string of capacity C, you must allocate C + 1 bytes. Even for an empty string MyString(), allocating a 1-byte buffer containing "\0" prevents data_ from ever being nullptr, ensuring c_str() can safely return data_ unconditionally without branching.
// Allocation invariant:
data_ = new char[capacity_ + 1];
std::memcpy(data_, src, size_);
data_[size_] = '\0'; // Always null-terminated at index size_!Always allocate capacity + 1 bytes; data_[size_] must unconditionally be '\0' to guarantee O(1) c_str() compatibility.
The nullptr Trap: Bulletproof C-String Constructors
The nullptr Trap: Bulletproof C-String Constructors
When an interviewer asks you to write MyString(const char* s), what happens if a caller passes MyString(nullptr)?
If you blindly write size_ = std::strlen(s);, std::strlen(nullptr) triggers an immediate segmentation fault (Undefined Behavior)! A robust production implementation guards against nullptr by defaulting to an empty string:
MyString(const char* str) {
if (!str) {
data_ = new char[1]{'\0'};
size_ = 0;
capacity_ = 0;
} else {
size_ = std::strlen(str);
capacity_ = size_;
data_ = new char[capacity_ + 1];
std::memcpy(data_, str, size_ + 1); // Copies payload + null byte!
}
}
MyString s(nullptr); // Safe: creates valid empty string "", size = 0Never pass unvalidated raw pointers to std::strlen; treat nullptr as an empty string.
Deep Copy vs Copy-and-Swap Assignment (Strong Exception Safety)
Deep Copy vs Copy-and-Swap Assignment (Strong Exception Safety)
A naive copy assignment operator typically looks like:
// BUG: Vulnerable to self-assignment and throws midway!
MyString& operator=(const MyString& other) {
delete[] data_; // Self-assignment kills other.data_!
data_ = new char[other.size_ + 1]; // If new throws bad_alloc, *this is corrupt!
std::memcpy(data_, other.data_, other.size_ + 1);
return *this;
}
If s = s; is called, delete[] data_ destroys the very memory that is about to be copied. Furthermore, if new throws std::bad_alloc, the original string's data has already been destroyed, violating exception safety.
The industry standard solution is the Copy-and-Swap Idiom:
- Construct a temporary copy
MyString tmp(other);(if this throws,*thisis completely untouched!). - Swap the internal pointers of
tmpand*thisin non-throwing O(1) operations. - When
tmpleaves scope, its destructor safely frees the old buffer.
MyString& operator=(const MyString& other) {
if (this != &other) {
MyString tmp(other); // Deep copy into temporary
swap(tmp); // Atomic O(1) pointer swap
}
return *this;
}The Copy-and-Swap idiom provides Strong Exception Safety and handles self-assignment automatically.
noexcept Move Semantics: Leaving the Moved-From Object in a Valid State
noexcept Move Semantics: Leaving the Moved-From Object in a Valid State
In C++, moving an object transfers ownership of its underlying heap resources in O(1) time without copying. However, according to the C++ Standard, a moved-from object must be left in a valid, destructible state.
If you simply write:
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
other.data_ = nullptr; // DANGEROUS if other.c_str() is subsequently called!
}
If the moved-from string is later accessed via other.c_str(), printing it dereferences nullptr and crashes. The robust idiom delegates to MyString() (which allocates new char[1]{'\0'}) and then swaps with other:
MyString(MyString&& other) noexcept : MyString() {
swap(other);
}
Now this gets other's live heap buffer, and other receives a valid empty "\0" string!
MyString s1("Data");
MyString s2 = std::move(s1);
// s1.c_str() is "" (valid, non-null, size 0)
// s2.c_str() is "Data"Leave moved-from strings in a valid empty state (pointing to '\0') rather than a naked nullptr.
Small String Optimization (SSO): The 15-Byte Inline Stack Buffer
Small String Optimization (SSO): The 15-Byte Inline Stack Buffer
In real-world applications, over 70% of strings (keys, names, codes, UUID prefixes) are under 16 characters long. Dynamic heap allocations for every 4-byte or 8-byte string create heavy memory fragmentation and CPU cache misses.
Small String Optimization (SSO) replaces the pointer with a stack-allocated buffer when the string is short. By placing a char inlineBuf_[16] inside an anonymous union with char* heapBuf_, strings up to 15 characters (plus the \0 terminator) are stored directly inside the MyString stack frame with 0 heap allocations! When appending causes the string to exceed 15 characters, it automatically transitions to a dynamic heap buffer.
union {
char inlineBuf_[16]; // 15 characters + '\0'
char* heapBuf_; // Dynamic buffer for > 15 characters
};
size_t size_{0};
size_t capacity_{15};SSO eliminates heap allocations for strings <= 15 chars by storing characters directly inside the 24-byte stack object.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.