-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
53: Practice Exercise: Refactoring Raw Pointers to Smart Pointers
Think about the difference between borrowing a physical book from a friend and using a digital library subscription. With the physical book, the responsibility is entirely on you. You have to remember to bring it back, or it's gone from your friend's shelf forever—and if you lose it, that's a permanent loss (a "leak" in their collection). But with a digital subscription, the system handles the access. Once your subscription expires or you click "return," the system revokes your access automatically. You don't have to remember to "delete" your access to the book; the system's logic takes care of it for you.
In C++, raw pointers are those physical books. You call new to get a resource, and you must remember to call delete. If you forget, or if your code throws an exception before it hits that delete line, you've got a memory leak. Smart pointers are the digital subscription. They wrap the raw pointer in an object that knows exactly when it's no longer needed and cleans up the memory automatically.
The Fragility of Manual Cleanup
I've spent way too many late nights hunting down memory leaks in legacy codebases that looked like this. Let's look at a simple SessionManager that handles user connections. In the old way, we'd do something like this:
class UserSession { public: UserSession(int id) : sessionId(id) {} ~UserSession() { /* cleanup resources */ } void process() { /* logic here */ } private: int sessionId; }; class SessionManager { UserSession* currentSession; public: void startSession(int id) { if (currentSession) delete currentSession; // Manual cleanup currentSession = new UserSession(id); } ~SessionManager() { delete currentSession; // If you forget this, you leak! } };This looks fine on the surface, but it's brittle. If
startSessionthrows an exception between thedeleteand thenew, or if someone adds areturnstatement halfway through the function, you're in trouble. You're manually managing a lifecycle, and humans are notoriously bad at that.Switching to Exclusive Ownership
Most of the time, a resource has one clear owner. In the example above, the
SessionManagerowns theUserSession. This is the perfect use case forstd::unique_ptr. It ensures that there is exactly one pointer to the memory, and when that pointer goes out of scope, the memory is freed immediately.Here is how I would refactor that same code:
#include <memory> class SessionManager { std::unique_ptr<UserSession> currentSession; public: void startSession(int id) { // No manual delete needed! // assign replaces the old object and deletes it automatically. currentSession = std::make_unique<UserSession>(id); } // No destructor needed! unique_ptr cleans itself up. };Notice how much noise disappeared. I don't have to write a destructor. I don't have to remember to check if
currentSessionexists before replacing it.std::make_unique(introduced in C++14) is the gold standard here because it's more efficient and safer than callingnewinside the constructor.Handling Shared Resources
Now, what if multiple parts of your program need to keep a resource alive, but you don't know who will be the last one to finish using it? Imagine a
Texturein a game engine used by twenty differentSpriteobjects. You can't useunique_ptrbecause there isn't one single owner.This is where
std::shared_ptrcomes in. It uses "reference counting." Every time you copy the pointer, a counter goes up. Every time a pointer is destroyed, the counter goes down. When the counter hits zero, the memory is finally deleted.class Texture { /* ... */ }; class Sprite { std::shared_ptr<Texture> texture; public: Sprite(std::shared_ptr<Texture> tex) : texture(tex) {} }; // Usage auto sharedTex = std::make_shared<Texture>(); Sprite s1(sharedTex); Sprite s2(sharedTex); // The texture stays alive as long as s1, s2, or sharedTex exist.A quick word of caution: don't use
shared_ptrjust because you're lazy. Reference counting has a small performance overhead. Always start withunique_ptr. Only "upgrade" toshared_ptrwhen you actually have multiple owners. If you find yourself with twoshared_ptrs pointing at each other, you've created a circular reference, and neither will ever be deleted. That's when you'll needstd::weak_ptr, but we'll save that for another time.
📋 Practical Task
Refactoring the Legacy Particle System
You've been handed a legacy ParticleEmitter class. It currently uses a raw array of pointers to Particle objects. The current implementation is leaking memory because the destructor is incorrectly implemented, and it doesn't handle mid-loop errors.
Your Task: Refactor the following code to eliminate all raw new and delete calls. Use std::unique_ptr and std::vector to ensure the memory is managed automatically.
#include <iostream> #include <vector> class Particle { public: Particle(float x, float y) : x(x), y(y) { std::cout < "Particle created\n"; } ~Particle() { std::cout < "Particle destroyed\n"; } private: float x, y; }; class ParticleEmitter { Particle** particles; // Raw array of raw pointers int count; public: ParticleEmitter(int n) : count(n) { particles = new Particle*[n]; for (int i = 0; i < n; ++i) { particles[i] = new Particle(0.0f, 0.0f); } } ~ParticleEmitter() { // BUG: The original developer forgot to loop through // and delete individual particles before deleting the array! delete[] particles; } }; int main() { { ParticleEmitter emitter(5); } // Emitter goes out of scope here return 0; }Requirements:
- Replace
Particle** particleswith astd::vector<std::unique_ptr<Particle>>. - Replace the
new` calls in the constructor withstd::make_unique. - Remove the manual
delete[]from the destructor (or remove the destructor entirely if it's no longer needed). - Verify that "Particle destroyed" is printed 5 times when the program ends.
There are no comments for now.