Apakah aman untuk menghapus penunjuk kosong?

96

Misalkan saya memiliki kode berikut:

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

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

Apakah ini aman? Atau harus ptrdilemparkan ke char*sebelum dihapus?

An̲̳̳drew
sumber
Mengapa Anda melakukan manajemen memori sendiri? Struktur data apa yang Anda buat? Perlu melakukan manajemen memori eksplisit cukup jarang di C ++; Anda biasanya harus menggunakan kelas yang menanganinya untuk Anda dari STL (atau dari Boost dalam keadaan darurat).
Brian
6
Hanya untuk orang yang membaca, saya menggunakan variabel void * sebagai parameter untuk utas saya di win c ++ (lihat _beginthreadex). Biasanya mereka secara acut menunjuk ke kelas.
ryansstack
3
Dalam kasus ini, ini adalah pembungkus tujuan umum untuk baru / hapus, yang dapat berisi statistik pelacakan alokasi atau kumpulan memori yang dioptimalkan. Dalam kasus lain, saya telah melihat pointer objek salah disimpan sebagai variabel anggota void *, dan salah dihapus di destruktor tanpa mengembalikan ke jenis objek yang sesuai. Jadi saya ingin tahu tentang keamanan / jebakan.
Mengundurkan
1
Untuk pembungkus tujuan umum untuk new / delete Anda dapat membebani operator new / delete. Bergantung pada lingkungan apa yang Anda gunakan, Anda mungkin mendapatkan kaitan ke dalam manajemen memori untuk melacak alokasi. Jika Anda berada dalam situasi di mana Anda tidak tahu apa yang Anda hapus, anggap itu sebagai petunjuk kuat bahwa desain Anda kurang optimal dan perlu refactoring.
Hans
39
Saya pikir ada terlalu banyak pertanyaan yang dipertanyakan daripada menjawabnya. (Tidak hanya di sini, tetapi di semua SO)
Petruza

Jawaban:

24

Itu tergantung pada "aman." Ini biasanya akan berfungsi karena informasi disimpan bersama dengan penunjuk tentang alokasi itu sendiri, sehingga deallocator dapat mengembalikannya ke tempat yang tepat. Dalam pengertian ini "aman" selama pengalokasi Anda menggunakan tag batas internal. (Banyak yang melakukannya.)

Namun, seperti yang disebutkan dalam jawaban lain, menghapus void pointer tidak akan memanggil destruktor, yang bisa menjadi masalah. Dalam pengertian itu, ini tidak "aman."

Tidak ada alasan yang baik untuk melakukan apa yang Anda lakukan dengan cara Anda melakukannya. Jika Anda ingin menulis fungsi deallokasi Anda sendiri, Anda dapat menggunakan template fungsi untuk menghasilkan fungsi dengan jenis yang benar. Alasan yang baik untuk melakukannya adalah untuk menghasilkan pengalokasi kumpulan, yang bisa sangat efisien untuk tipe tertentu.

Seperti yang disebutkan dalam jawaban lain, ini adalah perilaku tidak terdefinisi di C ++. Secara umum adalah baik untuk menghindari perilaku yang tidak terdefinisi, meskipun topiknya sendiri rumit dan penuh dengan pendapat yang saling bertentangan.

Christopher
sumber
9
Bagaimana ini jawaban yang diterima? Tidak masuk akal untuk "menghapus penunjuk void" - keamanan adalah hal yang diperdebatkan.
Kerrek SB
6
"Tidak ada alasan yang baik untuk melakukan apa yang Anda lakukan dengan cara Anda melakukannya." Itu pendapatmu, bukan fakta.
rxantos
8
@rxantos Berikan contoh balasan di mana melakukan apa yang ingin dilakukan oleh penulis pertanyaan adalah ide yang bagus di C ++.
Christopher
Saya pikir jawaban ini sebenarnya sebagian besar masuk akal, tetapi saya juga berpikir bahwa jawaban apa pun untuk pertanyaan ini perlu setidaknya menyebutkan bahwa ini adalah perilaku yang tidak ditentukan.
Kyle Strand
@Christopher Cobalah menulis sistem pengumpul sampah tunggal yang bukan tipe spesifik tetapi hanya berfungsi. Fakta bahwa sizeof(T*) == sizeof(U*)untuk semua T,Umenyarankan bahwa itu harus mungkin untuk memiliki 1 non template, void *implementasi pengumpul sampah berbasis. Tapi kemudian, ketika gc benar-benar harus menghapus / membebaskan pointer, pertanyaan ini muncul. Untuk membuatnya bekerja, Anda memerlukan lambda function destructor wrappers (urgh) atau Anda akan membutuhkan semacam "tipe sebagai data" yang dinamis yang memungkinkan bolak-balik antara suatu tipe dan sesuatu yang dapat disimpan.
BitTickler
147

Menghapus melalui penunjuk kosong tidak ditentukan oleh Standar C ++ - lihat bagian 5.3.5 / 3:

Dalam alternatif pertama (hapus objek), jika tipe statis operan berbeda dari tipe dinamisnya, tipe statis harus kelas dasar tipe dinamis operan dan tipe statis harus memiliki destruktor virtual atau perilakunya tidak ditentukan . Di alternatif kedua (hapus array) jika tipe dinamis dari objek yang akan dihapus berbeda dari tipe statisnya, perilakunya tidak ditentukan.

Dan catatan kakinya:

Ini menyiratkan bahwa suatu objek tidak dapat dihapus menggunakan penunjuk berjenis void * karena tidak ada objek berjenis void

.


sumber
6
Apakah Anda yakin Anda menekan kutipan yang benar? Saya pikir catatan kaki mengacu pada teks ini: "Pada alternatif pertama (hapus objek), jika jenis statis dari operan berbeda dari jenis dinamis, jenis statis akan menjadi kelas dasar dari jenis dinamis operan dan statis tipe harus memiliki penghancur virtual atau perilakunya tidak terdefinisi. Pada alternatif kedua (hapus larik) jika tipe dinamis dari objek yang akan dihapus berbeda dari tipe statisnya, perilakunya tidak ditentukan. " :)
Johannes Schaub - litb
2
Anda benar - saya telah memperbarui jawabannya. Saya tidak berpikir itu meniadakan poin dasar?
Tidak, tentu saja tidak. Ia masih mengatakan itu UB. Terlebih lagi, sekarang dinyatakan secara normatif bahwa menghapus void * adalah UB :)
Johannes Schaub - litb
Isi memori alamat menunjuk dari pointer kosong dengan NULLmembuat perbedaan untuk manajemen memori aplikasi?
artu-hnrq
25

Ini bukan ide yang bagus dan bukan sesuatu yang akan Anda lakukan di C ++. Anda kehilangan info tipe Anda tanpa alasan.

Destruktor Anda tidak akan dipanggil pada objek dalam larik yang Anda hapus saat Anda memanggilnya untuk tipe non primitif.

Anda sebaiknya mengganti baru / hapus.

Menghapus void * mungkin akan membebaskan memori Anda dengan benar secara kebetulan, tetapi itu salah karena hasilnya tidak ditentukan.

Jika karena alasan tertentu tidak saya ketahui Anda perlu menyimpan pointer Anda dalam void * lalu membebaskannya, Anda harus menggunakan malloc dan gratis.

Brian R. Bondy
sumber
5
Anda benar tentang destruktor yang tidak dipanggil, tetapi salah tentang ukurannya yang tidak diketahui. Jika Anda memberikan menghapus pointer yang Anda dapatkan dari baru, itu tidak sebenarnya mengetahui ukuran dari hal yang sedang dihapus, seluruhnya terlepas dari jenis. Bagaimana hal itu tidak ditentukan oleh standar C ++, tetapi saya telah melihat implementasi di mana ukurannya disimpan tepat sebelum data yang dikembalikan oleh penunjuk oleh poin 'baru'.
KeyserSoze
Menghapus bagian tentang ukuran, meskipun standar C ++ mengatakan itu tidak ditentukan. Saya tahu bahwa malloc / free akan berfungsi untuk petunjuk void *.
Brian R. Bondy
Tidakkah Anda mengira Anda memiliki tautan web ke bagian standar yang relevan? Saya tahu bahwa beberapa implementasi baru / hapus yang telah saya lihat pasti berfungsi dengan benar tanpa pengetahuan jenis, tetapi saya akui saya belum melihat apa yang ditentukan standar. IIRC C ++ awalnya memerlukan jumlah elemen array saat menghapus array, tetapi tidak lagi melakukannya dengan versi terbaru.
KeyserSoze
Silakan lihat jawaban @Neil Butterworth. Jawabannya harus diterima menurut pendapat saya.
Brian R. Bondy
2
@keysersoze: Umumnya saya tidak setuju dengan pernyataan Anda. Hanya karena beberapa implementasi memang menyimpan ukuran sebelum memori yang dialokasikan tidak berarti itu adalah aturan.
13

Menghapus penunjuk kosong berbahaya karena penghancur tidak akan dipanggil pada nilai yang sebenarnya ditunjukkannya. Hal ini dapat mengakibatkan kebocoran memori / sumber daya dalam aplikasi Anda.

JaredPar
sumber
1
char tidak memiliki konstruktor / destruktor.
rxantos
9

Pertanyaan itu tidak masuk akal. Kebingungan Anda mungkin sebagian karena bahasa ceroboh yang sering digunakan orang dengan delete:

Anda gunakan deleteuntuk menghancurkan objek yang dialokasikan secara dinamis. Lakukan itu, Anda membentuk ekspresi hapus dengan penunjuk ke objek itu . Anda tidak pernah "menghapus penunjuk". Apa yang sebenarnya Anda lakukan adalah "menghapus sebuah objek yang dikenali dari alamatnya".

Sekarang kita melihat mengapa pertanyaan itu tidak masuk akal: Void pointer bukanlah "alamat sebuah objek". Itu hanya sebuah alamat, tanpa semantik apapun. Ini mungkin berasal dari alamat objek sebenarnya, tetapi informasi itu hilang, karena dikodekan dalam tipe penunjuk asli. Satu-satunya cara untuk memulihkan penunjuk objek adalah dengan mengembalikan penunjuk kosong ke penunjuk objek (yang mengharuskan penulis untuk mengetahui apa arti penunjuk). voiditu sendiri adalah tipe yang tidak lengkap dan karenanya tidak pernah menjadi tipe objek, dan penunjuk kosong tidak pernah dapat digunakan untuk mengidentifikasi objek. (Objek diidentifikasi secara bersama-sama berdasarkan jenis dan alamatnya.)

Kerrek SB
sumber
Memang, pertanyaan itu tidak masuk akal tanpa konteks sekitarnya. Beberapa kompiler C ++ masih akan dengan senang hati mengkompilasi kode yang tidak masuk akal tersebut (jika mereka merasa terbantu, mungkin akan mengeluarkan peringatan tentangnya). Jadi, pertanyaan yang diajukan untuk menilai risiko yang diketahui dari menjalankan kode lama yang berisi operasi keliru ini: apakah akan macet? membocorkan beberapa atau semua memori array karakter? hal lain yang khusus untuk platform?
Mengundurkan
Terima kasih atas tanggapan yang bijaksana. Upvoting!
Mengundurkan
1
@Andrew: Saya khawatir standarnya cukup jelas untuk ini: "Nilai operan dari deletemungkin berupa nilai penunjuk nol, penunjuk ke objek non-larik yang dibuat oleh ekspresi baru sebelumnya , atau penunjuk ke subobjek yang mewakili kelas dasar dari objek semacam itu. Jika tidak, perilakunya tidak ditentukan. " Jadi jika kompiler menerima kode Anda tanpa diagnostik, itu hanyalah bug di kompiler ...
Kerrek SB
1
@KerrekSB - Re itu hanyalah bug di kompiler - Saya tidak setuju. Standar tersebut mengatakan bahwa perilaku tidak ditentukan. Ini berarti kompilator / implementasi dapat melakukan apapun dan tetap sesuai dengan standar. Jika tanggapan kompiler mengatakan Anda tidak dapat menghapus penunjuk void *, tidak apa-apa. Jika tanggapan kompiler adalah menghapus hard drive, itu juga tidak masalah. OTOH, jika tanggapan kompiler tidak menghasilkan diagnostik apa pun melainkan menghasilkan kode yang membebaskan memori yang terkait dengan penunjuk itu, itu juga OK. Ini adalah cara sederhana untuk menghadapi bentuk UB seperti ini.
David Hammen
Hanya untuk menambahkan: Saya tidak membenarkan penggunaan delete void_pointer. Ini perilaku yang tidak terdefinisi. Pemrogram tidak boleh meminta perilaku tidak terdefinisi, bahkan jika responsnya tampak melakukan apa yang diinginkan oleh pemrogram.
David Hammen
6

Jika Anda benar-benar harus melakukan ini, mengapa tidak memotong tengah-tengah manusia (yang newdan deleteoperator) dan panggilan global operator newdan operator deletelangsung? (Tentu saja, jika Anda mencoba melengkapi operator newdan delete, Anda sebenarnya harus menerapkan ulang operator newdan operator delete.)

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

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

Perhatikan bahwa tidak seperti malloc(), operator newmelempar std::bad_allocpada kegagalan (atau memanggil new_handlerjika ada yang terdaftar).

bk1e
sumber
Ini benar, karena char tidak memiliki konstruktor / destruktor.
rxantos
5

Karena char tidak memiliki logika destruktor khusus. INI tidak akan berhasil.

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

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

D'ctor tidak akan dipanggil.


sumber
5

Jika Anda ingin menggunakan void *, mengapa Anda tidak menggunakan malloc / gratis saja? baru / hapus lebih dari sekedar pengelolaan memori. Pada dasarnya, new / delete memanggil konstruktor / destruktor dan ada lebih banyak hal yang terjadi. Jika Anda hanya menggunakan tipe bawaan (seperti char *) dan menghapusnya melalui void *, ini akan berhasil tetapi tetap tidak disarankan. Intinya adalah gunakan malloc / gratis jika Anda ingin menggunakan void *. Jika tidak, Anda dapat menggunakan fungsi template untuk kenyamanan Anda.

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);
}
muda
sumber
Saya tidak menulis kode di contoh - menemukan pola ini digunakan di beberapa tempat, anehnya mencampur manajemen memori C / C ++, dan bertanya-tanya apa bahayanya.
Mengundurkan
Menulis C / C ++ adalah resep untuk kegagalan. Siapapun yang menulis itu seharusnya menulis salah satunya.
David Thornley
2
@David Itu adalah C ++, bukan C / C ++. C tidak memiliki template, juga tidak menggunakan new dan delete.
rxantos
5

Banyak orang telah berkomentar mengatakan tidak, tidak aman untuk menghapus penunjuk kosong. Saya setuju dengan itu, tetapi saya juga ingin menambahkan bahwa jika Anda bekerja dengan void pointer untuk mengalokasikan array yang berdekatan atau yang serupa, Anda dapat melakukan ini newsehingga Anda dapat menggunakan deletedengan aman (dengan, ahem , sedikit kerja ekstra). Ini dilakukan dengan mengalokasikan penunjuk kosong ke wilayah memori (disebut 'arena') dan kemudian memasok penunjuk ke arena ke yang baru. Lihat bagian ini di FAQ C ++ . Ini adalah pendekatan umum untuk mengimplementasikan kumpulan memori di C ++.

Paul Morie
sumber
0

Hampir tidak ada alasan untuk melakukan ini.

Pertama-tama, jika Anda tidak tahu jenis datanya, dan yang Anda tahu hanyalah itu void*, maka Anda seharusnya memperlakukan data itu sebagai gumpalan data biner ( unsigned char*) tanpa tipe , dan gunakan malloc/ freeuntuk menghadapinya . Ini kadang-kadang diperlukan untuk hal-hal seperti data bentuk gelombang dan sejenisnya, di mana Anda perlu meneruskan void*petunjuk ke C apis. Tidak apa-apa.

Jika Anda melakukan mengetahui jenis data (yaitu memiliki ctor / dtor), tapi untuk beberapa alasan Anda berakhir dengan void*pointer (karena alasan apapun Anda memiliki) maka Anda benar-benar harus melemparkan kembali ke jenis yang Anda tahu itu untuk jadilah , dan panggil deleteitu.

bobobobo
sumber
0

Saya telah menggunakan void *, (alias tipe yang tidak diketahui) dalam kerangka saya untuk sementara dalam refleksi kode dan prestasi ambiguitas lainnya, dan sejauh ini, saya tidak memiliki masalah (kebocoran memori, pelanggaran akses, dll.) Dari kompiler mana pun. Hanya peringatan karena operasinya tidak standar.

Sangat masuk akal untuk menghapus yang tidak diketahui (void *). Pastikan saja penunjuk mengikuti pedoman ini, atau mungkin berhenti masuk akal:

1) Penunjuk yang tidak diketahui tidak boleh menunjuk ke tipe yang memiliki dekonstruktor sepele, dan ketika dicor sebagai penunjuk yang tidak diketahui, itu TIDAK PERNAH DIHAPUS. Hapus hanya penunjuk yang tidak dikenal SETELAH mentransmisikannya kembali ke tipe ASLI.

2) Apakah instance direferensikan sebagai pointer yang tidak dikenal dalam memori terikat tumpukan atau tumpukan? Jika pointer yang tidak diketahui mereferensikan sebuah instance pada stack, maka itu TIDAK PERNAH DIHAPUS!

3) Apakah Anda 100% positif pointer yang tidak diketahui adalah wilayah memori yang valid? Tidak, maka itu TIDAK PERNAH DITUNDA!

Secara keseluruhan, sangat sedikit pekerjaan langsung yang dapat dilakukan dengan menggunakan tipe penunjuk yang tidak diketahui (void *). Namun, secara tidak langsung, void * adalah aset yang bagus untuk diandalkan oleh developer C ++ saat ambiguitas data diperlukan.

zackery.fix
sumber
0

Jika Anda hanya ingin buffer, gunakan malloc / free. Jika Anda harus menggunakan new / delete, pertimbangkan kelas pembungkus sepele:

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;
Sanjaya R
sumber
0

Untuk kasus khusus char.

char adalah tipe intrinsik yang tidak memiliki destruktor khusus. Jadi argumen kebocoran itu bisa diperdebatkan.

sizeof (char) biasanya satu jadi tidak ada argumen penyelarasan juga. Dalam kasus platform langka di mana sizeof (char) bukan satu, mereka mengalokasikan memori yang cukup untuk karakter mereka. Jadi argumen penyelarasan juga bisa diperdebatkan.

malloc / free akan lebih cepat dalam kasus ini. Tetapi Anda kehilangan std :: bad_alloc dan harus memeriksa hasil malloc. Memanggil operator baru dan hapus global mungkin lebih baik karena melewati perantara.

rxantos
sumber
1
" sizeof (char) biasanya satu " sizeof (char) SELALU satu
penasaran
Tidak sampai saat ini (2019) orang berpikir bahwa newsebenarnya adalah melempar. Ini tidak benar. Ini tergantung pada kompiler dan kompilator. Lihat misalnya /GX[-] enable C++ EH (same as /EHsc)sakelar MSVC2019 . Juga pada sistem tertanam banyak yang memilih untuk tidak membayar pajak kinerja untuk pengecualian C ++. Jadi kalimat yang diawali dengan "But you forfeit std :: bad_alloc ..." patut dipertanyakan.
BitTickler