DeveloperBreeze

Deep Copy in C++: How to Avoid Shallow Copy Pitfalls

Overview

In C++, copying objects can lead to serious bugs if you're dealing with raw pointers. By default, C++ uses shallow copy, which means only the pointer's value is copied—not the data it points to.

This tutorial covers:

  • What shallow vs deep copy means
  • The problems caused by shallow copy
  • How to implement deep copy correctly
  • A practical class example with dynamic memory
  • When to use Rule of Three vs Rule of Five

What Is Shallow Copy?

A shallow copy copies the values of member variables as-is. If your class has a pointer member, both the original and copy point to the same memory.

class Shallow {
public:
    int* data;

    Shallow(int val) {
        data = new int(val);
    }

    ~Shallow() {
        delete data;
    }
};

Now consider:

Shallow a(10);
Shallow b = a;  // default copy constructor

This causes both a.data and b.data to point to the same memory. When both destructors run, delete is called twice on the same pointer — undefined behavior!


What Is Deep Copy?

A deep copy duplicates the actual data pointed to, not just the pointer.

class Deep {
public:
    int* data;

    Deep(int val) {
        data = new int(val);
    }

    // Copy constructor for deep copy
    Deep(const Deep& other) {
        data = new int(*other.data);
    }

    // Assignment operator for deep copy
    Deep& operator=(const Deep& other) {
        if (this != &other) {
            delete data;
            data = new int(*other.data);
        }
        return *this;
    }

    ~Deep() {
        delete data;
    }
};

Rule of Three

If your class handles dynamic memory:

  • Copy Constructor
  • Copy Assignment Operator
  • Destructor

You must implement all three. This is called the Rule of Three.


Example: Deep Copy for a String Wrapper

class String {
private:
    char* buffer;

public:
    String(const char* str) {
        buffer = new char[strlen(str) + 1];
        strcpy(buffer, str);
    }

    // Copy constructor
    String(const String& other) {
        buffer = new char[strlen(other.buffer) + 1];
        strcpy(buffer, other.buffer);
    }

    // Assignment operator
    String& operator=(const String& other) {
        if (this != &other) {
            delete[] buffer;
            buffer = new char[strlen(other.buffer) + 1];
            strcpy(buffer, other.buffer);
        }
        return *this;
    }

    ~String() {
        delete[] buffer;
    }

    void print() const {
        std::cout << buffer << std::endl;
    }
};

Usage

String a("Hello");
String b = a;       // deep copy
String c("World");
c = a;              // deep assignment

All objects manage their own memory independently.


Modern C++: Rule of Five

In C++11 and newer, also consider:

  • Move Constructor
  • Move Assignment Operator

This is the Rule of Five. Add move semantics if your class is performance-sensitive and uses resource ownership.


Conclusion

When your class uses raw pointers:

  • Avoid shallow copies.
  • Always implement deep copy logic.
  • Follow the Rule of Three (or Rule of Five).
  • Prefer std::string, std::vector, or smart pointers in modern C++.

Understanding deep copy is essential for writing robust, bug-free C++ code.

Related Posts

More content you might like

Tutorial

Avoiding Memory Leaks in C++ Without Smart Pointers

Even without smart pointers, you can manage memory safely in C++ using the RAII pattern. This approach:

  • Prevents memory leaks.
  • Simplifies exception handling.
  • Keeps your code clean and maintainable.

Apr 11, 2025
Read More
Tutorial

Implementing a Domain-Specific Language (DSL) with LLVM and C++

#ifndef DSL_AST_H
#define DSL_AST_H

#include <memory>
#include <llvm/IR/Value.h>
#include <llvm/IR/IRBuilder.h>

// Base class for all expression nodes.
class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual llvm::Value* codegen(llvm::IRBuilder<>& builder) = 0;
};

// Expression for numeric literals.
class NumberExprAST : public ASTNode {
public:
    NumberExprAST(double value) : value(value) {}
    llvm::Value* codegen(llvm::IRBuilder<>& builder) override;

private:
    double value;
};

// Expression for a binary operator.
class BinaryExprAST : public ASTNode {
public:
    BinaryExprAST(llvm::TokenType op, std::unique_ptr<ASTNode> lhs,
                  std::unique_ptr<ASTNode> rhs)
        : op(op), lhs(std::move(lhs)), rhs(std::move(rhs)) {}
    llvm::Value* codegen(llvm::IRBuilder<>& builder) override;

private:
    TokenType op;
    std::unique_ptr<ASTNode> lhs, rhs;
};

#endif // DSL_AST_H

Implementation: AST.cpp

Feb 12, 2025
Read More
Article
javascript

20 Useful Node.js tips to improve your Node.js development skills:

No preview available for this content.

Oct 24, 2024
Read More
Tutorial
javascript

Advanced JavaScript Tutorial for Experienced Developers

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name} makes a noise.`);
    }
}

const animal = new Animal('Dog');
animal.speak(); // Output: Dog makes a noise.

In this example, the Animal class has a constructor method to initialize the object and a speak method that can be called on any instance of the class.

Sep 02, 2024
Read More

Discussion 0

Please sign in to join the discussion.

No comments yet. Be the first to share your thoughts!