C++ • Object-Oriented Programming

Copy Constructor & Assignment Operator — Understand copy constructor and assignment operator at a high level (intro)

Master how C++ objects are duplicated and reassigned to prevent resource leaks and undefined behavior.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

From its earliest days, C++ distinguished itself from C by introducing classes — user-defined types that bundle data and behavior together. As soon as objects could own dynamically allocated resources such as heap memory, file handles, or network sockets, a fundamental question arose: what should happen when one object is used to initialize or overwrite another? In languages with garbage collection, such as Java or Python, this question is largely moot because the runtime tracks references automatically. In C++, however, the programmer bears direct responsibility for the lifetime of every resource, making the semantics of object copying a first-class design concern.

The evolution of copy semantics in C++ tracks the language's broader maturation. Bjarne Stroustrup's original "C with Classes" compiler already generated default member-wise copy operations, but the pitfalls of shallow copying — where two objects inadvertently share the same underlying resource — became apparent almost immediately. The community's response crystallized into guidelines like the Rule of Three, later extended to the Rule of Five with C++11 move semantics. Understanding the copy constructor and the copy-assignment operator is the gateway to all of these idioms.

1979 • C with Classes

Bjarne Stroustrup begins extending C at Bell Labs with class types, constructors, and destructors — laying the groundwork for compiler-generated copy operations.

1985 • First C++ Release

The first commercial release of C++ (Cfront 1.0) formalizes default member-wise copy construction and assignment. Programmers quickly encounter double-free bugs when classes manage raw pointers.

1991 • The Rule of Three Emerges

Community best practices coalesce: if a class defines a destructor, it should also define a copy constructor and a copy-assignment operator — the Rule of Three.

1998 • ISO C++98 Standard

The first ISO standard codifies the exact conditions under which the compiler will implicitly generate copy constructors and assignment operators, giving the Rule of Three a formal footing.

2011 • C++11 Move Semantics

Move constructors and move-assignment operators are introduced, extending the Rule of Three to the Rule of Five. The copy-and-swap idiom becomes the canonical safe implementation pattern.

The central question this lesson addresses is deceptively simple: how does C++ duplicate the state of one object into another, and what can go wrong if the programmer does not intervene? Answering this question properly is essential to writing correct, resource-safe C++ code.

SECTION 2

Core Principles & Definitions

Before diving into code, it is important to establish the vocabulary that C++ programmers use when discussing object duplication. Two distinct operations exist — copy construction and copy assignment — and they fire in different situations. Conflating them is a common source of confusion for students encountering C++ for the first time.

1

Copy Constructor

A special constructor that initializes a new object as a copy of an existing object. Its signature is T(const T& other). It is invoked whenever a new object is created from an existing one — for example, T b = a; or when passing by value.
2

Copy-Assignment Operator

An overloaded operator= that replaces the state of an already-initialized object with the state of another. Its canonical signature is T& operator=(const T& other). It is invoked on an object that already exists — for example, b = a; after b has been constructed.
3

Shallow vs. Deep Copy

A shallow copy duplicates the bits of each member, including pointer addresses. A deep copy allocates new resources and duplicates the pointed-to data. The compiler-generated defaults always perform a shallow (member-wise) copy.
4

Rule of Three

If a class defines any one of the destructor, copy constructor, or copy-assignment operator, it should define all three. Failure to do so almost always indicates a resource management bug.
5

Self-Assignment Safety

The copy-assignment operator must handle the case a = a; gracefully. A naïve implementation that frees resources before copying may destroy the very data it needs, leading to undefined behavior.
✦

KEY TAKEAWAY

Think of the copy constructor as a photocopier for brand-new objects: it creates a fresh sheet of paper with the same content. The copy-assignment operator, by contrast, is like erasing an existing whiteboard and rewriting it with content from another board. The whiteboard already existed and may have had old content that must be cleaned up first — that is why assignment is more complex than construction.

SECTION 3

Visual Explanation — Shallow vs. Deep Copy

The most dangerous pitfall in C++ copying is the shallow copy problem. When a class manages a raw pointer to heap memory, the compiler-generated copy constructor simply copies the pointer value — not the data it points to. Both the original and the copy then believe they own the same heap block, leading inevitably to a double-free when both destructors run. The diagram below illustrates this contrast.

Shallow Copy vs. Deep CopySHALLOW COPY (compiler default)DEEP COPY (user-defined)obj_adata_: 0xA0size_: 5obj_bdata_: 0xA0size_: 5Heap 0xA0[1, 2, 3, 4, 5]⚠ Both point to same memory!Double-free on destructionobj_adata_: 0xA0size_: 5obj_bdata_: 0xB0size_: 5Heap 0xA0[1, 2, 3, 4, 5]Heap 0xB0[1, 2, 3, 4, 5]✓ Independent heap allocationsSafe to destroy independentlyLifecycle with Shallow Copy:1. obj_a constructed → allocates 0xA02. obj_b = obj_a → copies ptr 0xA03. ~obj_b() → frees 0xA04. ~obj_a() → frees 0xA0 AGAIN! ← UBLifecycle with Deep Copy:1. obj_a constructed → allocates 0xA02. obj_b = obj_a → allocates 0xB0, copies3. ~obj_b() → frees 0xB0 ✓4. ~obj_a() → frees 0xA0 ✓
On the left, both obj_a and obj_b hold the same pointer value 0xA0, so destroying one invalidates the other. On the right, a deep copy allocates fresh memory at 0xB0 and copies the array contents, ensuring safe independent destruction.

The diagram makes clear why the compiler-generated copy constructor is insufficient for resource-owning classes. When obj_b's destructor runs and calls delete[] data_, the heap block at 0xA0 is freed. When obj_a's destructor subsequently runs, it attempts to free the same address — this is undefined behavior and typically crashes the program or silently corrupts the heap. The deep copy on the right eliminates this problem by giving each object its own allocation, at the cost of one extra new[] call and a memcpy (or element-wise copy).

SECTION 4

How Copy Construction & Assignment Work

Understanding when each operation fires is critical. C++ distinguishes between initialization (creating a new object) and assignment (overwriting an existing object). The equals sign in C++ is syntactically ambiguous — it can invoke either the copy constructor or the copy-assignment operator depending on context.

Copy Constructor Invocation Contexts

  • Direct initialization: MyClass b(a); — explicitly calls the copy constructor.
  • Copy initialization: MyClass b = a; — despite the = sign, this is initialization, not assignment, because b is being created.
  • Pass by value: void func(MyClass param); — when called as func(a), param is copy-constructed from a.
  • Return by value: MyClass func() { MyClass x; return x; } — conceptually copy-constructs the return value (though modern compilers apply copy elision / NRVO).

Copy-Assignment Operator Invocation

The copy-assignment operator is called when = is used on an already-constructed object: MyClass a; MyClass b; b = a;. Here, b was already default-constructed on the previous line, so this is genuine assignment. The assignment operator must handle two tasks that the copy constructor does not face: (1) releasing old resources held by the left-hand side, and (2) guarding against self-assignment.

Canonical Signatures

COPY CONSTRUCTOR SIGNATURE

T(const T& other);

Takes a const reference to another object of the same type. Cannot be called with a non-const rvalue in older standards. The parameter must be a reference to avoid infinite recursion (copying the argument would itself call the copy constructor).

COPY-ASSIGNMENT OPERATOR SIGNATURE

T& operator=(const T& other);

Returns a reference to *this to allow chained assignment (a = b = c). Takes a const reference to avoid modifying the source object. Must release any resources currently held by *this before copying from other.

💡

Copy Elision (RVO / NRVO)

Since C++17, the compiler is required to elide certain copies (mandatory copy elision). Even before C++17, Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) meant that the copy constructor was often never called for return-by-value. Do not rely on side effects in your copy constructor.

SECTION 5

Anatomy of a Resource-Managing Class

To solidify the concepts, let us examine a minimal class that manages a dynamically allocated integer array. This IntArray class illustrates the Rule of Three in action: it defines a destructor, a copy constructor, and a copy-assignment operator. The diagram below maps each special member function to its role in the object's lifecycle.

Object Lifecycle — The Rule of ThreeCLASS DEFINITIONclass IntArray { int* data_; size_t size_; public: /* Rule of Three members below */ };ConstructorIntArray(size_t n) : data_(new int[n]) , size_(n){ }Copy ConstructorIntArray(const IntArray& o) : data_(new int[o.size_]) , size_(o.size_){ copy(o.data_,...); }Destructor~IntArray() { delete[] data_;}Copy-Assignment OperatorIntArray& operator=(const IntArray& o) { if (this != &o) { // 1. self-assignment guard delete[] data_; // 2. release old resource data_ = new int[o.size_]; // 3. allocate new resource copy(o.data_, ...); size_ = o.size_; } return *this; }allocate like constructorcopy like copy-ctorfree like destructor
The copy-assignment operator combines the logic of all three other special member functions: it guards against self-assignment, frees the old resource (like the destructor), allocates fresh memory (like the constructor), and copies the data (like the copy constructor).

Notice how the copy-assignment operator's logic is essentially a destroy-then-copy-construct sequence. This pattern is so common that C++ programmers often factor it out into the copy-and-swap idiom, which achieves strong exception safety by leveraging the copy constructor to create a temporary, then swapping the temporary's guts with *this. We will explore this idiom in a subsequent lesson.

PropertyCopy ConstructorCopy-Assignment Operator
When invokedObject is being created for the first timeObject already exists and is being overwritten
Old state to clean up?No — the object has no prior stateYes — must release current resources first
Self-assignment concern?Not applicableYes — must guard against a = a
Return typeNone (constructor)T& (reference to *this)
Syntax triggerT b(a); T b = a;b = a; (b already constructed)

Key differences between copy constructor and copy-assignment operator

SECTION 6

Worked Example — Implementing a Safe IntArray

Let us implement the IntArray class step by step, writing a correct copy constructor and copy-assignment operator that perform deep copies.

Implementing Rule of Three for IntArray

  1. 1

    Step 1 — Define the Class with Raw Members

    We start with a class that owns a dynamically allocated array: class IntArray { private: int* data_; size_t size_; public: explicit IntArray(size_t n) : data_(new int[n]{}), size_(n) {} }; The constructor allocates an array of n zero-initialized integers on the heap.
    Class owns a heap resource via data_ — triggers the need for the Rule of Three.
  2. 2

    Step 2 — Write the Destructor

    Since the constructor calls new[], the destructor must call delete[]: ~IntArray() { delete[] data_; } Without this, every IntArray object would leak its heap allocation.
    Destructor ensures deterministic resource release.
  3. 3

    Step 3 — Write the Copy Constructor (Deep Copy)

    The copy constructor must allocate its own block of memory and copy the elements one by one (or use std::copy): IntArray(const IntArray& other) : data_(new int[other.size_]) , size_(other.size_) { std::copy(other.data_, other.data_ + size_, data_); } By allocating fresh memory in the initializer list, we guarantee that data_ points to independent storage before the body copies the values.
    Deep copy achieved — each object owns its own heap block.
  4. 4

    Step 4 — Write the Copy-Assignment Operator

    The assignment operator must handle self-assignment, free old memory, then allocate and copy: IntArray& operator=(const IntArray& other) { if (this != &other) { // self-assignment guard delete[] data_; // free old resource data_ = new int[other.size_]; // allocate new size_ = other.size_; std::copy(other.data_, other.data_ + size_, data_); } return *this; // enable chaining } The self-assignment check this != &other prevents deleting data_ before reading from it when both sides refer to the same object.
    Rule of Three fully satisfied. The class is safe to copy and assign.
  5. 5

    Step 5 — Verify with a Test Scenario

    Consider the following usage: IntArray a(3); // Constructor: allocates 3 ints a[0] = 10; a[1] = 20; a[2] = 30; IntArray b = a; // Copy constructor: b gets its own [10,20,30] b[0] = 99; // Modifies only b's array a = b; // Copy-assignment: a's old memory freed, // a gets copy of [99,20,30] After b[0] = 99, a[0] remains 10 because they are independent copies. After a = b, a's data is [99, 20, 30] — a fresh copy of b's data.
    Both copy construction and copy assignment produce fully independent objects with identical values.

SECTION 7

Strengths, Limitations & Common Pitfalls

Deep-copy semantics are straightforward but carry trade-offs. In performance-critical code, unnecessary copies can dominate runtime. Conversely, neglecting them can introduce catastrophic bugs. The table below summarizes the key considerations.

AspectStrengthLimitation / Pitfall
CorrectnessDeep copy ensures full value semantics — objects are independent.Forgetting to define one of the three (destructor, copy ctor, copy assign) leads to resource leaks or double-frees.
PerformanceWorks well for small or infrequently copied objects.Deep copies of large data structures incur O(n) allocation and copy costs. Move semantics (C++11) mitigate this.
Exception SafetyNaïve implementation provides the basic guarantee.If new[] throws after delete[], the object is left in a destroyed state. The copy-and-swap idiom solves this.
Self-assignmentA simple pointer comparison guards against the common case.Forgetting the guard causes the object to read from freed memory — undefined behavior.
Compiler defaultsFor classes with only built-in or well-behaved types, the implicit copy operations are correct and free.As soon as a raw pointer or OS handle is added, the implicit copies become dangerously wrong.

Trade-offs when implementing copy operations

✦

KEY TAKEAWAY

Think of the compiler-generated copy operations as a carbon-copy machine that only duplicates the top sheet of a multi-page document. If your class is a single page (no pointers, no external resources), the carbon copy is perfect. But if there are attached appendices (heap memory, file handles), the carbon copy only duplicates the table of contents — not the appendices themselves. You need a deep-copying mechanism that duplicates the entire packet.

SECTION 8

Connection to Move Semantics & Smart Pointers

The copy constructor and copy-assignment operator are the foundation upon which C++11's move semantics are built. A move constructor — T(T&& other) noexcept — transfers ownership of resources from a temporary or expiring object instead of duplicating them, turning an O(n) deep copy into an O(1) pointer swap. Understanding why copies are expensive provides the motivation for moves.

FeatureCopy Operations (this lesson)Move Operations (C++11)
Source object state afterUnchanged — still valid and usableMoved-from — valid but unspecified state
Performance costO(n) — duplicates all dataO(1) — swaps pointers / handles
When triggeredSource is an lvalue (named object)Source is an rvalue (temporary or std::move'd)
RuleRule of ThreeRule of Five (adds move ctor + move assign)

Copy vs. Move operations

Modern C++ style often advises the Rule of Zero: prefer std::unique_ptr and std::vector as members so that the compiler-generated copy and move operations do the right thing automatically. However, understanding the copy constructor and copy-assignment operator at a mechanical level is essential — it is what smart pointers and containers are implementing internally, and knowing the underlying semantics makes you a far more effective debugger and library author.

ℹ

What's Next?

In upcoming lessons, we will implement the copy-and-swap idiom for exception-safe assignment, introduce move constructors and move-assignment operators, and explore how RAII (Resource Acquisition Is Initialization) and smart pointers automate the patterns you have learned here.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain, in your own words, why the compiler-generated copy constructor is safe for a class struct Point { double x; double y; }; but potentially dangerous for a class class DynStr { char* buf_; size_t len_; };. What specific runtime error could occur with DynStr?
Show answer
For Point, the compiler's member-wise copy duplicates x and y by value — since they are plain doubles with no external resources, the copy is complete and correct. For DynStr, the compiler copies the pointer buf_ by value, meaning both the original and the copy point to the same heap-allocated character array. When one of them is destroyed and calls delete[] buf_, the other is left with a dangling pointer. Attempting to access or free that pointer again results in a double-free — undefined behavior that often manifests as a crash or heap corruption.

Question 2: BASIC

Given the following code, identify which line invokes the copy constructor and which invokes the copy-assignment operator: Widget a(42); Widget b = a; // Line A Widget c(10); c = a; // Line B
Show answer
Line A invokes the copy constructor. Even though it uses the = sign, b is being created (initialized) for the first time, so C++ treats this as copy initialization. Line B invokes the copy-assignment operator. Here c was already constructed on the previous line, so the = sign triggers operator=, which must handle releasing c's old state.

Question 3: INTERMEDIATE

Write a copy-assignment operator for the following class that is safe against self-assignment: class Buffer { double* arr_; int cap_; public: Buffer(int c) : arr_(new double[c]{}), cap_(c) {} ~Buffer() { delete[] arr_; } // TODO: copy-assignment operator };
Show answer
A correct implementation: Buffer& operator=(const Buffer& other) { if (this != &other) { delete[] arr_; cap_ = other.cap_; arr_ = new double[cap_]; std::copy(other.arr_, other.arr_ + cap_, arr_); } return *this; } The self-assignment guard this != &other prevents the object from deleting its own array and then trying to copy from it. The return of *this by reference enables chained assignment such as a = b = c. Note that this implementation provides only the basic exception-safety guarantee; if new double[cap_] throws, arr_ is a dangling pointer. The copy-and-swap idiom addresses this.

Question 4: APPLIED

A colleague's class manages a file descriptor: class FileHandle { int fd_; public: FileHandle(const char* path) : fd_(open(path, O_RDONLY)) {} ~FileHandle() { if (fd_ >= 0) close(fd_); } }; They report that sometimes after copying a FileHandle into a container, subsequent reads from the original object fail with EBADF (bad file descriptor). Diagnose the bug and propose a fix.
Show answer
The class violates the Rule of Three. It defines a destructor (which closes the file descriptor) but relies on the compiler-generated copy constructor and copy-assignment operator, which merely copy the integer fd_ bit-for-bit. When the container's copy is destroyed, it closes the file descriptor, leaving the original holding an invalid descriptor value. The fix is to either (a) delete the copy operations to make the class non-copyable (FileHandle(const FileHandle&) = delete;), (b) implement a deep copy using dup(fd_) to duplicate the OS-level file descriptor, or (c) use reference counting via std::shared_ptr. Option (a) is the simplest and often the most appropriate for handle-like types.

Question 5: CRITICAL THINKING

The naïve copy-assignment operator shown in this lesson deletes the old array before allocating the new one. If new[] throws std::bad_alloc after the deletion, what state is the object left in? Sketch (in pseudocode or C++) an alternative implementation that provides the strong exception-safety guarantee — either the assignment succeeds completely, or the original object is unchanged.
Show answer
If new[] throws after delete[], the object's data_ pointer is dangling and size_ may or may not have been updated — the object is in a broken state. The strong guarantee can be achieved via the copy-and-swap idiom: allocate first, then swap. IntArray& operator=(IntArray other) { // note: pass by value swap(*this, other); // non-throwing swap return *this; } Here, the parameter other is copy-constructed (which may throw), but the copy is made before any mutation of *this. If the copy construction throws, *this is untouched. If it succeeds, the swap (which should be noexcept) exchanges the internals, and other's destructor cleans up the old data. This is the idiomatic strong-exception-safe assignment in C++.

SUMMARY

Lesson Summary

Summary

The copy constructor (T(const T&)) initializes a brand-new object as a duplicate of an existing one, while the copy-assignment operator (T& operator=(const T&)) overwrites an already-constructed object with another's state. The compiler generates default versions of both that perform a shallow, member-wise copy — safe for classes of plain data, but dangerous for classes that manage dynamically allocated resources such as heap memory, file descriptors, or sockets. For such classes, a user-defined deep copy is essential.

The Rule of Three states that if a class defines any one of the destructor, copy constructor, or copy-assignment operator, it should define all three. The copy-assignment operator faces two challenges the copy constructor does not: it must release old resources and guard against self-assignment. Looking ahead, move semantics (C++11) extend these principles to rvalue references, and the copy-and-swap idiom provides a pattern for writing exception-safe assignment operators.

Varsity Tutors • C++ • Copy Constructor & Assignment Operator