Xóa một con trỏ void có an toàn không?


96

Giả sử tôi có mã sau:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Cái này có an toàn không? Hay phải ptrđược chuyển đến char*trước khi xóa?


Tại sao bạn lại tự quản lý bộ nhớ? Bạn đang tạo cấu trúc dữ liệu nào? Việc quản lý bộ nhớ rõ ràng là khá hiếm trong C ++; bạn thường nên sử dụng các lớp xử lý nó cho bạn từ STL (hoặc từ Boost trong một chụm).
Brian

6
Chỉ dành cho những người đang đọc, tôi sử dụng các biến void * làm tham số cho chuỗi của tôi trong win c ++ (xem _beginthreadex). Thông thường chúng chỉ đến các lớp một cách nhạy bén.
ryansstack

3
Trong trường hợp này, đó là một trình bao bọc mục đích chung cho mới / xóa, có thể chứa số liệu thống kê theo dõi phân bổ hoặc nhóm bộ nhớ được tối ưu hóa. Trong các trường hợp khác, tôi đã thấy các con trỏ đối tượng được lưu trữ không chính xác dưới dạng biến thành viên void * và bị xóa không chính xác trong trình hủy mà không truyền trở lại kiểu đối tượng thích hợp. Vì vậy, tôi tò mò về sự an toàn / cạm bẫy.
Đã rút vào

1
Đối với trình bao bọc mục đích chung cho mới / xóa, bạn có thể nạp chồng các toán tử mới / xóa. Tùy thuộc vào môi trường bạn sử dụng, bạn có thể nhận được các móc vào quản lý bộ nhớ để theo dõi phân bổ. Nếu bạn rơi vào tình huống mà bạn không biết những gì bạn đang xóa, hãy coi đó như một gợi ý mạnh mẽ rằng thiết kế của bạn chưa tối ưu và cần được cấu trúc lại.
Hans

38
Tôi nghĩ rằng có quá nhiều câu hỏi đặt ra câu hỏi thay vì trả lời nó. (Không chỉ ở đây, mà ở tất cả SO)
Petruza

Câu trả lời:


24

Nó phụ thuộc vào "an toàn." Nó thường sẽ hoạt động vì thông tin được lưu trữ cùng với con trỏ về bản thân việc phân bổ, vì vậy bộ phân bổ giao dịch có thể trả nó về đúng vị trí. Theo nghĩa này, nó là "an toàn" miễn là trình phân bổ của bạn sử dụng các thẻ ranh giới nội bộ. (Nhiều làm.)

Tuy nhiên, như đã đề cập trong các câu trả lời khác, việc xóa một con trỏ void sẽ không gọi hàm hủy, điều này có thể là một vấn đề. Theo nghĩa đó, nó không phải là "an toàn."

Không có lý do chính đáng để làm những gì bạn đang làm theo cách bạn đang làm. Nếu bạn muốn viết các hàm định vị của riêng mình, bạn có thể sử dụng các mẫu hàm để tạo các hàm với kiểu chính xác. Một lý do chính đáng để làm điều đó là tạo bộ phân bổ nhóm, có thể cực kỳ hiệu quả cho các loại cụ thể.

Như đã đề cập trong các câu trả lời khác, đây là hành vi không xác định trong C ++. Nói chung là tốt để tránh hành vi không xác định, mặc dù bản thân chủ đề này rất phức tạp và chứa đựng nhiều ý kiến ​​trái chiều.


9
Làm thế nào đây là một câu trả lời được chấp nhận? Không có ý nghĩa gì khi "xóa một con trỏ void" - an toàn là một điểm tranh luận.
Kerrek SB

6
"Không có lý do chính đáng để làm những gì bạn đang làm theo cách bạn đang làm." Đó là ý kiến ​​của bạn, không phải thực tế.
rxantos

8
@rxantos Cung cấp một ví dụ ngược lại trong đó việc thực hiện những gì tác giả của câu hỏi muốn làm là một ý tưởng hay trong C ++.
Christopher

Tôi nghĩ rằng câu trả lời này thực sự là hợp lý, nhưng tôi cũng nghĩ rằng bất kỳ câu trả lời nào cho câu hỏi này ít nhất cần phải đề cập rằng đây là hành vi không xác định.
Kyle Strand,

@Christopher Hãy thử viết một hệ thống thu gom rác không thuộc loại cụ thể nhưng hoạt động đơn giản. Thực tế là sizeof(T*) == sizeof(U*)đối với tất cả các T,Ugợi ý rằng có thể có 1 void *triển khai bộ thu gom rác dựa trên khuôn mẫu . Nhưng sau đó, khi gc thực sự phải xóa / giải phóng một con trỏ chính xác thì câu hỏi này sẽ nảy sinh. Để làm cho nó hoạt động, bạn cần có trình bao bọc hàm hủy hàm lambda (urgh) hoặc bạn sẽ cần một số loại động "loại như dữ liệu" cho phép qua lại giữa một loại và một cái gì đó có thể lưu trữ.
BitTickler

147

Xóa thông qua con trỏ void không được xác định bởi Tiêu chuẩn C ++ - xem phần 5.3.5 / 3:

Trong phương án thay thế đầu tiên (đối tượng xóa), nếu kiểu tĩnh của toán hạng khác với kiểu động của nó, kiểu tĩnh sẽ là lớp cơ sở của kiểu động của toán hạng và kiểu tĩnh phải có bộ hủy ảo hoặc hành vi là không xác định . Trong giải pháp thay thế thứ hai (xóa mảng) nếu kiểu động của đối tượng cần xóa khác với kiểu tĩnh của nó, thì hành vi đó là không xác định.

Và chú thích của nó:

Điều này ngụ ý rằng không thể xóa một đối tượng bằng con trỏ kiểu void * vì không có đối tượng nào thuộc kiểu void

.


6
Bạn có chắc chắn bạn nhấn đúng báo giá? Tôi nghĩ chú thích cuối trang đề cập đến văn bản này: "Trong phương án thay thế đầu tiên (xóa đối tượng), nếu kiểu tĩnh của toán hạng khác với kiểu động của nó, kiểu tĩnh sẽ là lớp cơ sở của kiểu toán hạng động và kiểu tĩnh kiểu sẽ có một trình hủy ảo hoặc hành vi không được xác định. Trong phương án thứ hai (xóa mảng) nếu kiểu động của đối tượng cần xóa khác với kiểu tĩnh của nó, thì hành vi đó là không xác định. " :)
Johannes Schaub - litb

2
Bạn đúng - Tôi đã cập nhật câu trả lời. Tôi không nghĩ rằng nó phủ nhận điểm cơ bản mặc dù?

Tất nhiên là không rồi. Nó vẫn nói đó là UB. Thậm chí nhiều hơn như vậy, bây giờ nó nói nó chuẩn mực mà xóa void * là UB :)
Johannes Schaub - litb

Lấp đầy bộ nhớ địa chỉ trỏ của con trỏ void bằng cách NULLtạo ra sự khác biệt nào cho việc quản lý bộ nhớ ứng dụng?
artu-hnrq

25

Đó không phải là một ý tưởng hay và không phải là điều bạn sẽ làm trong C ++. Bạn đang mất thông tin loại của bạn mà không có lý do.

Hàm hủy của bạn sẽ không được gọi trên các đối tượng trong mảng mà bạn đang xóa khi bạn gọi nó cho các kiểu không nguyên thủy.

Thay vào đó, bạn nên ghi đè mới / xóa.

Việc xóa khoảng trống * có thể tình cờ sẽ giải phóng bộ nhớ của bạn một cách chính xác, nhưng sai vì kết quả không được xác định.

Nếu vì lý do nào đó mà tôi không biết, bạn cần lưu trữ con trỏ của mình trong một khoảng trống * rồi giải phóng nó, bạn nên sử dụng malloc và miễn phí.


5
Bạn nói đúng về trình hủy không được gọi, nhưng sai về kích thước không xác định. Nếu bạn cho phép xóa một con trỏ mà bạn nhận được từ mới, trên thực tế , nó sẽ biết kích thước của thứ bị xóa, hoàn toàn khác với loại. Cách nó hoạt động không được tiêu chuẩn C ++ chỉ định, nhưng tôi đã thấy các triển khai trong đó kích thước được lưu trữ ngay trước khi dữ liệu mà con trỏ trả về bởi 'new' trỏ đến.
KeyserSoze vào

Đã xóa phần về kích thước, mặc dù tiêu chuẩn C ++ nói rằng nó là không xác định. Tôi biết rằng malloc / free sẽ hoạt động đối với con trỏ void *.
Brian R. Bondy

Không giả sử bạn có một liên kết web đến phần liên quan của tiêu chuẩn? Tôi biết rằng một số triển khai new / delete mà tôi đã xem qua chắc chắn hoạt động chính xác mà không có kiến ​​thức về loại, nhưng tôi thừa nhận rằng tôi chưa xem xét tiêu chuẩn chỉ định. IIRC C ++ ban đầu yêu cầu số phần tử mảng khi xóa mảng, nhưng không còn như vậy với các phiên bản mới nhất.
KeyserSoze

Hãy xem câu trả lời của @Neil Butterworth. Theo tôi, câu trả lời của anh ấy nên được chấp nhận.
Brian R. Bondy

2
@keysersoze: Nói chung tôi không đồng ý với tuyên bố của bạn. Chỉ vì một số triển khai đã lưu trữ kích thước trước khi bộ nhớ được cấp phát không có nghĩa là đó là một quy tắc.

13

Xóa một con trỏ void rất nguy hiểm vì các hàm hủy sẽ không được gọi trên giá trị mà nó thực sự trỏ tới. Điều này có thể dẫn đến rò rỉ bộ nhớ / tài nguyên trong ứng dụng của bạn.


1
char không có hàm tạo / hủy.
rxantos

9

Câu hỏi không có ý nghĩa. Sự nhầm lẫn của bạn một phần có thể do ngôn ngữ cẩu thả mà mọi người thường sử dụng delete:

Bạn sử dụng deleteđể hủy một đối tượng đã được cấp phát động. Làm như vậy, bạn tạo một biểu thức xóa với một con trỏ đến đối tượng đó . Bạn không bao giờ "xóa một con trỏ". Những gì bạn thực sự làm là "xóa một đối tượng được xác định bằng địa chỉ của nó".

Bây giờ chúng ta thấy tại sao câu hỏi không có ý nghĩa: Con trỏ void không phải là "địa chỉ của một đối tượng". Nó chỉ là một địa chỉ, không có bất kỳ ngữ nghĩa nào. Nó có thể đến từ địa chỉ của một đối tượng thực tế, nhưng thông tin đó bị mất, vì nó được mã hóa theo kiểu con trỏ ban đầu. Cách duy nhất để khôi phục một con trỏ đối tượng là chuyển con trỏ void trở lại một con trỏ đối tượng (điều này yêu cầu tác giả biết ý nghĩa của con trỏ). voidbản thân nó là một kiểu không hoàn chỉnh và do đó không bao giờ là kiểu của một đối tượng, và con trỏ void không bao giờ có thể được sử dụng để xác định một đối tượng. (Các đối tượng được xác định chung theo loại và địa chỉ của chúng.)


Phải thừa nhận rằng câu hỏi không có nhiều ý nghĩa nếu không có bất kỳ bối cảnh xung quanh nào. Một số trình biên dịch C ++ sẽ vẫn vui vẻ biên dịch mã vô nghĩa như vậy (nếu họ cảm thấy hữu ích, có thể đưa ra cảnh báo về nó). Vì vậy, câu hỏi được đặt ra để đánh giá những rủi ro đã biết khi chạy mã kế thừa có chứa thao tác không được khuyến khích này: nó có bị sập không? rò rỉ một số hoặc tất cả bộ nhớ mảng ký tự? cái gì khác dành riêng cho nền tảng?
Đã rút vào

Cảm ơn vì đã trả lời chu đáo. Ủng hộ!
Đã rút tiền

1
@Andrew: Tôi e rằng tiêu chuẩn khá rõ ràng về điều này: "Giá trị của toán hạng deletecó thể là giá trị con trỏ null, một con trỏ đến một đối tượng không phải mảng được tạo bởi một biểu thức mới trước đó hoặc một con trỏ tới một subobject đại diện cho một lớp cơ sở của một đối tượng như vậy. Nếu không, hành vi là không xác định. " Vì vậy, nếu một trình biên dịch mã của bạn chấp nhận mà không có một chẩn đoán, nó không có gì nhưng một lỗi trong trình biên dịch ...
Kerrek SB

1
@KerrekSB - Không có gì khác ngoài một lỗi trong trình biên dịch - Tôi không đồng ý. Tiêu chuẩn cho biết hành vi là không xác định. Điều này có nghĩa là trình biên dịch / triển khai có thể làm bất cứ điều gì mà vẫn tuân thủ tiêu chuẩn. Nếu phản hồi của trình biên dịch là nói rằng bạn không thể xóa con trỏ void *, thì không sao. Nếu phản hồi của trình biên dịch là xóa ổ cứng, điều đó cũng OK. OTOH, nếu phản ứng của trình biên dịch là không tạo ra bất kỳ chẩn đoán nào mà thay vào đó tạo mã giải phóng bộ nhớ được liên kết với con trỏ đó, điều đó cũng OK. Đây là một cách đơn giản để giao dịch với hình thức UB này.
David Hammen

Chỉ cần nói thêm: Tôi không dung túng việc sử dụng delete void_pointer. Đó là hành vi không xác định. Lập trình viên không bao giờ được gọi hành vi không xác định, ngay cả khi phản hồi dường như thực hiện những gì lập trình viên muốn thực hiện.
David Hammen

6

Nếu bạn thực sự phải làm điều này, tại sao không cắt bỏ người trung gian ( newvà các deletenhà điều hành) và gọi toàn cầu operator newoperator deletetrực tiếp? (Tất nhiên, nếu bạn đang cố gắng đo lường các toán tử newdelete, bạn thực sự phải thực hiện lại operator newoperator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Lưu ý rằng không giống như malloc(), operator newném std::bad_allockhi thất bại (hoặc gọi new_handlernếu một trong những đã được đăng ký).


Điều này đúng, vì char không có hàm tạo / hủy.
rxantos

5

Bởi vì char không có logic hủy đặc biệt. Điều này sẽ không hoạt động.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

Ông sẽ không được gọi.


5

Nếu bạn muốn sử dụng void *, tại sao bạn không chỉ sử dụng malloc / free? new / delete không chỉ là quản lý bộ nhớ. Về cơ bản, new / delete gọi một hàm tạo / hủy và có nhiều thứ khác đang diễn ra. Nếu bạn chỉ sử dụng các kiểu tích hợp sẵn (như char *) và xóa chúng qua void *, nó sẽ hoạt động nhưng vẫn không được khuyến khích. Điểm mấu chốt là sử dụng malloc / free nếu bạn muốn sử dụng void *. Nếu không, bạn có thể sử dụng các hàm mẫu để thuận tiện cho bạn.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}

Tôi đã không viết mã trong ví dụ - tình cờ thấy mẫu này được sử dụng ở một số nơi, tò mò trộn lẫn quản lý bộ nhớ C / C ++ và tự hỏi mối nguy hiểm cụ thể là gì.
Đã rút vào

Viết C / C ++ là một công thức dẫn đến thất bại. Ai đã viết điều đó đáng lẽ phải viết cái này hay cái kia.
David Thornley

2
@David Đó là C ++, không phải C / C ++. C không có mẫu, cũng như không sử dụng mới và xóa.
rxantos

5

Rất nhiều người đã nhận xét rằng không, không an toàn khi xóa con trỏ void. Tôi đồng ý với điều đó, nhưng tôi cũng muốn nói thêm rằng nếu bạn đang làm việc với con trỏ void để phân bổ các mảng liền kề hoặc thứ gì đó tương tự, bạn có thể làm điều này với newđể có thể sử dụng deletean toàn (với, ahem , một chút công việc phụ). Điều này được thực hiện bằng cách phân bổ một con trỏ rỗng vào vùng nhớ (được gọi là 'đấu trường') và sau đó cung cấp con trỏ tới đấu trường thành mới. Xem phần này trong Câu hỏi thường gặp về C ++ . Đây là một cách tiếp cận phổ biến để triển khai vùng nhớ trong C ++.


0

Hầu như không có lý do để làm điều này.

Trước hết, nếu bạn không biết loại của dữ liệu, và tất cả các bạn biết là nó void*, sau đó bạn thực sự chỉ nên được điều trị mà dữ liệu như một typeless blob của dữ liệu nhị phân ( unsigned char*), và sử dụng malloc/ freeđể đối phó với nó . Điều này đôi khi được yêu cầu đối với những thứ như dữ liệu dạng sóng và những thứ tương tự, nơi bạn cần chuyển các void*con trỏ đến C apis. Tốt rồi.

Nếu bạn làm biết loại dữ liệu (tức là nó có một ctor / dtor), nhưng đối với một số lý do bạn đã kết thúc với một void*con trỏ (vì lý do gì bạn có) sau đó bạn thực sự nên bỏ nó trở lại kiểu bạn biết điều đó để được , và kêu gọi deletenó.


0

Tôi đã sử dụng void *, (hay còn gọi là các loại không xác định) trong khuôn khổ của mình trong khi phản chiếu mã và các kỳ công khác về sự mơ hồ, và cho đến nay, tôi không gặp rắc rối nào (rò rỉ bộ nhớ, vi phạm quyền truy cập, v.v.) từ bất kỳ trình biên dịch nào. Chỉ cảnh báo do hoạt động không chuẩn.

Hoàn toàn hợp lý khi xóa một không xác định (void *). Chỉ cần đảm bảo rằng con trỏ tuân theo các nguyên tắc sau, nếu không nó có thể ngừng hoạt động:

1) Con trỏ không xác định không được trỏ đến một kiểu có giải mã lệnh tầm thường, và vì vậy khi được ép như một con trỏ không xác định, nó sẽ KHÔNG BAO GIỜ BỊ XÓA. Chỉ xóa con trỏ không xác định SAU KHI truyền nó trở lại kiểu GỐC.

2) Cá thể đang được tham chiếu như một con trỏ không xác định trong ngăn xếp bị ràng buộc hoặc bộ nhớ liên kết đống? Nếu con trỏ không xác định tham chiếu đến một cá thể trên ngăn xếp, thì nó KHÔNG BAO GIỜ BỊ XÓA!

3) Bạn có khẳng định 100% con trỏ không xác định là vùng bộ nhớ hợp lệ không? Không, vậy thì nó KHÔNG BAO GIỜ BỊ XÓA!

Nói chung, có rất ít công việc trực tiếp có thể được thực hiện bằng cách sử dụng kiểu con trỏ (void *) không xác định. Tuy nhiên, một cách gián tiếp, khoảng trống * là một tài sản tuyệt vời cho các nhà phát triển C ++ dựa vào khi cần phải nhập nhằng dữ liệu.


0

Nếu bạn chỉ muốn một bộ đệm, hãy sử dụng malloc / free. Nếu bạn phải sử dụng new / delete, hãy xem xét một lớp wrapper tầm thường:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;

0

Đối với trường hợp cụ thể của char.

char là một loại nội tại không có hàm hủy đặc biệt. Vì vậy, các đối số rò rỉ là một cuộc tranh luận.

sizeof (char) thường là một vì vậy cũng không có đối số căn chỉnh. Trong trường hợp hiếm nền tảng mà sizeof (char) không phải là một, chúng phân bổ bộ nhớ được căn chỉnh đủ cho char của chúng. Vì vậy, đối số căn chỉnh cũng là một tranh luận.

malloc / free sẽ nhanh hơn trong trường hợp này. Nhưng bạn đã mất std :: bad_alloc và phải kiểm tra kết quả của malloc. Gọi các toán tử mới và xóa toàn cầu có thể tốt hơn vì nó bỏ qua người trung gian.


1
" sizeof (char) thường là một " sizeof (char) LUÔN là một
tò mòguy

Không phải cho đến gần đây (2019), mọi người mới nghĩ rằng điều đó newthực sự được định nghĩa để ném. Đây không phải là sự thật. Nó phụ thuộc vào trình biên dịch và chuyển đổi trình biên dịch. Xem ví dụ /GX[-] enable C++ EH (same as /EHsc)công tắc MSVC2019 . Ngoài ra, trên các hệ thống nhúng, nhiều người chọn không trả thuế hiệu suất cho các ngoại lệ C ++. Vì vậy, câu bắt đầu bằng "But you forfeit std :: bad_alloc ..." là đáng nghi vấn.
BitTickler
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.