Việc kiểm tra null null trong C hoặc C ++ có nghĩa là gì?


21

Tôi đã học C ++ và tôi gặp khó khăn trong việc hiểu null. Cụ thể, các hướng dẫn tôi đã đọc đề cập đến việc thực hiện "kiểm tra null", nhưng tôi không chắc điều đó có nghĩa là gì hoặc tại sao nó lại cần thiết.

  • Chính xác thì null là gì?
  • "Kiểm tra null" có nghĩa là gì?
  • Tôi có luôn cần kiểm tra null không?

Bất kỳ ví dụ mã sẽ được nhiều đánh giá cao.



Tôi sẽ khuyên bạn nên có một số hướng dẫn tốt hơn, nếu tất cả những người bạn đọc nói về kiểm tra null mà không bao giờ giải thích chúng và cung cấp mã ví dụ ...
underscore_d

Câu trả lời:


26

Trong C và C ++, con trỏ vốn đã không an toàn, nghĩa là khi bạn hủy đăng ký một con trỏ, bạn có trách nhiệm đảm bảo rằng nó trỏ đến một nơi hợp lệ; đây là một phần của "quản lý bộ nhớ thủ công" (trái với các lược đồ quản lý bộ nhớ tự động được triển khai trong các ngôn ngữ như Java, PHP hoặc .NET runtime, không cho phép bạn tạo các tham chiếu không hợp lệ mà không cần nỗ lực đáng kể).

Một giải pháp phổ biến bắt được nhiều lỗi là đặt tất cả các con trỏ không trỏ đến bất cứ thứ gì như NULL(hoặc, trong C ++ chính xác 0) và kiểm tra xem trước khi truy cập con trỏ. Cụ thể, thông thường là khởi tạo tất cả các con trỏ tới NULL (trừ khi bạn đã có thứ gì đó để chỉ chúng khi bạn khai báo chúng) và đặt chúng thành NULL khi bạn deletehoặc free()chúng (trừ khi chúng ra khỏi phạm vi ngay sau đó). Ví dụ (bằng C, nhưng cũng có C ++ hợp lệ):

void fill_foo(int* foo) {
    *foo = 23; // this will crash and burn if foo is NULL
}

Một phiên bản tốt hơn:

void fill_foo(int* foo) {
    if (!foo) { // this is the NULL check
        printf("This is wrong\n");
        return;
    }
    *foo = 23;
}

Nếu không có kiểm tra null, việc chuyển một con trỏ NULL vào hàm này sẽ gây ra một segfault và bạn không thể làm gì - HĐH sẽ đơn giản giết chết tiến trình của bạn và có thể kết xuất lõi hoặc bật lên hộp thoại báo cáo sự cố. Với tính năng kiểm tra null, bạn có thể thực hiện xử lý lỗi thích hợp và phục hồi một cách duyên dáng - tự khắc phục sự cố, hủy bỏ thao tác hiện tại, viết một mục nhật ký, thông báo cho người dùng, bất cứ điều gì phù hợp.


3
@MrLister Ý bạn là gì, kiểm tra null không hoạt động trong C ++? Bạn chỉ cần khởi tạo con trỏ thành null khi bạn khai báo nó.
TZHX

1
Ý tôi là, bạn phải nhớ đặt con trỏ thành NULL nếu không nó sẽ không hoạt động. Và nếu bạn còn nhớ, nói cách khác, nếu bạn biết rằng con trỏ là NULL, bạn sẽ không cần phải gọi fill_foo. fill_foo kiểm tra xem con trỏ có giá trị không, nếu con trỏ có giá trị hợp lệ . Trong C ++, con trỏ không được đảm bảo là NULL có giá trị hợp lệ.
Ông Lister

4
Một khẳng định () sẽ là một giải pháp tốt hơn ở đây. Không có điểm nào cố gắng để "an toàn". Nếu NULL được thông qua, rõ ràng là nó sai, vậy tại sao không chỉ sụp đổ một cách rõ ràng để làm cho lập trình viên nhận thức đầy đủ? (Và trong sản xuất, điều đó không thành vấn đề, bởi vì bạn đã chứng minh rằng không ai sẽ gọi fill_foo () bằng NULL, phải không? Thực sự, điều đó không khó lắm.)
Ambroz Bizjak

7
Đừng quên đề cập rằng một phiên bản thậm chí tốt hơn của chức năng này nên sử dụng các tham chiếu thay vì con trỏ, làm cho kiểm tra NULL trở nên lỗi thời.
Doc Brown

4
Đây không phải là quản lý bộ nhớ thủ công, và một chương trình được quản lý cũng sẽ nổ tung, (hoặc ít nhất đưa ra một ngoại lệ, giống như một chương trình gốc trong hầu hết các ngôn ngữ), nếu bạn cố gắng bỏ qua một tham chiếu null.
Mason Wheeler

7

Các câu trả lời khác bao gồm khá nhiều câu hỏi chính xác của bạn. Kiểm tra null được thực hiện để đảm bảo rằng con trỏ bạn nhận được thực sự trỏ đến một thể hiện hợp lệ của một loại (đối tượng, nguyên thủy, v.v.).

Tôi sẽ thêm lời khuyên của riêng tôi ở đây, mặc dù. Tránh kiểm tra null. :) Null kiểm tra (và các hình thức lập trình phòng thủ khác) làm lộn xộn mã, và thực sự làm cho nó dễ bị lỗi hơn các kỹ thuật xử lý lỗi khác.

Kỹ thuật yêu thích của tôi khi nói đến con trỏ đối tượng là sử dụng mẫu Null Object . Điều đó có nghĩa là trả về một (con trỏ - hoặc thậm chí tốt hơn, tham chiếu đến một mảng) hoặc danh sách trống thay vì null hoặc trả về một chuỗi rỗng ("") thay vì null hoặc thậm chí chuỗi "0" (hoặc một cái gì đó tương đương với "không có gì "Trong ngữ cảnh) nơi bạn mong đợi nó được phân tích cú pháp thành một số nguyên.

Như một phần thưởng, đây là một chút gì đó mà bạn có thể chưa biết về con trỏ null, được (lần đầu tiên chính thức) được CAR Hoare thực hiện cho ngôn ngữ Algol W vào năm 1965.

Tôi gọi đó là sai lầm tỷ đô của tôi. Đó là phát minh của tài liệu tham khảo null vào năm 1965. Vào thời điểm đó, tôi đang thiết kế hệ thống loại toàn diện đầu tiên cho các tài liệu tham khảo bằng ngôn ngữ hướng đối tượng (ALGOL W). Mục tiêu của tôi là đảm bảo rằng tất cả việc sử dụng tài liệu tham khảo phải tuyệt đối an toàn, với việc kiểm tra được thực hiện tự động bởi trình biên dịch. Nhưng tôi không thể cưỡng lại sự cám dỗ để đưa vào một tài liệu tham khảo null, đơn giản vì nó rất dễ thực hiện. Điều này đã dẫn đến vô số lỗi, lỗ hổng và sự cố hệ thống, có thể gây ra hàng tỷ đô la đau đớn và thiệt hại trong bốn mươi năm qua.


6
Null Object thậm chí còn tệ hơn là chỉ có một con trỏ null. Nếu thuật toán X yêu cầu dữ liệu Y mà bạn không có, thì đó là một lỗi trong chương trình của bạn , mà bạn chỉ đơn giản là che giấu bằng cách giả vờ rằng bạn làm.
DeadMG

Nó phụ thuộc vào ngữ cảnh và cách kiểm tra "hiện diện dữ liệu" sẽ kiểm tra null trong cuốn sách của tôi. Theo kinh nghiệm của tôi, nếu một thuật toán hoạt động, giả sử, một danh sách và danh sách trống, thì thuật toán đơn giản là không có gì để làm và nó thực hiện điều đó bằng cách chỉ sử dụng các câu lệnh điều khiển tiêu chuẩn như for / foreach.
Yam Marcovic

Nếu thuật toán không có gì để làm, thì tại sao bạn thậm chí gọi nó? Và lý do bạn có thể muốn gọi nó ở nơi đầu tiên là vì nó làm một cái gì đó quan trọng .
DeadMG

@DeadMG Bởi vì các chương trình là về đầu vào và trong thế giới thực, không giống như các bài tập về nhà, đầu vào có thể không liên quan (ví dụ: trống). Mã vẫn được gọi một trong hai cách. Bạn có hai tùy chọn: hoặc bạn kiểm tra mức độ liên quan (hoặc sự trống rỗng) hoặc bạn thiết kế các thuật toán của mình để chúng đọc và hoạt động tốt mà không kiểm tra rõ ràng mức độ liên quan bằng cách sử dụng các câu lệnh có điều kiện.
Yam Marcovic

Tôi đến đây để đưa ra gần như cùng một nhận xét, vì vậy đã cho bạn phiếu bầu của tôi thay thế. Tuy nhiên, tôi cũng sẽ nói thêm rằng đây là đại diện cho một vấn đề lớn hơn về các đối tượng zombie - bất cứ khi nào bạn có các đối tượng có khởi tạo nhiều giai đoạn (hoặc phá hủy) không sống hoàn toàn nhưng không hoàn toàn chết. Khi bạn thấy mã "an toàn" trong các ngôn ngữ mà không có quyết toán xác định đã thêm kiểm tra trong mọi chức năng để xem liệu đối tượng đã được xử lý hay chưa, thì đây là vấn đề chung khiến nó đứng đầu. Bạn không bao giờ nên if-null, bạn nên làm việc với các trạng thái có các đối tượng họ cần cho cả đời.
ex0du5

4

Giá trị con trỏ null đại diện cho một "hư không" được xác định rõ; nó là một giá trị con trỏ không hợp lệ được đảm bảo để so sánh không bằng với bất kỳ giá trị con trỏ nào khác. Việc cố gắng hủy đăng ký một con trỏ null dẫn đến hành vi không xác định và thường sẽ dẫn đến lỗi thời gian chạy, vì vậy bạn muốn chắc chắn rằng một con trỏ không phải là NULL trước khi cố gắng hủy đăng ký. Một số hàm thư viện C và C ++ sẽ trả về một con trỏ null để chỉ ra một điều kiện lỗi. Ví dụ, hàm thư viện mallocsẽ trả về giá trị con trỏ null nếu nó không thể phân bổ số byte đã được yêu cầu và cố gắng truy cập bộ nhớ thông qua con trỏ đó (thường) sẽ dẫn đến lỗi thời gian chạy:

int *p = malloc(sizeof *p * N);
p[0] = ...; // this will (usually) blow up if malloc returned NULL

Vì vậy, chúng tôi cần đảm bảo malloccuộc gọi thành công bằng cách kiểm tra giá trị của pNULL:

int *p = malloc(sizeof *p * N);
if (p != NULL) // or just if (p)
  p[0] = ...;

Bây giờ, hãy treo tất của bạn một phút, điều này sẽ có một chút gập ghềnh.

Có một con trỏ null giá trị và một con trỏ null liên tục , và cả hai không nhất thiết phải giống nhau. Các con trỏ null giá trị là bất cứ điều gì đánh giá cao việc sử dụng kiến trúc cơ bản để đại diện cho "hư không". Giá trị này có thể là 0x00000000 hoặc 0xFFFFFFFF hoặc 0xDEADBEEF hoặc một cái gì đó hoàn toàn khác. Đừng cho rằng các con trỏ null giá trị luôn luôn là 0.

Hằng số con trỏ null , OTOH, luôn là biểu thức tích phân có giá trị 0. Theo như mã nguồn của bạn , 0 (hoặc bất kỳ biểu thức tích phân nào ước lượng thành 0) đại diện cho một con trỏ null. Cả C và C ++ đều định nghĩa macro NULL là hằng số con trỏ null. Khi mã của bạn được biên dịch, các con trỏ null liên tục sẽ được thay thế bằng con trỏ null phù hợp giá trị trong mã máy tạo ra.

Ngoài ra, hãy lưu ý rằng NULL chỉ là một trong nhiều giá trị con trỏ không hợp lệ có thể có ; nếu bạn khai báo một biến con trỏ tự động mà không khởi tạo nó một cách rõ ràng, chẳng hạn như

int *p;

giá trị được lưu trữ ban đầu trong biến là không xác định và có thể không tương ứng với địa chỉ bộ nhớ hợp lệ hoặc có thể truy cập. Thật không may, không có cách nào (di động) để biết liệu giá trị con trỏ không phải NULL có hợp lệ hay không trước khi thử sử dụng nó. Vì vậy, nếu bạn đang xử lý các con trỏ, thông thường nên khởi tạo rõ ràng chúng thành NULL khi bạn khai báo chúng và đặt chúng thành NULL khi chúng không chủ động chỉ vào bất cứ thứ gì.

Lưu ý rằng đây là một vấn đề trong C hơn là C ++; C ++ thành ngữ không nên sử dụng con trỏ nhiều như vậy.


3

Có một vài phương pháp, tất cả đều thực hiện cùng một việc.

int * foo = NULL; // đôi khi được đặt thành 0x00 hoặc 0 hoặc 0L thay vì NULL

kiểm tra null (kiểm tra xem con trỏ có null không), phiên bản A

if (foo == NULL)

kiểm tra null, phiên bản B

if (! foo) // vì NULL được định nghĩa là 0, nên foo sẽ trả về một giá trị từ một con trỏ null

kiểm tra null, phiên bản C

nếu (foo == 0)

Trong ba, tôi thích sử dụng kiểm tra đầu tiên vì nó nói rõ cho các nhà phát triển trong tương lai biết bạn đang cố kiểm tra VÀ điều đó cho thấy rõ rằng bạn mong muốn foo là một con trỏ.


2

Bạn không. Lý do duy nhất để sử dụng một con trỏ trong C ++ là vì bạn rõ ràng muốn có sự hiện diện của các con trỏ null; mặt khác, bạn có thể lấy một tài liệu tham khảo, cả về mặt ngữ nghĩa đều dễ sử dụng hơn và đảm bảo không có giá trị.


1
@James: 'mới' trong chế độ kernel?
Nemanja Trifunovic

1
@James: Việc triển khai C ++ đại diện cho các khả năng mà phần lớn các lập trình viên C ++ thích thú. Điều đó bao gồm tất cả các tính năng ngôn ngữ C ++ 03 (ngoại trừ export) và tất cả các tính năng thư viện C ++ 03 TR1 một đoạn tốt của C ++ 11.
DeadMG

5
Tôi làm mong muốn mọi người sẽ không nói rằng "tài liệu tham khảo đảm bảo không null." Họ không. Thật dễ dàng để tạo một tham chiếu null như một con trỏ null và chúng truyền theo cùng một cách.
mjfgates

2
@Stargazer: Câu hỏi là dư thừa 100% khi bạn chỉ sử dụng các công cụ theo cách mà các nhà thiết kế ngôn ngữ và thực hành tốt gợi ý bạn nên làm.
DeadMG

2
@DeadMG, không quan trọng là nó có thừa hay không. Bạn đã không trả lời câu hỏi . Tôi sẽ nói lại lần nữa: -1.
riwalk

-1

Nếu bạn không kiểm tra giá trị NULL, đặc biệt, nếu đó là một con trỏ tới một cấu trúc, bạn có thể đã gặp một lỗ hổng bảo mật - tính trung thực của con trỏ NULL. Việc hủy bỏ con trỏ NULL có thể dẫn đến một số lỗ hổng bảo mật nghiêm trọng khác như tràn bộ đệm, tình trạng chủng tộc ... có thể cho phép kẻ tấn công chiếm quyền kiểm soát máy tính của bạn.

Nhiều nhà cung cấp phần mềm như Microsoft, Oracle, Adobe, Apple ... phát hành bản vá phần mềm để khắc phục các lỗ hổng bảo mật này. Tôi nghĩ bạn nên kiểm tra giá trị NULL của từng con trỏ :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.