A classic C++ interview task is to implement a String class. In an interview setting, nobody expects a full replacement for std::string, but the class should at least manage resources correctly.
At a minimum, the design should support three things:
- It can be declared like a built-in type such as
int, and it supports copying and assignment. - It can be passed to functions and returned from them.
- It can serve as the element type of standard containers such as
vector,list, anddeque.
Using it as the key type of std::map is a stricter requirement and can be set aside here.
In practical terms, the class should allow code like this to compile and run without memory errors:
void foo(String x)
{
}
void bar(const String& x)
{
}
String baz()
{
String ret("world");
return ret;
}
int main()
{
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s4 = baz();
std::vector<String> svec;
svec.push_back(s0);
svec.push_back(s1);
svec.push_back(baz());
svec.push_back("good job");
}
The version below is a good interview answer because it prioritizes correctness and ease of implementation over performance. It is the kind of code you can write on a whiteboard with a low chance of making a mistake. In a sense, it trades runtime efficiency for simpler code.
Keep the data model simple
The simplest possible String stores just one data member: a char*.
That choice has an obvious advantage: the implementation stays small and straightforward. The downside is that some operations become more expensive. For example, size() must scan the string, so it runs in linear time.
Still, for interview purposes, simplicity matters more. So this design uses only one member, char* data_, with one important invariant:
- every valid
Stringobject has a non-nulldata_ data_always points to a null-terminated character array
That invariant makes the class easy to integrate with C string functions such as the str*() family.
Which operations are essential
The must-have operations are the usual copy-control members:
- constructor
- destructor
- copy constructor
- copy assignment operator
These used to be called the “big three.” In modern C++, move constructor and move assignment can also be added. To keep the focus on resource management, there is no need to discuss operator[] and similar overloads yet.
With those decisions made, the class naturally becomes:
#include <utility>
#include <string.h>
class String
{
public:
String()
: data_(new char[1])
{
*data_ = '\0';
}
String(const char* str)
: data_(new char[strlen(str) + 1])
{
strcpy(data_, str);
}
String(const String& rhs)
: data_(new char[rhs.size() + 1])
{
strcpy(data_, rhs.c_str());
}
/* Delegate constructor in C++11
String(const String& rhs)
: String(rhs.data_)
{
}
*/
~String()
{
delete[] data_;
}
/* Traditional:
String& operator=(const String& rhs)
{
String tmp(rhs);
swap(tmp);
return *this;
}
*/
String& operator=(String rhs) // yes, pass-by-value
{
swap(rhs);
return *this;
}
// C++ 11
String(String&& rhs)
: data_(rhs.data_)
{
rhs.data_ = nullptr;
}
String& operator=(String&& rhs)
{
swap(rhs);
return *this;
}
// Accessors
size_t size() const
{
return strlen(data_);
}
const char* c_str() const
{
return data_;
}
void swap(String& rhs)
{
std::swap(data_, rhs.data_);
}
private:
char* data_;
};
Why this version works well in an interview
Several details make this implementation especially robust.
Allocation and release are tightly controlled
new char[] appears only in constructors, and delete[] appears only in the destructor.
That alone removes a lot of room for mistakes. Memory ownership is easy to reason about, and cleanup logic is centralized.
Assignment uses the modern pass-by-value style
The assignment operator is written as:
String& operator=(String rhs)
This is intentional. The parameter is copied when needed, then swapped into place. It is a compact and safe way to implement assignment, and it follows the style recommended in modern C++ guidelines. The older copy-and-swap form with a const String& parameter is shown in comments for comparison.
The functions stay tiny
Nearly every member function is just one or two lines long. There are no branches and no special-case logic scattered around the class. That is valuable under interview pressure.
The destructor does not need a null check
There is no need to test whether data_ is NULL before calling delete[]. Deleting a null pointer is already safe in C++.
The const char* constructor does not validate the input
The constructor
String(const char* str)
does not check whether str is valid. This is one of those debates that never really ends. In this implementation, the initializer list already uses str, so adding an assert() in the constructor body would not solve the problem anyway.
What this design gives you
This is about as small as a correct String implementation can get while still behaving like a real value type. It can be copied, assigned, passed by value, returned from functions, and stored in standard containers. It also keeps the code short enough to be realistic in an interview.
Useful follow-up exercises
If you want to extend it, a few natural next steps are:
- Add operator overloads such as
operator==,operator<, andoperator[]. - Implement another version with an
int size_;member to trade extra space for faster operations likesize(). - Test how much C++11 rvalue references and move semantics improve the performance of direct insertion sort on this
Stringtype compared with C++98/03. Standard library implementations make use of the same idea.