Menu

Con trỏ NULL trong C: NULL là gì và cách kiểm tra

NULL là địa chỉ mà một con trỏ giữ khi nó không trỏ tới đâu cả. Bài này giải thích NULL thực chất là gì, vì sao dereference nó gây crash, nó khác gì với 0 và với con trỏ chưa khởi tạo, cùng những cách viết phòng thủ giúp code tránh lỗi null.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Con trỏ (pointer) luôn giữ một giá trị nào đó. Khi nó chưa có đối tượng nào để trỏ tới, như trước khi được gán, sau khi thứ nó trỏ tới đã bị giải phóng, hoặc khi một hàm không có gì để trả về, nó cần một giá trị mang nghĩa rõ ràng là "không có gì ở đây". Giá trị đó là con trỏ null, và NULL là cái tên bạn viết ra.

Kỷ luật xoay quanh nó rất gọn mà lại luôn đáng công: gán con trỏ bằng NULL khi nó chưa có đích, kiểm tra NULL trước khi dereference, và trả về NULL khi không có kết quả.

NULL thực chất là gì

NULL là một macro, được định nghĩa trong <stddef.h> và cũng được kéo vào qua <stdio.h>, <stdlib.h>, <string.h> cùng nhiều header khác. Nó được mở rộng thành một hằng con trỏ null, thường là ((void*)0) hoặc đơn giản là 0.

Chuẩn C đảm bảo tính chất quan trọng nhất: một con trỏ null luôn so sánh khác với con trỏ tới bất kỳ đối tượng hay hàm có thật nào. Hai con trỏ null cùng kiểu thì luôn bằng nhau.

Phần lớn hệ thống in con trỏ null ra là (nil) hoặc 0x0, tức địa chỉ 0. Đó là cách cài đặt phổ biến chứ không phải quy tắc; chuẩn C không hề hứa các bit đều bằng 0. Điều nó hứa là phép so sánh, nên hãy viết p == NULL, đừng viết bất cứ thứ gì phụ thuộc vào cách biểu diễn.

Dereference NULL gây crash

Đây là lý do của mọi phép kiểm tra null từng được viết ra:

int *p = NULL;
printf("%d\n", *p);   // undefined behavior - almost certainly a crash

Trên Windows, macOS và Linux, trang đầu tiên của không gian địa chỉ được cố tình để trống, nên CPU chặn lần truy cập đó và hệ điều hành dừng chương trình. Bạn sẽ thấy:

Segmentation fault (core dumped)          # Linux
zsh: segmentation fault  ./program        # macOS

Cú crash tức thì đó là một tính năng. Nó biến một lỗi logic thành một thất bại ồn ào ngay tại dòng gây ra lỗi, thay vì một lần hỏng dữ liệu âm thầm mà ba tuần sau bạn mới phát hiện. Xem bài segmentation fault để biết cách đọc thông báo crash và tìm ra dòng lỗi.

Vì thế trong C, "null pointer" không phải là ngoại lệ mà bạn có thể bắt. Không có cơ chế nào để phục hồi; chỉ có cách kiểm tra trước.

Kiểm tra những gì có thể thất bại

Các hàm có thể thất bại sẽ báo hiệu bằng cách trả về NULL. Kiểm tra không phải là phép lịch sự tùy chọn: không kiểm tra thì sẽ crash.

Thư viện chuẩn có đầy những hàm như vậy: malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Tất cả đều trả lời "không có kết quả" bằng NULL. Xem bài cấp phát bộ nhớ động để hiểu đầy đủ về việc cấp phát.

Có hai cách viết phép kiểm tra, cả hai đều phổ biến:

if (p == NULL) { /* handle */ }   // explicit - preferred for clarity
if (!p)        { /* handle */ }   // terse - relies on NULL being falsy

Trong một điều kiện, con trỏ null được coi là sai còn con trỏ hợp lệ được coi là đúng, nên cả hai cách đều chạy. Cách viết tường minh dễ đọc hơn khi người lạ lướt qua code của bạn để tìm bug.

NULL, 0 và con trỏ chưa khởi tạo

Ba tình huống khác nhau mà người mới hay nhầm lẫn:

int *a = NULL;     "points at nothing" - testable, safe to check
int *b;            uninitialized - holds garbage, NOT testable
int *c = &x;       points at a real object

Nguy hiểm nhất là b. Các byte của nó là bất cứ thứ gì còn sót lại trong ô nhớ stack đó, có thể bằng 0 (và trông như ổn), cũng có thể là một địa chỉ trông rất hợp lý từ một lời gọi hàm trước đó. Không phép kiểm tra nào phân biệt được nó với một con trỏ hợp lệ, và hành vi còn thay đổi giữa bản build debug và bản build release.

Khởi tạo mọi con trỏ. Nếu chưa có địa chỉ nào, thì NULL chính là địa chỉ. Biên dịch với -Wall -Wextra và trình biên dịch sẽ cảnh báo giúp bạn nhiều chỗ dùng biến chưa khởi tạo.

Về 0: trong ngữ cảnh con trỏ, hằng số nguyên 0 chính là hằng con trỏ null, nên p = 0; là C hợp lệ. Dù vậy vẫn nên dùng NULL. Nó thể hiện rõ ý định, và nó quan trọng trong các lời gọi variadic, nơi trình biên dịch không thể tự chuyển kiểu giúp bạn:

execl("/bin/ls", "ls", 0);       // risky - may pass an int where a pointer is expected
execl("/bin/ls", "ls", (char *)NULL);   // correct

C23 bổ sung nullptr, một từ khóa có kiểu riêng, loại bỏ hoàn toàn kiểu mơ hồ này; NULL vẫn đúng và chạy được ở mọi nơi.

Các cách viết phòng thủ

Kiểm tra ngay đầu hàm nhận con trỏ.

Gán NULL sau khi giải phóng. free không thay đổi con trỏ của bạn, nó chỉ trả lại vùng nhớ. Con trỏ cũ còn sót lại là một con trỏ treo (dangling pointer), và dùng nó là undefined behavior, thường không crash ngay.

Có hai điều trong ví dụ trên đáng ghi nhớ. free(NULL) được định nghĩa là không làm gì cả, nên code dọn dẹp không bao giờ cần kiểm tra trước. Và gán p = NULL sau free biến một lỗi use-after-free, vốn có thể âm thầm làm hỏng dữ liệu, thành một cú crash tức thì và dễ gỡ lỗi.

Trả về NULL để báo "không có kết quả", và ghi rõ điều đó.

// returns a pointer to the matching element, or NULL if there is none
int *find(int *arr, int n, int target);

Ghi lại điều đó trong chú thích phía trên hàm. Người gọi biết có thể nhận NULL sẽ viết phép kiểm tra; người không biết thì sẽ không viết.

Danh sách kiểm tra nhanh

  • Khởi tạo mọi con trỏ, bằng một địa chỉ thật hoặc bằng NULL.
  • Kiểm tra giá trị trả về của malloc, calloc, realloc và fopen trước khi dùng.
  • Kiểm tra tham số con trỏ ở đầu mọi hàm có thể bị gọi với đầu vào sai.
  • Gán con trỏ bằng NULL ngay sau free.
  • Viết NULL chứ không phải 0 mỗi khi muốn nói tới con trỏ.
  • Build với -Wall -Wextra và, khi có thể, chạy với sanitizer: gcc -fsanitize=address,undefined bắt được lỗi dereference null kèm báo cáo chính xác.

Câu hỏi thường gặp

Con trỏ NULL trong C là gì?

Là con trỏ giữ hằng con trỏ null, một giá trị được đảm bảo luôn khác với địa chỉ của mọi đối tượng có thật. Đây là cách chuẩn để nói "con trỏ này chưa trỏ tới đâu cả", và NULL là macro đại diện cho nó, được định nghĩa trong <stddef.h> và một số header khác.

Chuyện gì xảy ra nếu dereference con trỏ NULL trong C?

Đó là undefined behavior. Trong thực tế, trên máy tính và máy chủ, chương trình crash ngay với lỗi segmentation fault vì địa chỉ 0 được cố tình để trống, không ánh xạ. Trên hệ thống nhúng không có cơ chế bảo vệ bộ nhớ, nó có thể âm thầm đọc hoặc làm hỏng thứ gì đó, và như vậy còn tệ hơn nhiều.

NULL có giống 0 trong C không?

Trong ngữ cảnh con trỏ thì có: hằng số nguyên 0 chính là hằng con trỏ null, nên cả p = 0; lẫn p == NULL đều hợp lệ. Nhưng hãy dùng NULL cho con trỏ và 0 cho số: người đọc biết ngay bạn muốn nói tới cái nào, và các lời gọi hàm variadic vẫn an toàn, vì ở đó một số 0 trơn có thể được truyền như int thay vì con trỏ.

Con trỏ NULL khác con trỏ chưa khởi tạo như thế nào?

Con trỏ NULL chắc chắn không trỏ tới đâu, và bạn có thể kiểm tra điều đó. Con trỏ chưa khởi tạo giữ bất kỳ byte nào tình cờ nằm trong vùng nhớ đó, có thể là một địa chỉ trông rất hợp lệ, nên không có phép kiểm tra nào bắt được nó. Luôn khởi tạo con trỏ bằng NULL khi bạn chưa có giá trị nào tốt hơn.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu