11 lỗi hay gặp khi quản lý bộ nhớ động

Con trỏ và cấp phát bộ nhớ động là điểm mạnh của ngôn ngữ C, giúp tối ưu tài nguyên memory. Tuy nhiên, khi sử dụng cấp phát bộ nhớ động, phát sinh khá nhiều bug cho lập trình viên chưa có kinh nghiệm. Dưới đây là 1 số lỗi hay gặp khi sử dụng cấp phát bộ nhớ động

Không check giá trị trả về của hàm malloc, calloc, realloc

Hàm malloc() trả về con trỏ trỏ tới vùng nhớ được cấp phát. Trong trường hợp không đủ vùng nhớ, hàm malloc() trả về NULL, việc truy cập vào con trỏ NULL gây ra lỗi segmentation fault.
Ví dụ 1: không check giá trị trả về hàm malloc()

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

#define SZ 10

void main(void)
{
	int i;
	int* p = (int*)malloc(SZ * sizeof(int));

	for (i = 0; i < SZ; i++)
	{
		p[i] = i * i;
	}

	free(p);
}

Giải thích:
Nếu câu lệnh cấp phát malloc() trả về NULL, chương trình trên bị crash ở dòng code p[i] = i * i; vì access con trỏ NULL.
Do vậy cần check giá trị trả về hàm malloc, xem ví dụ 2.

Ví dụ 2: check giá trị trả về hàm malloc()

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

#define SZ 10

void main(void)
{
	int i;
	int* p = (int*)malloc(SZ * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return;
	}
	
	for (i = 0; i < SZ; i++)
	{
	    p[i] = i * i;
	}

	free(p);
}

Giải thích:
Nếu hàm malloc() trả về NULL, in ra thông báo và kết thúc hàm. Đây chỉ là ví dụ minh họa xử lý trong trường hợp NULL. Tùy vào design source code, mà sẽ có cách xử lí khác nhau như khi hàm malloc() trả về NULL, thực hiện retry lại hàm malloc() để cấp memory.

Không khởi tạo vùng nhớ sau khi cấp phát

Ngôn ngữ C sử dụng hàm malloc() cấp phát vùng nhớ động theo block. Có thể quên không khởi tạo vùng nhớ hoặc tưởng nhầm rằng, hàm malloc() khởi tạo giá trị vùng nhớ là 0. Việc sử dụng vùng nhớ không được khởi tạo có thể gây ra một số bug tiềm ẩn.

Ví dụ 3: không khởi tạo vùng nhớ sau khi cấp phát

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

#define SZ 10

void main(void)
{
	int i;
	int* p = (int*)malloc(SZ * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return;
	}
	
	for (i = 0; i < SZ; i++)
	{
	    p[i] = p[i] * i;
	}

	free(p);
}

Giải thích:
Do chưa khởi tạo sau khi cấp phát vùng nhớ, giá trị p[i] là giá trị rác (garbage value), dẫn đến phép toán p[i] = p[i] * i cho kết quả sai.
Cần khởi tạo giá trị sau khi cấp phát vùng nhớ bằng hàm memset() hoặc dùng hàm calloc() thay cho hàm malloc().

Double free vùng nhớ

Hàm free(p) giải phóng vùng nhớ được cấp phát, giá trị con trỏ p vẫn chứa địa chỉ vùng nhớ đã giải phóng. Nếu tiếp tục gọi hàm free(p) sẽ gây lỗi heap, vì giải phóng vùng nhớ trái phép.

Ví dụ 4: gọi hàm free() 2 lần

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

#define SZ 10

void main(void)
{
	int i;
	int* p = (int*)malloc(SZ * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return;
	}

	free(p);
	free(p);
}

Giải thích:
Việc free() 2 lần gây ra lỗi heap corruption. Nên gán p = NULL sau lệnh free(), sẽ giúp hạn chế lỗi. Trong trường hợp free() 2 lần, hàm free(p) không được thực hiện nếu p = NULL.

Free() vùng nhớ không được tạo ra bởi hàm malloc(), calloc(), realloc()

Chỉ sử dụng hàm free() để giải phóng vùng nhớ được cấp phát bởi malloc(), calloc(), realloc(). Nếu cố sử dụng hàm free() để giải phóng vùng nhớ biến, mảng tĩnh,…, sẽ gây ra lỗi segmentation fault.

Ví dụ 5: free vùng nhớ biến local

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

void main(void)
{
	int i;
	int* p = &i;
	free(p);
}

Không giải phóng vùng nhớ được cấp phát

Việc không giải phóng vùng nhớ sẽ gây memory leak.

Ví dụ 6: cấp phát mà không giải phóng vùng nhớ

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

void main(void)
{
	int* p = (int*)malloc(10 * sizeof(int));
	
	/* do something*/
	/* not free() */
}

Tính kích thước của mảng được cấp phát động

Một số developer sử dụng toán tử sizeof() tính kích thước mảng nhớ động, dẫn đến kết quả sai. Vì toán tử sizeof() chỉ được sử dụng để tính kích thước mảng tĩnh, không sử dụng tính kích thước mảng động. Nếu bạn sử dụng mảng động, sizeof(p) trả về kích thước con trỏ p.

Ví dụ 7: sử dụng toán tử sizeof() tính kích thước mảng động

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

void main(void)
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return;
	}
	printf("size of pointer: %d", sizeof(p));
}

Output:

size of pointer: 4

Để tính toán kích thước vùng nhớ động, chúng ta có 2 cách: sử dụng hàm _msize() hoặc lưu kích thước vùng nhớ khi cấp phát vào phần tử đầu tiên.

Ví dụ 8: Sử dụng hàm _msize() tính kích thước bộ nhớ động

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

void main(void)
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return;
	}
	printf("size of memory: %d", _msize(p));
}

Output:

size of memory: 40

Ví dụ 9: lưu kích thước khi cấp phát vào phần tử đầu tiên

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

int* arrAlloc(int);

void main(void)
{
	int sz = 10;
	int* p = arrAlloc(sz);
	int* temp = p + 1;

	// Access length of array via pointer temp
	int len = temp[-1];
	for (size_t i = 0; i < len; i++)
	{
	    printf("temp[%d] = %d\n", i, temp[i]);
	}
	// Free memory 
	free(p);
	p = NULL;
}

int* arrAlloc(int sz)
{
	int* p = (int*)malloc((sz + 1) * sizeof(int));
	if (p == NULL)
	{
	    printf("Error in memory allocation");
	    return NULL;
	}
	p[0] = sz;

	// Initialize array
	for (size_t i = 1; i < sz + 1; i++)
	{
	    p[i] = i;
	}
	return p;
}

Giải thích:

int* p = (int*)malloc((sz + 1) * sizeof(int));
…
p[0] = sz;

Cấp phát thêm 1 phần tử để lưu kích thước mảng

int* p = arrAlloc(sz);

Con trỏ p trỏ tới vùng nhớ gồm 11 phần tử như sau

Output:

temp[0] = 1
temp[1] = 2
temp[2] = 3
temp[3] = 4
temp[4] = 5
temp[5] = 6
temp[6] = 7
temp[7] = 8
temp[8] = 9
temp[9] = 10

Truyền kích thước 0 vào hàm malloc()

Truyền kích thước 0 cho hàm malloc(), hàm malloc() cấp phát vùng nhớ 0 byte trong vùng nhớ heap, và trả về con trỏ khác NULL. Điều này gây ra bug, khi truy cập vùng nhớ 0 byte. Xem ví dụ sau:

Ví dụ 10: cấp phát vùng nhớ động 0 byte

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

void main(void)
{
	int* p = (int*)malloc(0 * sizeof(int));
	if (!p)
	{
	    printf("Error in memory allocation");
       return;
	}

	p[0] = 1;
	
	free(p);
	p = NULL;
}

Output:
Lỗi heap corruption tại câu lệnh p[0] = 1

Không count số lần cấp phát bộ nhớ động

Sử dụng biến global gCnt1 và gCnt2 để count số lượng cấp phát bộ nhớ động và số lần giải phóng bộ nhớ. Mỗi lần cấp phát thành công, tăng biến gCnt1 lên 1 đơn vị. Tương tự, mỗi lần giải phóng bộ nhớ, tăng biến gCnt2 lên 1 đơn vị. Đến cuối chương trình nếu biến gCnt1 = gCnt2, chương trình ko bị leak memory.

Ví dụ 11: implement hàm check memory leak

static unsigned int gCnt1 = 0;
static unsigned int gCnt2 = 0;

void* Memory_Allocate(size_t size)
{
    void* p = NULL;
    p = malloc(size);
    if (NULL != p)
    {
        ++gCnt1;
    }
    else
    {
        printf("Error in memory allocation");
    }
    return (p);
}
void Memory_Deallocate(void* p)
{
    if (p != NULL)
    {
        free(p);
        ++gCnt2;
    }
}
int Check_Memory_Leak(void)
{
    int iRet = 0;
    if (gCnt1 != gCnt2)
    {
        iRet = -1;
    }
    else
    {
        iRet = 0;
    }
    return iRet;
}

Truy cập vào phần tử nằm ngoài vùng nhớ cấp phát

Một số developer mắc phải lỗi access vào phần tử nằm ngoài vùng nhớ cấp phát, gây ra lỗi access violation. Để tránh lỗi này chúng ta có thể thêm các điều kiện kiểm tra trước khi access array.

Ví dụ 12: kiểm tra điều kiện tránh lỗi access violation

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

int main()
{
    int* p = NULL;
    int n = 10;
    int pos = 0;

    p = (int*)malloc(sizeof(int) * n);
    if (p == NULL)
    {
        return -1;
    }

    for (pos = 0; pos < n; pos++)
    {
        p[pos] = 10;
    }

    do {
        printf("Enter the array index = ");
        scanf("%d", &pos);
    } while (pos >= n && pos < 0);

    printf("p[%d] = %d", pos, p[pos]);
   
    free(p);
    p = NULL;

    return 0;
}

Giải thích:
Vòng lặp do while() chỉ cho phép người dùng nhập giá trị index hợp lệ

Output:

Enter the array index = 12
Enter the array index = 11
Enter the array index = 10
Enter the array index = 9
p[9] = 10

Modify con trỏ gốc

Sau khi cấp phát vùng nhớ, nếu bạn thay đổi giá trị biến con trỏ, tức là bạn thay đổi địa chỉ vùng nhớ được cấp phát, không lưu địa chỉ vùng nhớ cấp phát ban đầu -> không free vùng nhớ -> memory leak. Nếu cố free(), gây ra lỗi access violation.

Ví dụ 13: lỗi access violation do free vùng nhớ trái phép

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

int main()
{
    int* p = NULL;
    int n = 10;

    p = (int*)malloc(sizeof(int) * n);
    if (p == NULL)
    {
        return -1;
    }

    p++;
   
    free(p);
    p = NULL;

    return 0;
}

Giải thích:
Sau khi cấp phát p = 0x00f25020, sau lệnh p++ <=> p = p + 4 = 0x00f25024. Địa chỉ vùng nhớ bị thay đổi, vùng nhớ 0x00f25024 là vùng nhớ invalid, dẫn đến câu lệnh free(p) gây ra lỗi access violation.
Để tránh lỗi này, chúng ta chỉ nên thao tác tính toán trên con trỏ temp thay vì trên con trỏ gốc. Xem ví dụ 14.

Ví dụ 14: Thao tác tính toán trên con trỏ temp

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

int main()
{
    int* p = NULL;
    int n = 10;

    p = (int*)malloc(sizeof(int) * n);
    if (p == NULL)
    {
        return -1;
    }
    int* temp = p;

    temp++;
   
    free(p);
    p = NULL;

    return 0;
}

Dangling pointer

Cả 2 con trỏ p1, p2 cùng trỏ vào 1 vùng nhớ. Nếu gọi hàm free(p1), sau đó vẫn thao tác tính toán trên con trỏ p2, dẫn đến lỗi heap corruption. Con trỏ p2 được gọi là dangling pointer.

Ví dụ 15: Dangling pointer

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

int main()
{
    int* p1 = NULL;
    int* p2 = NULL;

    p1 = (int*)malloc(sizeof(int));
    if (p1 == NULL)
    {
        return -1;
    }
    *p1 = 100;
    printf(" *piData1 = %d\n", *p1);
    p2 = p1;
    printf(" *piData1 = %d\n", *p2);
    free(p1);

    *p2 = 50;
    printf(" *piData2 = %d\n", *p2);
    return 0;
}

Giải thích:
Cả 2 con trỏ cùng trỏ vào 1 vùng nhớ động, free(p1) giải phóng vùng nhớ, p2 trỏ vào vùng nhớ invalid -> *p2 = 50 gây ra lỗi heap corruption hoặc access violation.

Be the first to comment

Leave a Reply