Home About Me

A Minimal but Correct String Class for C++ Interviews

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:

  1. It can be declared like a built-in type such as int, and it supports copying and assignment.
  2. It can be passed to functions and returned from them.
  3. It can serve as the element type of standard containers such as vector, list, and deque.

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 String object has a non-null data_
  • 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:

  1. Add operator overloads such as operator==, operator<, and operator[].
  2. Implement another version with an int size_; member to trade extra space for faster operations like size().
  3. Test how much C++11 rvalue references and move semantics improve the performance of direct insertion sort on this String type compared with C++98/03. Standard library implementations make use of the same idea.