Smart pointer trong ngôn ngữ C++

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.

www.daynhauhoc.com

7 Likes

Hi
Theo mình thì smart poiter rất hay nó giúp ích rất nhiều khi dùng với RAII. Tuy nhiên không phải lúc nào cũng dùng. Khi biết tài nguyên nó trỏ tới thuộc quyền sở hữu của đối tượng nào thì nên dùng còn không thì dùng con trỏ thường.
VD

#include <iostream>
#include <memory>

#define MAX 10

class DataSample {
    public:
        DataSample() : id(DataSample::value++) {
            std::cout << "Contructor " << this->id << std::endl;
        }
        ~DataSample() {
            std::cout << "Destroy " << this->id << std::endl;
        }
        int getId() {
            return this->id;
        }
    private:
        int id;
        static int value;
};

int DataSample::value = 0;

typedef DataSample* PoiterDataSample;

class Buffer {
    public:
        Buffer() : buffer(std::make_unique<DataSample[]>(MAX)) {
            //TODO
        }
        PoiterDataSample getBuffer() {
            return this->buffer.get();
        }
    private:
        std::unique_ptr<DataSample[]> buffer; //Tài nguyên bộ đệm thuộc sở hữu Buffer.
};

void ShowBuffer(PoiterDataSample pDataSample) { //Không xác định tài nguyên này của ai.
    for(int index  = 0; index < MAX; index++) {
        std::cout << pDataSample[index].getId() << std::endl;
    }
}

int main(int argc, char **argv) {
    auto buffer = std::make_unique<Buffer>();
    ShowBuffer(buffer->getBuffer());
    return 0;
}
1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?