1. Sự cần thiết của việc cấp phát bộ nhớ động

C++ hỗ trợ 3 kiểu cấp phát bộ nhớ cơ bản, trong đó bạn đã được thấy 2 loại.

  • Cấp phát bộ nhớ tĩnh: Áp dụng cho các biến tĩnh và toàn cục (static and global variables). Phần bộ nhớ dành cho các biến kiểu này chỉ được cấp phát một lần khi chương trình chạy và sẽ tồn tại trong suốt vòng đời của chương trình.
  • Cấp phát bộ nhớ tự động: Áp dụng cho các tham số truyền vào của hàm và các biến cục bộ (local variables). Phần bộ nhớ dành cho các biến kiểu này được cấp phát khi chương trình đi vào một khối (block) có liên quan, và được giải phóng khi chương trình thoát ra khỏi khối này, bao nhiên lần tùy ý.
  • Cấp phát bộ nhớ động: Chính là chủ đề của bài học hôm nay.

Cả cấp phát tĩnh và cấp phát tự động đều có hai điểm chung:

  • Kích thước của biến/mảng phải được biết tại thời điểm biên dịch (compile time).
  • Việc cấp phát và giải phóng bộ nhớ diễn ra tự động (khi biến được thể hiện/hủy).

Trong hầu hết các trường hợp, hai điều trên đều ổn. Tuy nhiên, bạn sẽ gặp các tình huống mà trong đó một hoặc cả hai ràng buộc này sẽ gây ra lỗi, thường là khi xử lý dữ liệu đầu vào bên ngoài (từ người dùng hoặc từ file).

Ví dụ: Chúng ta muốn sử dụng một chuỗi để giữ tên của người dùng, nhưng chúng ta không biết được độ dài của tên này, cho đến khi người dùng nhập nó. Hoặc chúng ta có thể muốn đọc vào một số lượng các bản ghi dữ liệu từ đĩa, nhưng chúng ta không biết trước được có tất cả bao nhiêu bản ghi. Hay khi ta muốn tạo ra một trò chơi, với số lượng quái vật thay đổi theo thời gian, khi một số quái vật chết và những con mới được sinh ra, luôn cố gắng hạ gục người chơi.

Nếu chung ta phải khai bóa kích thước của mọi thứ tại thời điểm biên dịch (compile time), điều tốt nhất chúng ta có thể làm là cố gắng đoán kích thước tối đa của các biến mà chúng ta sẽ cần, và hy vọng rằng như vậy là đủ:

char name[25]; // let's hope their name is less than 25 chars!
Record record[500]; // let's hope there are less than 500 records!
Monster monster[40]; // 40 monsters maximum
Polygon rendering[30000]; // this 3d rendering better not have more than 30,000 polygons!

Đây là một giải pháp không tốt vì ít nhất 4 lý do:

  • Đầu tiên, nó dẫn đến việc lãng phí bộ nhớ nếu các biến không thực sự được sử dụng. Ví dụ, nếu chúng ta cấp phát 25 ký tự char cho mọi tên mà người dùng nhập vào, nhưng tên trung bình chỉ dài 12 ký tự, như vậy chúng ta đang sử dụng thừa nhiều hơn gấp dôi những gì ta thật sự cần. Hoặc nhìn vào mảng rendering ở ví dụ trên: Nếu một mảng rendering chỉ sử dụng 10.000 polygons (đa giác), chúng ta sẽ bị thừa ra phần bộ nhớ ứng với 20.000 polygons không được sử dụng!
  • Thứ hai, làm thế nào để chúng ta biết được những bits nào của bộ nhớ thực sự được sử dụng? Đối với string, điều này rất dễ: một string bắt đầu với ký tự null (\0) rõ ràng là không được sử dụng. Nhưng còn mảng monster[24] thì sao? Con quái vật đang được xét hiện còn sống hay đã chết? Để xác định điều này, cần triển khai một số cách để các thành phần thụ động có thể nói lên một cách chủ động (tell active from inactive items), điều này sẽ làm tăng thêm sự phức tạp và có thể tiêu tốn thêm nhiều bộ nhớ.
  • Thứ ba, hầu hết các biến thông thường (bao gồm các mảng cố định) đều được cấp phát trong một phân vùng bộ nhớ được gọi là stack. Dung lượng của vùng nhớ stack dành cho một chương trình thường khá nhỏ – Visual Studio mặc định thiết lập kích thước vùng nhớ stack là 1MB. Nếu bạn vượt quá con số này, lỗi stack overflow (tràn vùng nhớ stack, hay tràn ngăn xếp) sẽ xảy ra, và hệ điều hành có thể sẽ đóng chương trình của bạn lại.

Trên Visual Studio, bạn có thể thấy điều này diễn ra khi chạy đoạn chương trình sau:

int main()
{
    int array[1000000]; // allocate 1 million integers (probably 4MB of memory)
}

Việc bị giới hạn chỉ 1MB cho vùng bộ nhớ stack sẽ là vấn đề đối với nhiều chương trình, đặc biệt là các chương trình liên quan đến đồ họa.

Thứ tư, và quan trọng nhất, nó có thể dẫn đến các hạn chế gây ra bởi con người và/hoặc các lỗi tràn mảng. Điều gì sẽ xảy ra khi người dùng cố đọc 600 bản ghi từ đĩa, nhưng chúng ta chỉ cấp phát bộ nhớ cho tối đa 500 bản ghi? Hoặc là chúng ta phải đưa ra một lỗi cho người dùng, hoặclà chỉ đọc 500 bản ghi hoặc (trong trường hợp xấu nhất là chúng ta không xử lý trường hợp này) xảy ra lỗi tràn mảng các bản ghi gây crash ứng dụng.

Thật may là những vấn đề này được giải quyết dễ dàng thông qua việc cấp phát bộ nhớ động. Cấp phát bộ nhớ động là cách để các chương trình đang chạy có thể yêu cầu bộ nhớ từ hệ điều hành khi cần. Phần bộ nhớ này không đến từ vùng nhớ stack bị giới hạn của chương trình – thay vào đó, nó được cấp phát từ một vùng bộ nhớ lớn hơn nhiều do hệ điều hành quản lý, được gọi là heap. Trên các máy tính hiện đại, heap có thể có kích thước lê đến hàng gigabytes.

2. Cấp phát động các biến đơn lẻ

Để cấp phát động một biến đơn lẻ, chúng ta có thể sử dụng dạng non-array (dạng scalar – dạng vô hướng) của toán tử new:

new int; // dynamically allocate an integer (and discard the result)

Trong ví dụ trên, chúng ta đang yêu cầu hệ thống cấp phát bộ nhớ cho một giá trị kiểu integer. Toán tử new tạo ra đối tượng mà sẽ sử dụng phần bộ nhớ được cấp phát, và sau đó trả về một con trỏ chứa địa chỉ của phần bộ nhớ đã được cấp phát.

Thông thường, chúng ta sẽ gán giá trị trả về của toán tử new cho một biến con trỏ để sau này chùng ta có thể truy cập vào phần bộ nhớ được cấp phát.

int *ptr = new int; // dynamically allocate an integer and assign the address to ptr so we can access it later

Chúng ta có thể dereference con trỏ để truy cập vào phần bộ nhớ được cấp phát:

*ptr = 7; // assign value of 7 to allocated memory

Từ đoạn code trên, bạn có thể thấy được một trong những trường hợp hữu dụng của con trỏ. Nếu không có con trỏ để giữ địa chỉ của phần bộ nhớ vừa được cấp phát, chúng ta sẽ không thể nào truy cập được vào phần bộ nhớ vừa được cấp phát này.

3. Cơ chế hoạt động của cấp phát bộ nhớ động

Máy tính của bạn có bộ nhớ (có thể là rất nhiều bộ nhớ) khả dụng dành cho các ứng dụng để sử dụng. Khi bạn chạy một ứng dụng, hệ điểu hành của bạn sẽ tải ứng dụng vào trong một số phần bộ nhớ đó. Phần bộ nhớ này được sử dụng bởi ứng dụng của bạn, được chia thành các vùng khác nhau, mỗi vùng sẽ phục vụ một mục đích khác nhau. Một vùng chứa code, một vùng được sử dụng cho các hoạt động bình thường (lưu dấu các hàm nào được gọi, tạo và hủy các biến toàn cục và biến cục bộ, v.v…). Chúng ta sẽ nói nhiều hơn về những điều này sau. Tuy nhiên, phần lớn bộ nhớ khả dụng đều chỉ “nằm ở đó”, và đợi để được trao cho các chương trình mà yêu cầu nó.

Khi bạn cấp phát bộ nhớ động, bạn thật sự đang yêu cầu hệ điều hành dành một phần của bộ nhớ khả dụng cho ứng dụng của bạn sử dụng. Nếu có thể đáp ứng yêu cầu này, hệ điều hành sẽ trả về địa chỉ của phần bộ nhớ đó cho ứng dụng của bạn. Sau đó, ứng dụng của bạn có thể sử dụng phần bộ nhớ này theo ý muốn. Khi ứng dụng của bạn đã chạy xong và không sử dụng tới phần bộ nhớ này nữa, nó có thể đưa phần bộ nhớ này trở lại hệ điều hành để trao cho những chương trình khác.

Không giống như cấp phát bộ nhớ tĩnh và cấp phát bộ nhớ tự động, đối với cấp phát bộ nhớ động, chương trình sẽ tự chịu trách nhiệm việc yêu cầu và xử lý phần bộ nhớ đã được cấp phát động.

4. Khởi tạo một biến được cấp phát động

Khi bạn cấp phát động một biến, bạn có thể khởi tạo nó thông qua khởi tạo trực tiếp – direct initialization hoặc khởi tạo đồng đều – uniform initialization (trong C++ 11). 

int *ptr1 = new int (5); // use direct initialization
int *ptr2 = new int { 6 }; // use uniform initialization

5. Xóa biến đơn lẻ

Khi đã sử dụng xong một biến được cấp phát động, chúng ta cần giải phóng bộ nhớ để tái sử dụng bộ nhớ cho tác vụ khác. Đối với các biến đơn lẻ, điều này được thực hiện thông qua dạng non-array (dạng scalar – dạng vô hướng) của toán tử delete:

// assume ptr has previously been allocated with operator new
delete ptr; // return the memory pointed to by ptr to the operating system
ptr = 0; // set ptr to be a null pointer (use nullptr instead of 0 in C++11)

6. Việc “xóa” bộ nhớ có ý nghĩa gì?

Toán tử delete không thực sự xóa đi bất cứ thứ gì. Nó chỉ đơn giản là trả lại phần bộ nhớ đã được sử dụng xong (phần bộ nhớ này hiện tại không được trỏ đến bởi bất cứ biến nào) cho hệ điều hành. Hệ điều hành sau đó sẽ tùy ý phân bổ phần bộ nhớ này cho một ứng dụng khác (hoặc tiếp tục cho ứng dụng này vào lần sau).

Mặc dù có vẻ như chúng ta đang xóa đi một biến con trỏ, nhưng không phải vậy, biến con trỏ (pointer variable) vẫn có phạm vi giống như lúc trước, và có thể được gán cho một giá trị mới, giống như bất kỳ biến nào khác.

Lưu ý rằng, việc xóa đi một con trỏ mà con trỏ đó đang không trỏ đến bất kỳ phần bộ nhớ được cấp phát động nào, có thể gây ra lỗi khiến chương trình crash.

7. Con trỏ lơ lửng

C++ sẽ không bảo đảm về những việc có thể xảy ra đối với những phần nội dung của bộ nhớ đã được thu hồi, hoặc với giá trị của con trỏ đang bị xóa. Trong hầu hết các trường hợp, phần bộ nhớ được trả về hệ điều hành sẽ chứa cùng các giá trị mà nó đã có trước khi nó được trả về, và con trỏ sẽ được để cho trỏ đến phần bộ nhớ đã được thu hồi hiện tại.

Một con trỏ đang trỏ tới phần bộ nhớ đã được thu hồi, được gọi là một con trỏ lơ lửng (dangling pointer). Việc dereferencing hoặc xóa một con trở lơ lửng sẽ dẫn đến lỗi undefined behavior (hành vi không xác định). Hãy xem xét đoạn chương trình ví dụ sau:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int main()
{
    int *ptr = new int; // dynamically allocate an integer
    *ptr = 7; // put a value in that memory location
 
    delete ptr; // return the memory to the operating system.  ptr is now a dangling pointer.
 
    std::cout << *ptr; // Dereferencing a dangling pointer will cause undefined behavior
    delete ptr; // trying to deallocate the memory again will also lead to undefined behavior.
 
    return 0;
}

Trong đoạn chương trình trên, giá trị 7 mà trước đó đã được gán cho phần bộ nhớ được cấp phát có thể vẫn còn ở đó, nhưng cũng có khả năng là giá trị tại địa chỉ bộ nhớ đó có thể đã thay đổi. Và cũng có khả năng phần bộ nhớ này có thể đã được cấp phát cho một ứng dụng khác (hoặc cho tác vụ riêng của hệ điều hành), và việc cố gắng truy cập đến phần bộ nhớ này sẽ khiến hệ điều hành tắt chương trình ví dụ ngay lập tức.

Việc thu hồi bộ nhớ có thể tạo ra nhiều con trỏ lơ lửng. Xem ví dụ sau:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include <iostream>
 
int main()
{
    int *ptr = new int; // dynamically allocate an integer
    int *otherPtr = ptr; // otherPtr is now pointed at that same memory location
 
    delete ptr; // return the memory to the operating system.  ptr and otherPtr are now dangling pointers.
    ptr = 0; // ptr is now a nullptr
 
    // however, otherPtr is still a dangling pointer!
 
    return 0;
}

Có một vài  thực hành tốt có thể hữu ích ở đây.

Đầu tiên, hãy cố gắng tránh việc có nhiều con trỏ cùng trỏ tới một phần bộ nhớ động. Nếu không thể làm được điều trên, hãy cố gắng làm rõ ràng về việc con trỏ nào “sở hữu” phần bộ nhớ này (và chịu trách nhiệm xóa phần bộ nhớ này) và những con trỏ nào chỉ là truy cập tới phần bộ nhớ này.

Thứ hai, khi bạn xóa đi một con trỏ, nếu muốn con trỏ đó không đi ra ngoài phạm vi ngay sau đó, thì hãy gán giá trị 0 cho nó. (hoặc giá trị nullptr trong C++11). Một lát nữa, chúng ta sẽ nói nhiều hơn về các con trỏ null và việc tại sao chúng lại hữu dụng.

Quy tắc: Hãy gán giá trị 0 (hoặc nullptr trong C++ 11) cho con trỏ đã được xóa, nếu không chúng sẽ đi ra khỏi phạm vi ngay sau đó.

  • CHÚ Ý: Giải thích về việc “con trỏ đi ra ngoài pham vi – pointer goes out of scope”:
  • Một con trỏ là một biến chứa địa chỉ của một số vùng nhớ hay đối tượng (object) trong bộ nhớ. Biến con trỏ có thể được phân bổ:

+ Trên vùng nhớ stack (dưới dạng biến tự động cục bộ trong một hàm hoặc khối câu lệnh)

+ Trên vùng nhớ heap (dưới dạng một vùng nhớ hay đối tượng được sinh ra nhờ từ khóa new hoặc dưới dạng biến thành viên của đối tượng của một class)

+ Một cách tĩnh (dưới dạng một biến toàn cục hoặc biến thành viên được khai báo static của class), lúc này biến con trỏ sẽ nằm ở đâu đó trong phân vùng data của chương trình.

Vùng nhớ hay đối tượng mà con trỏ trỏ đến (tham chiếu) đến cũng có thể được cấp phát bộ nhớ ở ba vị trí trên. Tuy nhiên, nói chung, trong c++, một vùng nhớ hay đối tượng thường sẽ được cấp phát bộ nhớ bằng toán tử new.

Các biến cục bộ đi ra khỏi phạm vi khi luồng thực thi của chương trình rời khỏi khối câu lệnh hoặc rời khỏi hàm mà chúng được khai báo bên trong đó. Tức là, sự hiện diện của các biến cục bộ này trên vùng nhớ stack sẽ biến mất. Tương tự, các biến thành viên của một đối tượng cũng sẽ biến mất khi đối tượng cha của chúng đi ra khỏi phạm vi hoặc bị xóa khỏi vùng nhớ heap.

Nếu một con trỏ đi ra khỏi phạm vi, hoặc đối tượng cha của nó bị xóa (tức là có một biến con trỏ là biến thành viên của một class => đối tượng cha của con trỏ chính là cái đối tượng được tạo ra từ class mà con trỏ được khai báo trong đó), thì đối tượng mà con trỏ trỏ đến/tham chiếu đến vẫn sẽ tồn tại trong bộ nhớ. Do đó quy tắc phải nhớ ở đây là: Bất kỳ đoạn code cấp phát vùng nhớ/đối tượng nào cũng đều sở hữu vùng nhớ/đối tượng đó, và phải có trách nhiệm xóa vùng nhớ/đối tượng đó đi bằng từ khóa delete khi không còn cần thiết.

8. Toán tử new có thể thất bại

Khi yêu cầu bộ nhớ từ hệ điều hành, có khả năng xảy ra những trường hợp hiếm hoi mà hệ điều hành có thể không còn bộ nhớ để cấp phát.

Theo mặc định, nếu toán tử new thất bại, ngoại lệ bad_alloc sẽ được ném ra. Nếu ngoại lệ này không được xử lý đúng cách, chương trình sẽ bị chấm dứt (crash) với một lỗi không xử lý ngoại lệ.

Trong nhiều trường hợp, việc ném ra một ngoại lệ (hoặc cứ để mặc kệ cho chương trình của bạn bị crash) là điều không mong muốn, do đó, có một dạng thay thế của từ khóa new có thể được sử dụng để ra lệnh cho từ khóa new trả về một con trỏ null nếu bộ nhớ không thể được cấp phát. Điều này được thực hiện bằng cách thêm hằng số std::nothrow vào giữa từ khóa new và kiểu dữ liệu cấp phát:

int *value = new (std::nothrow) int; // value will be set to a null pointer if the integer allocation fails

Trong ví dụ trên, nếu từ khóa new thất bại trong việc cấp phát bộ nhớ, nó sẽ trả về một con trỏ null thay vì địa chỉ của phần bộ nhớ được cấp phát.

Lưu ý rằng, nếu sau đó bạn cố gắng dereference phần bộ nhớ này, sẽ dẫn đến lỗi undefined behavior – hành vi không xác định, và rất có thể chương trình của bạn sẽ crash. Do đó, thực hành tốt nhất ở đây là kiểm tra tất cả các yêu cầu cấp phát bộ nhớ để đảm bảo rằng chúng thực sự thành công trước khi sử dụng phần bộ nhớ được cấp phát.

int *value = new (std::nothrow) int; // ask for an integer's worth of memory
if (!value) // handle case where new returned null
{
    // Do error handling here
    std::cout << "Could not allocate memory";
}

Bởi vì việc yêu cầu cấp phát bộ nhớ thường hiếm khi thất bại (và hầu như không bao giờ, trong môi trường dev), nên việc kiểm tra yêu cầu cấu phát bộ nhớ thường bị quên.

9. Con trỏ null và cấp phát bộ nhớ động

Con trỏ null (con trỏ trỏ đến địa chỉ 0 hoặc nullptr) đặc biệt hữu ích khi xử lý cấp phát bộ nhớ động. Trong bối cảnh cấp phát bộ nhớ động, về cơ bản, một con trỏ null sẽ cho biết rằng “không có phần bộ nhớ nào được cấp phát cho con trỏ này”. Điều này cho phép chúng ta làm những việc như cấp phát bộ nhớ một cách có điều kiện:

// If ptr isn't already allocated, allocate it
if (!ptr)
    ptr = new int;

Việc xóa đi một con trỏ null sẽ không ảnh hưởng gì, vì vậy chúng ta không cần phải viết đoạn code sau:

if (ptr)
    delete ptr;

Thay vào đó bạn có thể chỉ cần viết:

delete ptr;

Nếu ptr không null, biến đã được cấp phát bộ nhớ động sẽ được xóa. Nếu nó là null, sẽ không có điều gì xảy ra.

10. Lỗi memory leaks – rò rỉ bộ nhớ

Bộ nhớ được cấp phát động sẽ được cấp phát cho đến khi nó được giải phóng một cách rõ ràng hoặc cho đến khi chương trình kết thúc (và hệ điều hành sẽ dọn dẹp nó). Tuy nhiên, các con trỏ được sử dụng để giữ các địa chỉ bộ nhớ được cấp phát động sẽ tuân theo các quy tắc phạm vi thông thường dành cho các biến cục bộ. Sự không phù hợp này có thể tạo ra các vấn đề khá thú vị.

Hãy xem hàm sau:

void doSomething()
{
    int *ptr = new int;
}

Hàm này cấp phát động cho một biến kiểu integer, nhưng không bao giờ giải phóng nó bằng từ khóa delete. Do các biến con trỏ cũng chỉ là các biến bình thường, nên khi hàm kết thúc, biến ptr sẽ đi ra khỏi phạm vi. Và bởi vì ptr là biến duy nhất giữ địa chỉ của giá trị integer được cấp phát động, nên khi ptr bị hủy, sẽ không còn tham chiếu nào tới phần bộ nhớ được cấp phát động nữa. Điều này có nghĩa là chương trình hiện tại đã “bị mất đi” địa chỉ của phần bộ nhớ được cấp phát động. Do đó, phần bộ nhớ được cấp phát động cho biến kiểu integer này sẽ không thể được xóa/giải phóng.

Đây gọi là lỗi rò rỉ bộ nhớ (memory leak). Lỗi rò rỉ bộ nhớ xảy ra khi chương trình của bạn bị mất địa chỉ của một số phần bộ nhớ được cấp phát động, trước khi đưa chúng trở lại cho hệ điều hành. Khi điều này xảy ra, chương trình của bạn không thể xóa phần bộ nhớ đã được cấp phát động, bởi vì lúc này chương trình không còn có thể biết được phần bộ nhớ đó nằm ở đâu nữa. Hệ điều hành cũng không thể sử dụng phần bộ nhớ này, bởi vì phần bộ nhớ này được coi là vẫn đang được sử dụng bởi chương trình của bạn.

Lỗi rò rỉ bộ nhớ sẽ ăn hết những phần bộ nhớ trống trong khi chương trình đang chạy, làm cho dung lượng bộ nhớ khả dụng ít đi không chỉ đối với chương trình này, mà còn ảnh hưởng tới các chương trình khác nữa. Những chương trình bị lỗi rò rỉ bộ nhớ nghiêm trọng có thể ăn hết bộ nhớ khả dụng, khiến máy tính chạy chậm hoặc thậm chí bị crash. Chỉ sau khi chương trình của bạn chấm dứt, hệ điều hành mới có thể dọn sạch và “lấy lại” tất cả các phần bộ nhớ bị rò rỉ.

Mặc dù lỗi rò rỉ bộ nhớ có thể xuất phát từ việc một con trỏ đi ra ngoài phạm vi, nhưng vẫn có những cách khác có thể dẫn đến lỗi này. Ví dụ, một lỗi rò rỉ bộ nhớ có thể xảy ra nếu một con trỏ giữ địa chỉ của phần bộ nhớ được cấp phát động, được gán một giá trị khác:

int value = 5;
int *ptr = new int; // allocate memory
ptr = &value; // old address lost, memory leak results

Có thể sửa lỗi trong đoạn code trên bằng cách xóa con trỏ, trước khi gán lại giá trị cho nó:

int value = 5;
int *ptr = new int; // allocate memory
delete ptr; // return memory back to operating system
ptr = &value; // reassign pointer to address of value

Lỗi rò rỉ bộ nhớ cũng có thể xảy ra thông qua việc cấp phát kép:

int *ptr = new int;
ptr = new int; // old address lost, memory leak results

Địa chỉ được trả về từ lần cấp phát bộ nhớ thứ hai sẽ ghi đè lên địa chỉ của lần cấp phát thứ nhất. Do đó, lần cấp phát bộ nhớ thứ nhất sẽ trở thành một lỗi rò rỉ bộ nhớ.

Tương tự, điều này có thể tránh được bằng cách đảm bảo rằng bạn đã xóa con trỏ bằng từ khóa delete trước khi gán lại cho nó một giá trị địa chỉ khác.

11. Tổng kết

Toán tử new và delete cho phép chúng ta cấp phát động (bộ nhớ) cho các biến đơn lẻ trong chương trình của mình.

Phần bộ nhớ được cấp phát động thì không có phạm vi và sẽ ở trong trạng thái được cấp phát cho đến khi bạn giải phóng nó hoặc chương trình kết thúc.

Hãy cẩn thận để tránh không thực hiện dereferencing các con trỏ null và con trỏ lơ lửng.

Trong bài học tiếp theo, chúng ta sẽ xem xét việc sử dụng từ khóa new và delete để cấp phát và xóa các mảng.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!