Demystifying Dynamic Arrays in C++

by | C++, DSA, Programming

Today, we’ll explore how to work with dynamic arrays in C++, focusing on the most efficient methods and best practices. Whether you’re building a game engine or developing a data processing application, understanding dynamic arrays is crucial for managing memory effectively.

Introduction

Dynamic arrays in C++ allow us to allocate memory dynamically during runtime, making them perfect for situations where we don’t know the size of our data in advance. While C++ offers multiple ways to create dynamic arrays, we’ll focus on the most efficient and modern approaches.

📚 Dynamic Arrays Quick Reference
std::vector
A dynamic array container from the STL that automatically manages memory, resizes as needed, and provides safe element access with bounds checking.
Dynamic Array
An array whose size can be modified during runtime, allocated on the heap rather than the stack, requiring manual memory management if not using containers.
Capacity
The amount of space allocated for a dynamic array. In vectors, it may be larger than the current size to optimize performance during insertions.
RAII
Resource Acquisition Is Initialization – A C++ technique where resource management is tied to object lifetime, ensuring automatic cleanup when objects go out of scope.
Smart Pointer
Objects that act like pointers but provide automatic memory management. std::unique_ptr automatically deletes dynamic arrays when going out of scope.
Memory Leak
A failure to free dynamically allocated memory when it’s no longer needed. Avoided by using vectors or smart pointers instead of raw dynamic arrays.

Using std::vector

The Standard Template Library’s std::vector is the modern C++ solution for dynamic arrays. It manages its own memory allocation and deallocation, automatically resizing when elements are added or removed. Unlike raw arrays, vector tracks its size and capacity separately, offering range checking in debug builds and providing efficient operations like push_back, insert, and emplace_back. It guarantees contiguous memory storage, making it as efficient as traditional arrays while being much safer to use.

Vector Example
#include <iostream>
#include <vector>

int main() {
    // Initialize a vector
    std::vector<int> numbers;

    // Add elements
    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_back(30);

    // Access elements
    std::cout << "First element: " << numbers[0] << std::endl;

    // Add elements at specific position
    numbers.insert(numbers.begin() + 1, 15);

    // Print all elements
    std::cout << "All elements: ";
    for(const int& num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // Get size
    std::cout << "Size: " << numbers.size() << std::endl;

    return 0;
}
First element: 10
All elements: 10 15 20 30
Size: 4

Understanding push_back vs emplace_back

When adding elements to a vector, C++ offers two primary methods: push_back and emplace_back. The key difference lies in how they handle object construction and placement in the vector's memory.

While both functions ultimately place objects in the vector, emplace_back can be more efficient because it forwards constructor arguments directly, allowing objects to be built in place. In contrast, push_back requires a fully constructed object to work with. Let's see this difference in action with a clear example:

push_back vs emplace_back in Action
#include <iostream>
#include <vector>
#include <string>

class User {
public:
    // Constructor
    User(const std::string& n, int a)
        : name(n), age(a) {
        std::cout << "Constructing User: " << name << std::endl;
    }

    // Copy constructor
    User(const User& other)
        : name(other.name), age(other.age) {
        std::cout << "Copying User: " << name << std::endl;
    }

    // Move constructor
    User(User&& other) noexcept
        : name(std::move(other.name)), age(other.age) {
        std::cout << "Moving User: " << name << std::endl;
    }

private:
    std::string name;
    int age;
};

int main() {
    std::vector<User> users;

    std::cout << "Using push_back with temporary:" << std::endl;
    users.push_back(User("Alice", 25));  // Constructs temporary, then moves it

    std::cout << "\nUsing push_back with variable:" << std::endl;
    User bob("Bob", 30);                 // Constructs User
    users.push_back(bob);                // Copies it into vector

    std::cout << "\nUsing emplace_back:" << std::endl;
    users.emplace_back("Charlie", 35);   // Constructs directly in vector

    return 0;
}
Using push_back with temporary:
Constructing User: Alice
Moving User: Alice

Using push_back with variable:
Constructing User: Bob
Copying User: Bob

Using emplace_back:
Constructing User: Charlie

This example shows three different scenarios we commonly encounter:

1. When using push_back with a temporary object, we see a construction followed by a move operation.

2. When using push_back with an existing variable, we see the original construction plus a copy operation (unless we explicitly std::move it).

3. When using emplace_back, we see just a single construction happening directly in the vector's memory.

💡 Best Practices:

Use emplace_back when:

• You have constructor arguments rather than an existing object

• You want to construct objects directly in the vector

• You're working with objects that are expensive to copy or move

Use push_back when:

• You already have an object you want to add

• The code's intent is clearer with push_back

• You're working with simple types where the performance difference is minimal

Manual Dynamic Arrays

While std::vector is the go-to choice, manual dynamic array creation remains essential knowledge. It's crucial for low-level programming where direct memory control is needed, working with C-style APIs, implementing custom data structures, or maintaining legacy codebases. Understanding manual allocation also provides insights into how higher-level containers work under the hood.

Manual Dynamic Array
#include <iostream>

int main() {
    int size = 5;
    int* arr = new int[size];

    // Initialize array
    for(int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }

    // Print elements
    std::cout << "Elements: ";
    for(int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // Don't forget to deallocate!
    delete[] arr;
    return 0;
}
Elements: 0 10 20 30 40

Using Smart Pointers (C++14 and later)

C++14 introduced std::make_unique as a safer way to manage dynamic arrays. This factory function creates a std::unique_ptr, a smart pointer that automatically deletes the array when it goes out of scope. Unlike raw pointers, unique_ptr follows the RAII principle (Resource Acquisition Is Initialization), ensuring proper cleanup even when exceptions occur. The make_unique function also provides exception safety during object construction.

Smart Pointer Dynamic Array
#include <iostream>

int main() {
    int size = 5;
    auto array = std::make_unique<int[]>(size);

    // Initialize array
    for(int i = 0; i < size; i++) {
        array[i] = i * 10;
    }

    // Print elements
    std::cout << "Elements: ";
    for(int i = 0; i < size; i++) {
        std::cout << array[i] << " ";
    }
    std::cout << std::endl;

    // No need to delete - memory is automatically managed!
    return 0;
}
Elements: 0 10 20 30 40

💡 Best Practice: Use smart pointers when you need manual array control but want automatic memory management. They help prevent memory leaks while providing low-level access.

Best Practices

  • Use vector::reserve() when you know the approximate size in advance
  • Prefer range-based for loops for iteration
  • Use emplace_back() instead of push_back() for objects
  • Always check bounds when accessing elements directly
  • Use smart pointers instead of raw pointers when manual array control is needed

Conclusion

Dynamic arrays are fundamental to modern C++ programming, offering flexibility in memory management and data handling. While std::vector remains the go-to solution for most scenarios, understanding manual array management and smart pointers equips you with the tools needed for specialized use cases. Remember to prioritize memory safety and consider your specific performance requirements when choosing between these approaches.

💡 Key Takeaway: Start with std::vector, use smart pointers when you need more control, and reserve manual array management for specific performance-critical scenarios.

Congratulations on reading to the end of this tutorial. Visit the Further Reading section below to use our online compiler and explore related articles and references.

Have fun and happy coding!

Further Reading

  • Online C++ Compiler

    Test and experiment with the dynamic array examples from this blog post using our free online C++ compiler. Practice creating vectors, working with manual arrays, and implementing smart pointers. Try modifying the code samples to understand how different array sizes and data types affect memory management.

  • std::vector Documentation

    Complete reference for std::vector functionality and implementation details.

  • C++ FAQ: vector vs array

    Detailed comparison between different array types in C++.

  • std::make_unique Documentation

    Reference for creating unique pointers using make_unique in C++14 and later.

  • std::unique_ptr Documentation

    Documentation for smart pointers in C++.

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 ✨