Có phải thực hành tốt để NULL một con trỏ sau khi xóa nó?


150

Tôi sẽ bắt đầu bằng cách nói, sử dụng con trỏ thông minh và bạn sẽ không bao giờ phải lo lắng về điều này.

Các vấn đề với mã sau đây là gì?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

Điều này đã gây ra bởi một câu trả lời và bình luận cho một câu hỏi khác. Một bình luận từ Neil Butterworth đã tạo ra một số upvote:

Đặt con trỏ thành NULL sau khi xóa không phải là thông lệ tốt trong C ++. Có những lúc nó là một điều tốt để làm, và có những lúc nó là vô nghĩa và có thể che giấu lỗi.

Có rất nhiều trường hợp nó sẽ không giúp đỡ. Nhưng theo kinh nghiệm của tôi, nó không thể làm tổn thương. Ai đó soi sáng cho tôi.


6
@Andre: Về mặt kỹ thuật, nó không được xác định. Điều có thể xảy ra là bạn truy cập cùng bộ nhớ như trước đây, nhưng giờ đây nó có thể được sử dụng bởi một thứ khác. Nếu bạn xóa bộ nhớ hai lần, nó có khả năng làm hỏng việc thực hiện chương trình của bạn theo một cách khó tìm. Tuy nhiên, nó an toàn cho deletemột con trỏ null, đó là một lý do khiến con trỏ không có thể tốt.
David Thornley

5
@ André Pena, nó không được xác định. Thường thì nó thậm chí không lặp lại. Bạn đặt con trỏ thành NULL để làm cho lỗi hiển thị rõ hơn khi gỡ lỗi và có thể làm cho nó lặp lại nhiều hơn.
Đánh dấu tiền chuộc

3
@ André: Không ai biết. Đó là hành vi không xác định. Nó có thể gặp sự cố với vi phạm quyền truy cập hoặc có thể ghi đè lên bộ nhớ được sử dụng bởi phần còn lại của ứng dụng. Tiêu chuẩn ngôn ngữ không đảm bảo cho những gì xảy ra và vì vậy bạn không thể tin tưởng vào ứng dụng của mình một khi nó đã xảy ra. Nó có thể đã bắn tên lửa hạt nhân hoặc định dạng ổ cứng của bạn. nó có thể làm hỏng bộ nhớ ứng dụng của bạn hoặc nó có thể khiến quỷ bay ra khỏi mũi bạn. Tất cả các cược đã tắt.
jalf

16
Quỷ bay là một tính năng, không phải là một lỗi.
bóng

3
Câu hỏi này không phải là một bản sao vì câu hỏi khác là về C và câu hỏi này là về C ++. Rất nhiều câu trả lời xoay quanh những thứ như con trỏ thông minh, không có sẵn trong C ++.
Adrian McCarthy

Câu trả lời:


85

Đặt con trỏ thành 0 (là "null" trong C ++ tiêu chuẩn, NULL xác định từ C có phần khác nhau) để tránh sự cố khi xóa hai lần.

Hãy xem xét những điều sau đây:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

Trong khi:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

Nói cách khác, nếu bạn không đặt con trỏ bị xóa thành 0, bạn sẽ gặp rắc rối nếu bạn thực hiện xóa hai lần. Một đối số chống lại việc thiết lập các con trỏ thành 0 sau khi xóa sẽ là làm như vậy chỉ cần che dấu hai lần xóa các lỗi và bỏ qua chúng.

Rõ ràng tốt nhất là không có lỗi xóa hai lần, nhưng tùy thuộc vào ngữ nghĩa sở hữu và vòng đời đối tượng, điều này có thể khó đạt được trong thực tế. Tôi thích một lỗi xóa mặt nạ kép hơn UB.

Cuối cùng, một sidenote liên quan đến việc quản lý phân bổ đối tượng, tôi khuyên bạn nên xem xét std::unique_ptrquyền sở hữu nghiêm ngặt / số ít, std::shared_ptrcho quyền sở hữu chung hoặc thực hiện con trỏ thông minh khác, tùy thuộc vào nhu cầu của bạn.


13
Ứng dụng của bạn sẽ không luôn bị sập khi xóa hai lần. Tùy thuộc vào những gì xảy ra giữa hai lần xóa, bất cứ điều gì cũng có thể xảy ra. Rất có thể, bạn sẽ làm hỏng heap của mình và một lúc nào đó bạn sẽ gặp sự cố trong một đoạn mã hoàn toàn không liên quan. Mặc dù segfault thường tốt hơn là âm thầm bỏ qua lỗi, segfault không được đảm bảo trong trường hợp này và đó là tiện ích đáng ngờ.
Adam Rosenfield

28
Vấn đề ở đây là thực tế là bạn đã xóa hai lần. Làm cho con trỏ NULL chỉ che giấu thực tế rằng nó không sửa nó hoặc làm cho nó an toàn hơn. Hãy tưởng tượng một nhân vật chính trở lại một năm sau đó và thấy foo bị xóa. Bây giờ anh ta tin rằng anh ta có thể sử dụng lại con trỏ, tiếc là anh ta có thể bỏ lỡ lần xóa thứ hai (nó thậm chí có thể không ở cùng chức năng) và bây giờ việc sử dụng lại con trỏ giờ đã bị xóa bởi lần xóa thứ hai. Bất kỳ quyền truy cập nào sau lần xóa thứ hai bây giờ là một vấn đề lớn.
Martin York

11
Đúng là cài đặt con trỏ để NULLcó thể che dấu một lỗi xóa kép. (Một số người có thể coi mặt nạ này thực sự là một giải pháp - nhưng nó không phải là một giải pháp tốt vì nó không đi đến gốc rễ của vấn đề.) Nhưng không đặt nó thành mặt nạ NULL xa hơn (FAR!) vấn đề phổ biến của việc truy cập dữ liệu sau khi nó đã bị xóa.
Adrian McCarthy

AFAIK, std :: auto_ptr đã không được chấp nhận trong tiêu chuẩn c ++ sắp tới
rafak

Tôi sẽ không nói phản đối, nó làm cho nó có vẻ như ý tưởng đã biến mất. Thay vào đó, nó được thay thế bằng unique_ptr, điều mà auto_ptrcố gắng làm, với ngữ nghĩa di chuyển.
GManNickG

55

Đặt con trỏ thành NULL sau khi bạn đã xóa những gì nó chỉ ra chắc chắn không thể làm tổn thương, nhưng nó thường là một phần trợ giúp ban nhạc cho một vấn đề cơ bản hơn: Tại sao bạn lại sử dụng một con trỏ ở vị trí đầu tiên? Tôi có thể thấy hai lý do điển hình:

  • Bạn chỉ đơn giản muốn một cái gì đó được phân bổ trên đống. Trong trường hợp bọc nó trong một đối tượng RAII sẽ an toàn và sạch sẽ hơn nhiều. Kết thúc phạm vi của đối tượng RAII khi bạn không còn cần đối tượng nữa. Đó là cách std::vectorhoạt động, và nó giải quyết vấn đề vô tình để lại con trỏ để giải phóng bộ nhớ xung quanh. Không có con trỏ.
  • Hoặc có lẽ bạn muốn một số ngữ nghĩa sở hữu chia sẻ phức tạp. Con trỏ được trả về newcó thể không giống với con trỏ deleteđược gọi trên. Trong khi đó, nhiều đối tượng có thể đã sử dụng đối tượng. Trong trường hợp đó, một con trỏ dùng chung hoặc một cái gì đó tương tự sẽ được ưu tiên hơn.

Nguyên tắc nhỏ của tôi là nếu bạn để lại các con trỏ xung quanh trong mã người dùng, bạn đang làm sai. Con trỏ không nên ở đó để chỉ vào rác ở nơi đầu tiên. Tại sao không có một đối tượng chịu trách nhiệm đảm bảo tính hợp lệ của nó? Tại sao phạm vi của nó không kết thúc khi đối tượng trỏ tới?


15
Vì vậy, bạn đang đưa ra lập luận rằng không nên có một con trỏ thô ngay từ đầu và bất cứ điều gì liên quan đến con trỏ đã nói không nên được ban phước với thuật ngữ "thực hành tốt"? Đủ công bằng.
Đánh dấu tiền chuộc

7
Vâng, nhiều hay ít. Tôi sẽ không nói rằng không có gì liên quan đến một con trỏ thô có thể được gọi là một thực hành tốt. Chỉ cần đó là ngoại lệ chứ không phải là quy tắc. Thông thường, sự hiện diện của con trỏ là một chỉ báo cho thấy có gì đó không đúng ở cấp độ sâu hơn.
jalf

3
nhưng để trả lời câu hỏi ngay lập tức, không, tôi không thấy cách đặt con trỏ thành null có thể gây ra lỗi.
jalf

7
Tôi không đồng ý - có những trường hợp khi sử dụng một con trỏ tốt. Ví dụ, có 2 biến trên ngăn xếp và bạn muốn chọn một trong số chúng. Hoặc bạn muốn truyền một biến tùy chọn cho một hàm. Tôi muốn nói, bạn không bao giờ nên sử dụng một con trỏ thô kết hợp với new.
rlbond

4
khi một con trỏ đi ra khỏi phạm vi, tôi không thấy bất cứ điều gì hoặc bất cứ ai có thể cần phải đối phó với nó.
jalf

43

Tôi đã có một thực tiễn tốt hơn nữa: Nếu có thể, hãy kết thúc phạm vi của biến!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

17
Vâng, RAII là bạn của bạn. Gói nó trong một lớp và nó trở nên đơn giản hơn. Hoặc không tự xử lý bộ nhớ bằng cách sử dụng STL!
Brian

24
Vâng, đó thực sự là lựa chọn tốt nhất. Không trả lời câu hỏi mặc dù.
Đánh dấu tiền chuộc

3
Đây dường như chỉ là sản phẩm phụ của việc sử dụng khoảng thời gian phạm vi chức năng và không thực sự giải quyết được vấn đề. Khi bạn đang sử dụng con trỏ, bạn thường chuyển các bản sao của chúng sâu vài lớp và sau đó phương pháp của bạn thực sự vô nghĩa trong nỗ lực giải quyết vấn đề. Mặc dù tôi đồng ý rằng thiết kế tốt sẽ giúp bạn cách ly các lỗi, tôi không nghĩ rằng phương pháp của bạn là phương tiện chính cho mục đích đó.
San Jacinto

2
Hãy nghĩ về nó, nếu bạn có thể làm điều này, tại sao bạn lại không quên đống và rút hết bộ nhớ ra khỏi ngăn xếp?
San Jacinto

4
Ví dụ của tôi là cố ý tối thiểu. Ví dụ, thay vì mới, có thể đối tượng được tạo bởi một nhà máy, trong trường hợp đó, nó không thể đi vào ngăn xếp. Hoặc có thể nó không được tạo ở đầu phạm vi, nhưng nằm trong một số cấu trúc. Điều tôi đang minh họa là cách tiếp cận này sẽ tìm thấy bất kỳ việc sử dụng sai con trỏ nào trong thời gian biên dịch , trong khi NULLing nó sẽ tìm thấy bất kỳ việc sử dụng sai nào trong thời gian chạy .
Don Neufeld

31

Tôi luôn đặt một con trỏ tới NULL(ngay bây giờ nullptr) sau khi xóa (các) đối tượng mà nó trỏ tới.

  1. Nó có thể giúp bắt được nhiều tài liệu tham khảo đến bộ nhớ đã giải phóng (giả sử nền tảng của bạn bị lỗi trên một con trỏ rỗng).

  2. Nó sẽ không bắt được tất cả các tham chiếu đến bộ nhớ miễn phí nếu, ví dụ, bạn có các bản sao của con trỏ nằm xung quanh. Nhưng một số tốt hơn không có.

  3. Nó sẽ che dấu xóa hai lần, nhưng tôi thấy những thứ đó ít phổ biến hơn nhiều so với truy cập vào bộ nhớ đã được giải phóng.

  4. Trong nhiều trường hợp, trình biên dịch sẽ tối ưu hóa nó đi. Vì vậy, lập luận rằng nó không cần thiết không thuyết phục tôi.

  5. Nếu bạn đã sử dụng RAII, thì không có nhiều deletemã trong mã của bạn để bắt đầu, do đó, lập luận rằng việc gán thêm gây ra sự lộn xộn không thuyết phục được tôi.

  6. Thật tiện lợi, khi gỡ lỗi, để xem giá trị null thay vì con trỏ cũ.

  7. Nếu điều này vẫn làm phiền bạn, hãy sử dụng một con trỏ thông minh hoặc một tham chiếu thay thế.

Tôi cũng đặt các loại xử lý tài nguyên khác thành giá trị không có tài nguyên khi tài nguyên là miễn phí (thường chỉ có trong hàm hủy của trình bao bọc RAII được viết để đóng gói tài nguyên).

Tôi đã làm việc trên một sản phẩm thương mại lớn (9 triệu báo cáo) (chủ yếu bằng C). Tại một thời điểm, chúng tôi đã sử dụng ma thuật macro để loại bỏ con trỏ khi bộ nhớ được giải phóng. Điều này ngay lập tức phơi bày rất nhiều lỗi ẩn nấp đã được khắc phục kịp thời. Theo như tôi có thể nhớ, chúng tôi chưa bao giờ có lỗi kép.

Cập nhật: Microsoft tin rằng đó là một cách thực hành tốt cho bảo mật và khuyến nghị thực hành trong các chính sách SDL của họ. Rõ ràng MSVC ++ 11 sẽ tự động dậm con trỏ bị xóa (trong nhiều trường hợp) nếu bạn biên dịch với tùy chọn / SDL.


12

Đầu tiên, có rất nhiều câu hỏi hiện có về chủ đề này và các chủ đề liên quan chặt chẽ, ví dụ Tại sao không xóa đặt con trỏ thành NULL? .

Trong mã của bạn, vấn đề xảy ra (sử dụng p). Ví dụ: nếu ở đâu đó bạn có mã như thế này:

Foo * p2 = p;

sau đó thiết lập p để NULL hoàn thành rất ít, vì bạn vẫn còn con trỏ p2 phải lo lắng.

Điều này không có nghĩa là việc đặt một con trỏ tới NULL luôn là vô nghĩa. Ví dụ: nếu p là biến thành viên trỏ đến tài nguyên thì tuổi thọ của nó không hoàn toàn giống với lớp chứa p, thì đặt p thành NULL có thể là một cách hữu ích để chỉ ra sự hiện diện hay vắng mặt của tài nguyên.


1
Tôi đồng ý rằng đôi khi nó sẽ không giúp ích gì, nhưng bạn dường như ngụ ý rằng nó có thể gây hại tích cực. Đó là ý định của bạn, hay tôi đã đọc sai?
Đánh dấu tiền chuộc

1
Liệu có một bản sao của con trỏ không liên quan đến câu hỏi liệu biến con trỏ có nên được đặt thành NULL hay không. Đặt nó thành NULL là một cách thực hành tốt trên cùng một cơ sở làm sạch bát đĩa sau khi bạn dùng xong bữa tối là một cách tốt - trong khi đó không phải là biện pháp bảo vệ chống lại tất cả các lỗi mà mã có thể có, nó giúp tăng cường sức khỏe mã tốt.
Franci Penov

2
@Franci Rất nhiều người dường như không đồng ý với bạn. Và liệu có một bản sao chắc chắn có liên quan hay không nếu bạn cố gắng sử dụng bản sao sau khi bạn xóa bản gốc.

3
Franci, có một sự khác biệt. Bạn làm sạch bát đĩa vì bạn sử dụng chúng một lần nữa. Bạn không cần con trỏ sau khi xóa nó. Nó sẽ là điều cuối cùng bạn làm. Thực hành tốt hơn là tránh hoàn toàn tình hình.
GManNickG

1
Bạn có thể sử dụng lại một biến, nhưng sau đó nó không còn là trường hợp lập trình phòng thủ; đó là cách bạn thiết kế giải pháp cho vấn đề hiện tại. OP đang thảo luận về việc liệu phong cách phòng thủ này có phải là thứ chúng ta nên cố gắng không, chúng ta sẽ không đặt con trỏ thành null. Và lý tưởng, cho câu hỏi của bạn, có! Đừng sử dụng con trỏ sau khi bạn xóa chúng!
GManNickG

7

Nếu có thêm mã sau delete, Có. Khi con trỏ bị xóa trong hàm tạo hoặc ở cuối phương thức hoặc hàm, Không.

Điểm của dụ ngôn này là để nhắc nhở lập trình viên, trong thời gian chạy, đối tượng đã bị xóa.

Một cách thực hành tốt hơn nữa là sử dụng Smart Pointers (chia sẻ hoặc có phạm vi) để tự động xóa các đối tượng mục tiêu của chúng.


Tất cả mọi người (bao gồm cả người hỏi ban đầu) đồng ý rằng con trỏ thông minh là con đường để đi. Mã phát triển. Có thể không có nhiều mã hơn sau khi xóa khi bạn lần đầu tiên, nhưng điều đó có thể thay đổi theo thời gian. Đưa vào bài tập giúp khi điều đó xảy ra (và chi phí gần như không có gì trong thời gian trung bình).
Adrian McCarthy

3

Như những người khác đã nói, delete ptr; ptr = 0;sẽ không khiến quỷ bay ra khỏi mũi của bạn. Tuy nhiên, nó không khuyến khích việc sử dụng ptrnhư một loại cờ. Mã trở nên bị vấy bẩn deletevà đặt con trỏ thành NULL. Bước tiếp theo là phân tán if (arg == NULL) return;mã của bạn để bảo vệ chống lại việc sử dụng NULLcon trỏ vô tình . Sự cố xảy ra khi kiểm traNULL trở thành phương tiện chính của bạn để kiểm tra trạng thái của một đối tượng hoặc chương trình.

Tôi chắc chắn rằng có một mùi mã về việc sử dụng một con trỏ làm cờ ở đâu đó nhưng tôi chưa tìm thấy.


9
Không có gì sai khi sử dụng một con trỏ làm cờ. Nếu bạn đang sử dụng một con trỏ và NULLkhông phải là một giá trị hợp lệ, thì có lẽ bạn nên sử dụng một tham chiếu thay thế.
Adrian McCarthy

2

Tôi sẽ thay đổi câu hỏi của bạn một chút:

Bạn sẽ sử dụng một con trỏ chưa được khởi tạo? Bạn có biết, một cái mà bạn đã không đặt thành NULL hoặc phân bổ bộ nhớ mà nó trỏ tới?

Có hai kịch bản trong đó việc đặt con trỏ thành NULL có thể được bỏ qua:

  • biến con trỏ đi ra khỏi phạm vi ngay lập tức
  • bạn đã quá tải ngữ nghĩa của con trỏ và đang sử dụng giá trị của nó không chỉ như một con trỏ bộ nhớ, mà còn như một khóa hoặc giá trị thô. Cách tiếp cận này tuy nhiên bị các vấn đề khác.

Trong khi đó, lập luận rằng việc đặt con trỏ thành NULL có thể ẩn lỗi đối với tôi nghe có vẻ như tranh luận rằng bạn không nên sửa lỗi vì bản sửa lỗi có thể ẩn một lỗi khác. Các lỗi duy nhất có thể hiển thị nếu con trỏ không được đặt thành NULL sẽ là các lỗi cố gắng sử dụng con trỏ. Nhưng việc đặt nó thành NULL thực sự sẽ gây ra chính xác lỗi tương tự như hiển thị nếu bạn sử dụng nó với bộ nhớ đã giải phóng, phải không?


(A) "nghe có vẻ như tranh luận rằng bạn không nên sửa lỗi" Không đặt con trỏ thành NULL không phải là lỗi. (B) "Nhưng đặt nó thành NULL thực sự sẽ gây ra chính xác cùng một lỗi" Không. Cài đặt thành NULL ẩn xóa hai lần . (C) Tóm tắt: Cài đặt thành NULL ẩn xóa hai lần, nhưng hiển thị các tham chiếu cũ. Không cài đặt thành NULL có thể ẩn các tham chiếu cũ, nhưng hiển thị xóa hai lần. Cả hai bên đều đồng ý rằng vấn đề thực sự là sửa các tham chiếu cũ và xóa hai lần.
Vịt Mooing

2

Nếu bạn không có ràng buộc nào khác buộc bạn phải đặt hoặc không đặt con trỏ thành NULL sau khi bạn xóa nó (một ràng buộc như vậy đã được Neil Butterworth đề cập ), thì sở thích cá nhân của tôi là để nguyên.

Đối với tôi, câu hỏi không phải "đây có phải là một ý tưởng tốt?" nhưng "hành vi nào tôi sẽ ngăn chặn hoặc cho phép thành công bằng cách làm điều này?" Ví dụ, nếu điều này cho phép mã khác thấy rằng con trỏ không còn khả dụng, tại sao mã khác thậm chí còn cố gắng xem các con trỏ được giải phóng sau khi chúng được giải phóng? Thông thường, đó là một lỗi.

Nó cũng làm nhiều việc hơn mức cần thiết cũng như cản trở việc gỡ lỗi sau khi chết. Bạn càng ít chạm vào bộ nhớ sau khi bạn không cần nó, bạn càng dễ dàng tìm ra lý do tại sao một cái gì đó bị hỏng. Nhiều lần tôi đã dựa vào thực tế rằng bộ nhớ ở trạng thái tương tự như khi một lỗi cụ thể xảy ra để chẩn đoán và sửa lỗi đã nói.


2

Hoàn toàn vô hiệu hóa sau khi xóa mạnh mẽ gợi ý cho người đọc rằng con trỏ đại diện cho một cái gì đó là tùy chọn về mặt khái niệm . Nếu tôi thấy điều đó đã được thực hiện, tôi sẽ bắt đầu lo lắng rằng mọi nơi trong nguồn con trỏ sẽ được sử dụng rằng nó nên được thử nghiệm đầu tiên chống lại NULL.

Nếu đó là những gì bạn thực sự muốn nói, tốt hơn là làm cho nó rõ ràng trong nguồn bằng cách sử dụng cái gì đó như boost :: tùy chọn

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

Nhưng nếu bạn thực sự muốn mọi người biết con trỏ đã "xấu đi", tôi sẽ thỏa thuận 100% với những người nói rằng điều tốt nhất để làm là làm cho nó vượt ra khỏi phạm vi. Sau đó, bạn đang sử dụng trình biên dịch để ngăn chặn khả năng xảy ra tình trạng xấu trong thời gian chạy.

Đó là em bé trong tất cả nước tắm C ++, không nên vứt nó đi. :)


2

Trong một chương trình có cấu trúc tốt với kiểm tra lỗi thích hợp, không có lý do gì để không gán nó null. 0đứng một mình như một giá trị không hợp lệ được công nhận toàn cầu trong bối cảnh này. Thất bại khó khăn và Thất bại sớm.

Nhiều đối số chống lại việc gán 0cho thấy rằng nó có thể ẩn một lỗi hoặc làm phức tạp luồng điều khiển. Về cơ bản, đó là một lỗi ngược dòng (không phải lỗi của bạn (xin lỗi vì lỗi xấu)) hoặc một lỗi khác thay cho lập trình viên - thậm chí có thể là một dấu hiệu cho thấy dòng chương trình đã phát triển quá phức tạp.

Nếu lập trình viên muốn giới thiệu việc sử dụng một con trỏ có thể là giá trị đặc biệt và viết tất cả các né tránh cần thiết xung quanh đó, thì đó là một sự phức tạp mà họ đã cố tình đưa ra. Việc kiểm dịch càng tốt, bạn càng sớm phát hiện ra các trường hợp sử dụng sai và càng ít có khả năng lây lan sang các chương trình khác.

Các chương trình có cấu trúc tốt có thể được thiết kế bằng các tính năng C ++ để tránh những trường hợp này. Bạn có thể sử dụng tài liệu tham khảo hoặc bạn chỉ có thể nói "truyền / sử dụng đối số null hoặc không hợp lệ là một lỗi" - một cách tiếp cận có thể áp dụng tương tự cho các thùng chứa, chẳng hạn như con trỏ thông minh. Tăng hành vi nhất quán và đúng đắn ngăn cấm các lỗi này đi xa.

Từ đó, bạn chỉ có một phạm vi và bối cảnh rất hạn chế nơi con trỏ null có thể tồn tại (hoặc được cho phép).

Điều tương tự có thể được áp dụng cho con trỏ không const. Theo sau giá trị của một con trỏ là không đáng kể vì phạm vi của nó quá nhỏ và việc sử dụng không đúng được kiểm tra và xác định rõ. Nếu bộ công cụ và kỹ sư của bạn không thể theo dõi chương trình sau khi đọc nhanh hoặc có kiểm tra lỗi không phù hợp hoặc luồng chương trình không nhất quán / khoan hồng, bạn có vấn đề khác, lớn hơn.

Cuối cùng, trình biên dịch và môi trường của bạn có thể có một số người bảo vệ cho những lần bạn muốn đưa ra lỗi (viết nguệch ngoạc), phát hiện truy cập vào bộ nhớ đã giải phóng và bắt các UB liên quan khác. Bạn cũng có thể giới thiệu chẩn đoán tương tự vào các chương trình của mình, thường xuyên mà không ảnh hưởng đến các chương trình hiện có.


1

Hãy để tôi mở rộng những gì bạn đã đặt vào câu hỏi của bạn.

Đây là những gì bạn đã đặt vào câu hỏi của bạn, ở dạng gạch đầu dòng:


Đặt con trỏ thành NULL sau khi xóa không phải là thông lệ tốt trong C ++. Có những lúc:

  • đó là một điều tốt để làm
  • và thời gian khi nó là vô nghĩa và có thể ẩn lỗi.

Tuy nhiên, không có lần nào điều này là xấu ! Bạn sẽ không giới thiệu nhiều lỗi hơn bằng cách vô hiệu hóa nó, bạn sẽ không rò rỉ bộ nhớ, bạn sẽ không khiến hành vi không xác định xảy ra.

Vì vậy, nếu nghi ngờ, chỉ cần null nó.

Phải nói rằng, nếu bạn cảm thấy rằng bạn phải vô hiệu hóa một số con trỏ, thì với tôi điều này nghe có vẻ như bạn chưa tách ra một phương thức đủ, và nên xem phương pháp tái cấu trúc gọi là "Phương thức trích xuất" để tách phương thức thành các bộ phận riêng biệt.


Tôi không đồng ý với "không có lúc nào điều này là xấu." Hãy xem xét số lượng máy cắt mà thành ngữ này giới thiệu. Bạn có một tiêu đề được bao gồm trong mọi đơn vị xóa một cái gì đó và tất cả các vị trí xóa đó chỉ trở nên hơi đơn giản.
GManNickG

Có những lúc nó tệ. Nếu ai đó cố gắng hủy đăng ký con trỏ đã xóa của bạn khi họ không nên, thì có lẽ nó sẽ không gặp sự cố và lỗi đó là 'ẩn'. Nếu họ hủy đăng ký con trỏ đã xóa của bạn vẫn còn một số giá trị ngẫu nhiên trong đó, bạn có thể nhận thấy và lỗi sẽ dễ thấy hơn.
Carson Myers

@Carson: Trải nghiệm của tôi hoàn toàn ngược lại: Việc hủy bỏ một nullptr sẽ gần như tất cả các cách làm hỏng Ứng dụng và có thể bị bắt bởi một trình gỡ lỗi. Việc hủy bỏ một con trỏ lơ lửng thường không tạo ra vấn đề ngay lập tức, nhưng thường sẽ dẫn đến kết quả không chính xác hoặc các lỗi khác.
MikeMB

@MikeMB Tôi hoàn toàn đồng ý, quan điểm của tôi về điều này đã thay đổi đáng kể trong quá khứ ~ 6,5 năm
Carson Myers

Về việc trở thành một lập trình viên, tất cả chúng ta đều là người khác 6-7 năm trước :) Tôi thậm chí không chắc mình đã dám trả lời câu hỏi C / C ++ ngày hôm nay :)
Lasse V. Karlsen

1

Đúng.

"Tác hại" duy nhất có thể làm là đưa sự kém hiệu quả (một hoạt động lưu trữ không cần thiết) vào chương trình của bạn - nhưng chi phí này sẽ không đáng kể liên quan đến chi phí phân bổ và giải phóng khối bộ nhớ trong hầu hết các trường hợp.

Nếu bạn không làm điều đó, một ngày nào đó bạn sẽ gặp phải một số lỗi khó chịu.

Tôi luôn sử dụng macro để xóa:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(và tương tự cho một mảng, free (), giải phóng tay cầm)

Bạn cũng có thể viết các phương thức "tự xóa" lấy tham chiếu đến con trỏ của mã gọi, để chúng buộc con trỏ của mã gọi đến NULL. Ví dụ: để xóa một cây con của nhiều đối tượng:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

biên tập

Vâng, các kỹ thuật này vi phạm một số quy tắc về việc sử dụng macro (và vâng, ngày nay bạn có thể đạt được kết quả tương tự với các mẫu) - nhưng bằng cách sử dụng trong nhiều năm, tôi chưa bao giờ truy cập vào bộ nhớ chết - một trong những điều khó khăn và khó khăn nhất và tốn nhiều thời gian nhất để gỡ lỗi các vấn đề bạn có thể gặp phải. Trong thực tế trong nhiều năm, họ đã loại bỏ một cách hiệu quả một lớp lỗi từ mọi đội tôi đã giới thiệu cho họ.

Ngoài ra còn có nhiều cách bạn có thể thực hiện như trên - Tôi chỉ cố gắng minh họa ý tưởng buộc mọi người phải NULL một con trỏ nếu họ xóa một đối tượng, thay vì cung cấp một phương tiện để họ giải phóng bộ nhớ không NULL con trỏ của người gọi .

Tất nhiên, ví dụ trên chỉ là một bước tiến tới một con trỏ tự động. Điều mà tôi không đề xuất vì OP đặc biệt hỏi về trường hợp không sử dụng con trỏ tự động.


2
Macro là một ý tưởng tồi, đặc biệt khi chúng trông giống như các chức năng bình thường. Nếu bạn muốn làm điều này, hãy sử dụng chức năng templated.

2
Wow ... Tôi chưa bao giờ thấy bất cứ điều gì như anObject->Delete(anObject)làm mất hiệu lực anObjectcon trỏ. Điều đó thật đáng sợ. Bạn nên tạo một phương thức tĩnh cho việc này để bạn buộc phải thực hiện TreeItem::Delete(anObject)ít nhất.
D.Shawley

Xin lỗi, đã nhập nó dưới dạng một hàm thay vì sử dụng dạng chữ hoa "đây là một macro" thích hợp. Đã sửa. Cũng thêm một bình luận để giải thích bản thân tốt hơn.
Jason Williams

Và bạn nói đúng, ví dụ bash nhanh chóng của tôi là rác rưởi! Đã sửa :-). Tôi chỉ cố gắng nghĩ ra một ví dụ nhanh để minh họa ý tưởng này: bất kỳ mã nào xóa con trỏ sẽ đảm bảo rằng con trỏ được đặt thành NULL, ngay cả khi người khác (người gọi) sở hữu con trỏ đó. Vì vậy, luôn luôn chuyển trong một tham chiếu đến con trỏ để nó có thể bị buộc phải NULL tại điểm xóa.
Jason Williams

1

"Có những lúc nó là một việc nên làm, và có những lúc nó là vô nghĩa và có thể che giấu lỗi"

Tôi có thể thấy hai vấn đề: Mã đơn giản đó:

delete myObj;
myobj = 0

trở thành một lớp lót trong môi trường đa luồng:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

"Thực hành tốt nhất" của Don Neufeld không được áp dụng luôn. Ví dụ, trong một dự án ô tô, chúng tôi phải đặt con trỏ về 0 ngay cả trong các hàm hủy. Tôi có thể tưởng tượng trong phần mềm quan trọng an toàn quy tắc như vậy không phải là hiếm. Theo dõi họ sẽ dễ dàng hơn (và khôn ngoan) hơn là cố gắng thuyết phục nhóm / người kiểm tra mã cho mỗi con trỏ sử dụng trong mã, rằng một dòng vô hiệu hóa con trỏ này là dư thừa.

Một mối nguy hiểm khác là dựa vào kỹ thuật này trong các trường hợp ngoại lệ - sử dụng mã:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

Trong mã như vậy hoặc bạn tạo ra rò rỉ tài nguyên và hoãn sự cố hoặc quá trình gặp sự cố.

Vì vậy, hai vấn đề này tự phát qua đầu tôi (Herb Sutter chắc chắn sẽ nói thêm) làm cho tôi tất cả các câu hỏi thuộc loại "Làm thế nào để tránh sử dụng con trỏ thông minh và thực hiện công việc một cách an toàn với con trỏ bình thường" là lỗi thời.


Tôi không thấy, làm thế nào 4 lớp lót phức tạp hơn đáng kể so với 3 lớp (dù sao cũng nên sử dụng lock_guards) và nếu kẻ hủy diệt của bạn ném bạn thì bạn vẫn gặp rắc rối.
MikeMB

Khi tôi lần đầu tiên nhìn thấy câu trả lời này, tôi đã không hiểu tại sao bạn lại muốn vô hiệu hóa một con trỏ trong hàm hủy, nhưng bây giờ tôi làm - đó là trong trường hợp đối tượng sở hữu con trỏ được sử dụng sau khi nó bị xóa!
Đánh dấu tiền chuộc

0

Luôn luôn có con trỏ Dangling để lo lắng về.


1
Sẽ rất khó để đặt "con trỏ lơ lửng" thay vì "cái này"? :) Ít nhất nó cho câu trả lời của bạn một số chất.
GManNickG

4
Nó thậm chí còn là chủ đề của một mẹo W3C QA, "Đừng dùng 'nhấp vào đây' như liên kết văn bản": w3.org/QA/Tips/noClickHere
outis

0

Nếu bạn định phân bổ lại con trỏ trước khi sử dụng lại (hủy bỏ nó, chuyển nó đến một hàm, v.v.), làm cho con trỏ NULL chỉ là một thao tác bổ sung. Tuy nhiên, nếu bạn không chắc chắn liệu nó có được phân bổ lại hay không trước khi sử dụng lại, đặt nó thành NULL là một ý tưởng hay.

Như nhiều người đã nói, tất nhiên việc sử dụng con trỏ thông minh sẽ dễ dàng hơn nhiều.

Chỉnh sửa: Như Thomas Matthews đã nói trong câu trả lời trước đó , nếu một con trỏ bị xóa trong hàm hủy, thì không cần gán NULL cho nó vì nó sẽ không được sử dụng lại vì đối tượng đã bị hủy.


0

Tôi có thể tưởng tượng việc đặt một con trỏ thành NULL sau khi xóa nó trở nên hữu ích trong những trường hợp hiếm hoi khi có một kịch bản hợp pháp là sử dụng lại nó trong một hàm (hoặc đối tượng) duy nhất. Mặt khác, nó không có ý nghĩa - một con trỏ cần trỏ đến một cái gì đó có ý nghĩa miễn là nó tồn tại - thời gian.


0

Nếu mã không thuộc về phần quan trọng nhất về hiệu năng của ứng dụng của bạn, hãy giữ cho nó đơn giản và sử dụng shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

Nó thực hiện đếm tham chiếu và an toàn chủ đề. Bạn có thể tìm thấy nó trong không gian tên tr1 (std :: tr1, #include <memory>) hoặc nếu trình biên dịch của bạn không cung cấp nó, hãy lấy nó từ boost.

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.