Skip to content
Shafin Chowdhury
Go back

C++ Copy Constructor: How Object Copying Works

Software Developer & Computer Science Student

Published:
6 min read
Edit page
Table of Contents8 topics

In C++, objects represent actual chunks of physical memory. Unlike languages like Java or Python—where assigning a variable simply copies a reference or pointer to the same object on the heap—C++ creates an independent duplicate of the object by default.

The function responsible for orchestrating this duplicate creation is the copy constructor.

Understanding how the copy constructor works, when it is invoked, and why its parameter must be a reference is essential for writing safe, performant C++ code.


What You’ll Learn

  • What a copy constructor is and why it exists
  • The exact syntax: ClassName(const ClassName& other)
  • Why the parameter must be passed by reference (preventing infinite recursion)
  • The four scenarios where C++ invokes the copy constructor
  • The difference between direct initialization and copy initialization
  • The default compiler-generated copy constructor
  • The critical difference between the copy constructor and the copy assignment operator (operator=)
  • A complete working example of a user-defined copy constructor
  • Why dynamically allocated pointer members complicate copying (leading into shallow vs deep copy)

What Is a Copy Constructor?

A copy constructor is a special constructor that initializes a brand new object using the data from an existing object of the same class.

Its canonical signature is:

ClassName(const ClassName& other);

Breaking Down the Signature

  • ClassName(...): It is a constructor, so it shares the class name with no return type.
  • other: The existing object that serves as the source blueprint for the copy.
  • const: Guarantees that the copy operation will not modify the source object.
  • & (Reference): Mandatory!

[!WARNING] Why Must the Parameter Be a Reference? If you attempted to pass the parameter by value:

Student(Student other); // COMPILE ERROR!

To pass an argument by value to a function, C++ must create a copy of the argument. To create that copy, C++ must invoke the copy constructor. But invoking the copy constructor would again require passing by value, which would invoke the copy constructor again, leading to infinite recursive calls until the call stack overflows!

Therefore, C++ strictly enforces that the copy constructor parameter must be a reference.


When Is the Copy Constructor Invoked?

C++ invokes the copy constructor automatically in four common situations:

1. Direct Initialization from Another Object

Student s1("Shafin", 101);
Student s2(s1); // Copy constructor invoked directly

2. Copy Initialization

Student s3 = s1; // Copy constructor invoked (NOT assignment operator!)

Even though this uses the = syntax, because s3 is being declared and initialized on the same line, C++ invokes the copy constructor.

3. Passing an Object by Value to a Function

void displayStudent(Student s) { // Passed by value: copy constructor invoked
    // ...
}

When displayStudent(s1) is called, a temporary copy of s1 is constructed in the function’s stack frame via the copy constructor.

4. Returning an Object by Value from a Function

Student createTopStudent() {
    Student top("Alice", 1);
    return top; // Copy (or move) constructor invoked upon return
}

(Note: Modern C++ compilers often optimize away this copy using Return Value Optimization / Copy Elision, but conceptually, returning by value requires a copy or move).


The Compiler-Generated Copy Constructor

If you do not define a copy constructor in your class, the C++ compiler automatically synthesizes a default copy constructor for you.

The default copy constructor performs a memberwise copy:

  • Every fundamental data member (int, double, char, bool) is copied by value.
  • Every object data member (such as string or vector) has its own copy constructor called.

For simple classes where all members are standard types, the compiler-generated copy constructor works perfectly:

#include <iostream>
#include <string>

using namespace std;

class Point {
public:
    int x;
    int y;

    Point(int xCoord, int yCoord) : x(xCoord), y(yCoord) {}
    // No custom copy constructor defined: compiler generates one automatically!
};

int main() {
    Point p1(10, 20);
    Point p2 = p1; // Compiler-generated copy constructor runs

    cout << "p1: (" << p1.x << ", " << p1.y << ")" << endl;
    cout << "p2: (" << p2.x << ", " << p2.y << ")" << endl;

    // Mutating p2 has zero effect on p1
    p2.x = 99;
    cout << "\nAfter mutating p2.x:" << endl;
    cout << "p1.x: " << p1.x << " (unchanged)" << endl;
    cout << "p2.x: " << p2.x << endl;

    return 0;
}
Output:
p1: (10, 20)
p2: (10, 20)

After mutating p2.x:
p1.x: 10 (unchanged)
p2.x: 99

Authoring a User-Defined Copy Constructor

When you need custom logging, metrics tracking, or specialized value modifications during copying, you write a user-defined copy constructor:

#include <iostream>
#include <string>

using namespace std;

class Student {
private:
    string name;
    int id;

public:
    // Parameterized constructor
    Student(string studentName, int studentId)
        : name(studentName), id(studentId) {
        cout << "[Parameterized Constructor] Created: " << name << endl;
    }

    // User-Defined Copy Constructor
    Student(const Student& other)
        : name(other.name), id(other.id) {
        cout << "[Copy Constructor] Copied student: " << other.name
             << " to new instance." << endl;
    }

    void display() const {
        cout << "Name: " << name << " | ID: " << id << endl;
    }
};

int main() {
    cout << "1. Creating s1..." << endl;
    Student s1("Shafin", 101);

    cout << "\n2. Creating s2 as a copy of s1..." << endl;
    Student s2 = s1; // Copy constructor runs

    cout << "\n3. Creating s3 via direct initialization..." << endl;
    Student s3(s1);  // Copy constructor runs

    cout << "\n4. Verifying objects:" << endl;
    s1.display();
    s2.display();
    s3.display();

    return 0;
}
Output:
1. Creating s1...
[Parameterized Constructor] Created: Shafin

2. Creating s2 as a copy of s1...
[Copy Constructor] Copied student: Shafin to new instance.

3. Creating s3 via direct initialization...
[Copy Constructor] Copied student: Shafin to new instance.

4. Verifying objects:
Name: Shafin | ID: 101
Name: Shafin | ID: 101
Name: Shafin | ID: 101

Copy Constructor vs Copy Assignment Operator (operator=)

A frequent source of bugs for university students is confusing the copy constructor with the copy assignment operator:

+------------------------------+-----------------------------------------+
| Aspect                       | Copy Constructor                        |
+------------------------------+-----------------------------------------+
| Purpose                      | Initializes a BRAND NEW object.         |
| Invocation Timing            | At object declaration/creation.         |
| Example                      | Student s2 = s1;  or  Student s2(s1);   |
| Memory State                 | Target memory is uninitialized.         |
+------------------------------+-----------------------------------------+
| Aspect                       | Copy Assignment Operator                |
+------------------------------+-----------------------------------------+
| Purpose                      | Overwrites an ALREADY EXISTING object.  |
| Invocation Timing            | After both objects already exist.       |
| Example                      | s2 = s1; (where s2 was declared before) |
| Memory State                 | Target object already owns resources.   |
+------------------------------+-----------------------------------------+

Observe the difference in code:

Student s1("Shafin", 101); // Parameterized constructor

Student s2 = s1;           // COPY CONSTRUCTOR: s2 is being born right here!

Student s3("Temporary", 999); // Parameterized constructor for s3
s3 = s1;                   // COPY ASSIGNMENT OPERATOR: s3 already exists!

The Dangerous Limitation: Raw Pointer Members

If your class only holds standard values (strings, integers, floats, vectors), the default copy constructor is completely safe.

However, consider what happens when a class owns dynamically allocated heap memory via raw pointers:

class DynamicStudent {
public:
    string name;
    double* cgpa; // Raw pointer to heap memory

    DynamicStudent(string n, double gpa) : name(n) {
        cgpa = new double(gpa); // Allocate on heap
    }

    ~DynamicStudent() {
        delete cgpa; // Free heap memory
    }
};

If we execute:

DynamicStudent s1("Shafin", 3.9);
DynamicStudent s2 = s1; // Default copy constructor runs!

What does the compiler-generated memberwise copy do?

  • It copies name correctly.
  • It copies the raw pointer value stored in cgpa.

Both s1.cgpa and s2.cgpa now store the exact same hexadecimal memory address on the heap!

Stack Objects                        Heap Memory
+-----------------------+
| s1                    |
|   name: "Shafin"      |
|   cgpa: 0x7ffee40 ----+------------> [ 3.9 ]
+-----------------------+                ^
                                         |
+-----------------------+                |
| s2 (Shallow Copy)     |                |
|   name: "Shafin"      |                |
|   cgpa: 0x7ffee40 ----+----------------+
+-----------------------+

When s2 and s1 leave scope at the end of the function:

  1. s2’s destructor runs: delete s2.cgpa; (Heap memory freed).
  2. s1’s destructor runs: delete s1.cgpa; (CRASH: DOUBLE FREE ERROR!).

This fatal behavior is called a shallow copy.


Common Beginner Mistakes

1. Passing the Argument by Value in the Copy Constructor

Writing Student(Student other) causes an immediate compilation failure due to infinite recursion. Always write Student(const Student& other).

2. Forgetting to Pass Objects to Functions by const Reference

Passing large objects by value invokes the copy constructor every single time:

void inspect(Student s); // Inefficient: triggers full copy constructor

void inspect(const Student& s); // Optimal: zero copies, read-only view

Unless you specifically require an independent, modifiable local copy inside the function, always pass objects by const &.

3. Assuming s2 = s1; Always Calls the Assignment Operator

If s2 is being declared on that line (Student s2 = s1;), C++ calls the copy constructor, not the assignment operator.


Best Practices

  1. Pass the source object by const &: ClassName(const ClassName& other) prevents modification of the source and avoids recursion.
  2. Use member initializer lists in user-defined copy constructors: Initialize each member in declaration order.
  3. Pass non-primitive parameters by const &: Save CPU cycles and memory by avoiding unnecessary copy constructor invocations in ordinary functions.
  4. Beware of pointer members: If your class contains raw pointers to dynamic resources, you must write a custom copy constructor that performs a deep copy.

Summary

  • The copy constructor initializes a new object from an existing object of the same class.
  • Its parameter must be a reference (const ClassName&) to prevent infinite recursion.
  • Invoked during direct initialization, copy initialization, pass-by-value, and return-by-value.
  • The compiler synthesizes a default memberwise copy constructor if none is declared.
  • Copy construction initializes new objects; copy assignment updates existing objects.
  • Copying classes with raw pointer members leads to shared ownership and double-free crashes.

What’s Next in the Series?

Now that we know how the copy constructor functions and why memberwise copying fails for pointer-owning classes, we need to examine how to solve this memory hazard.

In the next guide, we explore Shallow Copy vs Deep Copy: double deletion, dangling pointers, and how to allocate fresh heap resources for copies.


Continue Learning C++ OOP


Edit page
Share this post:

Related Articles

Continue exploring related topics and foundational concepts.


Previous Post
C++ Shallow Copy vs Deep Copy Explained
Next Post
Composition vs Inheritance in C++: Object Design Trade-Offs