Stack và Heap là gì? Phần 2: Heap


Ở phần trước chúng ta đã nói về Stack và những đặc điểm của vùng nhớ này. Nhưng do giới hạn về kích thước bộ nhớ có thể được lưu trữ trên Stack cũng như kích thước đó phải được xác định trước khi biên dịch chương trình, đâ gây một ràng buộc không nhỏ lên lập trình viên chúng ta. Từ đó, Heap ra đời để giải phóng chúng ta khỏi những giới hạn đó. Tuy nhiên, cái gì cũng có giá của nó, còn đó là gì thì các bạn cùng mình tìm hiểu trong bài này nhé.

Heap

Nếu Stack được coi như là một chồng đĩa được sắp xếp ngay ngắn và có thứ tự đàng hoàng, thì Heap hoàn toàn ngược lại. Vùng nhớ này giống như là một đống đĩa hổ lốn và bừa bãi, như có ai đó vừa làm đổ chồng đĩa của bạn rồi bỏ chạy để nguyên hiện trường vậy.


Nhưng điều này là tốt, do vùng nhớ Heap mà máy tính cung cấp cho chương trình lớn hơn nhiều so với Stack, đồng thời chúng ta cũng có thể yêu cầu thêm nếu muốn. Heap không giới hạn kích thước bộ nhớ được cấp phát và cho phép ta làm điều đó trong khi chương trình đang chạy.

Nói cách khác, Heap không quan tâm đến kích thước được yêu cầu là bao nhiêu khi biên dịch. Khi được yêu cầu cấp phát bộ nhớ trong Heap, nó mới bắt đầu đọc giá trị kích thước cần cấp phát, sau đó đi tìm nó cho bạn. Có thể hình dung như vầy, trong Stack, bạn phải xài 1 cái giá để chứa những chiếc đĩa và bạn không để nhét thêm đĩa vô Stack nếu nó quá lớn hay Stack đã đầy, điều này gây nhiều bất tiện trong việc lưu trữ đĩa.

Nhưng với Heap, chúng chỉ là một đống hổ lốn nằm trên sàn, bạn chỉ cần đến đó, chọn chiếc đĩa bạn thích và sử dụng. 

Tuy nhiên, đó là trách nhiệm của bạn trong việc giải phóng bộ nhớ đã được cấp phát bởi Heap

Sử dụng vùng nhớ Heap như thế nào?

Để yêu cầu cấp phát bộ nhớ trong Heap, ta sử dụng từ khóa new trong C++. Sau khi sử dụng xong, bạn cần phải gọi delete để trả lại vùng nhớ đó cho máy tính. Nếu chúng ta quên giải phóng bộ nhớ sau khi sử dụng xong, bộ nhớ đó sẽ không thể được sử dụng nữa do máy tính tưởng rằng bạn vẫn đang sử dụng nó. Dẫn đến rò rỉ bộ nhớ (memory leak).

Nói khái niệm thế là đủ, chúng ta sẽ bắt đầu tiến vào phần coding.

Yêu cầu cấp phát bộ nhớ đơn giản


Bắt đầu với từ khóa new, đây là một từ đã được định nghĩa sẵn trong C++ giống như int, float hay if...else. Từ khóa này sẽ yêu cầu cấp phát bộ nhớ từ Heap, để có thể sử dụng, ta phải thêm sau từ khóa này một kiểu dữ liệu nhất định. Ví dụ ở trên mình lấy int, điều này là cần thiết để biết cần phải yêu cầu bộ nhớ kích thước bao nhiêu (trong trường hợp này là 4 byte).

Sau đó, new sẽ trả về địa chỉ bộ nhớ được Heap cấp phát. Ta sẽ sử dụng con trỏ để lưu giữ giá trị đó. Tất nhiên sẽ là ổn nếu ta sử dụng một biến tham chiếu để lưu trữ. Nhưng điều nà sẽ gây ít nhiều khó khăn cho bạn. Vì giá trị con trỏ lưu trữ có thể thay đổi được (trỏ đến một địa chỉ khác) còn tham chiếu thì không. Chính vì thế, sử dụng tham chiếu đến một bộ nhớ được cấp phát bởi Heap làm mất đi độ linh động khi lập trình. Cấp phát xong, bạn có thể tương tác với biến một cách bình thường thông qua con trỏ.

Khi yêu cầu cấp phát từ Heap, ta có thể gán giá trị cần lưu luôn bằng cách:
int *var = new int(2);
Vậy là var sẽ trỏ tới địa chỉ có giá trị bằng 2 luôn, tiết kiệm cho ta được một dòng code.

Điều quan trọng cần nhớ chính là, khi đã sử dụng xong và không cần bộ nhớ đó nữa, ta phải tự tay giải phóng nó bằng delete để bộ nhớ đó có thể được tái sử dụng.

Dưới đây là một ví dụ xấu về việc quên giải phóng bộ nhớ.
Bạn có phát hiện ra lỗi sai không? Đúng vậy, có vẻ người lập trình viên này đã yêu cầu cấp phát bộ nhớ mới mà quên giải phóng bộ nhớ cũ. Bây giờ, máy tính vẫn tưởng anh ta đang xài bộ nhớ cũ đó còn anh ta thì đã mất dấu nó. Thứ duy nhất giúp anh ta tương tác với bộ nhớ cũ chính là con trỏ var nhưng nó đã được lưu địa chỉ bộ nhớ mới. Điều này rất tệ, khiến máy tính và anh chàng lập trình viên đều không thể can thiệp vào bộ nhớ cũ và nó sẽ bị bỏ mặc ở đó trong suốt cuộc đời còn lại. Các bạn nhớ nhé, đừng "đem con bỏ chợ" như thế.

Yêu cầu cấp phát mảng

Ta khai báo mảng trên Heap như sau:
int *arr= new int[<độ dài mảng>];
Dành cho bạn nào chưa biết, mảng là một kiểu cấu trúc dữ liệu trong đó các phần tử trong mảng nằm ở các bộ nhớ liên tiếp nhau và giá trị được trả về khi ta khởi tạo mảng (cả trong vùng nhớ Stack lẫn Heap) là con trỏ trỏ đến phần tử đầu tiên của mảng. Vậy nên, việc mình dùng con trỏ trong khai báo mảng là hoàn toàn hợp lý. Tương tác với mảng cũng giống như mảng bình thưởng. Điều khác biệt là bây giờ bạn có thể khai báo mảng với độ dài lấy từ biến cũng được. Còn nữa, khi giải phóng mảng, ta dùng lệnh delete[] thay cho delete thông thường:
delete[] arr;
Tất cả chỉ có vậy. Dưới đây là đoạn code mẫu cho những điều mình nói ở trên:

Các đặc điểm của Heap

  • Linh động hơn trong việc cấp phát bộ nhớ.
  • Dung lượng vùng nhớ lớn
  • Tốc độ truy xuất chậm hơn Stack một chút
  • Giải phóng bộ nhớ là trách nhiệm của lập trình viên
  • Không có variable scope như Stack, bộ nhớ có thể được truy xuất ở ngoài hàm miễn sao nó chưa được giải phóng

Tại sao khai báo mảng trong Stack cần phải là hằng số?

Sau khi đề cập đến khai báo mảng, chắc cũng có nhiều bạn tự hỏi tại sao Stack không thể tạo mảng có độ dài là một biến số. Vì thông thường, ta phải khai báo mảng với độ dài là một số lớn trong các trường hợp độ dài mảng phải dựa vào những yếu tố như dữ liệu người dùng. Khá là phiền phức.

Thật ra, cha đẻ của C++, Bjarne Stroustrup cho rằng việc này là cần thiết để tránh lỗi trong chương trình. Mặc dù bây giờ cũng có nhiều trình biên dịch cho phép khởi tạo mảng với độ dài là biến nhưng không được sử dụng rộng rãi. Vậy nên chúng ta hãy coi đây là một trong những nguyên tắc mà chúng ta cần phải tuân theo.

Kết

Vậy là đã xong, mình hy vọng là bây giờ bạn đã biết Stack là gì và Heap là gì. Nếu thích, hãy like và theo dõi trang của mình trên facebook để mình có thể tiếp tục làm những bài viết hữu ích.

Nhận xét

Bài đăng phổ biến từ blog này

Phép phân tích ma trận A=LU

Độc lập tuyến tính và phụ thuộc tuyến tính

Thuật toán tính lũy thừa nhanh. Giải thích một cách đơn giản