Lập trình C: Cấp phát bộ nhớ

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Tổng quan

Cho đến thời điểm này thì chúng ta đã biết là tên của một mảng thật ra là một con trỏ trỏ tới phần tử đầu tiên của mảng. Hơn nữa, ngoài cách định nghĩa một mảng thông thường có thể định nghĩa một mảng như là một biến con trỏ. Tuy nhiên, nếu một mảng được khai báo một cách bình thường, kết quả là một khối bộ nhớ cố định được dành sẵn tại thời điểm bắt đầu thực thi chương trình, trong khi điều này không xảy ra nếu mảng được khai báo như là một biến con trỏ. Sử dụng một biến con trỏ để biểu diễn một mảng đòi hỏi việc gán một vài ô nhớ khởi tạo trước khi các phần tử mảng được xử lý. Sự cấp phát bộ nhớ như vậy thông thường được thực hiện bằng cách sử dụng hàm thư viện malloc().

Xét một ví dụ là một mảng số nguyên một chiều ary có 20 phần tử có thể được khai báo là int *ary; thay vì int ary[20];

Tuy nhiên, ary sẽ không được tự động gán một khối bộ nhớ khi nó được khai báo như là một biến con trỏ, trong khi một khối ô nhớ đủ để chứa 10 số nguyên sẽ được dành sẵn nếu ary được khai báo như là một mảng. Nếu ary được khai báo như là một con trỏ, số lượng bộ nhớ có thể được gán như sau: ary = malloc(20 *sizeof(int));

Thao tác này dẫn đến trình biên dịch dành một khối bộ nhớ có kích thước (tính theo byte) tương đương với kích thước của một số nguyên. Ở đây, một khối bộ nhớ cho 20 số nguyên được cấp phát. 20 con số gán với 20 bytes (một byte cho một số nguyên) và được nhân với sizeof(int), sizeof(int) sẽ trả về kết quả 2, nếu máy tính dùng 2 bytes để lưu trữ một số nguyên. Nếu một máy tính sử dụng 1 byte để lưu một số nguyên, hàm sizeof() không đòi hỏi ở đây. Tuy nhiên, sử dụng nó sẽ tạo khả năng uyển chuyển cho mã lệnh. Hàm malloc() trả về một con trỏ chứa địa chỉ vị trí bắt đầu của vùng nhớ được cấp phát. Nếu không gian bộ nhớ yêu cầu không có, malloc() trả về giá trị NULL. Sự cấp phát bộ nhớ theo cách này, nghĩa là, khi được yêu cầu trong một chương trình được gọi là Cấp phát bộ nhớ động.

Trước khi tiếp tục, chúng ta hãy thảo luận về khái niệm Cấp phát bộ nhớ động. Một chương trình C có thể lưu trữ các thông tin trong bộ nhớ của máy tính theo hai cách chính. Phương pháp thứ nhất bao gồm các biến toàn cục và cục bộ - bao gồm các mảng. Trong trường hợp các biến toàn cục và biến tĩnh, sự lưu trữ là cố định suốt thời gian thực thi chương trình. Các biến này đòi hỏi người lập trình phải biết trước tổng số dung lượng bộ nhớ cần thiết cho mỗi trường hợp. Phương pháp thứ hai, thông tin có thể được lưu trữ thông qua Hệ thống cấp phát động của C. Trong phương pháp này, sự lưu trữ thông tin được cấp phát từ vùng nhớ còn tự do và khi cần thiết.

Hàm malloc()

malloc() là một trong các hàm cấp phát vùng nhớ thường được dùng nhất, nó cho phép thực hiện việc cấp phát bộ nhớ từ vùng nhớ còn tự do. Tham số của malloc() là một số nguyên xác định số byte cần thiết.

Một ví dụ khác, xét mảng ký tự hai chiều ch_ary có 10 dòng và 20 cột. Sự khai báo và cấp phát bộ nhớ trong trường hợp này phải như sau:

char (*ch_ary)[20];
ch_ary = (char*)malloc(10*20*sizeof(char));

Như đã nói ở trên, malloc() trả về một con trỏ trỏ đến kiểu rỗng (void). Tuy nhiên, vì ch_ary là một con trỏ kiểu char, sự chuyển đổi kiểu là cần thiết. Trong câu lệnh trên, (char*) đổi kiểu trả về của malloc()  hành một con trỏ trỏ đến kiểu char.

Tuy nhiên, nếu sự khai báo của mảng phải chứa phép gán các giá trị khởi tạo thì mảng phải được khai báo theo cách bình thường, không thể dùng một biến con trỏ:

int ary[10] = {1,2,3,4,5,6,7,8,9,10};
//hoc:
int ary[] = {1,2,3,4,5,6,7,8,9,10};

Ví dụ sau đây tạo một mảng một chiều và sắp xếp mảng theo thứ tự tăng dần. Chương trình sử dụng con trỏ và hàm malloc() để gán bộ nhớ.

#include<stdio.h>
#include<malloc.h>

main() {
  int
  *p, n, i, j, temp;
  printf("\nNhap so luong phan tu ban muon lam viec: ");
  scanf("%d", &n);
  p = (int*)malloc(n * sizeof(int));
  for(i = 0; i < n; ++i) {
    printf("\nPhan tu thu %d: ", i + 1);
    scanf("%d", p + i);
  }
  
  //Sp xếp tăng dn:
  for(i = 0; i < n - 1; ++i)
    for(j = i + 1; j < n; ++j)
      if(*(p + i) > *(p + j)) {
        temp = *(p + i);
        *(p + i) = *(p + j);
        *(p + j) = temp;
      }
  
  printf("\nSau khi sap xep tang dan, ta duoc:\n");
  for(i = 0; i < n; ++i)
    printf("%d\n", *(p + i));

  return 0;
}

Chú ý lệnh p = (int*)malloc(n*sizeof(int)); . Ở đây, p được khai báo như một con trỏ trỏ đến một mảng và được gán bộ nhớ sử dụng malloc().

Dữ liệu được đọc vào sử dụng hàm scanf(): scanf("%d",p+i);

Trong scanf(), biến con trỏ được sử dụng để lưu dữ liệu vào trong mảng.

Các phần tử mảng đã lưu trữ được hiển thị bằng hàm printf(): printf("%d\n", *(p + i));

Chú ý dấu * trong trường hợp này, vì giá trị lưu trong vị trí đó phải được hiển thị. Không có dấu *, printf() sẽ hiển thị địa chỉ.

Hàm free()

Hàm này có thể được sử dụng để giải phóng bộ nhớ khi nó không còn cần thiết.

Dạng tổng quát của hàm free(): void free( void *ptr );

Hàm free() giải phóng không gian được trỏ bởi ptr, không gian được giải phóng này có thể sử dụng trong tương lai. ptr đã sử dụng trước đó bằng cách gọi đến malloc(), calloc(), hoặc realloc().

Ví dụ bên dưới sẽ hỏi bạn có bao nhiêu số nguyên sẽ được bạn lưu vào trong một mảng. Sau đó sẽ cấp phát bộ nhớ động bằng cách sử dụng malloc và lưu số lượng số nguyên, in chúng ra, và sau đó xóa bộ nhớ cấp phát bằng cách sử dụng free.

#include <stdio.h>
#include <stdlib.h> //thư vin cha các hàm malloc và free

int main() {
  int number;
  int *ptr;
  int i;
  printf("Ban muon luu tru bao nhieu so nguyen? ");
  scanf("%d", &number);
  ptr = (int *) malloc (number * sizeof(int)); //cp phát vùng nh  if(ptr != NULL) {
    for(i = 0; i < number; i++) {
      *(ptr+i) = i;
    }
    for(i=number ; i>0 ; i--) {
      printf("%d\n", *(ptr+(i-1))); //in ra màn hình theo th t ngược
    }
    free(ptr); //gii phóng vùng nh    return 0;
  }
  else {
    printf("\nCap phat vung nho khong thanh cong - khong du vung nho.\n");
    return 1;
  }
}

Kết quả sẽ là như sau nếu giá trị được nhập vào là 3:

c: hàm free()

Hàm calloc()

calloc tương tự như malloc, nhưng khác biệt chính là mặc nhiên các giá trị được lưu trong không gian bộ nhớ đã cấp phát là 0. Với malloc, cấp phát bộ nhớ có thể có giá trị bất kỳ.

calloc yêu cầu hai đối số. Đối số thứ nhất là số các biến mà bạn muốn cấp phát bộ nhớ cho. Đối số thứ hai là kích thước của mỗi biến.

void *calloc( size_t num, size_t size );

Giống như malloc, calloc sẽ trả về một con trỏ rỗng (void) nếu sự cấp phát bộ nhớ là thành công, ngược lại nó sẽ trả về một con trỏ NULL.

Ví dụ bên dưới chỉ ra cho bạn gọi hàm calloc như thế nào và tham chiếu đến ô nhớ đã cấp phát sử dụng một chỉ số mảng. Giá trị khởi tạo của vùng nhớ đã cấp phát được in ra trong vòng lặp for.

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

int main() {
  float *calloc1, *calloc2;
  int i;

  calloc1 = (float *) calloc(3, sizeof(float));
  calloc2 = (float *) calloc(3, sizeof(float));

  if(calloc1 != NULL && calloc2 != NULL) {
    for(i = 0; i < 3; i++) {
      printf("\ncalloc1[%d] mang gia tri %05.5f ", i, calloc1[i]);
      printf("\ncalloc2[%d] mang gia tri %05.5f", i, *(calloc2 + i));
    }
    free(calloc1);
    free(calloc2);
    return 0;
  }
  else {
    printf("Not enough memory\n");
    return 1;
  }
}

Kết quả demo:

c: hàm calloc()

Ở tất cả các dòng máy khác nhau thì các mảng calloc1 và calloc2 phải chứa các giá trị 0. calloc đặc biệt hữu dụng khi bạn đang sử dụng mảng đa chiều. Đây là một ví dụ khác minh họa cách dùng của hàm calloc().

/* Chương trình này s yêu cu nhp s lượng phn t, cp phát vùng nh, nhp liu cho tng phn t, tính tng, cui cùng in ra s lượng phn t và tng. */
#include <stdio.h>
#include <stdlib.h>

main() {
  int *a, i, n, sum = 0;
  printf("\n%s%s", "Demo tao mot mang dong. \n\n", "Moi ban nhap vao kich thuoc mang: ");
  scanf("%d", &n); //ly s phn t mng
  a = (int *) calloc (n, sizeof(int)); //cp phát vùng nh  for(i = 0; i < n; i++){ //tiến hành nhp liu cho tng phn t mng
    printf("Enter value %d: ", i+1);
    scanf("%d", a + i);
  }
  /* sum the values */
  for(i = 0; i < n; i++ )
    sum += a[i];
  free(a); //gii phóng vùng nh  /* in ra s luong phn t và tng */
  printf("\n%s%7d\n%s%7d\n\n", "So luong phan tu: ", n, "Tong cua cac phan tu: ", sum);

  return 0;
}

Hàm realloc()

Giả sử chúng ta đã cấp phát một số bytes cho một mảng nhưng sau đó nhận ra là bạn muốn thêm các giá trị. Bạn có thể sao chép mọi thứ vào một mảng lớn hơn, cách này không hiệu quả. Hoặc bạn có thể cấp phát thêm các bytes sử dụng bằng cách gọi hàm realloc, mà dữ liệu của bạn không bị mất đi.

realloc() bao gồm hai đối số. Đối số thứ nhất là một con trỏ tham chiếu đến bộ nhớ. Đối số thứ hai là tổng số bytes bạn muốn cấp phát thêm.

void *realloc( void *ptr, size_t size );

Nếu đối số thứ hai là số 0 thì tương đương với việc gọi hàm free().

Nếu cấp phát vùng nhớ thành công thì realloc sẽ trả về một con trỏ rỗng (void), ngược lại một con trỏ NULL được trả về.

Ví dụ sau đây sử dụng calloc để cấp phát đủ bộ nhớ cho một mảng int có năm phần tử. Sau đó realloc được gọi để mở rộng mảng để có thể chứa bảy phần tử.

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

int main() {
  int *ptr;
  int i;

  ptr = (int *)calloc(5, sizeof(int *));

  if(ptr!=NULL) {
    *ptr = 1;
    *(ptr + 1) = 2;
    ptr[2] = 4;
    ptr[3] = 8;
    ptr[4] = 16;
    /* ptr[5] = 32; lnh này s không thc hin được  đây vì hết vùng nh */
    ptr = (int *)realloc(ptr, 7 * sizeof(int)); //cp phát thêm hai vùng nh na
    if(ptr!=NULL) {
      printf("Bay gio se cap phat them vung nho... \n");
      ptr[5] = 32; /* lúc này câu lnh này mi hp l */
      ptr[6] = 64;
      for(i = 0;i < 7; i++) {
        printf("ptr[%d] chua gia tri %d\n", i, ptr[i]);
      }
      realloc(ptr, 0); /* tương đương gi ti hàm free(ptr); */
      return 0;
    }
    else {
      printf("Khong cap phat them duoc - khong du vung nho!.\n");
      return 1;
    }
  }
  printf("Khong cap phat them duoc - khong du vung nho!.\n");
  return 1;
}

Kết quả:

c: hàm relloc()

Chú ý hai cách khác nhau được sử dụng khi khởi tạo mảng: ptr[2] = 4 là tương đương với *(ptr + 2) = 4 (chỉ dễ đọc hơn!).

Trước khi sử dụng realloc, việc gán một giá trị đến phần tử ptr[5] không gây ra lỗi cho trình biên dịch. Chương trình vẫn thực thi, nhưng ptr[5] không chứa giá trị mà bạn đã gán.

» Tiếp: Cấu trúc (Struct)
« Trước: Con trỏ (Pointer)
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!