Creating content is not easy, if convenient, please click to follow, thank you.
Click on “C++ Players, please get ready” below, select “Follow/Pin/Star the public account” for valuable content and benefits, delivered to you first! Recently, some friends said they did not receive the articles pushed on the same day, which is due to WeChat changing the push mechanism, causing friends who did not star the public account to miss the articles pushed on that day, and unable to receive some practical knowledge and information. Therefore, it is recommended that everyone star ⭐️, so you can receive the push immediately in the future.

1. Considerations on “Whether to Use”
In the world of C++, memory management is an unavoidable topic. Every C++ developer has been troubled by the paired use of <span>new</span> and <span>delete</span>, and has spent hours debugging strange bugs caused by memory leaks or dangling pointers. These issues are the “two sides of the same coin” of C++’s powerful performance and low-level control capabilities, and are challenges that developers must face.
To tame this performance beast, modern C++ introduces “smart pointers” as a powerful tool. They are a prime implementation of the RAII (Resource Acquisition Is Initialization) design philosophy, binding the lifecycle of resources to the lifecycle of objects, thus completely solving many pain points of manual memory management. In no time, “using smart pointers instead of raw pointers” has become an essential skill for modern C++ programmers.
2. Smart Pointers: Clear Expression of Ownership
In modern C++, when we discuss pointers, the first thing we should discuss is “ownership”. Ownership determines who is responsible for releasing memory. The core value of smart pointers is to explicitly reflect the concept of ownership in the type system, allowing the compiler and the code itself to ensure the correct release of resources.
<span>std::unique_ptr</span>: Exclusive Ownership
<span>std::unique_ptr</span> is the purest embodiment of the ownership concept. It represents “exclusive” ownership of the managed object—at any time, only one <span>unique_ptr</span> can point to a given object. When this <span>unique_ptr</span> is destroyed (for example, when it goes out of scope), the object it points to is automatically deleted.
The most wonderful thing is that <span>std::unique_ptr</span> is a “zero-cost abstraction”. In most cases, it does not incur any additional performance overhead, and the compiled size and performance are exactly the same as raw pointers.
#include <iostream>
#include <memory>
class Widget {
public:
Widget() { std::cout << "Widget created\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
void do_something() { std::cout << "Doing something\n"; }
};
// Factory function returns an object with exclusive ownership
std::unique_ptr<Widget> create_widget() {
return std::make_unique<Widget>();
}
int main() {
// Create using std::make_unique, safe and efficient
std::unique_ptr<Widget> u_ptr1 = create_widget();
u_ptr1->do_something();
// Ownership transferred from u_ptr1 to u_ptr2
// u_ptr1 becomes a null pointer
std::unique_ptr<Widget> u_ptr2 = std::move(u_ptr1);
if (u_ptr2) {
std::cout << "u_ptr2 has ownership\n";
}
if (!u_ptr1) {
std::cout << "u_ptr1 has lost ownership\n";
}
// At the end of main, u_ptr2 goes out of scope, and the managed Widget object is automatically destroyed
return 0;
}
- Prefer using
<span>std::make_unique</span>to create<span>unique_ptr</span>, as it guarantees no memory leak even if the constructor throws an exception. - When a function needs to return an object allocated on the heap,
<span>std::unique_ptr</span>is the best choice, as it clearly expresses the transfer of ownership. <span>std::unique_ptr</span>is an ideal tool for implementing the Pimpl idiom (Pointer to Implementation).
<span>std::shared_ptr</span>: Shared Ownership
When a resource needs to be managed by multiple owners, <span>std::shared_ptr</span> comes into play. It tracks how many <span>shared_ptr</span> instances point to the same object through a reference counting mechanism. The managed object is only deleted when the last <span>shared_ptr</span> is destroyed.
This flexibility does come at a cost.<span>std::shared_ptr</span> has some performance overhead compared to <span>std::unique_ptr</span>:
- Memory overhead: It requires an additional “control block” to store the reference count and other information.
- Performance overhead: The increment and decrement of the reference count must be atomic operations to ensure thread safety in a multi-threaded environment, which incurs some performance loss.
#include <iostream>
#include <memory>
#include <vector>
class Gadget {
public:
Gadget() { std::cout << "Gadget created\n"; }
~Gadget() { std::cout << "Gadget destroyed\n"; }
};
int main() {
// Use std::make_shared for better performance (single memory allocation)
std::shared_ptr<Gadget> s_ptr1 = std::make_shared<Gadget>();
std::cout << "Use count: " << s_ptr1.use_count() << std::endl; // Output 1
{
std::shared_ptr<Gadget> s_ptr2 = s_ptr1;
std::cout << "Use count: " << s_ptr1.use_count() << std::endl; // Output 2
} // s_ptr2 goes out of scope, reference count decreases by 1
std::cout << "Use count: " << s_ptr1.use_count() << std::endl; // Output 1
// At the end of main, s_ptr1 goes out of scope, reference count becomes 0, Gadget object is destroyed
return 0;
}
- Always use
<span>std::make_shared</span>, as it allocates memory for the object and control block in one go, reducing the number of memory allocations and improving cache efficiency. - Only use
<span>std::shared_ptr</span>when shared ownership is truly needed. If<span>std::unique_ptr</span>can meet the requirements, prefer the former.
<span>std::weak_ptr</span>: The Key to Breaking Circular References
<span>std::shared_ptr</span>‘s reference counting mechanism, while powerful, has a fatal flaw: circular references. If two objects each have a <span>shared_ptr</span> pointing to the other, their reference counts will never reach zero, leading to memory leaks.
<span>std::weak_ptr</span> was created to solve this problem. It is a non-owning smart pointer that can “observe” an object managed by <span>shared_ptr</span> without increasing its reference count.
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next;
// Use weak_ptr to break circular references
std::weak_ptr<Node> prev;
Node() { std::cout << "Node created\n"; }
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto head = std::make_shared<Node>();
auto tail = std::make_shared<Node>();
head->next = tail;
tail->prev = head; // weak_ptr does not increase head's reference count
// If tail->prev were a shared_ptr, both head and tail would have a reference count of 2
// After going out of scope, both reference counts would become 1, and neither could be destroyed
// Using weak_ptr allows both Nodes to be correctly destroyed when the program exits
return 0;
}
<span>std::weak_ptr</span> cannot directly access the object; it must be promoted to a <span>std::shared_ptr</span> by calling the <span>lock()</span> method. If the observed object has already been destroyed, <span>lock()</span> will return a null <span>shared_ptr</span>. This makes <span>weak_ptr</span> a safe mechanism to check whether an object still exists.
3. The Rational Existence of Raw Pointers: As “Observers”
Since smart pointers are so powerful, why do we still need raw pointers? Because in modern C++, the role of raw pointers has fundamentally changed: they no longer represent ownership, but rather a non-owning “observer”. They are only used to access or “observe” an object without participating in the management of that object’s lifecycle.
Core Scenario: Non-owning Access
This is the most core and rational use of raw pointers in modern C++. When the lifecycle of an object is managed by an external smart pointer or other mechanisms, and a function or class only needs to temporarily access that object for a limited time, using raw pointers is the clearest and most efficient way.
#include <iostream>
#include <memory>
class Resource {
public:
void use() { std::cout << "Using resource...\n"; }
};
// This function only observes (uses) Resource, does not own it
void process_resource(Resource* res) {
if (res) {
res->use();
}
}
int main() {
auto owner = std::make_unique<Resource>();
// Safely get a raw pointer from the smart pointer
process_resource(owner.get());
// owner still manages the Resource's lifecycle
// process_resource function must not delete res
return 0;
}
Key Principle: When you obtain a raw pointer from a smart pointer (via the <span>.get()</span> method) and pass it, you are making a commitment: the lifecycle of this raw pointer will not exceed that of the smart pointer that owns it. The code receiving the raw pointer must not attempt to <span>delete</span> it.
Interoperability with C-style APIs and Legacy Code
In the real world, we often need to interact with C language libraries or old C++ codebases that do not use smart pointers. The interfaces of these APIs almost always use raw pointers. In such cases, passing raw pointers is unavoidable and completely reasonable.
// Assume this is a function from a C language library
extern "C" void c_style_api(int* data, int size);
void modern_cpp_function() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// .data() returns a raw pointer to the underlying array
c_style_api(vec.data(), vec.size());
}
Performance Limit Scenarios
<span>shared_ptr</span>‘s atomic reference counting operations can become measurable bottlenecks in high-concurrency or extremely performance-sensitive code hotspots. After profiling confirms this, using raw pointers as a performance optimization may be necessary.
But it must be emphasized: This absolutely falls into the category of “premature optimization”. Without solid performance data to support it, one should not abandon the use of smart pointers for this reason. For 99% of application scenarios, the safety of smart pointers is far more important than this negligible performance difference.
Optional Non-owning Parameters
When a function parameter is optional, using raw pointers and signaling “no object” with <span>nullptr</span> is a very simple and widely understood pattern.
#include <iostream>
class Logger {
public:
void log(const std::string& message) { std::cout << message << std::endl; }
};
// logger is an optional dependency
void execute_task(Logger* logger = nullptr) {
// ... perform core task ...
if (logger) {
logger->log("Task completed.");
}
}
int main() {
Logger my_logger;
execute_task(&my_logger); // provide logger
execute_task(nullptr); // do not provide logger
return 0;
}
4. Conclusion
- Smart pointers are owners, they loudly declare in the type system: “I am responsible for the life and death of this object!”
- Raw pointers are observers, they quietly indicate: “I am just taking a look temporarily, without interfering with its lifecycle.”
The final advice is:Let your code “speak”. The next time you write <span>*</span> or <span>ptr</span>, take a moment to think: what kind of relationship do you want to express here? Is it exclusive ownership, shared ownership, or merely a non-owning observation? Choose the type that best expresses your intent, and your code will become safer, clearer, and easier to maintain. This is the correct path of modern C++.
This article is based on a thorough review of relevant authoritative literature and materials, forming a professional and reliable content. All data in the full text is traceable. Special note: Data and materials have been authorized. The content of this article does not involve any biased views, describing the facts objectively with a neutral attitude.