Quản lý bộ nhớ trong C

Xin chào các nhà mã hóa tương lai! Hôm nay, chúng ta sẽ khám phá thế giới kỳ diệu của quản lý bộ nhớ trong C. Đừng lo lắng nếu bạn mới bắt đầu học lập trình; tôi sẽ dẫn dắt bạn qua hành trình này từng bước một, giống như tôi đã làm cho hàng trăm sinh viên trong những năm dạy học của mình. Vậy, hãy mang theo mũ bảo hiểm ảo của bạn, và chúng ta cùng khám phá công trường bộ nhớ máy tính!

C - Memory Management

Các hàm quản lý bộ nhớ động trong C

Trước khi chúng ta bắt đầu xây dựng những tòa nhà bộ nhớ cao tầng, hãy làm quen với các công cụ mà chúng ta sẽ sử dụng. Trong C, chúng ta có một bộ các hàm giúp chúng ta quản lý bộ nhớ động. Hãy tưởng tượng các hàm này như một bộ công cụ đáng tin cậy của bạn:

Hàm Mục đích
malloc() Phân bổ một khối bộ nhớ
calloc() Phân bổ và khởi tạo nhiều khối bộ nhớ
realloc() Thay đổi kích thước khối bộ nhớ đã phân bổ
free() Trả bộ nhớ đã phân bổ về hệ thống

Các hàm này giống như đội ngũ công nhân xây dựng cho các tòa nhà bộ nhớ của chúng ta. Mỗi hàm có công việc đặc biệt riêng, và chúng ta sẽ làm quen với tất cả chúng.

Phân bổ bộ nhớ động

Hãy tưởng tượng bạn đang lên kế hoạch cho một buổi tiệc, nhưng bạn không chắc chắn bao nhiêu khách sẽ đến. Đó là lúc phân bổ bộ nhớ động phát huy tác dụng! Thay vì.setup một số lượng cố định ghế, bạn có thể thêm hoặc bớt chúng theo nhu cầu. Hãy xem chúng ta làm điều này như thế nào trong C.

Hàm malloc()

Siêu anh hùng phân bổ bộ nhớ đầu tiên của chúng ta là malloc(). Nó có nghĩa là "phân bổ bộ nhớ" và được sử dụng để yêu cầu một khối bộ nhớ từ hệ thống.

#include <stdio.h>
#include <stdlib.h>

int main() {
int *numbers;
int size = 5;

numbers = (int*)malloc(size * sizeof(int));

if (numbers == NULL) {
printf("Phân bổ bộ nhớ thất bại!\n");
return 1;
}

for (int i = 0; i < size; i++) {
numbers[i] = i * 10;
printf("numbers[%d] = %d\n", i, numbers[i]);
}

free(numbers);
return 0;
}

Hãy phân tích này:

  1. Chúng ta bao gồm <stdlib.h> vì đó là nơi malloc() cư trú.
  2. Chúng ta khai báo một con trỏ numbers để lưu trữ mảng bộ nhớ phân bổ động.
  3. malloc(size * sizeof(int)) yêu cầu đủ bộ nhớ để chứa 5 số nguyên.
  4. Chúng ta ép kết quả thành (int*)malloc() trả về một con trỏ void.
  5. Luôn kiểm tra xem malloc() có thành công không! Nếu nó trả về NULL, chúng ta đã hết may mắn (và bộ nhớ).
  6. Bây giờ chúng ta có thể sử dụng numbers như một mảng bình thường.
  7. Đừng quên free() bộ nhớ khi bạn đã xong!

Hàm calloc()

Bây giờ, hãy gặp calloc(), người gọn gàng trong việc phân bổ bộ nhớ. Trong khi malloc() cho bạn bộ nhớ với bất kỳ rác nào có trước đó, calloc() dọn dẹp sau mình, khởi tạo tất cả bộ nhớ đã phân bổ thành zero.

#include <stdio.h>
#include <stdlib.h>

int main() {
int *numbers;
int size = 5;

numbers = (int*)calloc(size, sizeof(int));

if (numbers == NULL) {
printf("Phân bổ bộ nhớ thất bại!\n");
return 1;
}

for (int i = 0; i < size; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}

free(numbers);
return 0;
}

Những khác biệt chính ở đây:

  1. calloc() lấy hai đối số: số lượng phần tử và kích thước của mỗi phần tử.
  2. Tất cả các phần tử được khởi tạo thành zero, vì vậy đầu ra của chúng ta sẽ toàn zero.

Thay đổi kích thước và giải phóng bộ nhớ

Đôi khi, buổi tiệc của chúng ta lớn hơn hoặc nhỏ hơn dự kiến. Đó là lúc realloc() phát huy tác dụng!

Hàm realloc()

realloc() giống như một phù thủy có thể mở rộng hoặc thu nhỏ khối bộ nhớ của chúng ta.

#include <stdio.h>
#include <stdlib.h>

int main() {
int *numbers;
int size = 5;

numbers = (int*)malloc(size * sizeof(int));

if (numbers == NULL) {
printf("Phân bổ bộ nhớ thất bại!\n");
return 1;
}

for (int i = 0; i < size; i++) {
numbers[i] = i * 10;
printf("numbers[%d] = %d\n", i, numbers[i]);
}

// Mở rộng mảng của chúng ta
size = 10;
numbers = (int*)realloc(numbers, size * sizeof(int));

if (numbers == NULL) {
printf("Thay đổi kích thước bộ nhớ thất bại!\n");
return 1;
}

// Điền các phần tử mới
for (int i = 5; i < size; i++) {
numbers[i] = i * 10;
}

// In tất cả các phần tử
for (int i = 0; i < size; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}

free(numbers);
return 0;
}

Dưới đây là những gì đang xảy ra:

  1. Chúng ta bắt đầu với 5 phần tử, như trước đây.
  2. Chúng ta sử dụng realloc() để mở rộng mảng của chúng ta lên 10 phần tử.
  3. realloc() giữ nguyên dữ liệu gốc và cho chúng ta thêm không gian.
  4. Chúng ta điền các phần tử mới và in tất cả ra.

Hàm free()

Cuối cùng nhưng không kém phần quan trọng, chúng ta có free(), đội ngũ dọn dẹp của đội ngũ quản lý bộ nhớ của chúng ta. Luôn nhớ free() bộ nhớ bạn đã phân bổ khi bạn đã xong với nó!

#include <stdio.h>
#include <stdlib.h>

int main() {
int *numbers = (int*)malloc(5 * sizeof(int));

if (numbers == NULL) {
printf("Phân bổ bộ nhớ thất bại!\n");
return 1;
}

for (int i = 0; i < 5; i++) {
numbers[i] = i * 10;
printf("numbers[%d] = %d\n", i, numbers[i]);
}

free(numbers);  // Dọn dẹp!
numbers = NULL; // Thói quen tốt để tránh sử dụng bộ nhớ đã giải phóng

// Thử sử dụng 'numbers' bây giờ sẽ là một ý kiến tồi!

return 0;
}

Nhớ rằng:

  1. Luôn free() bộ nhớ bạn đã phân bổ khi bạn đã xong với nó.
  2. Đặt con trỏ thành NULL sau khi giải phóng để tránh sử dụng bộ nhớ đã giải phóng.
  3. Never try to free() memory you didn't allocate dynamically.

Và thế là xong, các bạn! Chúng ta đã xây dựng các tòa nhà quản lý bộ nhớ, học cách phân bổ không gian cho khách mời của chúng ta, điều chỉnh không gian khi cần thiết, và dọn dẹp sau khi tiệc kết thúc. Nhớ rằng, quản lý bộ nhớ tốt giống như một người chủ tốt - luôn đảm bảo bạn có đủ không gian cho khách của mình, linh hoạt trong việc điều chỉnh không gian, và dọn dẹp kỹ lưỡng khi tiệc kết thúc!

Tiếp tục luyện tập các khái niệm này, và sớm bạn sẽ trở thành kiến trúc sư bộ nhớ của chương trình của mình. Chúc các bạn may mắn và hy vọng các chương trình của bạn luôn chạy không có lỗi rò rỉ bộ nhớ!

Credits: Image by storyset