Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.
Chúng ta đã cùng nhau đi qua cả một chương dành riêng để nói về con trỏ trong ngôn ngữ C++.
Hãy cùng xem xét một ví dụ về cấp phát động dưới đây:
void someFunction()
{
Resource *ptr = new Resource; // Resource is a struct or class
// do stuff with ptr here
delete ptr;
}
Đoạn code trên được viết khá rõ ràng. Tuy nhiên, chúng ta thường dễ quên giải phóng vùng nhớ mà con trỏ ptr
đang quản lý. Mặc dù chúng ta có yêu cầu giải phóng vùng nhớ tại cuối hàm, có rất nhiều cách khiến con trỏ ptr
không được giải phóng khi phải thoát ra khỏi hàm sớm hơn:
#include <iostream>
void someFunction()
{
Resource *ptr = new Resource;
int x;
std::cout << "Enter an integer: ";
std::cin >> x;
if (x == 0)
return; // the function returns early, and ptr won’t be deleted!
// do stuff with ptr here
delete ptr;
}
Hậu quả có thể xảy đến là vùng nhớ con trỏ ptr
đang quản lý chưa được giải phóng.
Cơ bản là con trỏ thô trong ngôn ngữ C++ không có cơ chế tự động giải phóng sau khi sử dụng.
Smart pointers là một giải pháp
Smart pointers là những lớp class (lớp) quản lý con trỏ trong ngôn ngữ C++, nó cung cấp hàm destructor được tự động gọi đến khi đối tượng của class đó đi ra khỏi phạm vi khối lệnh chứa nó. Vậy, nếu chúng ta cấp phát vùng nhớ cho nó trong phần khởi tạo (constructor), vùng nhớ đó sẽ được đảm bảo giải phóng khi đối tượng smart pointer bị hủy.
Ngôn ngữ C++ cung cấp cho chúng ta 4 loại smart pointer khác nhau: std::auto_ptr, std::unique_ptr, std::shared_ptr, and std::weak_ptr.
Trong đó, chúng ta nên tránh sử dụng std::auto_ptr, nó đã được loại bỏ trong chuẩn C++17 vì một số khả năng gây lỗi.
Và mình sẽ giới thiệu đến các bạn 2 loại smart pointers trong khóa học này:
- Unique pointer
- Shared pointer
Đây là 2 loại smart pointer mình thấy được sử dụng phổ biến.
Unique pointer
std::unique_ptr là một giải pháp thay thế cho std::auto_ptr trong chuẩn C++11. Nó được sử dụng để quản lý các vùng nhớ mà không cấp quyền sử dụng chung tài nguyên cho các đối tượng khác. std::unique_ptr được định nghĩa trong thư viện .
Dưới đây là một ví dụ đơn giản sử dụng unique pointer:
#include <iostream>
#include <memory>
struct Resource
{
Resource()
{
std::cout << "Resource acquired\n";
}
~Resource()
{
std::cout << "Resource destroyed\n";
}
};
int main()
{
// allocate a Resource object and have it owned by std::unique_ptr
std::unique_ptr<Resource> res(new Resource);
return 0;
}
Bởi vì đối tượng std::unique_ptr
trên được cấp phát trên vùng nhớ Stack, điều này đảm bảo đối tượng đó sẽ bị hủy khi ra khỏi khối lệnh chứa nó, và vùng nhớ đã cấp phát cho res
quản lý cũng được giải phóng.
Truy cập đối tượng đang được quản lý bởi std::unique_ptr
Lớp std::unique_ptr có định nghĩa toán tử ( * )
và toán tử ( -> )
giúp cho chúng ta có thể truy xuất đến vùng nhớ đang được quản lý giống như con trỏ thông thường. Toán tử ( * )
trả về đối tượng đang được quản lý, và toán tử ( -> )
trả về con trỏ trỏ đến đối tượng đó.
Sau đây là một ví dụ:
#include <iostream>
#include <string>
#include <memory> // for std::unique_ptr
struct Resource
{
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
std::string getData() { return "I'm data"; }
};
int main()
{
std::unique_ptr<Resource> res(new Resource);
if (res) // use implicit cast to bool to ensure res contains a Resource
std::cout << (*res).getData() << std::endl;
return 0;
}
Trong chương trình trên, chúng ta sử dụng toán tử ( * )
để lấy được đối tượng đang được quản lý. Chúng ta cần lưu ý nên kiểm tra xem res
có đang quản lý một đối tượng hay không trước khi truy xuất.
std::make_unique
Khi nói đến std::unique_ptr
chúng ta cần biết đến một hàm đi kèm là std::make_unique()
. Hàm này cho phép khởi tạo một đối tượng với kiểu được yêu cầu.
#include <memory> // for std::unique_ptr and std::make_unique
#include <iostream>
struct Fraction
{
int m_numerator = 0;
int m_denominator = 1;
Fraction(int numerator = 0, int denominator = 1) :
m_numerator(numerator), m_denominator(denominator)
{
}
};
int main()
{
// Create a single dynamically allocated Fraction with numerator 3 and denominator 5
std::unique_ptr<Fraction> f1 = std::make_unique<Fraction>(3, 5);
std::cout << (*f1).m_numerator << "/" << (*f1).m_denominator << std::endl;
// Create a dynamically allocated a Fraction here
// We can also use automatic type deduction to good effect here
auto f2 = std::make_unique<Fraction>(4, 6);
std::cout << (*f2).m_numerator << "/" << (*f2).m_denominator << std::endl;
return 0;
}
Các bạn có thể sử dụng std::make_unique
hay không tùy ý, nhưng nhiều người vẫn khuyên sử dụng hàm này để tạo đối tượng kiểu std::unique_ptr
hơn.
Trả về đối tượng kiểu std::unique_ptr từ một hàm
Một std::unique_ptr
có thể được trả về và sử dụng an toàn từ một hàm. Ví dụ:
std::unique_ptr<Resource> createResource()
{
return std::make_unique<Resource>();
}
int main()
{
std::unique_ptr<Resource> ptr = createResource();
// do whatever
return 0;
}
Không có hiện tượng leak memory ở đây. Sau khi đối tượng trả về từ hàm createResource()
được gán lại cho một std::unique_ptr
khác, đối tượng đang được quản lý được chuyển giao hoàn toàn cho ptr
ở ví dụ trên. Sau khi ptr
ra khỏi khối lệnh hàm main, đối tượng Resource vẫn được giải phóng bình thường.
Shared pointer
Khác với std::unique_ptr
, std::shared_ptr
được thiết kế để nhiều đối tượng có thể sử dụng, chia sẻ, cùng quản lý chung một tài nguyên. std::shared_ptr
cung cấp cơ chế theo dõi số lượng đối tượng std::shared_ptr
đang chia sẻ cùng 1 tài nguyên với nhau. Tài nguyên được quản lý sẽ không bị hệ điều hành thu hồi cho đến khi đối tượng std::shared_ptr
còn lại duy nhất đang quản lý nó bị hủy.
Cũng giống std::unique_ptr
, std::shared_ptr
được định nghĩa bên trong thư viện :
#include <iostream>
#include <memory> // for std::shared_ptr
struct Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::shared_ptr
Resource *res = new Resource;
std::shared_ptr<Resource> ptr1(res);
{
std::shared_ptr<Resource> ptr2(ptr1); // use copy initialization to make another std::shared_ptr pointing to the same thing
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, but nothing happens
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed
Kết quả in ra:
Resource acquired
Killing one shared pointer
Killing another shared pointer
Resource destroyed
Trong chương trình trên, chúng ta tạo ra một đối tượng kiểu Resource, và dùng một đối tượng std::shared_ptr
để quản lý nó. Trong một khối lệnh con khác, đối tượng ptr2
cũng với kiểu std::shared_ptr
trỏ đến cùng một đối tượng Resource. Khi đối tượng ptr2
ra khỏi khối lệnh, đối tượng Resource không bị thu hồi, vì đối tượng ptr1
vẫn đang quản lý nó. Khi đối tượng ptr1
ra khỏi khối lệnh hàm main, đối tượng ptr1
thông báo rằng nó không còn chia sẻ tài nguyên với đối tượng std::shared_ptr
nào khác, nên đối tượng Resource bị thu hồi.
Cần lưu ý rằng đối tượng std::shared_ptr
được tạo ra sau đó muốn quản lý chung tài nguyên với đối tượng std::shared_ptr
ban đầu, nó phải được khởi tạo bằng chính đối tượng std::shared_ptr
ban đầu:
#include <iostream>
#include <memory> // for std::shared_ptr
struct Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Resource *res = new Resource;
std::shared_ptr<Resource> ptr1(res);
{
std::shared_ptr<Resource> ptr2(res); // create ptr2 directly from res (instead of ptr1)
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, and the allocated Resource is destroyed
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed again
Chương trình trên bị crash vì con trỏ res
bị hủy 2 lần.
std::make_shared
Nếu hàm std::make_unique()
được dùng cho std::unique_ptr
, thì hàm std::make_shared()
đặc dụng cho std::shared_ptr
.
#include <iostream>
#include <memory> // for std::shared_ptr
struct Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// allocate a Resource object and have it owned by std::shared_ptr
auto ptr1 = std::make_shared<Resource>();
{
auto ptr2 = ptr1; // create ptr2 using copy initialization of ptr1
std::cout << "Killing one shared pointer\n";
} // ptr2 goes out of scope here, but nothing happens
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 goes out of scope here, and the allocated Resource is destroyed
Lý do sử dụng std::make_shared()
cũng tương tự như std::make_unique()
, nó an toàn hơn và đơn giản hơn.
=========================================================
Trên đây là những phần cơ bản nhất khi tìm hiểu về smart pointers trong ngôn ngữ C++. Minh xin giới thiệu đến các bạn một số tài nguyên để tiếp tục nghiên cứu sâu hơn trong phần này:
https://www.codeproject.com/Articles/541067/Cplusplus-Smart-Pointers
https://www.learncpp.com/cpp-tutorial/15-1-intro-to-smart-pointers-move-semantics/
Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.
Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn.