Wiki
Smart pointer
Existing English translations were reused. 2 current source blocks remain in Simplified Chinese, without triggering paid translation.
1.What problem does this section solve?
new/deleteYou can manually manage dynamic memory, but it requires you to remember to release resources on all paths. Once a function returns early,return throws an exception, or multiple objects hold pointers to each other, memory leaks, double frees, and dangling pointers easily occur.
Smart pointers encode "who is responsible for freeing the object" into the type:
| Smart pointers | Ownership semantics | Typical scenarios |
|---|---|---|
std::unique_ptr<T> |
Exclusive ownership | Default choice; an object has only one owner |
std::shared_ptr<T> |
Shared ownership | The object genuinely needs multiple owners to jointly extend its lifetime |
std::weak_ptr<T> |
Weak reference, non-owning | Observe the object managed by shared_ptr, commonly used to break cyclic references |
Smart pointers are essentially RAII: when a smart pointer object goes out of scope, its destructor automatically releases the managed object.
2.C++ Standard Edition
std::unique_ptr:C++11std::shared_ptr/std::weak_ptr:C++11std::make_unique:C++14std::make_shared:C++11
The required header file is <memory>.
3.Learning Sequence
- First look at the problem with manual
new/deleteon early return. - Use
unique_ptrto solve single ownership and automatic release. - Use
std::moveto transferunique_ptrownership. - Only use
shared_ptrwhen ownership is genuinely shared. - Use
weak_ptrto observe objects and break reference cycles. - Function parameters should express true semantics: borrowing, transfer, and sharing are three different things.
4.Example code
4.1.Example 1: Problems with the old approach and RAII in unique_ptr
This example deliberately causes the function to return early. In the old style, delete was not executed; in the unique_ptr version, destruction occurs automatically even if the function returns early.
#include <iostream>
#include <memory>
#include <string>
class Resource
{
std::string name_;
public:
explicit Resource(const std::string& name) : name_(name)
{
std::cout << name_ << " created\n";
}
~Resource()
{
std::cout << name_ << " destroyed\n";
}
void use() const
{
std::cout << name_ << " used\n";
}
};
bool old_style()
{
Resource* res = new Resource("old resource");
res->use();
std::cout << "old_style: early return\n";
return false; // res 没有 delete,析构函数不会执行
}
bool modern_style()
{
auto res = std::make_unique<Resource>("modern resource");
res->use();
std::cout << "modern_style: early return\n";
return false; // res 离开作用域,自动 delete
}
int main()
{
std::cout << std::boolalpha;
bool old_ok = old_style();
std::cout << "old_ok = " << old_ok << "\n";
std::cout << "---\n";
bool modern_ok = modern_style();
std::cout << "modern_ok = " << modern_ok << "\n";
return 0;
}
Running result:
old resource created
old resource used
old_style: early return
old_ok = false
---
modern resource created
modern resource used
modern_style: early return
modern resource destroyed
modern_ok = false
Note that the first output does not contain old resource destroyed, which is the most common error when manually adding new/delete in complex paths.
4.2.Example 2: Exclusive Ownership and Transfer of unique_ptr
unique_ptrIt cannot be copied, only moved. After moving, the original pointer becomes a null pointer.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Student
{
std::string name_;
public:
explicit Student(const std::string& name) : name_(name)
{
std::cout << "Student " << name_ << " created\n";
}
~Student()
{
std::cout << "Student " << name_ << " destroyed\n";
}
const std::string& name() const
{
return name_;
}
};
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_ownership(std::unique_ptr<Student> student)
{
std::cout << "take ownership of " << student->name() << "\n";
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
auto p = std::make_unique<Student>("Alice");
auto owner = std::move(p);
std::cout << "p is null? " << (p == nullptr ? "yes" : "no") << "\n";
std::cout << "owner has " << owner->name() << "\n";
take_ownership(std::move(owner));
std::cout << "owner is null? " << (owner == nullptr ? "yes" : "no") << "\n";
return 0;
}
Running result:
Student Alice created
p is null? yes
owner has Alice
take ownership of Alice
Student Alice destroyed
owner is null? yes
4.3.Example 3: Shared ownership of shared_ptr
shared_ptrManage objects through reference counting. Only after the last shared_ptr is destroyed or reset does the object get released.
#include <iostream>
#include <memory>
#include <string>
class File
{
std::string name_;
public:
explicit File(const std::string& name) : name_(name)
{
std::cout << "File " << name_ << " opened\n";
}
~File()
{
std::cout << "File " << name_ << " closed\n";
}
void read() const
{
std::cout << "read from " << name_ << "\n";
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
auto file1 = std::make_shared<File>("log.txt");
std::cout << "count after create = " << file1.use_count() << "\n";
{
auto file2 = file1;
std::cout << "count in scope = " << file1.use_count() << "\n";
file2->read();
}
std::cout << "count after scope = " << file1.use_count() << "\n";
file1.reset();
std::cout << "file1 is empty? " << (file1 == nullptr ? "yes" : "no") << "\n";
return 0;
}
Running result:
File log.txt opened
count after create = 1
count in scope = 2
read from log.txt
count after scope = 1
File log.txt closed
file1 is empty? yes
shared_ptr 很方便,但不是"更安全的默认指针"。它有引用计数开销,也会让所有权关系变复杂。默认优先考虑 unique_ptr。
4.4.Example 4: weak_ptr Breaking shared_ptr circular references
If two objects point to each other with shared_ptr, their reference counts may never drop to zero. A common approach is: use shared_ptr for the primary direction, and weak_ptr for the reverse observation relationship.
#include <iostream>
#include <memory>
#include <string>
class Node
{
public:
std::string name;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
explicit Node(const std::string& n) : name(n)
{
std::cout << "Node " << name << " created\n";
}
~Node()
{
std::cout << "Node " << name << " destroyed\n";
}
};
int main()
{
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
a->next = b; // A 拥有下一个节点
b->prev = a; // B 只观察前一个节点,不增加 A 的引用计数
std::cout << "a count = " << a.use_count() << "\n";
std::cout << "b count = " << b.use_count() << "\n";
if (auto p = b->prev.lock())
{
std::cout << "B prev is " << p->name << "\n";
}
return 0;
}
Running result:
Node A created
Node B created
a count = 1
b count = 2
B prev is A
Node A destroyed
Node B destroyed
weak_ptr cannot be accessed directly; you need to call lock() first. If the object is still alive, lock() returns a temporary shared_ptr; if the object has already been released, it returns a null pointer.
4.4.1.What relationship does this example express?
This code can be viewed as a doubly linked list with only two nodes:
A ----next----> B
A <---prev----- B
Although both nodes can find each other, theownership implications are not the same in the two directions:
AthroughnextownsB; therefore,nextusestd::shared_ptr<Node>.BthroughprevobservationAbut is not responsible for extending the lifetime ofA; therefore,prevusestd::weak_ptr<Node>.
It can be simply understood as:
shared_ptrMeans "I own this object; it cannot be destroyed for now."weak_ptrMeans "I know where this object is, but I am not responsible for keeping it alive."
4.4.2.How does the reference count change?
When two nodes are first created:
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
At this time a has A,b has B:
| object | has its shared_ptr |
reference count |
|---|---|---|
A |
a |
1 |
B |
b |
1 |
Execute:
a->next = b;
a->next is also a shared_ptr, which together with b shares B,so the reference count of B changes from 1 to 2.
Then execute:
b->prev = a;
b->prev is weak_ptr, only observes A, and does not increment the reference count. Therefore, the final output is:
a count = 1
b count = 2
The correspondence is:
| object | has itsshared_ptr |
reference count |
|---|---|---|
A |
a |
1 |
B |
b,a->next |
2 |
4.4.3.Why can't the reverse pointer also use shared_ptr
Assume the member is changed to:
std::shared_ptr<Node> prev;
Thus A through next owns B. B and through prev owns A:
A ----shared_ptr----> B
A <---shared_ptr---- B
When main() ends, the local variables a and b are destroyed, but the two nodes still hold references to each other internally:
A 的引用计数至少为 1:来自 B.prev
B 的引用计数至少为 1:来自 A.next
它们的引用计数都无法降到 0,因此两个对象都不会析构,这就是 shared_ptr 的循环引用问题。
Using weak_ptr as a back pointer, B.prev does not participate in the reference count of A. The circular ownership is broken, and the object can be released normally.
4.4.4.What is the function of lock()?
weak_ptrdoes not own the object, so it cannot guarantee that the object it observes still exists. For this reason, one cannot directly write:
// 错误:weak_ptr 不支持直接使用 -> 访问对象
std::cout << b->prev->name << "\n";
Must be called before access:
b->prev.lock()
lock() will attempt to obtain a weak_ptr based on shared_ptr:
- If the object still exists, a valid
shared_ptris returned. - If the object has been destroyed, an empty
shared_ptris returned.
Therefore, the original code is written as:
if (auto p = b->prev.lock())
{
std::cout << "B prev is " << p->name << "\n";
}
This code can be understood in three steps:
b->prev.lock()Try to obtain a pointer toA'sshared_ptr.- Save the return value into the local variable
p. ifCheck whetherpis non-null, and enter the code block only if the object still exists.
In this example,A is still managed by the variable a, so lock() succeeds,p points to A, and can be safely accessed:
p->name
During the lifetime of p , it also temporarily holds A , so the reference count of A temporarily changes from 1 to 2. After leaving the if code block, p is destroyed, and the reference count changes from 2 back to 1.
4.4.5.What happens when lock() fails?
In the following example,A after being destroyed,weak_ptr itself can still exist, but it is already in an expired state:
std::weak_ptr<Node> observer;
{
auto owner = std::make_shared<Node>("A");
observer = owner;
} // owner 销毁,A 的引用计数变成 0,A 被释放
if (auto p = observer.lock())
{
std::cout << p->name << "\n";
}
else
{
std::cout << "observed object has been destroyed\n";
}
At this point observer.lock() returns empty shared_ptr; the program enters else and does not access objects that have already been destroyed.
You can also first use expired() to determine:
if (!observer.expired())
{
// 对象可能仍然存在
}
However, when actual access to the object is needed, one should still directly use lock() and check the return value. Because between the call to expired() and the call to lock() the object's lifetime may still change; lock() can combine "checking whether the object exists" and "temporarily acquiring ownership" into a single safe operation.
4.4.6.Why can both nodes be destructed when the program ends?
main() At the end, local variables are destroyed in the reverse order of their creation, so b is destroyed first, then a:
bAfter destruction,B's reference count changes from 2 to 1 becausea->nextstill ownsB.aAfter destruction,A's reference count changes from 1 to 0, soAis destructed.AWhen is destroyed, its membernextis also destroyed, andB's reference count changes from 1 to 0.- Finally,
Bis destructed.
So in the end, it can be seen that:
Node A destroyed
Node B destroyed
The most critical point here is not "which type of smart pointer must be used for the forward and backward pointers respectively," but rather to first determine the ownership relationship:The party truly responsible for the object's lifetime uses shared_ptr, while the party only responsible for observation uses weak_ptr.
4.5.Example 5: Do not abuse function parameters shared_ptr
Function parameters should express true semantics:
- Just temporary use of an object: use
T&orconst T&. - Function may not have an object: use
T*orconst T*. - Function needs to take over an object: use
std::unique_ptr<T>. - Function needs to co-own an object: use
std::shared_ptr<T>.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Robot
{
std::string name_;
public:
explicit Robot(std::string name) : name_(std::move(name)) {}
const std::string& name() const
{
return name_;
}
};
void print_robot(const Robot& robot)
{
std::cout << "borrow: " << robot.name() << "\n";
}
// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_robot(std::unique_ptr<Robot> robot)
{
std::cout << "take: " << robot->name() << "\n";
}
void share_robot(std::shared_ptr<Robot> robot)
{
std::cout << "share count inside = " << robot.use_count() << "\n";
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
auto robot = std::make_unique<Robot>("R1");
print_robot(*robot);
take_robot(std::move(robot));
std::cout << "robot is null? " << (robot == nullptr ? "yes" : "no") << "\n";
auto shared_robot = std::make_shared<Robot>("R2");
std::cout << "share count before = " << shared_robot.use_count() << "\n";
share_robot(shared_robot);
std::cout << "share count after = " << shared_robot.use_count() << "\n";
return 0;
}
Running result:
borrow: R1
take: R1
robot is null? yes
share count before = 1
share count inside = 2
share count after = 1
5.Key syntax explanation
| Syntax | Meaning | Notes |
|---|---|---|
std::make_unique<T>(...) |
Create an exclusively owned object | C++ Available since 14 |
std::move(p) |
Transfer ownership of unique_ptr out |
After the move, the original pointer is usually null |
std::make_shared<T>(...) |
Create a shared-ownership object | Reference counting has overhead |
sp.use_count() |
View the shared reference count | Usable for teaching and debugging; do not rely on it in business logic |
std::weak_ptr<T> |
Weak reference; does not increase the reference count | Before access, must lock() |
p.reset() |
Release the currently managed object | For shared_ptr, this decrements the reference count by one |
6.Common Errors
- Copying
unique_ptras a plain pointer.unique_ptrcan only be moved, not copied. - Although it is only borrowing the object, the parameter is written as
shared_ptr. This implies that the function also participates in shared ownership. - Use one
shared_ptrraw pointer to create anothershared_ptr. This produces two control blocks and can eventually lead to double free. - Two objects each hold the other with
shared_ptr, causing a circular reference. - Use a smart pointer to manage a stack object. For example, pass the address of a local variable to
unique_ptr; when it leaves the scope, it will incorrectly free stack memory.
7.Usage Recommendations
- Prefer not to use pointers if you can avoid them; prefer ordinary objects and references.
- When dynamic ownership is needed, prefer
unique_ptr. - Use
shared_ptronly when there really are multiple owners. - When only observing an object managed by
shared_ptr, useweak_ptr. - To create smart pointers, prefer
make_uniqueandmake_shared. - Function parameters should express semantics: do not mix borrowing, transfer, and sharing.
8.Summary
- Smart pointers are a typical application of RAII.
unique_ptrIt expresses exclusive ownership and is the default first choice.shared_ptrIt expresses shared ownership; do not abuse it just because it is convenient.weak_ptrIt does not own the object and is often used to resolve circular references.- The most important thing about smart pointers is not "automatic delete," but making ownership relationships clear.