Đặt biến thành NULL sau khi miễn phí


156

Trong công ty của tôi có một quy tắc mã hóa cho biết, sau khi giải phóng bất kỳ bộ nhớ nào, hãy đặt lại biến thành NULL. Ví dụ ...

void some_func () 
{
    int *nPtr;

    nPtr = malloc (100);

    free (nPtr);
    nPtr = NULL;

    return;
}

Tôi cảm thấy rằng, trong các trường hợp như mã được hiển thị ở trên, cài đặt thành NULLkhông có bất kỳ ý nghĩa nào. Hay tôi đang thiếu một cái gì đó?

Nếu không có ý nghĩa gì trong những trường hợp như vậy, tôi sẽ đưa nó lên "nhóm chất lượng" để loại bỏ quy tắc mã hóa này. Vui lòng cho lời khuyên.


2
nó luôn hữu ích để có thể kiểm tra nếu ptr == NULLtrước khi làm bất cứ điều gì với nó. Nếu bạn không vô hiệu hóa các con trỏ miễn phí của mình, bạn vẫn nhận được ptr != NULLnhưng con trỏ vẫn không sử dụng được.
Ki Jéy

Con trỏ lơ lửng có thể dẫn đến các lỗ hổng có thể khai thác như Sử dụng sau khi miễn phí .
Toàn cảnh

Câu trả lời:


285

Đặt con trỏ không sử dụng thành NULL là một kiểu phòng thủ, bảo vệ chống lại các lỗi con trỏ lơ lửng. Nếu một con trỏ lơ lửng được truy cập sau khi nó được giải phóng, bạn có thể đọc hoặc ghi đè lên bộ nhớ ngẫu nhiên. Nếu một con trỏ null được truy cập, bạn sẽ gặp sự cố ngay lập tức trên hầu hết các hệ thống, cho bạn biết ngay lỗi đó là gì.

Đối với các biến cục bộ, có thể hơi vô nghĩa nếu "rõ ràng" rằng con trỏ không được truy cập nữa sau khi được giải phóng, do đó kiểu này phù hợp hơn với dữ liệu thành viên và biến toàn cục. Ngay cả đối với các biến cục bộ, nó có thể là một cách tiếp cận tốt nếu hàm tiếp tục sau khi bộ nhớ được giải phóng.

Để hoàn thành kiểu, bạn cũng nên khởi tạo con trỏ tới NULL trước khi chúng được gán một giá trị con trỏ thực.


3
Tôi không hiểu tại sao bạn lại "khởi tạo con trỏ tới NULL trước khi chúng được gán một giá trị con trỏ thực"?
Paul Biggar

26
@Paul: Trong trường hợp cụ thể, tờ khai có thể đọc int *nPtr=NULL;. Bây giờ, tôi đồng ý rằng điều này sẽ là dư thừa, với một malloc theo ngay trong dòng tiếp theo. Tuy nhiên, nếu có mã giữa khai báo và khởi tạo đầu tiên, ai đó có thể bắt đầu sử dụng biến ngay cả khi nó chưa có giá trị. Nếu bạn khởi tạo null, bạn sẽ nhận được segfault; không có, bạn có thể lại đọc hoặc ghi bộ nhớ ngẫu nhiên. Tương tự như vậy, nếu biến sau này chỉ được khởi tạo một cách có điều kiện, các truy cập bị lỗi sau đó sẽ cung cấp cho bạn sự cố ngay lập tức nếu bạn nhớ khởi tạo null.
Martin v. Löwis

1
Cá nhân tôi nghĩ rằng trong bất kỳ cơ sở mã hóa không tầm thường nào cũng gặp lỗi cho việc hủy bỏ hội thảo null cũng mơ hồ như nhận được lỗi cho việc hủy bỏ hội thảo một địa chỉ mà bạn không sở hữu. Cá nhân tôi không bao giờ bận tâm.
wilmustell

9
Wilhelm, vấn đề là với một sự bổ nhiệm con trỏ null, bạn sẽ có được một sự cố xác định và vị trí thực tế của vấn đề. Một truy cập xấu có thể hoặc không thể bị sập, và dữ liệu hoặc hành vi bị hỏng theo những cách không mong muốn ở những nơi không mong muốn.
Amit N Nikol

4
Trên thực tế, việc khởi tạo con trỏ tới NULL có ít nhất một nhược điểm đáng kể: nó có thể ngăn trình biên dịch cảnh báo bạn về các biến chưa được khởi tạo. Trừ khi logic của mã của bạn thực sự xử lý rõ ràng giá trị đó cho con trỏ (tức là nếu (nPtr == NULL) dos somebody ...) tốt hơn là để nguyên như vậy.
Eric

37

Đặt con trỏ về NULLsau freelà một thực tiễn đáng ngờ thường được phổ biến như một quy tắc "lập trình tốt" trên một tiền đề sai lầm rõ ràng. Đó là một trong những sự thật giả thuộc về loại "âm thanh đúng" nhưng trong thực tế đạt được hoàn toàn không có gì hữu ích (và đôi khi dẫn đến hậu quả tiêu cực).

Bị cáo buộc, đặt một con trỏ thành NULLsau freeđược cho là để ngăn chặn vấn đề "gấp đôi miễn phí" đáng sợ khi cùng một giá trị con trỏ được truyền đến freenhiều lần. Tuy nhiên, trong thực tế, trong 9 trường hợp, 10 vấn đề thực sự "miễn phí kép" xảy ra khi các đối tượng con trỏ khác nhau có cùng giá trị con trỏ được sử dụng làm đối số cho free. Không cần phải nói, thiết lập một con trỏ đến NULLsau khi freeđạt được hoàn toàn không có gì để ngăn chặn vấn đề trong những trường hợp như vậy.

Tất nhiên, có thể gặp vấn đề "nhân đôi miễn phí" khi sử dụng cùng một đối tượng con trỏ làm đối số free. Tuy nhiên, trong các tình huống thực tế như thế thường chỉ ra một vấn đề với cấu trúc logic chung của mã, chứ không phải là "miễn phí kép" ngẫu nhiên. Một cách thích hợp để xử lý vấn đề trong các trường hợp như vậy là xem xét và suy nghĩ lại về cấu trúc của mã để tránh tình huống khi cùng một con trỏ được truyền đến freenhiều lần. Trong những trường hợp như vậy, đặt con trỏ đến NULLvà xem xét vấn đề "đã sửa" không gì khác hơn là một nỗ lực để quét vấn đề dưới thảm. Nó chỉ đơn giản là không hoạt động trong trường hợp chung, bởi vì vấn đề với cấu trúc mã sẽ luôn tìm cách khác để tự thể hiện.

Cuối cùng, nếu mã của bạn được thiết kế đặc biệt để dựa vào giá trị con trỏ là NULLhay không NULL, việc đặt giá trị con trỏ thành NULLsau là hoàn toàn tốt free. Nhưng như một quy tắc "thực hành tốt" chung (như trong "luôn đặt con trỏ của bạn thành NULLsau free"), một lần nữa, nó là một giả mạo nổi tiếng và khá vô dụng, thường được theo sau bởi một số lý do hoàn toàn tôn giáo, giống như tôn giáo.


1
Chắc chắn rồi. Tôi không nhớ đã từng gây ra lỗi miễn phí kép bằng cách đặt con trỏ thành NULL sau khi giải phóng, nhưng tôi đã gây ra nhiều điều không thể.
LnxPrgr3

4
@AnT "ngờ vực" thì hơi nhiều. Tất cả phụ thuộc vào trường hợp sử dụng. Nếu giá trị của con trỏ được sử dụng theo nghĩa đúng / sai, thì đó không chỉ là một thực tiễn hợp lệ, đó là một cách thực hành tốt nhất.
Coder

1
@Coder Hoàn toàn sai. Nếu giá trị của con trỏ được sử dụng theo nghĩa sai thực sự để biết liệu nó có trỏ đến một đối tượng trước khi gọi miễn phí hay không, thì nó không chỉ không thực hành tốt nhất, mà còn sai . Ví dụ : foo* bar=getFoo(); /*more_code*/ free(bar); /*more_code*/ return bar != NULL;. Ở đây, việc thiết barđể NULLsau khi cuộc gọi đến freesẽ làm cho chức năng để nghĩ rằng nó không bao giờ có một quầy bar và trả về giá trị sai!
David Schwartz

Tôi không nghĩ rằng lợi ích chính là bảo vệ chống lại sự tự do gấp đôi, thay vào đó là bắt những con trỏ lơ lửng sớm hơn và đáng tin cậy hơn. Ví dụ: khi giải phóng một cấu trúc chứa tài nguyên, con trỏ vào bộ nhớ được phân bổ, xử lý tệp, v.v., khi tôi giải phóng các con trỏ bộ nhớ chứa và đóng các tệp được chứa, tôi thành viên tương ứng. Sau đó, nếu một trong các tài nguyên được truy cập thông qua một con trỏ lơ lửng do nhầm lẫn, chương trình có xu hướng bị lỗi ngay tại đó, mọi lúc. Mặt khác, không có NULLing, dữ liệu được giải phóng có thể chưa được ghi đè và lỗi có thể không dễ dàng lặp lại.
jimhark

1
Tôi đồng ý rằng mã có cấu trúc tốt sẽ không cho phép trường hợp con trỏ được truy cập sau khi được giải phóng hoặc trường hợp nó được giải phóng hai lần. Nhưng trong thế giới thực, mã của tôi sẽ được sửa đổi và / hoặc được xác nhận bởi một người có lẽ không biết tôi và không có thời gian và / hoặc kỹ năng để thực hiện mọi việc một cách chính xác (vì hạn chót luôn luôn là ngày hôm qua). Do đó, tôi có xu hướng viết các chức năng chống đạn không làm sập hệ thống ngay cả khi sử dụng sai.
mfloris

34

Hầu hết các phản hồi đã tập trung vào việc ngăn chặn miễn phí gấp đôi, nhưng đặt con trỏ thành NULL có lợi ích khác. Khi bạn giải phóng một con trỏ, bộ nhớ đó có sẵn để được phân bổ lại bằng một lệnh gọi khác đến malloc. Nếu bạn vẫn còn con trỏ ban đầu, bạn có thể gặp phải lỗi khi bạn cố sử dụng con trỏ sau khi rảnh và làm hỏng một số biến khác, và sau đó chương trình của bạn rơi vào trạng thái không xác định và tất cả các loại điều xấu có thể xảy ra (sập nếu bạn Thật may mắn, dữ liệu bị hỏng nếu bạn không may mắn). Nếu bạn đã đặt con trỏ thành NULL sau khi rảnh, mọi nỗ lực đọc / ghi thông qua con trỏ đó sau đó sẽ dẫn đến một segfault, thường thích hợp hơn cho hỏng bộ nhớ ngẫu nhiên.

Vì cả hai lý do, có thể nên đặt con trỏ thành NULL sau free (). Nó không phải luôn luôn cần thiết, mặc dù. Ví dụ: nếu biến con trỏ đi ra khỏi phạm vi ngay sau free (), không có nhiều lý do để đặt nó thành NULL.


1
+1 Đây thực sự là một điểm rất tốt. Không phải lý do về "gấp đôi miễn phí" (hoàn toàn không có thật), nhưng điều này . Tôi không phải là fan hâm mộ của NULL-ing của con trỏ sau đó free, nhưng điều này thực sự có ý nghĩa.
AnT

Nếu bạn có thể truy cập một con trỏ sau khi giải phóng nó qua cùng một con trỏ đó, nhiều khả năng bạn sẽ truy cập một con trỏ sau khi giải phóng đối tượng mà nó trỏ đến thông qua một số con trỏ khác. Vì vậy, điều này không giúp ích gì cho bạn cả - bạn vẫn phải sử dụng một số cơ chế khác để đảm bảo bạn không truy cập một đối tượng thông qua một con trỏ sau khi giải phóng nó qua một con trỏ khác. Bạn cũng có thể sử dụng phương pháp đó để bảo vệ trong cùng trường hợp con trỏ.
David Schwartz

1
@DavidSchwartz: Tôi không đồng ý với nhận xét của bạn. Khi tôi phải viết một chồng cho một bài tập đại học vài tuần trước, tôi có một vấn đề, tôi đã điều tra một vài giờ. Tôi đã truy cập một số bộ nhớ đã có sẵn tại một số điểm (miễn phí là một số dòng quá sớm). Và đôi khi nó dẫn đến hành vi rất kỳ lạ. Nếu tôi đã đặt con trỏ thành NULL sau khi giải phóng nó, thì sẽ có một segfault "đơn giản" và tôi sẽ tiết kiệm được một vài giờ làm việc. Vì vậy, +1 cho câu trả lời này!
mozzbozz

2
@katze_sonne Ngay cả đồng hồ dừng cũng đúng hai lần một ngày. Nhiều khả năng là việc thiết lập các con trỏ tới NULL sẽ che giấu các lỗi bằng cách ngăn chặn các truy cập sai đến các đối tượng đã được giải phóng khỏi việc phân tách mã để kiểm tra NULL và sau đó âm thầm không kiểm tra một đối tượng mà nó nên kiểm tra. (Có lẽ việc đặt các con trỏ tới NULL sau khi có các bản dựng gỡ lỗi cụ thể có thể hữu ích hoặc đặt chúng thành một giá trị khác với NULL được đảm bảo để segfault có thể có ý nghĩa. Nhưng sự vô lý này đã xảy ra để giúp bạn một lần không phải là một đối số có lợi .)
David Schwartz

@DavidSchwartz Chà, nghe có vẻ hợp lý ... Cảm ơn bình luận của bạn, tôi sẽ xem xét điều này trong tương lai! :) +1
mozzbozz

20

Đây được coi là thực hành tốt để tránh ghi đè bộ nhớ. Trong chức năng trên, nó không cần thiết, nhưng đôi khi nó được thực hiện nó có thể tìm thấy các lỗi ứng dụng.

Thay vào đó hãy thử một cái gì đó như thế này:

#if DEBUG_VERSION
void myfree(void **ptr)
{
    free(*ptr);
    *ptr = NULL;
}
#else
#define myfree(p) do { void ** __p = (p); free(*(__p)); *(__p) = NULL; } while (0)
#endif

DEBUG_VERSION cho phép bạn giải phóng hồ sơ trong mã gỡ lỗi, nhưng cả hai đều có chức năng giống nhau.

Chỉnh sửa : Đã thêm làm ... trong khi như đề xuất bên dưới, cảm ơn.


3
Phiên bản macro có một lỗi nhỏ nếu bạn sử dụng nó sau câu lệnh if không có dấu ngoặc.
Đánh dấu tiền chuộc

Điều gì với (void) 0? Mã này làm: if (x) myfree (& x); khác do_foo (); trở thành if (x) {free (* (& x)); * (& x) = null; } khoảng trống 0; khác do_foo (); Khác là một lỗi.
jmucchiello

Macro đó là một nơi hoàn hảo cho toán tử dấu phẩy: free ( (p)), * (p) = null. Tất nhiên vấn đề tiếp theo là nó đánh giá * (p) hai lần. Nó phải là {void * _pp = (p); miễn phí (* _ pp); * _pp = null; } Không phải là niềm vui tiền xử lý.
jmucchiello

5
Macro không nên ở trong dấu ngoặc trần, nó phải ở trong một do { } while(0)khối để if(x) myfree(x); else dostuff();không bị vỡ.
Chris Lutz

3
Như Lutz đã nói, cơ thể vĩ mô do {X} while (0)là IMO cách tốt nhất để tạo ra một cơ thể vĩ mô "cảm thấy thích và hoạt động như" một chức năng. Hầu hết các trình biên dịch tối ưu hóa vòng lặp anyway.
Mike Clark

7

Nếu bạn đạt đến con trỏ đã được free () d, nó có thể bị hỏng hoặc không. Bộ nhớ đó có thể được phân bổ lại cho một phần khác trong chương trình của bạn và sau đó bạn bị hỏng bộ nhớ,

Nếu bạn đặt con trỏ thành NULL, thì nếu bạn truy cập nó, chương trình luôn gặp sự cố với segfault. Không còn nữa ,, đôi khi nó hoạt động '', không còn nữa ,, gặp sự cố không thể đoán trước ''. Đó là cách dễ dàng hơn để gỡ lỗi.


5
Chương trình không phải lúc nào cũng gặp sự cố với segfault. Nếu cách bạn truy cập con trỏ có nghĩa là một phần bù đủ lớn được áp dụng cho nó trước khi hội thảo, thì nó có thể đạt tới bộ nhớ có thể định địa chỉ: ((MyHugeSturation *) 0) -> fieldNearTheEnd. Và đó là ngay cả trước khi bạn xử lý phần cứng không phân biệt quyền truy cập 0. Tuy nhiên, chương trình có nhiều khả năng gặp sự cố với segfault.
Steve Jessop

7

Đặt con trỏ vào freebộ nhớ 'd có nghĩa là mọi nỗ lực truy cập bộ nhớ đó thông qua con trỏ sẽ ngay lập tức bị sập, thay vì gây ra hành vi không xác định. Nó làm cho nó dễ dàng hơn nhiều để xác định nơi mọi thứ đã đi sai.

Tôi có thể thấy lập luận của bạn: vì nPtrsẽ ra khỏi phạm vi ngay sau nPtr = NULLđó, dường như không có lý do gì để đặt nó NULL. Tuy nhiên, trong trường hợp của một structthành viên hoặc một nơi nào khác mà con trỏ không ngay lập tức đi ra khỏi phạm vi, nó có ý nghĩa hơn. Không rõ ngay lập tức liệu con trỏ đó có được sử dụng lại bởi mã không nên sử dụng nó hay không.

Có khả năng quy tắc được nêu mà không tạo ra sự khác biệt giữa hai trường hợp này, bởi vì việc tự động thực thi quy tắc sẽ khó khăn hơn nhiều, chứ đừng nói đến việc các nhà phát triển tuân theo nó. Sẽ không hại gì khi đặt con trỏ đến NULLsau mỗi lần miễn phí, nhưng nó có khả năng chỉ ra những vấn đề lớn.


7

lỗi phổ biến nhất trong c là miễn phí gấp đôi. Về cơ bản bạn làm một cái gì đó như thế

free(foobar);
/* lot of code */
free(foobar);

và nó kết thúc khá tệ, hệ điều hành cố gắng giải phóng một số bộ nhớ đã được giải phóng và nói chung là segfault. Vì vậy, cách tốt nhất là đặt thành NULL, để bạn có thể thực hiện kiểm tra và kiểm tra xem bạn có thực sự cần giải phóng bộ nhớ này không

if(foobar != null){
  free(foobar);
}

cũng cần lưu ý rằng free(NULL)sẽ không làm gì cả nên bạn không phải viết câu lệnh if. Tôi không thực sự là một chuyên gia về hệ điều hành nhưng tôi khá đẹp ngay cả bây giờ hầu hết các hệ điều hành sẽ gặp sự cố miễn phí gấp đôi.

Đó cũng là một lý do chính tại sao tất cả các ngôn ngữ có bộ sưu tập rác (Java, dotnet) rất tự hào vì không gặp phải vấn đề này và cũng không phải để lại cho toàn bộ nhà phát triển quản lý bộ nhớ.


11
Bạn thực sự có thể chỉ gọi free () mà không cần kiểm tra - miễn phí (NULL) được định nghĩa là không làm gì cả.
Amber

5
Điều đó không che giấu lỗi sao? (Thích giải phóng quá nhiều.)
Georg Schölly

1
thanx, tôi hiểu rồi tôi đã thử:p = (char *)malloc(.....); free(p); if(p!=null) //p!=null is true, p is not null although freed { free(p); //Note: checking doesnot prevent error here }
Shaobo Wang

5
Như tôi đã nói, free(void *ptr) không thể thay đổi giá trị của con trỏ nó được thông qua. Nó có thể thay đổi nội dung của con trỏ, dữ liệu được lưu trữ tại địa chỉ đó , nhưng không phải là chính địa chỉ hoặc giá trị của con trỏ . Điều đó sẽ yêu cầu free(void **ptr)(dường như không được phép theo tiêu chuẩn) hoặc macro (được phép và hoàn toàn di động nhưng mọi người không thích macro). Ngoài ra, C không phải là về sự tiện lợi, đó là về việc cung cấp cho các lập trình viên nhiều quyền kiểm soát như họ muốn. Nếu họ không muốn thêm chi phí cho việc thiết lập con trỏ NULL, thì không nên ép buộc họ.
Chris Lutz

2
Có một vài điều trên thế giới cho thấy sự thiếu chuyên nghiệp của một phần của tác giả mã C. Nhưng chúng bao gồm "kiểm tra con trỏ cho NULL trước khi gọi free" (cùng với những thứ như "truyền kết quả của các hàm cấp phát bộ nhớ" hoặc "không sử dụng tên loại với sizeof").
AnT

6

Ý tưởng đằng sau điều này là ngăn chặn việc tái sử dụng con trỏ được giải phóng.


4

Điều này (có thể) thực sự quan trọng. Mặc dù bạn giải phóng bộ nhớ, một phần sau của chương trình có thể phân bổ một cái gì đó mới xảy ra để đáp xuống không gian. Con trỏ cũ của bạn bây giờ sẽ trỏ đến một đoạn bộ nhớ hợp lệ. Sau đó, có thể ai đó sẽ sử dụng con trỏ, dẫn đến trạng thái chương trình không hợp lệ.

Nếu bạn bỏ con trỏ ra, thì mọi nỗ lực sử dụng nó sẽ bị hủy bỏ 0x0 và sụp đổ ngay tại đó, rất dễ để gỡ lỗi. Con trỏ ngẫu nhiên chỉ vào bộ nhớ ngẫu nhiên là khó để gỡ lỗi. Rõ ràng là không cần thiết nhưng đó là lý do tại sao nó nằm trong một tài liệu thực hành tốt nhất.


Trên Windows, ít nhất, các bản dựng gỡ lỗi sẽ đặt bộ nhớ thành 0xdddddddd để khi bạn sử dụng một con trỏ để xóa bộ nhớ, bạn biết ngay lập tức. Cần có các cơ chế tương tự trên tất cả các nền tảng.
i_am_jorf

2
jeffamaphone, khối bộ nhớ đã xóa có thể đã được phân bổ lại và gán cho một đối tượng khác khi bạn sử dụng lại con trỏ.
Constantin

4

Từ tiêu chuẩn ANSI C:

void free(void *ptr);

Hàm miễn phí làm cho không gian được trỏ bởi ptr bị phân bổ, nghĩa là, được tạo sẵn để phân bổ thêm. Nếu ptr là một con trỏ null, không có hành động xảy ra. Mặt khác, nếu đối số không khớp với một con trỏ được trả về trước đó bởi hàm calloc, malloc hoặc realloc hoặc nếu không gian đã được giải quyết bằng một lệnh gọi đến free hoặc realloc, thì hành vi không được xác định.

"Hành vi không xác định" hầu như luôn luôn là một sự cố chương trình. Vì vậy, để tránh điều này, an toàn khi đặt lại con trỏ thành NULL. free () tự nó không thể làm điều này vì nó chỉ được truyền một con trỏ, không phải là một con trỏ đến một con trỏ. Bạn cũng có thể viết một phiên bản an toàn hơn () mà NULL là con trỏ:

void safe_free(void** ptr)
{
  free(*ptr);
  *ptr = NULL;
}

@DrPizza - Một lỗi (theo ý kiến ​​của tôi) là một nguyên nhân khiến chương trình của bạn không hoạt động như mong muốn. Nếu một đôi miễn phí ẩn phá vỡ chương trình của bạn, đó là một lỗi. Nếu nó hoạt động chính xác như dự định, thì đó không phải là lỗi.
Chris Lutz

@DrPizza: Tôi vừa tìm thấy một lý lẽ tại sao người ta nên đặt nó NULLđể tránh lỗi che giấu. stackoverflow.com/questions/1025589/ đá Có vẻ như trong cả hai trường hợp, một số lỗi bị ẩn.
Georg Schölly

1
Xin lưu ý rằng con trỏ trống đến con trỏ có vấn đề: c-faq.com/ptrs/genericpp.html
Bảo mật

3
@Chris, không, cách tiếp cận tốt nhất là cấu trúc mã. Đừng ném mallocs ngẫu nhiên và giải phóng tất cả trên cơ sở mã của bạn, giữ những thứ liên quan lại với nhau. "Mô-đun" phân bổ tài nguyên (bộ nhớ, tệp, ...) chịu trách nhiệm giải phóng tài nguyên đó và phải cung cấp một chức năng để thực hiện việc chăm sóc con trỏ. Đối với bất kỳ tài nguyên cụ thể nào, sau đó bạn có chính xác một nơi được phân bổ và một nơi được phát hành, cả hai đều gần nhau.
Bảo mật

4
@Chris Lutz: Hogwash. Nếu bạn viết mã giải phóng cùng một con trỏ hai lần, chương trình của bạn có lỗi logic. Che giấu lỗi logic đó bằng cách làm cho nó không bị sập không có nghĩa là chương trình này đúng: nó vẫn đang làm một cái gì đó vô nghĩa. Không có kịch bản trong đó viết miễn phí gấp đôi là hợp lý.
DrPizza

4

Tôi thấy điều này sẽ giúp ích rất ít vì theo kinh nghiệm của tôi khi mọi người truy cập vào cấp phát bộ nhớ được giải phóng thì hầu như luôn luôn vì họ có một con trỏ khác ở đâu đó. Và sau đó, nó mâu thuẫn với một tiêu chuẩn mã hóa cá nhân khác là "Tránh sự lộn xộn vô dụng", vì vậy tôi không làm điều đó vì tôi nghĩ rằng nó hiếm khi giúp và làm cho mã hơi khó đọc hơn.

Tuy nhiên - Tôi sẽ không đặt biến thành null nếu con trỏ không được sử dụng lại, nhưng thường thì thiết kế cấp cao hơn cho tôi một lý do để đặt nó thành null. Ví dụ: nếu con trỏ là thành viên của một lớp và tôi đã xóa những gì nó trỏ đến thì "hợp đồng" nếu bạn thích lớp đó là thành viên đó sẽ trỏ đến một cái gì đó hợp lệ bất cứ lúc nào vì vậy nó phải được đặt thành null vì lý do đó. Một sự khác biệt nhỏ nhưng tôi nghĩ một điều quan trọng.

Trong c ++, điều quan trọng là luôn luôn nghĩ ai là người sở hữu dữ liệu này khi bạn phân bổ một số bộ nhớ (trừ khi bạn đang sử dụng con trỏ thông minh nhưng ngay cả khi đó một số suy nghĩ là bắt buộc). Và quá trình này có xu hướng dẫn đến các con trỏ nói chung là thành viên của một số lớp và nói chung bạn muốn một lớp luôn ở trạng thái hợp lệ và cách dễ nhất để làm điều đó là đặt biến thành viên thành NULL để chỉ ra điểm đó không có gì bây giờ

Một mô hình phổ biến là đặt tất cả các con trỏ thành viên thành NULL trong hàm tạo và xóa lệnh gọi hàm hủy trên bất kỳ con trỏ nào tới dữ liệu mà thiết kế của bạn nói rằng lớp đó sở hữu . Rõ ràng trong trường hợp này, bạn phải đặt con trỏ thành NULL khi bạn xóa một cái gì đó để cho biết rằng bạn không sở hữu bất kỳ dữ liệu nào trước đó.

Vì vậy, để tóm tắt, có, tôi thường đặt con trỏ thành NULL sau khi xóa một cái gì đó, nhưng nó là một phần của thiết kế lớn hơn và suy nghĩ về người sở hữu dữ liệu thay vì mù quáng tuân theo quy tắc chuẩn mã hóa. Tôi sẽ không làm như vậy trong ví dụ của bạn vì tôi nghĩ rằng không có lợi ích gì khi làm như vậy và nó thêm "sự lộn xộn" mà theo kinh nghiệm của tôi là chịu trách nhiệm cho các lỗi và mã xấu như loại điều này.


4

Gần đây tôi bắt gặp cùng một câu hỏi sau khi tôi đang tìm kiếm câu trả lời. Tôi đã đi đến kết luận này:

Đó là cách thực hành tốt nhất và người ta phải tuân theo điều này để làm cho nó di động trên tất cả các hệ thống (được nhúng).

free()là một hàm thư viện, nó thay đổi khi một người thay đổi nền tảng, vì vậy bạn không nên mong đợi rằng sau khi chuyển con trỏ đến hàm này và sau khi giải phóng bộ nhớ, con trỏ này sẽ được đặt thành NULL. Đây có thể không phải là trường hợp đối với một số thư viện được triển khai cho nền tảng.

vì vậy luôn luôn đi cho

free(ptr);
ptr = NULL;

3

Quy tắc này hữu ích khi bạn đang cố gắng tránh các tình huống sau:

1) Bạn có một chức năng thực sự dài với quản lý bộ nhớ và logic phức tạp và bạn không muốn vô tình sử dụng lại con trỏ để xóa bộ nhớ sau này trong chức năng.

2) Con trỏ là biến thành viên của một lớp có hành vi khá phức tạp và bạn không muốn vô tình sử dụng lại con trỏ vào bộ nhớ đã xóa trong các hàm khác.

Trong kịch bản của bạn, nó không có ý nghĩa gì cả, nhưng nếu chức năng này dài hơn, nó có thể quan trọng.

Bạn có thể lập luận rằng việc đặt nó thành NULL thực sự có thể che giấu các lỗi logic sau này hoặc trong trường hợp bạn cho rằng nó hợp lệ, bạn vẫn gặp sự cố với NULL, vì vậy điều đó không thành vấn đề.

Nói chung, tôi khuyên bạn nên đặt nó thành NULL khi bạn nghĩ rằng đó là một ý tưởng tốt và không bận tâm khi bạn nghĩ rằng nó không xứng đáng. Thay vào đó hãy tập trung vào việc viết các hàm ngắn và các lớp được thiết kế tốt.


2

Để thêm vào những gì người khác đã nói, một phương pháp sử dụng con trỏ tốt là luôn kiểm tra xem đó có phải là con trỏ hợp lệ hay không. Cái gì đó như:


if(ptr)
   ptr->CallSomeMethod();

Hoàn toàn đánh dấu con trỏ là NULL sau khi giải phóng nó cho phép sử dụng loại này trong C / C ++.


5
Trong nhiều trường hợp, trong đó một con trỏ NULL không có ý nghĩa, thay vào đó nên viết một xác nhận.
Erich Kitzmueller

2

Đây có thể là một đối số để khởi tạo tất cả các con trỏ tới NULL, nhưng một cái gì đó như thế này có thể là một lỗi rất lén lút:

void other_func() {
  int *p; // forgot to initialize
  // some unrelated mallocs and stuff
  // ...
  if (p) {
    *p = 1; // hm...
  }
}

void caller() {
  some_func();
  other_func();
}

pkết thúc ở cùng một vị trí trên ngăn xếp như trước đây nPtr, vì vậy nó vẫn có thể chứa một con trỏ dường như hợp lệ. Việc chỉ định *pcó thể ghi đè lên tất cả các loại điều không liên quan và dẫn đến các lỗi xấu xí. Đặc biệt là nếu trình biên dịch khởi tạo các biến cục bộ bằng 0 trong chế độ gỡ lỗi nhưng không bật khi tối ưu hóa được bật. Vì vậy, các bản dựng gỡ lỗi không hiển thị bất kỳ dấu hiệu nào của lỗi trong khi các bản phát hành phát hành ngẫu nhiên ...


2

Đặt con trỏ vừa được giải phóng vào NULL không bắt buộc nhưng thực hành tốt. Theo cách này, bạn có thể tránh 1) sử dụng mũi nhọn tự do 2) giải phóng nó


2

Cài đặt một con trỏ tới NULL là để bảo vệ lại cái gọi là không có hai lần - một tình huống khi free () được gọi nhiều lần cho cùng một địa chỉ mà không phân bổ lại khối tại địa chỉ đó.

Miễn phí hai lần dẫn đến hành vi không xác định - thường là heap tham nhũng hoặc ngay lập tức làm hỏng chương trình. Gọi free () cho con trỏ NULL không làm gì cả và do đó được đảm bảo an toàn.

Vì vậy, cách tốt nhất trừ khi bạn chắc chắn rằng con trỏ rời khỏi phạm vi ngay lập tức hoặc ngay sau khi free () là đặt con trỏ đó thành NULL để ngay cả khi free () được gọi lại, bây giờ nó được gọi cho con trỏ NULL và hành vi không xác định bị lảng tránh.


2

Ý tưởng là nếu bạn cố gắng hủy bỏ con trỏ không còn hiệu lực sau khi giải phóng nó, bạn muốn thất bại nặng nề (segfault) thay vì âm thầm và bí ẩn.

Nhưng hãy cẩn thận. Không phải tất cả các hệ thống gây ra một segfault nếu bạn hủy đăng ký NULL. Bật (ít nhất là một số phiên bản) AIX, * (int *) 0 == 0 và Solaris có khả năng tương thích tùy chọn với tính năng "AIX" này.


2

Đối với câu hỏi ban đầu: Đặt con trỏ thành NULL trực tiếp sau khi giải phóng nội dung là một sự lãng phí hoàn toàn thời gian, với điều kiện mã đáp ứng tất cả các yêu cầu, được gỡ lỗi hoàn toàn và sẽ không bao giờ được sửa đổi nữa. Mặt khác, việc bảo vệ NULLing một con trỏ đã được giải phóng có thể khá hữu ích khi ai đó vô tư thêm một khối mã mới bên dưới free (), khi thiết kế của mô-đun gốc không chính xác, và trong trường hợp của nó -compiles-but-not-do-what-what-I-wish.

Trong bất kỳ hệ thống nào, có một mục tiêu không thể đạt được là làm cho nó dễ dàng nhất với điều đúng đắn và chi phí không thể giảm được của các phép đo không chính xác. Trong C, chúng tôi cung cấp một bộ công cụ rất sắc bén, rất mạnh, có thể tạo ra nhiều thứ trong tay của một công nhân lành nghề và gây ra tất cả các loại thương tích ẩn dụ khi xử lý không đúng cách. Một số khó hiểu hoặc sử dụng chính xác. Và mọi người, tự nhiên có nguy cơ không thích, làm những việc phi lý như kiểm tra một con trỏ cho giá trị NULL trước khi gọi miễn phí với nó.

Vấn đề đo lường là bất cứ khi nào bạn cố gắng phân chia tốt từ kém tốt, trường hợp càng phức tạp, bạn càng có nhiều khả năng nhận được một phép đo mơ hồ. Nếu mục tiêu là chỉ giữ các thực hành tốt, thì một số mục tiêu mơ hồ sẽ bị loại bỏ với thực tế không tốt. NẾU mục tiêu của bạn là loại bỏ những điều không tốt, thì sự mơ hồ có thể ở lại với điều tốt. Hai mục tiêu, chỉ giữ tốt hoặc loại bỏ rõ ràng xấu, dường như trái ngược nhau về mặt đường kính, nhưng thường có một nhóm thứ ba không phải ai cũng không phải ai khác, cả hai.

Trước khi bạn tạo một trường hợp với bộ phận chất lượng, hãy thử xem qua cơ sở dữ liệu lỗi để xem tần suất, nếu có, các giá trị con trỏ không hợp lệ gây ra các vấn đề phải được ghi lại. Nếu bạn muốn tạo sự khác biệt thực sự, hãy xác định vấn đề phổ biến nhất trong mã sản xuất của bạn và đề xuất ba cách để ngăn chặn nó


Câu trả lời tốt. Tôi muốn thêm một điều. Xem xét cơ sở dữ liệu lỗi là tốt để làm vì nhiều lý do. Nhưng trong bối cảnh của câu hỏi ban đầu, hãy nhớ rằng thật khó để biết có bao nhiêu vấn đề con trỏ không hợp lệ đã được ngăn chặn, hoặc ít nhất là bị bắt sớm đến mức không đưa nó vào cơ sở dữ liệu lỗi. Lịch sử lỗi cung cấp bằng chứng tốt hơn để thêm các quy tắc mã hóa.
jimhark

2

Có hai lý do:

Tránh sự cố khi giải phóng đôi

Được viết bởi RageZ trong một câu hỏi trùng lặp .

Lỗi phổ biến nhất trong c là miễn phí gấp đôi. Về cơ bản bạn làm một cái gì đó như thế

free(foobar);
/* lot of code */
free(foobar);

và nó kết thúc khá tệ, hệ điều hành cố gắng giải phóng một số bộ nhớ đã được giải phóng và nói chung là segfault. Vì vậy, cách tốt nhất là đặt thành NULL, để bạn có thể thực hiện kiểm tra và kiểm tra xem bạn có thực sự cần giải phóng bộ nhớ này không

if(foobar != NULL){
  free(foobar);
}

cũng cần lưu ý rằng free(NULL) sẽ không làm gì cả nên bạn không phải viết câu lệnh if. Tôi không thực sự là một chuyên gia về hệ điều hành nhưng tôi khá đẹp ngay cả bây giờ hầu hết các hệ điều hành sẽ gặp sự cố miễn phí gấp đôi.

Đó cũng là một lý do chính tại sao tất cả các ngôn ngữ có bộ sưu tập rác (Java, dotnet) rất tự hào vì không gặp phải vấn đề này và cũng không phải rời khỏi để phát triển toàn bộ quản lý bộ nhớ.

Tránh sử dụng con trỏ đã được giải phóng

Viết bởi Martin v. Löwis trong một câu trả lời khác .

Đặt con trỏ không sử dụng thành NULL là một kiểu phòng thủ, bảo vệ chống lại các lỗi con trỏ lơ lửng. Nếu một con trỏ lơ lửng được truy cập sau khi nó được giải phóng, bạn có thể đọc hoặc ghi đè lên bộ nhớ ngẫu nhiên. Nếu một con trỏ null được truy cập, bạn sẽ gặp sự cố ngay lập tức trên hầu hết các hệ thống, cho bạn biết ngay lỗi đó là gì.

Đối với các biến cục bộ, có thể hơi vô nghĩa nếu "rõ ràng" rằng con trỏ không được truy cập nữa sau khi được giải phóng, do đó kiểu này phù hợp hơn với dữ liệu thành viên và biến toàn cục. Ngay cả đối với các biến cục bộ, nó có thể là một cách tiếp cận tốt nếu hàm tiếp tục sau khi bộ nhớ được giải phóng.

Để hoàn thành kiểu, bạn cũng nên khởi tạo con trỏ tới NULL trước khi chúng được gán một giá trị con trỏ thực.


1

Khi bạn có một nhóm đảm bảo chất lượng tại chỗ, hãy để tôi thêm một điểm nhỏ về QA. Một số công cụ QA tự động cho C sẽ gắn cờ các bài tập cho các con trỏ được giải phóng là "gán vô dụng cho ptr". Ví dụ: PC-lint / FlexeLint từ Gimpel Software nói tst.c 8 Warning 438: Last value assigned to variable 'nPtr' (defined at line 5) not used

Có nhiều cách để chọn lọc các tin nhắn, vì vậy bạn vẫn có thể đáp ứng cả hai yêu cầu QA, nếu nhóm của bạn quyết định như vậy.


1

Luôn luôn nên khai báo một biến con trỏ với NULL như,

int *ptr = NULL;

Giả sử, ptr đang trỏ đến địa chỉ bộ nhớ 0x1000 . Sau khi sử dụng free(ptr), luôn luôn nên vô hiệu hóa biến con trỏ bằng cách khai báo lại với NULL . ví dụ:

free(ptr);
ptr = NULL;

Nếu không được khai báo lại thành NULL , biến con trỏ vẫn tiếp tục trỏ đến cùng một địa chỉ ( 0x1000 ), biến con trỏ này được gọi là con trỏ lơ lửng . Nếu bạn xác định một biến con trỏ khác (giả sử, q ) và tự động phân bổ địa chỉ cho con trỏ mới, có thể có cùng một địa chỉ ( 0x1000 ) cho biến con trỏ mới. Nếu trong trường hợp, bạn sử dụng cùng một con trỏ ( ptr ) và cập nhật giá trị tại địa chỉ được trỏ bởi cùng một con trỏ ( ptr ), thì chương trình sẽ kết thúc việc viết một giá trị đến vị trí mà q đang trỏ (vì pq là chỉ đến cùng một địa chỉ (0x1000 )).

ví dụ

*ptr = 20; //Points to 0x1000
free(ptr);
int *q = (int *)malloc(sizeof(int) * 2); //Points to 0x1000
*ptr = 30; //Since ptr and q are pointing to the same address, so the value of the address to which q is pointing would also change.

1

Câu chuyện dài: Bạn không muốn vô tình (do nhầm lẫn) truy cập vào địa chỉ mà bạn đã giải phóng. Bởi vì, khi bạn giải phóng địa chỉ, bạn cho phép địa chỉ đó trong heap được phân bổ cho một số ứng dụng khác.

Tuy nhiên, nếu bạn không đặt con trỏ thành NULL và do nhầm lẫn, hãy thử hủy tham chiếu con trỏ hoặc thay đổi giá trị của địa chỉ đó; BẠN CÓ THỂ VẪN LÀM ĐƯỢC. NHƯNG KHÔNG PHẢI NÓI RATNG BẠN MUỐN ĐĂNG KÝ.

Tại sao tôi vẫn có thể truy cập vị trí bộ nhớ mà tôi đã giải phóng? Bởi vì: Bạn có thể có bộ nhớ trống, nhưng biến con trỏ vẫn có thông tin về địa chỉ bộ nhớ heap. Vì vậy, như một chiến lược phòng thủ, vui lòng đặt nó thành NULL.

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.