C++ is a powerful language with many advantages, but it also presents numerous challenges for developers. Mastering C++ requires more than just a grasp of its syntax and experience with similar languages like C# or Java. It demands years of practice and strict discipline to avoid common pitfalls. This article examines some frequent errors made by C++ developers of all skill levels, particularly when they aren’t cautious.
Common Mistake #1: Misusing “new” and ”delete” Pairs
Completely freeing all dynamically allocated memory is a significant challenge, and even when achieved, it’s often vulnerable to exceptions. Take this example:
| |
If an exception occurs, the “a” object remains undeleted. A safer and more concise approach uses auto_ptr. Although deprecated in C++11, the old standard is still common. Consider replacing it with unique_ptr in C++11 or scoped_ptr from Boost if feasible:
| |
This ensures the “a” object is deleted upon exiting the scope, regardless of exceptions.
This is a basic illustration of this C++ issue. In more complex scenarios, deletion might be needed elsewhere, like an outer function or a different thread. Therefore, it’s best to completely avoid using new/delete in pairs and opt for appropriate smart pointers instead.
Common Mistake #2: Neglecting the Virtual Destructor
This oversight is a major contributor to memory leaks within derived classes when dynamic memory allocation is involved. There are situations where a virtual destructor might not be ideal, especially when a class isn’t designed for inheritance and size and performance are paramount. This is because a virtual destructor, like any virtual function, adds extra data to the class structure, like a pointer to a virtual table, increasing the size of each class instance.
However, in many instances, classes might be inherited even if not initially planned. Therefore, it’s a good habit to include a virtual destructor during class declaration. If performance reasons dictate no virtual functions within a class, add a comment in the class declaration file stating that the class shouldn’t be inherited. Using an IDE that automatically creates virtual destructors during class creation is another effective way to prevent this issue.
Classes/templates from the standard library deserve special attention. Not intended for inheritance, they lack virtual destructors. For example, if you create a new enhanced string class publicly inheriting from std::string, there’s a risk someone might use it incorrectly with a pointer or a reference to std::string, causing a memory leak.
| |
To prevent such C++ problems, favor private inheritance or composition when reusing classes/templates from the standard library.
Common Mistake #3: Deleting an Array With “delete” or Using a Smart Pointer

Dynamically sized temporary arrays are often necessary. After they are no longer needed, freeing the allocated memory is crucial. The issue is that C++ needs a special delete operator with [] brackets, which is easy to forget. The delete[] operator not only deletes the array’s memory but also calls destructors for all objects in the array. Even for primitive types without destructors, using the delete operator without [] brackets is incorrect. There’s no guarantee across all compilers that a pointer to an array will point to its first element, so omitting the [] brackets with delete might lead to undefined behavior.
Similarly, using smart pointers like auto_ptr, unique_ptr
If reference counting isn’t needed, which is usually the case with arrays, STL vectors are the most elegant solution. They handle memory release and provide additional functionalities.
Common Mistake #4: Returning a Local Object by Reference
While primarily a beginner’s error, it’s worth noting due to its prevalence in legacy code. Consider this code snippet aiming to optimize by avoiding unnecessary copying:
| |
Here, the “sum” object ends up pointing to the local “result” object. However, after the SumComplex function finishes, the “result” object, which resided on the stack, is destructed as the stack unwinds. This ultimately leads to undefined behavior, even with primitive types. To address performance concerns, consider return value optimization:
| |
Most modern compilers optimize this code to eliminate unnecessary copying if the return line includes an object’s constructor. The constructor will be directly executed on the “sum” object.
Common Mistake #5: Referencing a Deleted Resource
This problem is more common than one might think, especially in multithreaded applications. Consider the following code:
Thread 1:
| |
Thread 2:
| |
Thread 1:
| |
If both threads use the same connection ID, the result is undefined behavior. Access violation errors are notoriously difficult to debug.
In scenarios where multiple threads access a shared resource, storing pointers or references to it is risky, as another thread might delete it. Using smart pointers with reference counting, such as shared_ptr from Boost, is a safer alternative. Its use of atomic operations for incrementing/decrementing the reference counter ensures thread safety.
Common Mistake #6: Permitting Exceptions to Propagate from Destructors
While throwing exceptions from destructors is rarely necessary and there are better ways to handle such situations, they often occur implicitly. A simple attempt to log an object’s destruction, for instance, can lead to an exception. Let’s analyze this code:
| |
If an exception is thrown twice, such as during the destruction of both objects, the catch statement is bypassed. With two simultaneous exceptions, regardless of their types, the C++ runtime environment doesn’t know how to proceed and calls the terminate function, halting the program’s execution.
Therefore, the rule of thumb is: never allow exceptions to escape destructors. Even if it seems cumbersome, protect potential exceptions:
| |
Common Mistake #7: Incorrectly Using “auto_ptr”
Despite being deprecated in C++11 for several reasons, auto_ptr is still widely used because many projects rely on C++98. It has a specific behavior that might not be obvious to all C++ developers and can cause serious problems if not handled carefully. Copying an auto_ptr object transfers ownership from one object to another. For example, this code snippet:
| |
… will result in an access violation error. Only object “b” will hold a pointer to the Class A object, while “a” will be empty. Attempting to access a class member of the “a” object will lead to an access violation. There are numerous ways to misuse auto_ptr. Here are four crucial points to keep in mind:
- Avoid using auto_ptr within STL containers. Copying containers can leave source containers with invalid data. Some STL algorithms might also invalidate “auto_ptr"s.
- Refrain from using auto_ptr as a function argument. This results in copying and invalidates the value passed to the argument after the function call.
- When using auto_ptr for class data members, ensure proper copying within the copy constructor and assignment operator. Alternatively, disallow these operations by making them private.
- Whenever feasible, opt for modern smart pointers over auto_ptr.
Common Mistake #8: Working with Invalidated Iterators and References
This topic deserves its own book. Each STL container has specific conditions that invalidate iterators and references. Understanding these nuances is crucial when performing operations. Similar to the previous problem, this one is also common in multithreaded environments. Synchronization mechanisms are necessary to avoid issues. Consider this sequential code example:
| |
Logically, the code appears correct. However, adding a second element to the vector might trigger memory reallocation, invalidating both the iterator and the reference. Accessing them in the last two lines will then result in an access violation error.
Common Mistake #9: Passing Objects by Value

It’s generally understood that passing objects by value is detrimental to performance. However, many developers do it anyway to save typing effort, promising themselves they’ll optimize it later. This optimization rarely happens, leading to less performant code and unpredictable behavior:
| |
This code will compile. When calling the “func1” function, only a partial copy of the object “b” is created, meaning only the class “A” portion of object “b” is copied to object “a” (“slicing problem”). Therefore, inside the function, a method from class “A” will be called instead of one from class “B”, which is likely not the intended behavior.
Similar problems arise when handling exceptions. For instance:
| |
If an exception of type ExceptionB is thrown from the “func2” function, the catch block will catch it. However, due to the slicing problem, only the ExceptionA class part is copied. As a result, an incorrect method will be called, and re-throwing the exception will propagate the wrong exception to the outer try-catch block.
In conclusion, always pass objects by reference, not value.
Common Mistake #10: Employing User-Defined Conversions via Constructors and Conversion Operators
While user-defined conversions are useful in some cases, they can lead to unintended and difficult-to-locate conversions. Imagine a library with a string class:
| |
The first method aims to create a string of length n, while the second creates a string containing the provided characters. However, problems arise when you encounter code like this:
| |
In this example, s1 will become a string of size 123, not a string containing “123”. The second example, with single quotes instead of double quotes (which can happen accidentally), also calls the first constructor, creating a very large string. These are just simple examples; more complex situations can lead to confusion and unpredictable conversions that are hard to debug. To mitigate these issues, follow these two rules:
- Declare constructors with the explicit keyword to prevent implicit conversions.
- Instead of conversion operators, use explicit conversion methods. While requiring a bit more typing, this approach improves code clarity and helps prevent unexpected results.
Conclusion
C++ is a powerful language, forming the foundation of many beloved applications. It provides developers with exceptional flexibility through sophisticated features found in object-oriented programming languages. However, these features and flexibilities can become sources of confusion and frustration if not used carefully. This article aimed to shed light on how these common mistakes can impact your C++ development experience.