Lập trình C/C++

Cấp phát và giải phóng bộ nhớ trong lập trình C

Cấp phát và giải phóng bộ nhớ trong lập trình C
Được viết bởi Minh Hoàng

Series lập trình C/C++, ngôn ngữ lập trình hệ thống mạnh mẽ.

Tiếp tục loạt bài trong series về con trỏ, trong bài này chúng ta sẽ tìm hiểu những nội dung sau:

  1. Tổ chức bộ nhớ trong máy tính như thế nào?
  2. Tại sao cần cấp phát bộ nhớ cho con trỏ?
  3. Trong lập trình C có mấy cơ chế cấp phát bộ nhớ?
  4. Tại sao cần giải phóng bộ nhớ?
  5. Demo code thao tác cấp phát và giải phóng bộ nhớ của con trỏ
1. Tổ chức bộ nhớ trong máy tính như thế nào?
1. Tổ chức bộ nhớ trong máy tính như thế nào?

Trước khi tìm hiểu về cấp phát và giải phóng bộ nhớ, chúng ta cần biết được là bộ nhớ máy tính được tổ chức thế nào. Dưới đây là hình ảnh minh họa cho thứ tự các phân vùng trên bộ nhớ ảo:

Phân loại vùng nhớ trong máy tính

Phân loại vùng nhớ trong máy tính

1.1. Code Segment

Code segment (text segment): Là nơi lưu trữ mã máy dạng nhị phân. Có nghĩa là các chương trình mà chúng ta code là code trên ngôn ngữ tự nhiên, nhưng khi ở phân vùng này nó sẽ ở dạng mã máy nhị phân. Code segment chỉ chịu sự chi phối của hệ điều hành, người lập trình không thể can thiệp trực tiếp đến phân vùng này.

1.2. Data Segment

Data segment (initialized data segment): Là nơi chứa các biến kiểu static, biến toàn cục (global variable).

1.3. BSS Segment

BSS segment (uninitialized data segment) cũng được dùng để lưu trữ các biến kiểu static, biến toàn cục (global variable) nhưng chưa được khởi tạo giá trị cụ thể.

1.4. Heap

Là vùng nhớ không do CPU quản lý, người lập trình phải tự quản lý vùng nhớ này. Nó được sử dụng khi thực hiện cấp phát bộ nhớ động dùng cho con trỏ.

1.5. Stack

Call Stack (thường được gọi là Stack): Là vùng nhớ do CPU quản lý, người lập trình không thể can thiệp vào vùng nhớ này. Nếu cố tình can thiệp sẽ bị lỗi (code bên dưới, bạn chạy thử xem nó hiện thông báo lỗi như thế nào nhé). Vùng nhớ Stack được dùng để cấp phát bộ nhớ cho tham số của các hàm (function parameters) và biến cục bộ (local variables).

[code language=”c” highlight=”9,11″] #include <stdio.h>

int main()
{
int a = 5; // Khai báo 1 biến bình thường a => a thuộc vùng nhớ STACK

int *p; // Khai báo biến con trỏ p => p thuộc vùng nhớ HEAP

p = &a; // Cho p trỏ tới biến a => p lúc này cũng thuộc vùng nhớ STACK

free(p); // Vì vậy, nếu chúng ta thực hiện giải phóng vùng nhớ của p
// thì sẽ bị lỗi do đang giải phóng vùng nhớ STACK
return 0;
}
[/code]

2. Tại sao cần cấp phát bộ nhớ cho con trỏ?
2. Tại sao cần cấp phát bộ nhớ cho con trỏ?

– Vì nếu không cấp phát bộ nhớ cho con trỏ thì chúng ta không thể nhập dữ liệu trực tiếp cho con trỏ được.
Có thể hiểu thế này: khi muốn xây nhà thì chúng ta cần phải có đất. Khi người ta bán đất cho mình thì cũng giống như việc cấp phát bộ nhớ vậy. Không có đất thì chúng ta không thể xây nhà. Có đất rồi thì mới có chỗ để xây nhà. Có bộ nhớ rồi thì mới nhập dữ liệu được.

– Ở bài viết trước cách sử dụng con trỏ cơ bản: thì trong các ví dụ đó chúng ta chỉ cho con trỏ, trỏ đến ô nhớ của biến khác nên không cần phải cấp phát bộ nhớ nữa, vì khi đó con trỏ đã có ô nhớ để nhập liệu cho nó rồi (đó chính là ô nhớ mà nó trỏ đến).
Khi đó nó đã có nhà rồi (ở chung/ở ghép với đứa khác rồi) nên không cần phải mua đất nữa.

3. Trong lập trình C có mấy cơ chế cấp phát bộ nhớ?
3. Trong lập trình C có mấy cơ chế cấp phát bộ nhớ?

Trong lập trình C có 3 cơ chế cấp phát:

  1. malloc
  2. calloc
  3. realloc
Cơ chế malloc và calloc

Bảng sau sẽ thể hiện điểm giống nhau và khác nhau của 2 cơ chế này:

Mô tả Cơ chế malloc Cơ chế calloc
Cú pháp void* malloc (size_t size); void* calloc (size_t num, size_t size);
Ý nghĩa Cấp phát 1 vùng nhớ có kích thước là size Cấp phát 1 vùng nhớ chứa đủ num phần tử, mỗi phần tử có kích thước là size
Tham số 1 2
Kết quả trả về Con trỏ sẽ trỏ tới vùng nhớ vừa được cấp phát nếu thành công, con trỏ null nếu cấp phát thất bại.
Giá trị khởi tạo Giá trị rác Giá trị được khởi tạo bằng 0
Sử dụng Khi sử dụng phải tính toán (tổng số) kích thước vùng nhớ cần cấp phát trước rồi truyền vào cho malloc. Khi sử dụng calloc chỉ cần truyền vào số phần tử và kích thước 1 phần tử, thì calloc sẽ tự động tính toán và cấp phát vùng nhớ cần thiết.
Ví dụ Cấp phát mảng 10 phần tử kiểu int
Cách viết int *a = ( int * ) malloc( 10 * sizeof( int )); int *a = ( int * ) calloc( 10, sizeof( int ));

Lưu ý:giá trị khởi tạo cho vùng nhớ sau khi cấp phát thành công của 2 cơ chế malloccalloc là khác nhau. Do đó, tùy vào mục tiêu sử dụng mà dùng cơ chế phù hợp. Nếu bạn không quan tâm đến giá mặc định của vùng nhớ được cấp thì dùng malloc còn nếu muốn tất cả giá trị của toàn bộ ô nhớ sau khi được cấp là 0 thì dùng calloc.

// Khai báo cấp phát động bằng cơ chế calloc
int *a = ( int * )calloc( 10, sizeof( int ) );

/*
 * Tương đương với:
 */
// 1. Khai báo cấp phát động bằng cơ chế malloc
int *a = ( int * )malloc( 10 * sizeof( int ) );

// 2. Set giá trị mặc định cho toàn bộ ô nhớ sau khi cấp phát bằng 0 
memset( a, 0, 10 * sizeof( int ) );
Cơ chế realloc

– Có 2 dạng:

  1. Đối với vùng nhớ chưa được khởi tạo thì realloc có chức năng tạo mới vùng nhớ cho nó.

  2. Đối với vùng nhớ đã có sẵn (đã được tạo từ trước rồi) thì realloc có chức năng gia tăng / giảm bớt ô nhớ trên vùng nhớ đó.

– Cú pháp: void* realloc (void* ptr, size_t size);
Trong đó:
・ptr : là vùng nhớ mà con trỏ đang trỏ đến.
・size: là kích thước muốn tạo mới hoặc thay đổi.

4. Tại sao cần giải phóng bộ nhớ?
4. Tại sao cần giải phóng bộ nhớ?

– Vì bộ nhớ khi cấp phát cho con trỏ thuộc vùng nhớ HEAP(Là vùng nhớ CPU không quản lý, người lập trình phải tự quản lý vùng nhớ này) nên nếu như chúng ta không giải phóng thì những ô nhớ đó sẽ không bao giờ được giải phóng, do đó có thể đến một lúc nào đó sẽ xảy ra tình trạng tràn bộ nhớ (memory leak), dẫn đến máy bị đứng/ bị treo máy. Máy tính bây giờ cấu hình mạnh rồi nên tình trạng này chắc ít gặp. Tuy nhiên, bộ nhớ là có hạn, hãy tiết kiệm vì chính bạn, sử dụng xong thì nên giải phóng.

– Vậy có nghĩa là khi giải phóng bộ nhớ thì giá trị của con trỏ đang có sẽ bị mất!? Điều này là không đúng. Nếu như ngay khi chúng ta khai báo giải phóng mà có 1 tiến trình khác chiếm hữu ô nhớ đó thì giá trị hiện tại trên ô nhớ đó sẽ không còn nữa, còn nếu như không có tiến trình nào chiếm hữu thì giá trị hiện tại trên ô nhớ đó vẫn còn.
Giống như việc chúng ta bán đất, nếu sau khi bán có người mua, họ xây nhà, xây đường,… thì cây cối, đồ đạc trên đất chúng ta bán sẽ bị mất, còn không thì nó vẫn còn.

=> Bản chất của việc giải phóng bộ nhớ là thông báo cho chương trình biết là vùng nhớ này đã sử dụng xong rồi, không còn dùng nữa, hệ điều hành (CPU) có thể sử dụng nó cho 1 tiến trình khác. Còn việc giá trị của vùng nhớ sau khi được giải phóng còn hay không thì chúng ta không biết được.

– Cú pháp giải phóng vùng nhớ: void free (void* ptr);
Với: ptr là vùng nhớ mà con trỏ đang trỏ đến.

5. Demo code thao tác cấp phát và giải phóng bộ nhớ của con trỏ
5. Demo code thao tác cấp phát và giải phóng bộ nhớ của con trỏ

Demo 1: Nhập vào 2 số nguyên a, b. Tìm Min và Max của 2 số đó. Sử dụng con trỏ để làm.

[code language=”c” highlight=”59,60,64,65,70,71,97,98″] #include <stdio.h>
#include <stdlib.h> /* Khai báo thư viện này để dùng các hàm malloc, calloc, realloc, free */

/*
* Hàm tìm giá trị nhỏ nhất của 2 số
* [in ] 2 tham số là 2 con trỏ
* [out] 1 giá trị kiểu int
*/
// *a và *b ở đây: toán tử * biểu thị đây là 2 CON TRỎ a, b
int TimMin(int *a, int *b)
{
// Toàn bộ *a và *b ở đây: toán tử * để lấy GIÁ TRỊ của 2 con trỏ a, b
return *a < *b ? *a : *b;

/* Tương đương với cách viết:
if (*a < *b) {
return *a;
}
else {
return *b;
}
*/
}

/*
* Hàm tìm giá trị lớn nhất của 2 số
* [in ] 2 tham số là 2 con trỏ
* [out] 1 con trỏ kiểu int
*/
int* TimMax(int *a, int *b)
{
//return *a > *b ? a : b;
// Vì hàm return 1 con trỏ nên chỗ này ghi là a và b
// chứ nếu để là *a, *b thì có nghĩa là trả về giá trị.

if (*a > *b) {
return a;
}
else {
return b;
}
}

int main()
{
// Khai báo 2 con trỏ a, b
int *a, *b;

// Khởi tạo
a = NULL;
b = NULL;

/*
* 1. Vì 2 con trỏ a, b trỏ đến kiểu int VÀ các hàm malloc, calloc, realloc trả về 1 con trỏ vô kiểu void*
* Nên khi chúng ta sử dụng => cần ép kiểu cho nó, bằng cách đặt (int *) trước tên các hàm này.
* 2. a, b con trỏ trỏ đến kiểu int, nên chúng ta lấy kích thước của nó bằng cách dùng: sizeof(int *)
*/
// ① Cấp phát bộ nhớ theo cơ chế malloc
//a = (int *)malloc(sizeof(int *));
//b = (int *)malloc(sizeof(int *));

// ② Cấp phát bộ nhớ theo cơ chế calloc
// Số lượng con trỏ cần cấp phát 1, độ lớn 4bytes sizeof(int *)
//a = (int *)calloc(1, sizeof(int *));
//b = (int *)calloc(1, sizeof(int *));

// ③ Cấp phát bộ nhớ theo cơ chế realloc
// a, b là tạo mới vùng nhớ nên giá trị là 0, tr/h vùng nhớ đã có sẵn thì ghi tên biến con trỏ vào
// Kích thước là độ lớn của toàn vùng nhớ con trỏ dc cấp phát.
a = (int *)realloc(0, sizeof(int *));
b = (int *)realloc(0, sizeof(int *));

// Nhập liệu
printf( "\n Nhap vao a = " );
scanf_s("%d", a); // Nếu a là 1 biến bình thường thì sẽ ghi là: scanf("%d", &a);
// sử dụng &a để lấy địa chỉ nhập liệu cho biến a.
// Nhưng lúc này a đã là 1 con trỏ, bản chất con trỏ đã là 1 địa chỉ rồi
// nên ghi thẳng a, không dùng dấu & nữa.

printf( "\n Nhap vao b = " );
scanf_s("%d", b);

int Min = TimMin( a, b );

/* Đ/v hàm TimMax sẽ return 1 con trỏ nên sẽ có 2 cách để nhận kết quả */
/* Cách 1: Tạo 1 con trỏ *Max1 trỏ tới con trỏ mà được trả về bằng hàm TimMax */
int *Max1 = TimMax( a, b );

/* Cách 2: Tạo 1 biến thường Max2, thì trước hàm TimMax sẽ đặt dấu *
để lấy giá trị của con trỏ được trả về bằng hàm TimMax */
int Max2 = *TimMax( a, b );

// In kết quả
printf( "\n Min = %d & Max = %d, Max = %d", Min, *Max1, Max2 );

// Giải phóng con trỏ
free( a );
free( b );

getch();
return 0;
}
[/code]

Kết quả

Kết quả chương trình

Demo 2: Thực hiện cấp phát bộ nhớ con trỏ bằng cách tạo hàm cấp phát riêng.

[code language=”c” highlight=”10,20,47,48″] #include <stdio.h>
#include <stdlib.h> /* Khai báo thư viện này để dùng các hàm malloc, calloc, realloc, free */

/*
* Để tạo 1 hàm cấp phát ngoài hàm main thì có 2 cách:
* Cách 1: Truyền tham chiếu của con trỏ cần cấp phát vùng nhớ cho hàm.
* Cách 2: Truyền vào 1 mảng con trỏ cho hàm (Tức là đẩy con trỏ lên 1 mức cao hơn thành con trỏ cấp 2)
*/
// Cách 1: Truyền tham chiếu của con trỏ cần cấp phát vùng nhớ cho hàm.
void Cach1DungThamChieu(int *&x)
{
x = (int *)malloc(1 * sizeof(int *));
//Hoặc:
//x = (int *)calloc(1, sizeof(int *));
//Hoặc
//x = (int *)realloc(0, sizeof(int *));
}

// Cách 2: Truyền vào 1 mảng con trỏ cho hàm (Tức là đẩy con trỏ lên 1 mức cao hơn thành con trỏ cấp 2)
void Cach2DungConTroCap2(int **x)
{
*x = (int *)calloc(1, sizeof(int *));
}

// Tìm Min của 2 số
int TimMin(int *a, int *b)
{
return *a < *b ? *a : *b;
}

// Tìm Max của 2 số
int* TimMax(int *a, int *b)
{
return *a > *b ? a : b;
}

int main()
{
// Khai báo 2 con trỏ a, b
int *a, *b;

// Khởi tạo
a = NULL;
b = NULL;

// Cấp phát vùng nhớ
Cach1DungThamChieu( a );
Cach2DungConTroCap2( &b );

// Nhập liệu
printf( "\n Nhap vao a = " );
scanf_s( "%d", a );
printf( "\n Nhap vao b = " );
scanf_s( "%d", b );

int Min = TimMin( a, b );
int Max = *TimMax( a, b );

// In kết quả
printf("\n Min = %d & Max = %d", Min, Max);

// Giải phóng con trỏ
free( a );
free( b );

system( "pause" );
return 0;
}
[/code]

Cảm ơn bạn đã theo dõi. Đừng ngần ngại hãy cùng thảo luận với chúng tôi!

Giới thiệu

Minh Hoàng

Xin chào, tôi là Hoàng Ngọc Minh, hiện đang làm BrSE, tại công ty Toyota, Nhật Bản. Những gì tôi viết trên blog này là những trải nghiệm thực tế tôi đã đúc rút ra được trong cuộc sống, quá trình học tập và làm việc. Các bài viết được biên tập một cách chi tiết, linh hoạt để giúp bạn đọc có thể tiếp cận một cách dễ dàng nhất. Hi vọng nó sẽ có ích hoặc mang lại một góc nhìn khác cho bạn[...]

12 bình luận

Translate »