C ++ Instansiasi Objek

113

Saya seorang programmer C mencoba memahami C ++. Banyak tutorial yang mendemonstrasikan pembuatan instance objek menggunakan cuplikan seperti:

Dog* sparky = new Dog();

yang menyiratkan bahwa nanti Anda akan melakukan:

delete sparky;

yang masuk akal. Sekarang, jika alokasi memori dinamis tidak diperlukan, apakah ada alasan untuk menggunakan cara di atas daripada

Dog sparky;

dan biarkan destruktor dipanggil begitu sparky keluar dari ruang lingkup?

Terima kasih!

el_champo
sumber

Jawaban:

166

Sebaliknya, Anda harus selalu lebih memilih alokasi tumpukan, sejauh sebagai aturan praktis, Anda tidak boleh memiliki new / delete dalam kode pengguna Anda.

Seperti yang Anda katakan, saat variabel dideklarasikan di stack, destruktornya secara otomatis dipanggil saat berada di luar cakupan, yang merupakan alat utama Anda untuk melacak masa pakai resource dan menghindari kebocoran.

Jadi secara umum, setiap kali Anda perlu mengalokasikan sumber daya, apakah itu memori (dengan memanggil baru), pegangan file, soket atau apa pun, bungkus dalam kelas di mana konstruktor memperoleh sumber daya, dan destruktor melepaskannya. Kemudian Anda dapat membuat objek jenis itu di tumpukan, dan Anda dijamin bahwa sumber daya Anda dibebaskan saat berada di luar ruang lingkup. Dengan cara itu Anda tidak perlu melacak pasangan baru / hapus Anda di mana-mana untuk memastikan Anda menghindari kebocoran memori.

Nama paling umum untuk idiom ini adalah RAII

Lihat juga kelas penunjuk cerdas yang digunakan untuk membungkus hasil penunjuk pada kasus yang jarang terjadi saat Anda harus mengalokasikan sesuatu dengan yang baru di luar objek RAII khusus. Sebagai gantinya, Anda meneruskan penunjuk ke penunjuk cerdas, yang kemudian melacak masa pakainya, misalnya dengan penghitungan referensi, dan memanggil destruktor saat referensi terakhir keluar dari ruang lingkup. Pustaka standar memiliki std::unique_ptrmanajemen berbasis ruang lingkup sederhana, dan std::shared_ptryang menghitung referensi untuk mengimplementasikan kepemilikan bersama.

Banyak tutorial yang mendemonstrasikan pembuatan instance objek menggunakan cuplikan seperti ...

Jadi apa yang Anda temukan adalah bahwa kebanyakan tutorial payah. ;) Sebagian besar tutorial mengajari Anda praktik C ++ yang buruk, termasuk memanggil baru / hapus untuk membuat variabel bila tidak diperlukan, dan memberi Anda kesulitan melacak seumur hidup alokasi Anda.

jalf
sumber
Pointer mentah berguna saat Anda menginginkan semantik seperti auto_ptr (mentransfer kepemilikan), tetapi mempertahankan operasi swap non-lempar, dan tidak menginginkan overhead penghitungan referensi. Kasus tepi mungkin, tapi berguna.
Greg Rogers
2
Ini adalah jawaban yang benar, tetapi alasan saya tidak akan pernah terbiasa membuat objek di tumpukan adalah karena tidak pernah sepenuhnya jelas seberapa besar objek itu nantinya. Anda hanya meminta pengecualian stack-overflow.
dviljoen
3
Greg: Tentu. Tapi seperti yang Anda katakan, kasus tepi. Secara umum, petunjuk sebaiknya dihindari. Tetapi mereka menggunakan bahasa tersebut karena suatu alasan, tidak dapat disangkal. :) dviljoen: Jika objeknya besar, Anda membungkusnya dalam objek RAII, yang bisa dialokasikan di stack, dan berisi pointer ke data yang dialokasikan heap.
jalf
3
@dviljoen: Tidak, saya tidak. Kompiler C ++ tidak perlu mengasapi objek secara tidak perlu. Yang terburuk yang akan Anda lihat adalah biasanya ia dibulatkan ke kelipatan empat byte terdekat. Biasanya, kelas yang berisi pointer akan memakan ruang sebanyak pointer itu sendiri, jadi tidak ada biaya apa pun dalam penggunaan tumpukan.
jalf
20

Meskipun memiliki banyak hal di tumpukan mungkin merupakan keuntungan dalam hal alokasi dan pembebasan otomatis, ini memiliki beberapa kelemahan.

  1. Anda mungkin tidak ingin mengalokasikan objek yang besar di Stack.

  2. Pengiriman dinamis! Pertimbangkan kode ini:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Ini akan mencetak "B". Sekarang mari kita lihat apa yang terjadi saat menggunakan Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Ini akan mencetak "A", yang mungkin tidak intuitif bagi mereka yang terbiasa dengan Java atau bahasa berorientasi objek lainnya. Alasannya adalah Anda tidak memiliki penunjuk ke sebuah instance Blagi. Sebagai gantinya, sebuah instance Bdibuat dan disalin ke avariabel tipe A.

Beberapa hal mungkin terjadi tanpa disadari, terutama saat Anda baru mengenal C ++. Di C Anda memiliki petunjuk dan hanya itu. Anda tahu bagaimana menggunakannya dan mereka SELALU melakukannya. Dalam C ++ tidak demikian. Bayangkan saja apa yang terjadi, ketika Anda menggunakan a dalam contoh ini sebagai argumen untuk sebuah metode - segalanya menjadi lebih rumit dan itu TIDAK membuat perbedaan besar jika abertipe Aatau A*atau bahkan A&(panggilan-oleh-referensi). Banyak kombinasi yang mungkin dan semuanya berperilaku berbeda.

Alam semesta
sumber
2
-1: orang tidak dapat memahami semantik nilai & fakta sederhana bahwa polimorfisme memerlukan referensi / penunjuk (untuk menghindari pemotongan objek) tidak menimbulkan masalah apa pun dengan bahasa. Kekuatan c ++ tidak boleh dianggap sebagai kelemahan hanya karena beberapa orang tidak dapat mempelajari aturan dasarnya.
underscore_d
Terimakasih atas peringatannya. Saya memiliki masalah serupa dengan metode yang mengambil objek alih-alih penunjuk atau referensi, dan betapa berantakannya itu. Objek memiliki petunjuk di dalam dan mereka dihapus terlalu cepat karena penanganan ini.
BoBoDev
@underscored Saya setuju; paragraf terakhir dari jawaban ini harus dihapus. Jangan menganggap fakta bahwa saya mengedit jawaban ini berarti saya setuju; Saya hanya tidak ingin kesalahan di dalamnya dibaca.
TamaMcGlinn
@TamaTama setuju. Terima kasih untuk hasil edit yang bagus. Saya menghapus bagian opini.
UniversE
13

Nah, alasan untuk menggunakan pointer akan persis sama dengan alasan menggunakan pointer di C yang dialokasikan dengan malloc: jika Anda ingin objek Anda hidup lebih lama dari variabel Anda!

Bahkan sangat disarankan untuk TIDAK menggunakan operator baru jika Anda dapat menghindarinya. Apalagi jika Anda menggunakan pengecualian. Secara umum, jauh lebih aman membiarkan kompiler membebaskan objek Anda.

PierreBdR
sumber
13

Saya telah melihat anti-pola ini dari orang-orang yang tidak memahami operator & alamat. Jika mereka perlu memanggil fungsi dengan pointer, mereka akan selalu mengalokasikan di heap sehingga mereka mendapatkan pointer.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);
Mark Ransom
sumber
Alternatif lain: void FeedTheDog (Dog & hungerDog); Anjing anjing; FeedTheDog (anjing);
Scott Langham
1
@ScottLangham benar, tapi bukan itu yang ingin saya sampaikan. Terkadang Anda memanggil fungsi yang menggunakan penunjuk NULL opsional, atau mungkin itu adalah API yang sudah ada yang tidak dapat diubah.
Tandai Tebusan
7

Perlakukan heap sebagai real estat yang sangat penting dan gunakan dengan sangat bijaksana. Aturan dasar yang umum adalah menggunakan tumpukan jika memungkinkan dan menggunakan heap jika tidak ada cara lain. Dengan mengalokasikan objek pada stack Anda bisa mendapatkan banyak keuntungan seperti:

(1). Anda tidak perlu khawatir tentang pelepasan tumpukan jika ada pengecualian

(2). Anda tidak perlu khawatir tentang fragmentasi memori yang disebabkan oleh alokasi lebih banyak ruang daripada yang diperlukan oleh pengelola heap Anda.

Naveen
sumber
1
Harus ada pertimbangan tentang ukuran objek. Ukuran Stack terbatas, jadi objek yang sangat besar harus dialokasikan Heap.
Adam
1
@Adam Saya pikir Naveen masih benar di sini. Jika Anda dapat meletakkan benda besar di atas tumpukan, lakukanlah, karena itu lebih baik. Ada banyak alasan yang mungkin tidak bisa Anda lakukan. Tapi saya pikir dia benar.
McKay
5

Satu-satunya alasan yang saya khawatirkan adalah Dog sekarang ditempatkan di stack, bukan di heap. Jadi, jika Dog berukuran megabyte, Anda mungkin mengalami masalah,

Jika Anda memang perlu pergi ke rute baru / hapus, berhati-hatilah dengan pengecualian. Dan karena itu, Anda harus menggunakan auto_ptr atau salah satu jenis penunjuk cerdas penunjuk untuk mengelola masa pakai objek.

Roddy
sumber
1

Tidak ada alasan untuk baru (di heap) ketika Anda dapat mengalokasikan di tumpukan (kecuali karena alasan tertentu Anda memiliki tumpukan kecil dan ingin menggunakan tumpukan.

Anda mungkin ingin mempertimbangkan untuk menggunakan shared_ptr (atau salah satu variasinya) dari pustaka standar jika Anda ingin mengalokasikan di heap. Itu akan menangani melakukan penghapusan untuk Anda setelah semua referensi ke shared_ptr sudah tidak ada lagi.

Scott Langham
sumber
Ada banyak alasannya, contohnya lihat saya jawab.
dangerousdave
Saya kira Anda mungkin ingin objek hidup lebih lama dari ruang lingkup fungsi, tetapi OP tampaknya tidak menyarankan itulah yang mereka coba lakukan.
Scott Langham
0

Ada alasan tambahan, yang tidak disebutkan orang lain, mengapa Anda mungkin memilih untuk membuat objek Anda secara dinamis. Objek dinamis berbasis heap memungkinkan Anda memanfaatkan polimorfisme .

berbahayadave
sumber
2
Anda juga dapat bekerja dengan objek berbasis tumpukan secara polimorfis, saya tidak melihat perbedaan antara objek yang dialokasikan tumpukan / dan tumpukan dalam hal ini. Misalnya: void MyFunc () {Dog dog; Memberi makan anjing); } void Feed (Animal & animal) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (makanan); } // Dalam contoh ini GetFavouriteFood bisa menjadi fungsi virtual yang diganti oleh setiap hewan dengan implementasinya sendiri. Ini akan bekerja secara polimorfis tanpa melibatkan heap.
Scott Langham
-1: polimorfisme hanya membutuhkan referensi atau penunjuk; itu sepenuhnya agnostik dari durasi penyimpanan instance yang mendasarinya.
underscore_d
@underscore_d "Menggunakan kelas dasar universal berarti biaya: Objek harus dialokasikan heap agar polimorfik;" - Bjarne Stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave
LOL, aku tidak peduli jika Stroustrup sendiri yang mengatakannya, tapi itu kutipan yang sangat memalukan baginya, karena dia salah. Tidak sulit untuk menguji ini sendiri, Anda tahu: instantiate beberapa DerivedA, DerivedB, dan DerivedC di stack; instantiate juga penunjuk yang dialokasikan tumpukan ke Base dan konfirmasikan bahwa Anda dapat menempatkannya dengan A / B / C dan menggunakannya secara polimorfis. Menerapkan pemikiran kritis dan referensi Standar untuk klaim, meskipun klaim tersebut dibuat oleh penulis asli bahasa tersebut. Di sini: stackoverflow.com/q/5894775/2757035
underscore_d
Begini, saya memiliki sebuah objek yang berisi 2 keluarga terpisah dari hampir 1000 objek polimorfik, dengan durasi penyimpanan otomatis. Saya memberi contoh objek ini di tumpukan, dan dengan merujuk ke anggota tersebut melalui referensi atau petunjuk, itu mencapai kemampuan penuh polimorfisme atas mereka. Tidak ada dalam program saya yang secara dinamis / heap-dialokasikan (selain dari sumber daya yang dimiliki oleh kelas yang dialokasikan tumpukan), dan itu tidak mengubah kemampuan objek saya sedikit pun.
underscore_d
-4

Saya memiliki masalah yang sama di Visual Studio. Anda harus menggunakan:

KelasAnda-> classMethod ();

daripada:

yourClass.classMethod ();

Hamed
sumber
3
Ini tidak menjawab pertanyaan itu. Anda lebih suka mencatat bahwa seseorang harus menggunakan sintaks yang berbeda untuk mengakses objek yang dialokasikan di heap (melalui penunjuk ke objek) dan objek yang dialokasikan di stack.
Alexey Ivanov