A copy constructor is a special constructor that builds a new object as a copy of an existing object of the same type. C++ calls it implicitly in everyday situations: passing an object by value to a function, returning an object by value, and writing things like Cart b = a;. This chapter covers what the copy constructor looks like, when it runs, what the compiler-generated version actually does, and the cases where you have to write your own because the default does the wrong thing.
A copy constructor is a constructor whose only parameter is a reference to another instance of the same class. The canonical signature is:
The const and the & are both important. The reference avoids copying the source object (a copy constructor that copied its own parameter by value would call itself forever), and the const says the source isn't modified during the copy.
Here it is on a simple e-commerce class:
The first line of output comes from building mouse with the regular constructor. The second line comes from Product backup = mouse;, which invokes the copy constructor with mouse as the other argument. Inside the copy constructor, other.name reads the source's name and the initializer list copies it into the new object's name field.
The = in Product backup = mouse; does not call operator=. That syntax is copy initialization, and it runs the copy constructor, not the assignment operator. The distinction matters once you start writing your own assignment operators in a later lesson.
The compiler calls the copy constructor in three core situations:
Product b = a; or Product b(a); build b from an existing Product.Product p, the caller's argument is copied into p using the copy constructor.Product by value, the returned object is constructed in the caller's storage. In modern compilers this is often elided, but logically a copy is happening.This example exercises all three:
Without -fno-elide-constructors, modern compilers skip some of those copies via copy elision, an optimization that constructs the object directly in its final destination. The number of "copy ctor" lines printed can drop. Any time a Product flows in or out of a function by value, or Product b = a; is written, the copy constructor is in play conceptually.
Pass-by-value triggers the copy constructor every call. For a class with std::string or std::vector members, each copy allocates fresh heap memory. Pass non-trivial types by const ClassName& to avoid the copy for read-only access.
The reverse is also useful to know: when you write Product b; b = a;, that's two separate operations. The declaration Product b; runs the default constructor, and b = a; runs the copy assignment operator (a different special member that this course covers separately). Copy assignment is not the copy constructor.
If you don't write a copy constructor, the compiler generates one for you. The default version performs a member-wise copy: it goes through every data member in declaration order and copies it using that member's own copy behavior.
The generated copy constructor copied orderId as a plain int copy, customerEmail by calling std::string's own copy constructor (which allocates a new buffer and copies the characters), and total as a plain double copy. After the copy, the two Order objects are independent: changing copy.customerEmail doesn't touch original.customerEmail, because each one owns its own string buffer.
For classes whose members are all primitives, standard library containers, or other well-behaved types, the default copy constructor is exactly what you want. You don't need to write one.
The diagram shows what member-wise copy does: every field on the right is built from the corresponding field on the left, using whatever copy behavior that field's type defines. For std::string, that includes allocating a separate buffer, which is why the two strings can be modified independently afterward.
The default copy constructor works because each member knows how to copy itself correctly. std::string's copy constructor allocates a new buffer; std::vector's copy constructor allocates a new array. But the default copy constructor copies a raw pointer member by copying the pointer value, not the data it points to. Both objects end up pointing to the same memory.
That's a shallow copy: a bit-level copy of the pointer. The fix is a deep copy: allocate new memory and copy the contents.
Consider a Cart class that holds a dynamically-allocated array of product names:
Compiling this is fine, but running it is undefined behavior. With the default copy constructor, the line Cart b = a; copies a.items (a pointer) into b.items directly, so both carts point at the same heap array. When b.setItem(0, "HDMI Cable") runs, it also overwrites a's first item. Worse, when the program ends, both destructors run delete[] items, deleting the same memory twice. That's a classic double-free, and the program will typically crash or corrupt the heap.
Here is what the memory looks like right after the shallow copy:
Two carts, one heap array. Two destructors will eventually try to free that one array, and a write through one cart shows up in the other. Neither outcome is what anyone wants.
A shallow copy is cheaper than a deep copy (no allocation, no element-wise copy), but for any class that owns dynamically allocated memory through a raw pointer, the shallow copy is wrong, not just slow. The cost comparison is irrelevant when one option is broken.
The fix is to write a copy constructor that allocates new storage and copies each element. The signature stays Cart(const Cart& other), the body changes:
The initializer list copies customer and capacity directly (those are safe), but items is initialized by allocating a brand-new array sized to match the source. The loop body then copies each element from other.items into the new array. After the copy, a.items and b.items point at different arrays, so writing through b.items no longer affects a.items, and each destructor frees its own array safely.
The same memory now looks like this:
Two carts, two arrays, fully independent.
One subtle point: the member initializer list (: customer(other.customer), capacity(other.capacity), items(...)) initializes each field directly, in declaration order. That order matters because items(new std::string[other.capacity]) uses other.capacity, not the new object's capacity. Either works in this case because they're equal, but writing items(new std::string[capacity]) instead would rely on the order of initializer-list evaluation, which is a common source of bugs. The dedicated lesson on member initializer lists covers this in detail.
Deep copy is O(n) where n is the size of the owned array, and it allocates from the heap. For large carts (hundreds of items with large strings), this cost is significant. If a class is being copied frequently, pass by const reference instead, or switch to a class that supports cheap moves.
= deleteSometimes copying an object doesn't make sense. A class that represents a unique resource (an open file, a database connection, a hardware handle) often shouldn't be copyable, because two copies of a "connection" would both try to manage the same underlying thing.
C++11 added = delete for exactly this case. Mark the copy constructor as deleted, and the compiler will reject any attempt to copy:
If you uncomment either of the disabled lines, g++ refuses to compile and prints something like:
The error happens at compile time. Before = delete, the workaround was to declare the copy constructor private and not implement it, which produced a linker error or a confusing private-access error. = delete is clearer, works at compile time, and signals that the type is intentionally non-copyable.
Standard library types like std::unique_ptr, std::thread, std::mutex, and std::ifstream all delete their copy constructors for the same reason: it doesn't make sense to copy a unique resource handle. Attempting to copy a std::unique_ptr produces a deleted-function error that points back to the standard library header.
The Cart example exposed a pattern. When a destructor frees a resource (delete[] items), the default copy constructor is almost certainly wrong, because it shares the pointer instead of duplicating the resource. A custom destructor, a custom copy constructor, and a custom copy assignment operator tend to come as a set. This is called the Rule of Three: if any one of those three is written, all three are typically needed.
The reasoning, briefly:
| Special member | Why you write a custom one |
|---|---|
| Destructor | Class owns a resource the compiler doesn't know how to free |
| Copy constructor | Default would shallow-copy that resource, sharing ownership |
| Copy assignment | Default would shallow-copy that resource, leaking the old one |
The takeaway: don't write only one of these three for a resource-owning class. Either rely on the defaults (when the class composes well-behaved members like std::string and std::vector) or write all three.
Modern C++ also offers move semantics, which transfers ownership cheaply instead of copying. The Rule of Three then grows into the Rule of Five. For this lesson, the copy constructor by itself is enough; the rest of the family will fall into place later.
10 quizzes