Saya mengerti perlunya destruktor virtual. Tetapi mengapa kita membutuhkan destruktor virtual murni? Dalam salah satu artikel C ++, penulis telah menyebutkan bahwa kami menggunakan destructor virtual murni ketika kami ingin membuat abstrak kelas.
Tetapi kita dapat membuat abstrak kelas dengan membuat salah satu fungsi anggota sebagai virtual murni.
Jadi pertanyaan saya adalah
Kapan kita benar-benar membuat virtual destructor murni? Adakah yang bisa memberikan contoh waktu nyata yang baik?
Ketika kita membuat kelas abstrak, apakah itu praktik yang baik untuk membuat destructor juga virtual murni? Jika ya..kemudian mengapa?
c++
destructor
pure-virtual
Menandai
sumber
sumber
Jawaban:
Mungkin alasan sebenarnya bahwa penghancur virtual murni diizinkan adalah bahwa untuk melarang mereka berarti menambahkan aturan lain ke bahasa dan tidak perlu untuk aturan ini karena tidak ada efek buruk dapat datang dari memungkinkan destruktor virtual murni.
Tidak, virtual tua biasa sudah cukup.
Jika Anda membuat objek dengan implementasi default untuk metode virtualnya dan ingin membuatnya abstrak tanpa memaksa siapa pun untuk menimpa metode spesifik apa pun , Anda bisa membuat destructor virtual murni. Saya tidak melihat banyak gunanya tapi itu mungkin.
Perhatikan bahwa karena kompiler akan menghasilkan destruktor implisit untuk kelas turunan, jika penulis kelas tidak melakukannya, kelas turunan apa pun tidak akan abstrak. Oleh karena itu memiliki destruktor virtual murni di kelas dasar tidak akan membuat perbedaan untuk kelas turunan. Itu hanya akan membuat abstrak kelas dasar (terima kasih atas komentar @kappa ).
Orang mungkin juga berasumsi bahwa setiap kelas turunan mungkin perlu memiliki kode pembersihan khusus dan menggunakan destruktor virtual murni sebagai pengingat untuk menulis satu tetapi ini tampaknya dibuat-buat (dan tidak diperkuat).
Catatan: destructor adalah satu-satunya metode yang bahkan jika itu adalah murni maya memiliki untuk memiliki implementasi untuk instantiate kelas turunan (fungsi virtual ya murni dapat memiliki implementasi).
sumber
foof::bar
jika Anda ingin melihatnya sendiri.Yang Anda butuhkan untuk kelas abstrak adalah setidaknya satu fungsi virtual murni. Fungsi apa pun akan dilakukan; tetapi ketika itu terjadi, destructor adalah sesuatu yang dimiliki oleh setiap kelas — jadi selalu ada sebagai kandidat. Lebih jauh lagi, membuat destructor virtual murni (bukan hanya virtual) tidak memiliki efek samping perilaku selain membuat kelas abstrak. Dengan demikian, banyak panduan gaya merekomendasikan bahwa destuctor virtual murni digunakan secara konsisten untuk menunjukkan bahwa suatu kelas adalah abstrak — jika tanpa alasan lain selain menyediakan tempat yang konsisten, seseorang yang membaca kode dapat melihat apakah kelas itu abstrak.
sumber
Jika Anda ingin membuat kelas dasar abstrak:
... paling mudah untuk membuat abstrak kelas dengan membuat destructor dan virtual murni memberikan definisi (metode body) untuk itu.
Untuk ABC hipotetis kami:
Anda menjamin bahwa itu tidak dapat dipakai (bahkan internal ke kelas itu sendiri, ini sebabnya konstruktor pribadi mungkin tidak cukup), Anda mendapatkan perilaku virtual yang Anda inginkan untuk destruktor, dan Anda tidak perlu menemukan dan menandai metode lain yang tidak perlu pengiriman virtual sebagai "virtual".
sumber
Dari jawaban yang saya baca untuk pertanyaan Anda, saya tidak dapat menyimpulkan alasan yang bagus untuk benar-benar menggunakan destructor virtual murni. Misalnya, alasan berikut tidak meyakinkan saya sama sekali:
Menurut pendapat saya, destruktor virtual murni dapat bermanfaat. Misalnya, anggap Anda memiliki dua kelas myClassA dan myClassB dalam kode Anda, dan bahwa myClassB mewarisi dari myClassA. Untuk alasan yang disebutkan oleh Scott Meyers dalam bukunya "Lebih Efektif C ++", Butir 33 "Membuat abstrak kelas non-daun", adalah praktik yang lebih baik untuk benar-benar membuat kelas abstrak myAbstractClass dari mana myClassA dan myClassB mewarisi. Ini memberikan abstraksi yang lebih baik dan mencegah beberapa masalah yang timbul dengan, misalnya, salinan objek.
Dalam proses abstraksi (membuat kelas myAbstractClass), bisa jadi tidak ada metode myClassA atau myClassB adalah kandidat yang baik untuk menjadi metode virtual murni (yang merupakan prasyarat untuk myAbstractClass menjadi abstrak). Dalam hal ini, Anda mendefinisikan destructor virtual murni kelas abstrak.
Selanjutnya contoh nyata dari beberapa kode yang saya tulis sendiri. Saya memiliki dua kelas, Numerik / PhysicsParams yang berbagi properti umum. Karena itu saya membiarkan mereka mewarisi dari kelas abstrak IParams. Dalam hal ini, saya sama sekali tidak punya metode yang bisa murni virtual. Metode setParameter, misalnya, harus memiliki tubuh yang sama untuk setiap subkelas. Satu-satunya pilihan yang saya miliki adalah membuat destructor IParams menjadi virtual.
sumber
IParam
untuk dilindungi, seperti yang dicatat dalam beberapa komentar lainnya.Jika Anda ingin berhenti membuat instance dari kelas dasar tanpa membuat perubahan dalam kelas turunan Anda yang sudah diimplementasikan dan diuji, Anda menerapkan destruktor virtual murni di kelas dasar Anda.
sumber
Di sini saya ingin memberi tahu kapan kita membutuhkan destruktor virtual dan kapan kita membutuhkan destruktor virtual murni
Bila Anda ingin tidak ada yang bisa membuat objek kelas Base secara langsung, gunakan destruktor virtual murni
virtual ~Base() = 0
. Biasanya setidaknya diperlukan satu fungsi virtual murni, mari kita ambilvirtual ~Base() = 0
, karena fungsi ini.Ketika Anda tidak perlu hal di atas, hanya Anda yang membutuhkan penghancuran yang aman dari objek kelas turunan
Base * pBase = new Derived (); hapus pBase; destructor virtual murni tidak diperlukan, hanya destruktor virtual yang akan melakukan pekerjaan.
sumber
Anda masuk ke hipotesis dengan jawaban ini, jadi saya akan mencoba untuk membuat penjelasan yang lebih sederhana, lebih membumi untuk kejelasan demi.
Hubungan dasar desain berorientasi objek adalah dua: IS-A dan HAS-A. Saya tidak mengada-ada. Itulah sebutan mereka.
IS-A menunjukkan bahwa objek tertentu mengidentifikasi sebagai kelas yang di atasnya dalam hirarki kelas. Objek pisang adalah objek buah jika merupakan subkelas dari kelas buah. Ini berarti bahwa di mana pun kelas buah dapat digunakan, pisang dapat digunakan. Ini bukan refleksif. Anda tidak dapat mengganti kelas dasar untuk kelas tertentu jika kelas spesifik itu dipanggil.
Has-a menunjukkan bahwa objek adalah bagian dari kelas komposit dan ada hubungan kepemilikan. Ini berarti dalam C ++ bahwa itu adalah objek anggota dan dengan demikian tanggung jawab berada pada kelas pemilik untuk membuangnya atau melepaskan kepemilikan sebelum merusak dirinya sendiri.
Kedua konsep ini lebih mudah diwujudkan dalam bahasa pewarisan tunggal daripada dalam model pewarisan berganda seperti c ++, tetapi aturan dasarnya sama. Komplikasi muncul ketika identitas kelas ambigu, seperti meneruskan pointer kelas Banana ke fungsi yang mengambil pointer kelas Fruit.
Fungsi virtual adalah, pertama, hal run-time. Ini adalah bagian dari polimorfisme karena digunakan untuk memutuskan fungsi mana yang akan dijalankan pada saat dipanggil dalam program yang sedang berjalan.
Kata kunci virtual adalah arahan kompiler untuk mengikat fungsi dalam urutan tertentu jika ada ambiguitas tentang identitas kelas. Fungsi virtual selalu dalam kelas induk (sejauh yang saya tahu) dan menunjukkan kepada kompiler bahwa pengikatan fungsi anggota dengan nama mereka harus dilakukan dengan fungsi subkelas terlebih dahulu dan fungsi kelas induk setelahnya.
Kelas Buah dapat memiliki warna fungsi virtual () yang mengembalikan "NONE" secara default. Fungsi warna kelas Pisang () mengembalikan "YELLOW" atau "BROWN".
Tetapi jika fungsi yang mengambil pointer Buah memanggil warna () pada kelas Pisang yang dikirim kepadanya - fungsi warna () mana yang dipanggil? Fungsi biasanya akan memanggil Buah :: warna () untuk objek Buah.
Itu akan 99% dari waktu tidak menjadi apa yang dimaksudkan. Tetapi jika Fruit :: color () dinyatakan virtual maka Banana: color () akan dipanggil untuk objek karena fungsi color () yang benar akan terikat ke pointer Fruit pada saat panggilan. Runtime akan memeriksa objek apa yang ditunjuk oleh pointer karena itu ditandai virtual dalam definisi kelas Buah.
Ini berbeda dari mengesampingkan fungsi dalam subkelas. Dalam hal itu, pointer Buah akan memanggil Buah :: warna () jika yang diketahuinya adalah pointer IS-A ke Buah.
Jadi sekarang muncul ide "fungsi virtual murni". Ini adalah ungkapan yang agak disayangkan karena kemurnian tidak ada hubungannya dengan itu. Ini berarti bahwa ini dimaksudkan agar metode kelas dasar tidak pernah dipanggil. Memang fungsi virtual murni tidak bisa disebut. Namun itu masih harus didefinisikan. Tanda tangan fungsi harus ada. Banyak coder membuat implementasi kosong {} untuk kelengkapan, tetapi kompiler akan menghasilkan satu secara internal jika tidak. Dalam hal ini ketika fungsi dipanggil bahkan jika pointer ke Buah, Banana :: color () akan dipanggil karena hanya implementasi warna () yang ada.
Sekarang bagian terakhir dari teka-teki: konstruktor dan destruktor.
Konstruktor virtual murni ilegal, sepenuhnya. Itu baru saja keluar.
Tetapi destruktor virtual murni berfungsi jika Anda ingin melarang pembuatan instance kelas dasar. Hanya sub-kelas yang dapat dipakai jika destruktor dari kelas dasar adalah virtual murni. konvensi adalah untuk menetapkannya ke 0.
Anda harus membuat implementasi dalam hal ini. Kompiler tahu ini adalah apa yang Anda lakukan dan memastikan Anda melakukannya dengan benar, atau mengeluh bahwa ia tidak dapat menautkan ke semua fungsi yang diperlukan untuk dikompilasi. Kesalahan bisa membingungkan jika Anda tidak berada di jalur yang benar tentang bagaimana Anda memodelkan hierarki kelas Anda.
Jadi Anda dilarang dalam hal ini untuk membuat contoh Buah, tetapi diizinkan untuk membuat contoh Pisang.
Panggilan untuk menghapus pointer Buah yang menunjuk ke instance Banana akan memanggil Banana :: ~ Banana () terlebih dahulu dan kemudian memanggil Fuit :: ~ Fruit (), selalu. Karena bagaimanapun caranya, ketika Anda memanggil destruktor subclass, destructor kelas dasar harus mengikuti.
Apakah ini model yang buruk? Ini lebih rumit pada tahap desain, ya, tetapi dapat memastikan bahwa penghubungan yang benar dilakukan pada saat run-time dan bahwa fungsi subclass dilakukan di mana ada ambiguitas untuk tepatnya subclass mana yang sedang diakses.
Jika Anda menulis C ++ sehingga Anda hanya membagikan pointer kelas yang tepat tanpa pointer umum atau ambigu, maka fungsi virtual tidak benar-benar diperlukan. Tetapi jika Anda memerlukan fleksibilitas jenis waktu berjalan (seperti pada Apple Banana Orange ==> Buah) fungsi menjadi lebih mudah dan lebih fleksibel dengan kode yang tidak terlalu banyak. Anda tidak lagi harus menulis fungsi untuk setiap jenis buah, dan Anda tahu bahwa setiap buah akan merespons warna () dengan fungsi yang benar.
Saya harap penjelasan yang bertele-tele ini memperkuat konsep daripada membingungkan hal-hal. Ada banyak contoh bagus di luar sana untuk dilihat, dan melihat cukup dan benar-benar menjalankannya dan mengacaukannya dan Anda akan mendapatkannya.
sumber
Ini adalah topik lama yang sudah ada satu dekade :) Bacalah 5 paragraf terakhir dari Item # 7 pada buku "Effective C ++" untuk detailnya, dimulai dari "Kadang-kadang lebih mudah untuk memberi kelas destruktor virtual murni ...."
sumber
Anda meminta contoh, dan saya percaya yang berikut ini memberikan alasan untuk destruktor virtual murni. Saya menantikan balasan, apakah ini alasan yang bagus ...
Saya tidak ingin ada yang bisa melempar
error_base
tipe, tetapi tipe pengecualianerror_oh_shucks
danerror_oh_blast
memiliki fungsi yang sama dan saya tidak ingin menulisnya dua kali. Kompleksitas pImpl diperlukan untuk menghindari pemaparanstd::string
kepada klien saya, dan penggunaanstd::auto_ptr
mengharuskan pembuat salinan.Header publik berisi spesifikasi pengecualian yang akan tersedia untuk klien untuk membedakan berbagai jenis pengecualian yang dilemparkan oleh perpustakaan saya:
Dan di sini adalah implementasi bersama:
Kelas exception_string, dirahasiakan, menyembunyikan std :: string dari antarmuka publik saya:
Kode saya kemudian menimbulkan kesalahan sebagai:
Penggunaan templat untuk
error
sedikit serampangan. Ini menghemat sedikit kode dengan mengorbankan mengharuskan klien untuk menangkap kesalahan sebagai:sumber
Mungkin ada lagi PENGGUNAAN KASUS NYATA dari destruktor virtual murni yang sebenarnya tidak bisa saya lihat di jawaban lain :)
Pada awalnya, saya sepenuhnya setuju dengan jawaban yang ditandai: Itu karena melarang destructor virtual murni akan memerlukan aturan tambahan dalam spesifikasi bahasa. Tapi itu masih bukan use case yang Mark minta :)
Pertama bayangkan ini:
dan sesuatu seperti:
Cukup - kami memiliki antarmuka
Printable
dan beberapa "wadah" memegang apa pun dengan antarmuka ini. Saya pikir di sini cukup jelas mengapaprint()
metode virtual murni. Itu bisa memiliki beberapa badan tetapi jika tidak ada implementasi standar, virtual murni adalah "implementasi" yang ideal (= "harus disediakan oleh kelas turunan").Dan sekarang bayangkan persis sama kecuali bukan untuk mencetak tetapi untuk kehancuran:
Dan juga mungkin ada wadah serupa:
Ini kasus penggunaan yang disederhanakan dari aplikasi saya yang sebenarnya. Satu-satunya perbedaan di sini adalah bahwa metode "khusus" (destruktor) digunakan daripada "normal"
print()
. Tetapi alasan mengapa itu murni virtual masih sama - tidak ada kode default untuk metode ini. Agak membingungkan bisa menjadi kenyataan bahwa HARUS ada beberapa destruktor secara efektif dan kompiler benar-benar menghasilkan kode kosong untuk itu. Tetapi dari perspektif seorang programmer, virtualitas murni masih berarti: "Saya tidak punya kode default, itu harus disediakan oleh kelas turunan."Saya pikir ini bukan ide besar di sini, hanya lebih banyak penjelasan bahwa virtualitas murni bekerja sangat seragam - juga untuk para penghancur.
sumber
1) Ketika Anda ingin meminta kelas turunan untuk melakukan pembersihan. Ini jarang terjadi.
2) Tidak, tetapi Anda ingin itu virtual, meskipun.
sumber
kita perlu membuat destructor virtual karena fakta bahwa, jika kita tidak membuat virtual destructor maka kompiler hanya akan merusak isi kelas dasar, n semua kelas turunan akan tetap tidak berubah, kompiler bacuse tidak akan memanggil destruktor yang lain kelas kecuali kelas dasar.
sumber
delete
pointer ke kelas dasar padahal sebenarnya menunjuk ke turunannya.