DeveloperBreeze

Avoiding Memory Leaks in C++ Without Smart Pointers

Overview

C++ developers often face memory management headaches, especially when working on legacy systems that don’t use C++11 or newer. Smart pointers like std::unique_ptr and std::shared_ptr are powerful, but what if you’re stuck with raw pointers?

In this tutorial, you'll learn:

  • How memory leaks happen.
  • How to structure your code to avoid them.
  • A design pattern to manage dynamic memory safely (RAII without smart pointers).
  • A reusable ScopedPointer class to emulate unique_ptr in old C++.

The Problem: Memory Leaks from Raw Pointers

Consider this code:

void loadData() {
    char* buffer = new char[1024];
    // some processing...
    if (someCondition()) {
        return; // leak!
    }
    delete[] buffer;
}

What’s wrong? If someCondition() returns true, buffer is never deallocated.


Solution 1: Manual try/catch + delete (not scalable)

void loadData() {
    char* buffer = new char[1024];
    try {
        if (someCondition()) {
            throw std::runtime_error("Something went wrong");
        }
        // more code...
    } catch (...) {
        delete[] buffer;
        throw;
    }
    delete[] buffer;
}

Not elegant. Easy to forget or misplace deletes. Let's go better.


Solution 2: Use RAII Even Without Smart Pointers

RAII (Resource Acquisition Is Initialization) is a pattern where resource allocation is tied to object lifetime. When an object goes out of scope, its destructor cleans up.

Let’s build a small ScopedPointer class.


ScopedPointer.h

template <typename T>
class ScopedPointer {
private:
    T* ptr;

public:
    explicit ScopedPointer(T* p = nullptr) : ptr(p) {}

    ~ScopedPointer() {
        delete ptr;
    }

    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
    T* get() const { return ptr; }

    void reset(T* p = nullptr) {
        if (ptr != p) {
            delete ptr;
            ptr = p;
        }
    }

    // Prevent copy
    ScopedPointer(const ScopedPointer&) = delete;
    ScopedPointer& operator=(const ScopedPointer&) = delete;
};

For arrays:

template <typename T>
class ScopedArray {
private:
    T* ptr;

public:
    explicit ScopedArray(T* p = nullptr) : ptr(p) {}

    ~ScopedArray() {
        delete[] ptr;
    }

    T& operator[](int index) const { return ptr[index]; }
    T* get() const { return ptr; }

    void reset(T* p = nullptr) {
        if (ptr != p) {
            delete[] ptr;
            ptr = p;
        }
    }

    // Prevent copy
    ScopedArray(const ScopedArray&) = delete;
    ScopedArray& operator=(const ScopedArray&) = delete;
};

Usage

#include "ScopedPointer.h"

void loadData() {
    ScopedArray<char> buffer(new char[1024]);

    if (someCondition()) {
        return; // no memory leak!
    }

    // buffer is auto-deleted when going out of scope
}

Bonus: Integrating with Legacy C APIs

Some legacy APIs require raw pointers. You can still use get():

void legacyFunction(char* data);

void useLegacyAPI() {
    ScopedArray<char> buffer(new char[512]);
    legacyFunction(buffer.get());
}

Conclusion

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.

In newer projects, always prefer std::unique_ptr and std::shared_ptr. But in legacy systems, RAII with simple wrappers like ScopedPointer can save you.

Related Posts

More content you might like

Tutorial

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

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:

Apr 11, 2025
Read More
Tutorial

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

Implementation: Lexer.cpp

#include "DSL/Lexer.h"
#include <cctype>
#include <cstdlib>

Lexer::Lexer(const std::string& input) : input(input) {}

char Lexer::currentChar() {
    if (pos < input.size()) {
        return input[pos];
    }
    return '\0';
}

void Lexer::advance() {
    pos++;
}

void Lexer::skipWhitespace() {
    while (std::isspace(currentChar())) {
        advance();
    }
}

Token Lexer::number() {
    size_t start = pos;
    while (std::isdigit(currentChar()) || currentChar() == '.') {
        advance();
    }
    std::string numStr = input.substr(start, pos - start);
    double value = std::strtod(numStr.c_str(), nullptr);
    return { TokenType::Number, numStr, value };
}

Token Lexer::getNextToken() {
    skipWhitespace();

    char current = currentChar();

    if (current == '\0') {
        return { TokenType::EndOfFile, "", 0 };
    }
    if (std::isdigit(current) || current == '.') {
        return number();
    }

    Token token;
    token.text = std::string(1, current);
    token.value = 0;
    switch (current) {
        case '+': token.type = TokenType::Plus; break;
        case '-': token.type = TokenType::Minus; break;
        case '*': token.type = TokenType::Asterisk; break;
        case '/': token.type = TokenType::Slash; break;
        case '(': token.type = TokenType::LParen; break;
        case ')': token.type = TokenType::RParen; break;
        default: token.type = TokenType::Invalid; break;
    }
    advance();
    return token;
}

Feb 12, 2025
Read More
Tutorial
csharp

Developing a Real-Time Multiplayer Game with Unity and C#

  • Invite friends or use online services to test your game with real players.
  • Test the game in various network conditions to ensure it handles lag and disconnections gracefully.

Congratulations! You've developed a basic real-time multiplayer game using Unity and C#. This tutorial covered setting up the project, implementing player movement, synchronizing actions, managing game state, and optimizing network performance. With these foundations, you can expand your game with more complex features, such as different game modes, AI opponents, or matchmaking systems.

Aug 14, 2024
Read More
Code
csharp

Unity Inventory System using Scriptable Objects

using UnityEngine;
using UnityEngine.UI;

public class InventoryUI : MonoBehaviour
{
    public Inventory inventory;
    public GameObject inventoryPanel;
    public GameObject inventorySlotPrefab;

    void Start()
    {
        RefreshInventoryUI();
    }

    public void RefreshInventoryUI()
    {
        // Clear existing UI elements
        foreach (Transform child in inventoryPanel.transform)
        {
            Destroy(child.gameObject);
        }

        // Create new UI elements
        foreach (Item item in inventory.items)
        {
            GameObject slot = Instantiate(inventorySlotPrefab, inventoryPanel.transform);
            Image iconImage = slot.transform.GetChild(0).GetComponent<Image>();
            iconImage.sprite = item.icon;

            // Add more UI logic as needed (like item count for stackable items)
        }
    }
}
  • Data Management: Scriptable objects allow you to manage item data independently from game logic, making it easier to update and maintain.
  • Reusability: You can create item templates and reuse them across different scenes and projects.
  • Performance: Scriptable objects reduce memory overhead compared to prefab-based systems since they are shared across instances.

Aug 12, 2024
Read More

Discussion 0

Please sign in to join the discussion.

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