Command Palette

Search for a command to run...

MTH 4300

Lecture 10

The Shallow Copy Problem

When you copy an object, C++ by default copies each member value — a shallow copy. For objects that own heap memory, this creates two objects pointing to the same buffer:

Shallow copy — two pointers, one buffer
String a("hello");
String b = a;    // default: b.data_ = a.data_ — same pointer!

// When both go out of scope, ~String() runs on each.
// The same buffer is deleted twice: undefined behavior.

The destructor of a frees the buffer. The destructor of b then tries to free the same memory — a double free, which is undefined behavior (typically a crash).

To fix this, you must define a copy constructor and copy assignment operator that perform a deep copy — allocating fresh memory and copying the contents.

Copy Constructor

The copy constructor is called whenever an object is initialized from another object of the same type:

When the copy constructor runs
String a("hello");
String b = a;     // copy constructor
String c(a);      // copy constructor — same thing

void foo(String s); // copy constructor when called by value
foo(a);
Copy constructor — declaration and definition
// In the class:
String(const String& other);

// Definition:
String::String(const String& other)
    : len_(other.len_), data_(new char[len_ + 1]) {
    std::strcpy(data_, other.data_);
}

The parameter is const String& — pass by const reference to avoid triggering another copy and to allow copying from const objects.

Copy Assignment Operator

The copy assignment operator is called when you assign one existing object to another:

When copy assignment runs
String a("hello");
String b("world");
b = a;   // copy assignment (b already exists — this is not construction)
Copy assignment — declaration and definition
// In the class:
String& operator=(const String& other);

// Definition:
String& String::operator=(const String& other) {
    if (this == &other) return *this; // self-assignment guard

    delete[] data_;                   // free current buffer
    len_  = other.len_;
    data_ = new char[len_ + 1];
    std::strcpy(data_, other.data_);

    return *this;
}

The return type is String& to support chained assignment: a = b = c.

Self-Assignment

Without the guard if (this == &other) return *this, assigning an object to itself destroys its own buffer before reading from it:

Why self-assignment matters
String s("hello");
s = s; // without guard: delete[] data_ frees the buffer,
       // then strcpy reads from freed memory — undefined behavior

Always check for self-assignment at the top of operator=.

The Rule of Three

If a class needs any one of these, it almost certainly needs all three:

  1. Destructor — because the class owns a resource
  2. Copy constructor — to deep-copy that resource on initialization
  3. Copy assignment operator — to deep-copy and release the old resource on assignment

This is the Rule of Three. A class that defines a destructor for heap memory but omits the other two silently produces dangling pointers and double frees.

The standard library containers (std::vector, std::string) all follow this rule. Our String class is a simplified version of std::string.

String with Full Copy Semantics

String — declaration after Lecture 10
class String {
public:
    String();
    explicit String(const char* s);
    ~String();

    String(const String& other);             // copy constructor
    String& operator=(const String& other);  // copy assignment

    size_t size()  const;
    bool   empty() const;
    char   at(size_t i) const;
    const char* c_str() const;

private:
    size_t len_;
    char*  data_;
};

Exercises

Q1.

Without running the code, predict what this program prints — or describe what goes wrong.

#include <iostream>
#include <cstring>

// String WITHOUT a copy constructor — default shallow copy
struct BadString {
    char* data;
    size_t len;

    BadString(const char* s) : len(std::strlen(s)), data(new char[len + 1]) {
        std::strcpy(data, s);
    }
    ~BadString() { delete[] data; }
};

int main() {
    BadString a("hi");
    BadString b = a;               // shallow copy: b.data == a.data

    std::cout << b.data << "\n";   // line A
    return 0;
}

Q2.

Without running the code, predict what this program prints (using correctly-implemented String with a deep-copy constructor).

#include <iostream>

void print_it(String s) {        // takes String by value — triggers copy
    std::cout << s.c_str() << "\n";
}

int main() {
    String a("hello");
    String b = a;                // copy constructor

    print_it(b);                 // copy constructor again

    std::cout << a.c_str() << "\n"; // a is unchanged

    return 0;
}

Q3.

Trace through this copy assignment step by step. At each checkpoint, describe the state of a and b.

int main() {
    String a("hello");
    String b("world");
    // CHECKPOINT 1

    b = a;
    // CHECKPOINT 2

    a = a;
    // CHECKPOINT 3

    return 0;
}

Q4.

Each snippet has a bug related to copy semantics. Identify what's wrong.

// Snippet A
String::String(const String& other) {
    data_ = other.data_; // no new allocation
    len_  = other.len_;
}

// Snippet B — missing self-assignment guard
String& String::operator=(const String& other) {
    delete[] data_;
    len_  = other.len_;
    data_ = new char[len_ + 1];
    std::strcpy(data_, other.data_);
    return *this;
}

// Snippet C — wrong cleanup order
String& String::operator=(const String& other) {
    if (this == &other) return *this;
    len_  = other.len_;
    data_ = new char[len_ + 1]; // old pointer overwritten before being freed
    delete[] data_;
    std::strcpy(data_, other.data_);
    return *this;
}

// Snippet D — wrong return type
String String::operator=(const String& other) {
    if (this == &other) return *this;
    delete[] data_;
    len_  = other.len_;
    data_ = new char[len_ + 1];
    std::strcpy(data_, other.data_);
    return *this;
}

Q5.

Implement the copy constructor and copy assignment operator for String.

#include <cstring>

String::String(const String& other) {
    // TODO: deep copy other into this
}

String& String::operator=(const String& other) {
    // TODO: guard, free old buffer, deep copy, return *this
}

Practice Problems