Đồ thị chia sẻ_ptr, yếu_ptr & Quyền sở hữu
Hiểu quyền sở hữu được chia sẻ được tính theo tham chiếu với Shared_ptr, ngắt chu kỳ với Weak_ptr, tìm hiểu về các khối điều khiển và bí danh cũng như biết khi nào Shared_ptr là lựa chọn sai.
Shared_ptr: Quyền sở hữu chung
std::shared_ptrenables shared ownership — multipleshared_ptrinstances can own the same object. The object is destroyed when the last owningshared_ptris destroyed or reset.
Internally,shared_ptrmaintains a control block containing:
- A strong reference count (number ofshared_ptrowners)
- A weak reference count (number ofweak_ptrobservers)
- The deleter and allocator
shared_ptris larger than a raw pointer (typically 2 pointers: one to the object, one to the control block) and reference counting has atomic overhead on every copy/destruction. Use it only when ownership is genuinely shared.
make_shared and Reference Counting
std::make_sharedallocates the object AND control block in a single allocation (instead of two). This is more cache-friendly and avoids the exception-safety issue of rawnew.
#include <iostream>
#include <memory>
#include <string>
struct Logger {
std::string name;
Logger(std::string n) : name(std::move(n)) {
std::cout << name << " created\n";
}
~Logger() { std::cout << name << " destroyed\n"; }
void log(const std::string& msg) {
std::cout << "[" << name << "] " << msg << '\n';
}
};
int main() {
auto logger = std::make_shared<Logger>("AppLogger");
std::cout << "use_count: " << logger.use_count() << '\n'; // 1
{
auto copy = logger; // shared ownership
std::cout << "use_count: " << logger.use_count() << '\n'; // 2
copy->log("inside scope");
} // copy destroyed, count drops to 1
std::cout << "use_count: " << logger.use_count() << '\n'; // 1
logger->log("still alive");
} // logger destroyed — count drops to 0, Logger deletedweak_ptr: Breaking Cycles
std::weak_ptrobserves ashared_ptr-managed object without extending its lifetime. To access the object, calllock(), which returns ashared_ptr— ornullptrif the object was already destroyed.weak_ptris essential for breaking circular references that would otherwise leak.
#include <iostream>
#include <memory>
#include <string>
struct Employee;
struct Team {
std::string name;
std::vector<std::shared_ptr<Employee>> members;
Team(std::string n) : name(std::move(n)) {}
~Team() { std::cout << "Team " << name << " destroyed\n"; }
};
struct Employee {
std::string name;
std::weak_ptr<Team> team; // weak — does NOT own the team
Employee(std::string n) : name(std::move(n)) {}
~Employee() { std::cout << "Employee " << name << " destroyed\n"; }
void print_team() {
if (auto t = team.lock()) { // try to get shared_ptr
std::cout << name << " belongs to " << t->name << '\n';
} else {
std::cout << name << " has no team\n";
}
}
};
int main() {
auto team = std::make_shared<Team>("Engineering");
auto alice = std::make_shared<Employee>("Alice");
auto bob = std::make_shared<Employee>("Bob");
team->members.push_back(alice);
team->members.push_back(bob);
alice->team = team; // weak_ptr — no circular ownership
bob->team = team;
alice->print_team(); // "Alice belongs to Engineering"
team.reset(); // destroy team
alice->print_team(); // "Alice has no team" — weak_ptr expired
}If two objects holdshared_ptrto each other, neither can ever reach reference count zero — they keep each other alive forever. This is a memory leak, even though you are using smart pointers.
struct Node {
std::shared_ptr<Node> next; // if next points back, LEAK!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // circular — neither a nor b will ever be destroyedThe fix: make one direction a weak_ptr. In tree structures, children hold weak_ptr to parents (or raw observer pointers). In doubly-linked lists, use weak_ptr for the prev pointer.
Trình xây dựng bí danh & Enable_shared_from_this
Hàm tạo bí danh tạoshared_ptrchia sẻ quyền sở hữu vớishared_ptrhiện có nhưng trỏ đến một đối tượng khác (thường là thành viên). Đối tượng bí danh vẫn tồn tại miễn là có bất kỳshared_ptrbí danh nào tồn tại:
struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first); // shares p's control block
// p can go out of scope — Pair stays alive because sp_first still references itenable_shared_from_this is a CRTP base class that allows an object managed by shared_ptr to obtain a shared_ptr to itself via shared_from_this(). Without it, constructing a new shared_ptr from this tạo khối điều khiển thứ hai, dẫn đến không có hai lần.
shared_ptris overused. Reach for it only when ownership is genuinely shared across independent lifetimes. Do NOT useshared_ptras a lazy substitute for thinking about ownership.
Avoidshared_ptrwhen:
- A single owner exists — useunique_ptrinstead (zero overhead).
- The object lives on the stack — just use the object directly.
- You just need to pass a reference — passT&orT*.
- You are using it "because it's safe" —shared_ptrhas runtime cost (atomic refcount, extra allocation) and hides ownership semantics.
- In performance-critical paths — atomic refcount operations are surprisingly expensive under contention.
Legitimate shared ownership scenarios: caches (multiple lookups return same object), pub/sub systems, shared configuration objects, and graph data structures with genuinely shared nodes.
Control Block Internals
Understanding the control block helps explainshared_ptrcosts.make_sharedcombines the object and control block into one allocation. Constructing from rawnewrequires two separate allocations.
#include <memory>
#include <iostream>
struct Widget {
int data[4];
};
int main() {
// Two allocations: one for Widget, one for control block
std::shared_ptr<Widget> p1(new Widget{});
// Single allocation: Widget + control block together
auto p2 = std::make_shared<Widget>();
// Sizes (typical 64-bit platform)
std::cout << "sizeof(Widget*): " << sizeof(Widget*) << '\n'; // 8
std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n'; // 8
std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n'; // 16
// shared_ptr is 2 pointers: one to object, one to control block
// Control block contains: strong count, weak count, deleter, allocator
std::cout << "p2 use_count: " << p2.use_count() << '\n'; // 1
std::weak_ptr<Widget> w = p2;
std::cout << "p2 use_count: " << p2.use_count() << '\n'; // still 1
// weak_ptr does NOT increase strong count
}shared_ptruses reference counting — object dies when last owner is gonemake_sharedallocates object + control block in one allocation (prefer it always)weak_ptrquan sát mà không sở hữu - sử dụnglock()để truy cập an toàn, phá vỡ các tham chiếu vòng trònshared_ptrrộng 2 con trỏ và có chi phí đếm lại nguyên tử - không miễn phí- Mặc định là
unique_ptr; chỉ sử dụngshared_ptrkhi quyền sở hữu được chia sẻ thực sự - Không bao giờ tạo hai
shared_ptrtừ cùng một con trỏ thô - sử dụngenable_shared_from_thisđể tự tham chiếu
Câu đố — Kiểm tra kiến thức
(15 XP)1. Điều gì xảy ra khi hai đối tượng giữ`shared_ptr`với nhau?
2. Tại sao`std::make_shared`được ưu tiên hơn`shared_ptr<T>(new T(...))`?
3. Làm cách nào để bạn truy cập một đối tượng thông qua`weak_ptr`một cách an toàn?