Smart Pointers (unique_ptr, shared_ptr, weak_ptr)
Bài “Cấp phát bộ nhớ động” đã cho thấy rủi ro lớn nhất của new/delete: chỉ cần quên một lần delete, chương trình sẽ rò rỉ bộ nhớ. Smart pointer (con trỏ thông minh, từ C++11, trong thư viện <memory>) giải quyết vấn đề này bằng cách tự động gọi delete khi không còn cần thiết nữa.
Nguyên lý: RAII (Resource Acquisition Is Initialization)
Phần tiêu đề “Nguyên lý: RAII (Resource Acquisition Is Initialization)”Smart pointer dựa trên nguyên lý RAII: tài nguyên (ở đây là bộ nhớ) được gắn với vòng đời của một đối tượng. Khi đối tượng đó ra khỏi phạm vi (scope) và bị hủy, nó tự động giải phóng tài nguyên đi kèm - giống như cách một biến int bình thường tự động biến mất khi hàm kết thúc.
std::unique_ptr: sở hữu duy nhất
Phần tiêu đề “std::unique_ptr: sở hữu duy nhất”unique_ptr đảm bảo chỉ có một chủ sở hữu duy nhất cho vùng nhớ tại một thời điểm. Đây là smart pointer nên dùng mặc định trong hầu hết trường hợp:
#include <iostream>#include <memory>
int main() { std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl; // 42
// Không cần delete! Khi ptr ra khỏi scope (kết thúc hàm), bộ nhớ tự động được giải phóng
return 0;}unique_ptr không thể sao chép (vì chỉ có một chủ sở hữu), nhưng có thể chuyển quyền sở hữu (move):
#include <memory>#include <iostream>
int main() { std::unique_ptr<int> ptr1 = std::make_unique<int>(42); // std::unique_ptr<int> ptr2 = ptr1; // LỖI! Không thể sao chép unique_ptr
std::unique_ptr<int> ptr2 = std::move(ptr1); // OK - chuyển quyền sở hữu sang ptr2
std::cout << (ptr1 == nullptr) << std::endl; // 1 (true) - ptr1 không còn sở hữu gì nữa std::cout << *ptr2 << std::endl; // 42 - ptr2 giờ là chủ sở hữu
return 0;}std::shared_ptr: sở hữu chung, đếm tham chiếu
Phần tiêu đề “std::shared_ptr: sở hữu chung, đếm tham chiếu”shared_ptr cho phép nhiều con trỏ cùng sở hữu một vùng nhớ. Nó tự động đếm số lượng shared_ptr đang trỏ đến cùng một đối tượng (reference count); vùng nhớ chỉ thực sự bị giải phóng khi con trỏ cuối cùng trỏ đến nó cũng bị hủy:
#include <iostream>#include <memory>
int main() { std::shared_ptr<int> ptr1 = std::make_shared<int>(42); std::cout << ptr1.use_count() << std::endl; // 1
{ std::shared_ptr<int> ptr2 = ptr1; // Sao chép được! Cả hai cùng sở hữu std::cout << ptr1.use_count() << std::endl; // 2 } // ptr2 ra khỏi scope, reference count giảm về 1 - bộ nhớ CHƯA bị giải phóng
std::cout << ptr1.use_count() << std::endl; // 1 std::cout << *ptr1 << std::endl; // 42 - vẫn còn hợp lệ
return 0; // ptr1 ra khỏi scope, reference count về 0 - bộ nhớ được giải phóng}std::weak_ptr: quan sát mà không sở hữu
Phần tiêu đề “std::weak_ptr: quan sát mà không sở hữu”weak_ptr trỏ đến một đối tượng được quản lý bởi shared_ptr, nhưng không làm tăng reference count. Nó dùng để giải quyết vấn đề tham chiếu vòng (circular reference) - khi hai shared_ptr trỏ vòng qua lại lẫn nhau, khiến reference count không bao giờ về 0:
#include <iostream>#include <memory>
int main() { std::shared_ptr<int> shared = std::make_shared<int>(42); std::weak_ptr<int> weak = shared; // weak không làm tăng use_count
std::cout << shared.use_count() << std::endl; // 1 (vẫn là 1, không phải 2)
if (auto locked = weak.lock()) { // Cần "khóa" weak_ptr để dùng tạm thời std::cout << *locked << std::endl; // 42 }
return 0;}So sánh nhanh
Phần tiêu đề “So sánh nhanh”unique_ptr |
shared_ptr |
weak_ptr |
|
|---|---|---|---|
| Số chủ sở hữu | 1 (duy nhất) | Nhiều (đếm tham chiếu) | 0 (chỉ quan sát) |
| Có thể sao chép? | Không (chỉ move) | Có | Có |
| Overhead | Rất thấp | Cao hơn (phải đếm tham chiếu) | Thấp |
| Dùng khi nào | Mặc định, khi chỉ cần một chủ sở hữu | Khi nhiều nơi cùng cần giữ đối tượng sống | Phá vỡ vòng tham chiếu giữa các shared_ptr |
Vì sao nên ưu tiên smart pointer hơn new/delete?
Phần tiêu đề “Vì sao nên ưu tiên smart pointer hơn new/delete?”// Cách cũ - dễ quên delete, dễ gây rò rỉ bộ nhớvoid old_way() { int* ptr = new int(42); // ... nếu có return sớm hoặc exception ở đây, delete KHÔNG BAO GIỜ được gọi! delete ptr;}
// Cách hiện đại - luôn được giải phóng đúng lúc, kể cả khi có exceptionvoid modern_way() { auto ptr = std::make_unique<int>(42); // Không cần delete - tự động giải phóng khi ra khỏi scope, dù có exception hay không}Tổng kết
Phần tiêu đề “Tổng kết”- Smart pointer tự động gọi
deletekhi không còn cần thiết, dựa trên nguyên lý RAII unique_ptr: một chủ sở hữu duy nhất, nhẹ, nên dùng mặc địnhshared_ptr: nhiều chủ sở hữu, dùng đếm tham chiếu (reference counting) để biết khi nào giải phóngweak_ptr: quan sát mộtshared_ptrmà không làm tăng reference count, giúp tránh tham chiếu vòng- Trong C++ hiện đại, nên ưu tiên smart pointer thay vì gọi
new/deletetrực tiếp