How To Create a Vector of Pointers in C++

by | C++, Programming, Tips

In this guide, we’ll explore how to work with vectors of pointers in C++, understand why you might want to use them, and learn about their limitations and best practices.

📚 Quick Reference
std::vector
A dynamic array container in C++ that provides efficient storage and random access to elements.
Raw Pointer
A pointer that directly manages memory without automatic cleanup, requiring manual deletion to avoid leaks.
Smart Pointer
A C++ object that manages the lifetime of a dynamically allocated resource, automatically handling memory cleanup.
std::unique_ptr
A smart pointer that ensures exclusive ownership of a resource, automatically releasing it when out of scope.
std::shared_ptr
A smart pointer that allows shared ownership of a resource, cleaning it up when the last owner is destroyed.
std::weak_ptr
A smart pointer that breaks cyclic references by observing a shared resource without affecting its lifetime.
Virtual Destructor
A destructor in a base class declared with the virtual keyword to ensure proper cleanup of derived class objects.
Object Slicing
A phenomenon where the derived class portion of an object is “sliced off” when stored in a container of base class types.
Polymorphism
The ability to use a single interface to operate on objects of different types, enabled by pointers or references to base classes.
std::make_unique
A utility function in C++14 that creates a std::unique_ptr safely and efficiently.

Why Use a Vector of Pointers?

There are several compelling reasons to use a vector of pointers instead of storing objects directly:

  • Polymorphism: When working with inheritance hierarchies, storing pointers allows you to store derived class objects in a container of base class pointers.
  • Avoiding Object Slicing: Storing pointers prevents object slicing when working with polymorphic types.
  • Memory Efficiency: When dealing with large objects, storing pointers can be more memory-efficient as only the pointer is copied when the vector grows.
  • Non-Copyable Objects: Some objects (like std::mutex) cannot be copied, but pointers to them can be.

What is Object Slicing?

Object slicing occurs when an object of a derived class is assigned to a variable or container of the base class type. As a result, the derived class-specific data and methods are “sliced off,” leaving only the base class portion. This can lead to unexpected behavior when working with polymorphism.

Here’s an example that demonstrates object slicing and explains why it happens:

Example of Object Slicing
            
#include <iostream>  // For input/output
#include <vector>    // To use std::vector
#include <string>    // For std::string

// Base class: Animal
class Animal {
public:
    // A virtual function for polymorphism
    virtual std::string getDescription() const {
        return "Animal";
    }
};

// Derived class: Dog, inherits from Animal
class Dog : public Animal {
    std::string breed; // A specific attribute for Dog
public:
    // Constructor to initialize the breed
    Dog(const std::string& b) : breed(b) {}

    // Override the base class method to provide specific behavior
    std::string getDescription() const override {
        return "Dog of breed " + breed;
    }
};

int main() {
    // A vector of Animal objects (not pointers)
    std::vector<Animal> animals;

    // Add a Dog object to the vector
    animals.push_back(Dog("Golden Retriever"));
    // Object slicing occurs here:
    // - The Dog object is "sliced" into its Animal base class portion.
    // - The 'breed' data and the overridden 'getDescription' method are lost.

    // Attempt to call getDescription() on the first element of the vector
    std::cout << animals[0].getDescription() << std::endl;
    // Output: "Animal"
    // Explanation:
    // - Since the object was sliced, only the base class version of getDescription() is accessible.
    // - The derived class-specific implementation and data are no longer available.

    return 0;
}
        

Explanation:

When the derived class Dog object is stored in the std::vector<Animal>, only the base class portion of the Dog object is stored. This happens because the vector is designed to hold objects of type Animal, and any derived class-specific data (like breed) or behavior (like the overridden getDescription()) is “sliced off.”

As a result, calling getDescription() on the sliced object accesses the base class version of the method, leading to the output "Animal" instead of "Dog of breed Golden Retriever".

How to Avoid Object Slicing:

  • Use pointers (Animal*) or smart pointers (std::unique_ptr<Animal>) instead of storing objects directly.
  • Example fix: Replace std::vector<Animal> with std::vector<Animal*> or std::vector<std::unique_ptr<Animal>>.

Basic Implementation

When working with polymorphism in C++, storing objects directly in a container like std::vector can lead to object slicing. To preserve the full behavior and data of derived classes, we use pointers (or smart pointers) in the vector. Here’s how to implement a vector of pointers with polymorphic behavior:

Basic Implementation of Vector of Pointers
            
#include <iostream>  // For input/output operations
#include <vector>    // For std::vector
#include <string>    // For std::string

// Base class: Animal
class Animal {
protected:
    std::string name; // Name of the animal
public:
    // Constructor to initialize the name
    Animal(const std::string& n) : name(n) {}

    // Pure virtual function to enforce implementation in derived classes
    virtual std::string getDescription() const = 0;

    // Virtual destructor to ensure proper cleanup of derived objects
    virtual ~Animal() = default;
};

// Derived class: Dog, inherits from Animal
class Dog : public Animal {
    std::string breed; // Additional attribute specific to Dog
public:
    // Constructor to initialize name and breed
    Dog(const std::string& n, const std::string& b)
        : Animal(n), breed(b) {}

    // Override the base class method to provide specific behavior
    std::string getDescription() const override {
        return name + " is a " + breed + " dog.";
    }
};

int main() {
    // A vector of pointers to Animal objects
    std::vector<Animal*> animals;

    // Dynamically allocate a Dog object and store the pointer in the vector
    animals.push_back(new Dog("Rex", "German Shepherd"));

    // Loop through the vector and call getDescription() on each Animal pointer
    for (const auto* animal : animals) {
        std::cout << animal->getDescription() << std::endl;
    }

    // Cleanup: Manually delete each dynamically allocated object
    for (auto* animal : animals) {
        delete animal;
    }

    // Clear the vector to remove dangling pointers
    animals.clear();

    return 0;
}
        

Key Points:

  • Dynamic Allocation: Use new to create objects on the heap and store pointers in the vector.
  • Polymorphic Behavior: The getDescription() method called on Animal* invokes the derived class implementation due to the virtual keyword.
  • Manual Cleanup: Since raw pointers are used, you must explicitly delete each object to avoid memory leaks.
  • Virtual Destructor: Ensures proper cleanup of derived class resources when deleted via a base class pointer.

Note: Using raw pointers requires careful memory management. Consider using smart pointers like std::unique_ptr or std::shared_ptr to handle cleanup automatically and prevent memory leaks.

Why Not References?

While references are often preferred over pointers for accessing objects due to their simpler syntax and safety, they cannot be stored in standard containers like std::vector. This is because references have specific limitations:

  • Must be initialized when created: A reference cannot exist without being bound to an object.
  • Cannot be reassigned: Once a reference is initialized, it cannot point to a different object.

Here’s an example that demonstrates why storing references in a vector doesn’t work:

Why Do References Not Work in Vectors
            
#include <vector> // For std::vector

// A simple class to demonstrate the concept
class MyClass {};

int main() {
    MyClass obj1, obj2;

    // Attempting to create a vector of references
    // This won't compile because references must be initialized
    std::vector<MyClass&> vec_refs; // Error: References are not allowed in containers

    // Even if we could create the vector, we couldn't add elements to it
    // vec_refs.push_back(obj1); // Error: References cannot be reassigned or stored

    return 0;
}
        

Explanation:

The C++ standard does not allow containers like std::vector to store references. This is because references are not objects themselves; they are aliases for existing objects, and containers require objects that can be copied, reassigned, and managed independently.

Alternatives to Storing References:

  • Use pointers: Replace references with raw pointers or smart pointers like std::unique_ptr or std::shared_ptr. These can be stored in containers and allow dynamic memory management.
  • Use std::reference_wrapper: The std::reference_wrapper class allows you to store references in containers like std::vector. This is a lightweight and safe alternative to raw references.
Alternative: Using std::reference_wrapper
            
#include <iostream>       // For std::cout
#include <vector>         // For std::vector
#include <functional>     // For std::reference_wrapper

class MyClass {
    int id; // Unique identifier for each object
public:
    MyClass(int i) : id(i) {} // Constructor to initialize the ID
    void display() const {
        std::cout << "MyClass object with ID: " << id << std::endl;
    }
};

int main() {
    // Create MyClass objects
    MyClass obj1(1), obj2(2);

    // Create a vector of std::reference_wrapper to store references
    std::vector<std::reference_wrapper<MyClass>> vec_refs;

    // Add references to the vector using std::ref
    vec_refs.push_back(std::ref(obj1));
    vec_refs.push_back(std::ref(obj2));

    // Print message showing objects are added
    std::cout << "Objects added to vector of references." << std::endl;

    // Access and display the objects via the vector
    std::cout << "Accessing objects via vector:" << std::endl;
    vec_refs[0].get().display(); // Retrieve and display the first reference
    vec_refs[1].get().display(); // Retrieve and display the second reference

    // Modify an object via the reference in the vector
    std::cout << "Modifying obj1 via vector..." << std::endl;
    vec_refs[0].get() = MyClass(10); // Reassign obj1 through the vector

    // Display the modified objects
    std::cout << "After modification:" << std::endl;
    vec_refs[0].get().display(); // Display the modified obj1
    vec_refs[1].get().display(); // Display obj2 (unchanged)

    return 0;
}
        
        
Accessing objects via vector:
MyClass object with ID: 1
MyClass object with ID: 2
Modifying obj1 via vector...
After modification:
MyClass object with ID: 10
MyClass object with ID: 2
        

By using std::reference_wrapper, you can safely store and manage references in containers while avoiding the limitations of raw references. However, for most use cases, smart pointers are recommended as they provide more flexibility and automatic memory management.

Using Smart Pointers

Managing memory manually with raw pointers can be error-prone, leading to memory leaks and dangling pointers. Smart pointers in C++ simplify memory management by ensuring that dynamically allocated resources are automatically cleaned up when they are no longer needed. Here’s how to use smart pointers in a vector for polymorphic behavior:

Using Smart Pointers in Vectors
            
#include <iostream>      // For input/output operations
#include <vector>        // For std::vector
#include <memory>        // For smart pointers like std::unique_ptr
#include <string>        // For std::string

// Base class: Animal
class Animal {
public:
    // Pure virtual function to enforce implementation in derived classes
    virtual std::string getDescription() const = 0;

    // Virtual destructor ensures proper cleanup for derived objects
    virtual ~Animal() = default;
};

// Derived class: Dog, inherits from Animal
class Dog : public Animal {
    std::string breed; // Additional attribute specific to Dog
public:
    Dog(const std::string& b) : breed(b) {}

    // Override the base class method to provide specific behavior
    std::string getDescription() const override {
        return "Dog of breed: " + breed;
    }
};

// Derived class: Cat, inherits from Animal
class Cat : public Animal {
    bool isIndoor; // Specific attribute for Cat
public:
    Cat(bool indoor) : isIndoor(indoor) {}

    // Override to describe the Cat
    std::string getDescription() const override {
        return isIndoor ? "Indoor Cat" : "Outdoor Cat";
    }
};

// Derived class: Bird, inherits from Animal
class Bird : public Animal {
    std::string species; // Specific attribute for Bird
public:
    Bird(const std::string& s) : species(s) {}

    // Override to describe the Bird
    std::string getDescription() const override {
        return "Bird of species: " + species;
    }
};

int main() {
    // Create a vector of unique_ptr to Animal objects
    std::vector<std::unique_ptr<Animal>> animals;

    // Add different types of animals to the vector using std::make_unique
    animals.push_back(std::make_unique<Dog>("German Shepherd"));
    animals.push_back(std::make_unique<Cat>(true)); // Indoor Cat
    animals.push_back(std::make_unique<Bird>("Parrot"));

    // Iterate through the vector and call getDescription() on each Animal
    std::cout << "Descriptions of Animals in the vector:" << std::endl;
    for (const auto& animal : animals) {
        std::cout << animal->getDescription() << std::endl;
    }

    // Smart pointers automatically clean up resources when they go out of scope
    return 0;
}
            
        

Descriptions of Animals in the vector:
Dog of breed: German Shepherd
Indoor Cat
Bird of species: Parrot

Key Benefits of Smart Pointers:

  • Automatic Memory Management: No need for explicit delete. Resources are cleaned up automatically when the smart pointer goes out of scope.
  • Exception Safety: Prevents resource leaks during exceptions by ensuring proper cleanup.
  • Clear Ownership Semantics: std::unique_ptr enforces exclusive ownership, making it clear who owns a resource.

Common Mistakes to Avoid:

  • Copying Unique Pointers: std::unique_ptr cannot be copied. Use std::move if you need to transfer ownership.
  • Using Raw Pointers: Avoid mixing raw pointers with smart pointers to prevent double deletion.
  • Shared Ownership Mismanagement: Use std::shared_ptr with caution, especially in cyclic dependencies. Break cycles using std::weak_ptr.

By replacing raw pointers with smart pointers, you make your code more robust, safer, and easier to maintain. Smart pointers provide a modern and efficient way to manage resources in C++, and are strongly recommended in most scenarios.

Best Practices

  • Use Smart Pointers: Prefer std::unique_ptr or std::shared_ptr over raw pointers to ensure automatic memory management and avoid manual cleanup.
  • Leverage std::make_unique and std::make_shared: Always use these factory functions to create smart pointers instead of raw new, as they are safer and more efficient.
  • Choose the Right Smart Pointer:
    • std::unique_ptr: Use for exclusive ownership of an object.
    • std::shared_ptr: Use when ownership needs to be shared among multiple owners.
    • std::weak_ptr: Use to break cyclic references in std::shared_ptr networks.
  • Avoid Copying Smart Pointers: Use std::move when transferring ownership of std::unique_ptr. Copying std::unique_ptr is not allowed.
  • Use References When Possible: For function parameters, prefer references over pointers to avoid unnecessary null checks and make intent clear.
  • Make Use of Polymorphism: Store pointers to base classes when working with derived class objects to leverage polymorphic behavior.
  • Consider Object Lifetime: Ensure that the lifetime of objects stored in containers matches the container’s lifetime, especially when using raw pointers or smart pointers.
  • Be Exception-Safe: Smart pointers like std::unique_ptr automatically clean up resources during exceptions, so use them to write exception-safe code.
  • Combine References and Containers Safely: If you must store references, use std::reference_wrapper in containers like std::vector to avoid reference-related limitations.
  • Use Virtual Destructors in Base Classes: When storing derived objects via base class pointers, ensure proper cleanup by marking the base class destructor as virtual.

Common Pitfalls to Avoid

Here are some of the most critical mistakes to watch out for when working with vectors of pointers in C++:

  • Memory leaks: Forgetting to delete raw pointers leads to memory leaks.
    Solution: Use std::unique_ptr or std::shared_ptr to automate cleanup.
  • Dangling pointers: Accessing deleted pointers results in undefined behavior.
    Solution: Set pointers to nullptr after deletion or use smart pointers.
  • Object slicing: Storing derived objects in a vector of base class types causes loss of derived data.
    Solution: Use pointers or references to avoid slicing.
  • Missing virtual destructors: Not declaring base class destructors as virtual leads to improper cleanup.
    Solution: Always use virtual for base class destructors in polymorphic hierarchies.

Conclusion

Working with vectors of pointers in C++ can be a powerful tool, especially when dealing with polymorphic behavior, memory management, and non-copyable objects. While they offer flexibility and efficiency, they also come with potential pitfalls, such as memory leaks, dangling pointers, and object slicing. By understanding these challenges and adopting modern best practices, such as using smart pointers like std::unique_ptr and std::shared_ptr, you can write safer, more maintainable C++ code.

We’ve explored the reasons for using pointers, discussed the limitations of alternatives like references, and demonstrated how smart pointers make memory management automatic and error-free. Additionally, we’ve highlighted common mistakes to avoid and provided guidance on further reading to deepen your understanding of C++.

If you found this guide helpful, feel free to explore the Further Reading section or leave a comment with your thoughts and questions. Happy coding!

Further Reading

Expand your knowledge of C++ memory management, smart pointers, and best practices for writing modern and efficient C++ code. Here are some recommended resources:

Attribution and Citation

If you found this guide and tools helpful, feel free to link back to this page or cite it in your work!

Profile Picture
Senior Advisor, Data Science | [email protected] |  + posts

Suf is a senior advisor in data science with deep expertise in Natural Language Processing, Complex Networks, and Anomaly Detection. Formerly a postdoctoral research fellow, he applied advanced physics techniques to tackle real-world, data-heavy industry challenges. Before that, he was a particle physicist at the ATLAS Experiment of the Large Hadron Collider. Now, he’s focused on bringing more fun and curiosity to the world of science and research online.

Buy Me a Coffee ✨