Cần lưu ý rằng, trong trường hợp của C ++, một quan niệm sai lầm phổ biến rằng "bạn cần thực hiện quản lý bộ nhớ thủ công". Thực tế, bạn thường không thực hiện bất kỳ quản lý bộ nhớ nào trong mã của mình.
Các đối tượng có kích thước cố định (có tuổi thọ phạm vi)
Trong phần lớn các trường hợp khi bạn cần một đối tượng, đối tượng sẽ có thời gian xác định trong chương trình của bạn và được tạo trên ngăn xếp. Điều này hoạt động cho tất cả các loại dữ liệu nguyên thủy tích hợp, nhưng cũng cho các trường hợp của các lớp và cấu trúc:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Các đối tượng ngăn xếp được tự động loại bỏ khi chức năng kết thúc. Trong Java, các đối tượng luôn được tạo trên heap và do đó phải được loại bỏ bởi một số cơ chế như bộ sưu tập rác. Đây không phải là vấn đề đối với các đối tượng ngăn xếp.
Các đối tượng quản lý dữ liệu động (có phạm vi trọn đời)
Sử dụng không gian trên ngăn xếp hoạt động cho các đối tượng có kích thước cố định. Khi bạn cần một lượng không gian thay đổi, chẳng hạn như một mảng, một cách tiếp cận khác được sử dụng: Danh sách được gói gọn trong một đối tượng có kích thước cố định quản lý bộ nhớ động cho bạn. Điều này hoạt động vì các đối tượng có thể có chức năng dọn dẹp đặc biệt, hàm hủy. Nó được đảm bảo được gọi khi đối tượng đi ra khỏi phạm vi và thực hiện ngược lại với hàm tạo:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
Không có quản lý bộ nhớ nào trong mã nơi bộ nhớ được sử dụng. Điều duy nhất chúng ta cần đảm bảo là đối tượng chúng ta đã viết có hàm hủy phù hợp. Cho dù chúng tôi rời khỏi phạm vi như thế nào listTest
, có thể thông qua một ngoại lệ hoặc đơn giản bằng cách quay lại từ nó, hàm hủy ~MyList()
sẽ được gọi và chúng tôi không cần phải quản lý bất kỳ bộ nhớ nào.
(Tôi nghĩ rằng đó là một quyết định thiết kế hài hước khi sử dụng toán tử NOT nhị phân~
, để chỉ ra hàm hủy. Khi được sử dụng trên các số, nó đảo ngược các bit; tương tự, ở đây nó chỉ ra rằng những gì hàm tạo đã làm được đảo ngược.)
Về cơ bản tất cả các đối tượng C ++ cần bộ nhớ động đều sử dụng đóng gói này. Nó được gọi là RAII ("thu nhận tài nguyên là khởi tạo"), đây là một cách khá kỳ lạ để diễn đạt ý tưởng đơn giản mà các đối tượng quan tâm đến nội dung của chính họ; những gì họ có được là của họ để làm sạch.
Đối tượng đa hình và suốt đời vượt quá phạm vi
Bây giờ, cả hai trường hợp này đều dành cho bộ nhớ có thời gian tồn tại được xác định rõ ràng: Thời gian tồn tại giống như phạm vi. Nếu chúng ta không muốn một đối tượng hết hạn khi chúng ta rời khỏi phạm vi, có một cơ chế thứ ba có thể quản lý bộ nhớ cho chúng ta: một con trỏ thông minh. Con trỏ thông minh cũng được sử dụng khi bạn có phiên bản của các đối tượng có loại khác nhau khi chạy, nhưng có giao diện chung hoặc lớp cơ sở:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Có một loại con trỏ thông minh khác std::shared_ptr
, để chia sẻ các đối tượng giữa một số khách hàng. Họ chỉ xóa đối tượng được chứa của họ khi máy khách cuối cùng ra khỏi phạm vi, vì vậy chúng có thể được sử dụng trong các tình huống hoàn toàn không biết có bao nhiêu máy khách sẽ có và họ sẽ sử dụng đối tượng đó trong bao lâu.
Tóm lại, chúng tôi thấy rằng bạn không thực sự quản lý bộ nhớ thủ công. Tất cả mọi thứ được gói gọn và sau đó được chăm sóc bằng phương pháp quản lý bộ nhớ hoàn toàn tự động, dựa trên phạm vi. Trong trường hợp điều này là không đủ, con trỏ thông minh được sử dụng để đóng gói bộ nhớ thô.
Việc sử dụng con trỏ thô là chủ sở hữu tài nguyên ở bất kỳ nơi nào trong mã C ++, việc phân bổ thô bên ngoài các hàm tạo và delete
các lệnh gọi thô bên ngoài hàm hủy là điều gần như không thể quản lý khi ngoại lệ xảy ra và thường khó sử dụng một cách an toàn.
Tốt nhất: điều này hoạt động cho tất cả các loại tài nguyên
Một trong những lợi ích lớn nhất của RAII là nó không giới hạn bộ nhớ. Nó thực sự cung cấp một cách rất tự nhiên để quản lý các tài nguyên như tệp và ổ cắm (mở / đóng) và các cơ chế đồng bộ hóa như mutexes (khóa / mở khóa). Về cơ bản, mọi tài nguyên có thể được mua và phải được phát hành đều được quản lý theo cách chính xác giống như trong C ++ và không có quản lý nào còn lại cho người dùng. Tất cả được gói gọn trong các lớp thu nhận trong hàm tạo và giải phóng trong hàm hủy.
Ví dụ, một hàm khóa một mutex thường được viết như thế này trong C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Các ngôn ngữ khác làm cho điều này trở nên phức tạp hơn nhiều, bằng cách yêu cầu bạn thực hiện điều này bằng tay (ví dụ như trong một finally
mệnh đề) hoặc chúng sinh ra các cơ chế chuyên biệt giải quyết vấn đề này, nhưng không phải theo cách đặc biệt tao nhã (thường là sau này trong cuộc sống của chúng, khi có đủ người chịu đựng những thiếu sót). Các cơ chế như vậy là các tài nguyên thử trong Java và câu lệnh sử dụng trong C #, cả hai đều là xấp xỉ RAII của C ++.
Vì vậy, để tóm tắt, tất cả những điều này là một tài khoản rất hời hợt của RAII trong C ++, nhưng tôi hy vọng rằng nó giúp người đọc hiểu rằng bộ nhớ và thậm chí quản lý tài nguyên trong C ++ thường không phải là "thủ công", mà thực sự chủ yếu là tự động.