Bỏ qua để đến nội dung

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.

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
}

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;
}
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)
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ó exception
void 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
}
  • Smart pointer tự động gọi delete khi 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 định
  • shared_ptr: nhiều chủ sở hữu, dùng đếm tham chiếu (reference counting) để biết khi nào giải phóng
  • weak_ptr: quan sát một shared_ptr mà 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/delete trực tiếp