close up photo of programming of codes

codecungnhau.com

Một trang web về kỹ thuật lập trình

Smart pointer trong C++ và các kiểu của nó

Vấn đề của con trỏ bình thường

Con trỏ được sử dụng để truy cập các tài nguyên bên ngoài chương trình như từ bộ nhớ heap. Tuy nhiên, việc sử dụng con trỏ cũng có một số vấn đề. Chúng ta hãy thử tìm hiểu thông qua một chương trình C++ nhỏ minh họa như sau,

#include <iostream> 
using namespace std; 
class Rectangle { 
private: 
	int length; 
	int breadth; 
}; 
void fun() 
{ 
	// Tạo con trỏ p và cấp phát bộ nhớ động
	// cho đối tượng kiểu Rectangle
	Rectangle* p = new Rectangle(); 
} 
int main() 
{ 
	// Lặp vô hạn
	while (1) { 
		fun(); 
	} 
} 

Điều xảy ra là sẽ hàm fun() sẽ tạo một con trỏ ‘p’. Con trỏ này sẽ trỏ đến một đối tượng kiểu Rectangle có chiều dài (length) và chiều rộng (breadth). Khi hàm kết thúc, ‘p’ này sẽ bị xóa vì p là biến cục bộ của hàm, nhưng một Rectangle mới được cấp phát bên trong heap sẽ không được giải phóng. Và trong vòng lặp vô hạn, một lần nữa fun() sẽ được gọi. Một ‘p’ mới và một đối tượng Rectangle với chiều dài và chiều rộng mới được tạo ra. Vậy đối tượng trước đó thì sao, nó sẽ không bị xóa và đối tượng thêm mới cũng sẽ không bị xóa. Chương trình sẽ cấp phát vùng nhớ liên tục mà không giải phóng.

Điều này gây rò rỉ bộ nhớ heap. Từ từ toàn bộ bộ nhớ heap có thể không sử dụng được. Đến khi thiếu bộ nhớ heap, chương trình sẽ bị crash. Do đó, trước khi thoát khỏi fun() chúng ta nên sử dụng ‘delete p’. Nếu chúng ta không đề cập đến điều này, sẽ gây ra một vấn đề rất nghiêm trọng. Vì vậy, C++ đã giới thiệu thư viện <memory> với các smart pointer để giúp người dùng tránh gặp phải những vấn đề này.

Sự ra đời của Smart Pointer

Vấn đề với bộ nhớ heap là khi ta không cần, ta phải giải phóng nó. Nhưng vì nhiều lý do, việc giải phóng này bị bỏ qua. Điều đó gây ra vấn đề nghiêm trọng như rò rỉ bộ nhớ khiến chương trình bị crash. Do đó, C++ giới thiệu smart pointer tự động quản lý bộ nhớ và chúng sẽ giải phóng đối tượng khi chúng không được sử dụng. Hay khi ra khỏi phạm vi, nó sẽ tự động giải phóng bộ nhớ.

Hãy xem xét đoạn mã C++ đơn giản sau với con trỏ bình thường.

MyClass* ptr = new MyClass(); 
ptr->doSomething(); 
// Chúng ta phải delete(ptr) để tránh rò gỉ bộ nhớ

Bằng cách sử dụng Smart Pointer, ta có thể làm cho con trỏ hoạt động theo cách mà ta không cần phải gọi delete một cách tường minh. Smart pointer là một lớp bao bọc một con trỏ có toán tử như * và -> được nạp chồng. Các đối tượng của lớp smart pointer trông giống như một con trỏ nhưng có thể thực hiện nhiều việc mà một con trỏ bình thường không thể như tự động giải phóng, bộ đếm tham chiếu và hơn thế nữa.

Ý tưởng là sử dụng một lớp có con trỏ, hàm hủy và các toán tử được nạp chồng như * và ->. Vì hàm hủy được tự động gọi khi một đối tượng đi ra ngoài phạm vi, bộ nhớ được cấp phát động sẽ tự động giải phóng (hoặc bộ đếm tham chiếu sẽ giảm xuống). Hãy xem xét lớp smart pointer đơn giản sau đây.

#include <iostream> 
using namespace std; 
class SmartPtr { 
	int* ptr; // con trỏ thật sự 
public: 
	// Hàm xây dựng tường minh 
	explicit SmartPtr(int* p = NULL) { ptr = p; } 
	// Hàm hủy
	~SmartPtr() { delete (ptr); } 
	// Nạp chồng toán tử * 
	int& operator*() { return *ptr; } 
}; 
int main() 
{ 
	SmartPtr ptr(new int()); 
	*ptr = 20; 
	cout << *ptr; 
	// Chúng ta không cần gọi delete (ptr) 
	// Khi kết thúc, hàm hủy sẽ tự động được gọi 
	// thực hiện giải phóng vùng nhớ. 
	return 0; 
}

Viết một lớp smart pointer hoạt động với bất kỳ kiểu nào

Chúng ta có thể sử dụng khuôn mẫu để viết một lớp smart pointer chung như sau,

#include <iostream> 
using namespace std; 
// Lớp smart pointer tổng quát 
template <class T> 
class SmartPtr { 
	T* ptr; // con trỏ thật sự 
public: 
	// Hàm xây dựng tường minh 
	explicit SmartPtr(T* p = NULL) { ptr = p; } 
	// Hàm hủy
	~SmartPtr() { delete (ptr); } 
	// Nạp chồng toán tử * 
	T& operator*() { return *ptr; } 
	// Nạp chồng toán tử -> để các thành viên của T
	// có thể truy xuất như một con trỏ thông thường
	// Hữu ích nếu kiểu T là một class, struct hay union. 
	T* operator->() { return ptr; } 
}; 
int main() 
{ 
	SmartPtr<int> ptr(new int()); 
	*ptr = 20; 
	cout << *ptr; 
	return 0; 
} 

Các kiểu smart pointer

1. unique_ptr

Nếu một đối tượng được tạo và con trỏ unique_ptr P1 đang trỏ đến đối tượng này thì chỉ một con trỏ có thể trỏ đối tượng này cùng một lúc. Ta không thể chia sẻ với một con trỏ khác, nhưng ta có thể chuyển quyền điều khiển sang P2 bằng cách loại bỏ P1.

#include <iostream> 
using namespace std; 
#include <memory> 
class Rectangle { 
	int length; 
	int breadth; 
public: 
	Rectangle(int l, int b) 
	{ 
		length = l; 
		breadth = b; 
	} 
	int area() 
	{ 
		return length * breadth; 
	} 
}; 
int main() 
{ 
	unique_ptr<Rectangle> P1(new Rectangle(10, 5)); 
	cout << P1->area() << endl; // This'll print 50 
	// unique_ptr<Rectangle> P2(P1); 
	unique_ptr<Rectangle> P2; 
	P2 = move(P1); 
	// This'll print 50 
	cout << P2->area() << endl; 
	// cout<<P1->area()<<endl; 
	return 0; 
} 

Kết quả:

50
50

2. shared_ptr

Nếu sử dụng shared_ptr thì nhiều hơn một con trỏ có thể trỏ đến một cùng đối tượng tại một thời điểm và nó sẽ duy trì bộ đếm tham chiếu bằng phương thức use_count().

#include <iostream> 
using namespace std; 
#include <memory> 
class Rectangle { 
	int length; 
	int breadth; 
public: 
	Rectangle(int l, int b) 
	{ 
		length = l; 
		breadth = b; 
	} 
	int area() 
	{ 
		return length * breadth; 
	} 
}; 
int main() 
{ 
	shared_ptr<Rectangle> P1(new Rectangle(10, 5)); 
	// This'll print 50 
	cout << P1->area() << endl; 
	shared_ptr<Rectangle> P2; 
	P2 = P1; 
	// This'll print 50 
	cout << P2->area() << endl; 
	// This'll now not give an error,
	// This'll also print 50 now  
	cout << P1->area() << endl; 
	// This'll print 2 as Reference Counter is 2 
	cout << P1.use_count() << endl; 
	return 0; 
} 

Kết quả:

50
50
50
2

3. weak_ptr

weak_ptr là một smart pointer tham chiếu không sở hữu đến một đối tượng được quản lý bởi shared_ptr. Nó phải được chuyển đổi thành shared_ptr để truy cập đối tượng được tham chiếu.

weak_ptr có quyền sở hữu tạm thời. Khi một đối tượng chỉ cần được truy cập nếu nó tồn tại và có thể bị xóa bất kỳ lúc nào bởi người khác, thì weak_ptr được sử dụng để theo dõi đối tượng. Nó được chuyển đổi thành shared_ptr để đảm nhận quyền sở hữu tạm thời. Nếu shared_ptr ban đầu bị hủy tại lúc này, thì đối tượng sẽ tồn tại cho đến khi shared_ptr tạm thời bị hủy.

Một cách sử dụng khác cho weak_ptr là phá vỡ các tham chiếu khép kính hình thành bởi các đối tượng do shared_ptr quản lý. Bộ đếm tham chiếu shared_ptr không thể đạt đến 0 và bộ nhớ bị rò rỉ. Để ngăn chặn điều này, một trong những con trỏ trong chu trình có thể là weak_ptr.

Một hiện thực điển hình của weak_ptr lưu trữ hai con trỏ:

  • Một con trỏ đến khối điều khiển.
  • Một con trỏ lưu trữ shared_ptr.

Một con trỏ lưu trữ riêng biệt là cần thiết để đảm bảo rằng việc chuyển đổi shared_ptr thành weak_ptr và ngược lại hoạt động chính xác. Không thể truy cập con trỏ lưu trữ trong weak_ptr mà không khóa nó (lock) vào shared_ptr.

#include <iostream>
#include <memory>
 
std::weak_ptr<int> gw;
 
void observe()
{
    std::cout << "use_count == " << gw.use_count() << ": ";
    if (auto spt = gw.lock()) { // phải chuyển sang share_ptr để truy cập đối tượng
	std::cout << *spt << "\n";
    }
    else {
        std::cout << "gw is expired\n";
    }
}
 
int main()
{
    {
        auto sp = std::make_shared<int>(42);
		gw = sp;
		observe();
    }
    observe();
}

Kết quả:

use_count == 1: 42
use_count == 0: gw is expired

Đã đăng vào

trong

bởi

Thẻ:

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *