Mengapa programmer C ++ meminimalkan penggunaan 'baru'?


873

Saya menemukan Stack Overflow pertanyaan Memory leak with std :: string saat menggunakan std :: list <std :: string> , dan salah satu komentar mengatakan ini:

Berhenti menggunakan newterlalu banyak. Saya tidak dapat melihat alasan Anda menggunakan yang baru di mana pun Anda melakukannya. Anda dapat membuat objek berdasarkan nilai dalam C ++ dan ini merupakan salah satu keuntungan besar menggunakan bahasa.
Anda tidak harus mengalokasikan semuanya di heap.
Berhentilah berpikir seperti seorang programmer Java .

Saya tidak begitu yakin apa yang dia maksudkan dengan itu.

Mengapa objek harus dibuat dengan nilai dalam C ++ sesering mungkin, dan apa bedanya secara internal?
Apakah saya salah menafsirkan jawabannya?

Jawaban:


1037

Ada dua teknik alokasi memori yang banyak digunakan: alokasi otomatis dan alokasi dinamis. Secara umum, ada wilayah memori yang sesuai untuk masing-masing: tumpukan dan tumpukan.

Tumpukan

Tumpukan selalu mengalokasikan memori secara berurutan. Itu dapat melakukannya karena mengharuskan Anda untuk melepaskan memori dalam urutan terbalik (First-In, Last-Out: FILO). Ini adalah teknik alokasi memori untuk variabel lokal dalam banyak bahasa pemrograman. Ini sangat, sangat cepat karena membutuhkan pembukuan minimal dan alamat berikutnya untuk dialokasikan adalah implisit.

Dalam C ++, ini disebut penyimpanan otomatis karena penyimpanan diklaim secara otomatis pada akhir ruang lingkup. Segera setelah eksekusi blok kode saat ini (dibatasi penggunaan {}) selesai, memori untuk semua variabel di blok itu dikumpulkan secara otomatis. Ini juga merupakan momen di mana para penghancur dipanggil untuk membersihkan sumber daya.

Tumpukan

Tumpukan memungkinkan untuk mode alokasi memori yang lebih fleksibel. Pembukuan lebih kompleks dan alokasi lebih lambat. Karena tidak ada titik rilis implisit, Anda harus melepaskan memori secara manual, menggunakan deleteatau delete[](free dalam C). Namun, tidak adanya titik rilis tersirat adalah kunci untuk fleksibilitas tumpukan.

Alasan menggunakan alokasi dinamis

Bahkan jika menggunakan tumpukan lebih lambat dan berpotensi menyebabkan kebocoran memori atau fragmentasi memori, ada kasus penggunaan yang sangat baik untuk alokasi dinamis, karena kurang terbatas.

Dua alasan utama untuk menggunakan alokasi dinamis:

  • Anda tidak tahu berapa banyak memori yang Anda butuhkan pada waktu kompilasi. Misalnya, ketika membaca file teks menjadi string, Anda biasanya tidak tahu ukuran file itu, jadi Anda tidak dapat memutuskan berapa banyak memori yang dialokasikan sampai Anda menjalankan program.

  • Anda ingin mengalokasikan memori yang akan bertahan setelah meninggalkan blok saat ini. Misalnya, Anda mungkin ingin menulis fungsi string readfile(string path)yang mengembalikan konten file. Dalam hal ini, bahkan jika tumpukan dapat menampung seluruh isi file, Anda tidak dapat kembali dari fungsi dan mempertahankan blok memori yang dialokasikan.

Mengapa alokasi dinamis seringkali tidak perlu

Di C ++ ada konstruksi rapi yang disebut destruktor . Mekanisme ini memungkinkan Anda untuk mengelola sumber daya dengan menyelaraskan masa pakai sumber daya dengan masa pakai variabel. Teknik ini disebut RAII dan merupakan titik pembeda dari C ++. Ini "membungkus" sumber daya menjadi objek. std::stringadalah contoh sempurna. Cuplikan ini:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

sebenarnya mengalokasikan sejumlah variabel memori. The std::stringobjek mengalokasikan memori menggunakan tumpukan dan rilis di destructor. Dalam hal ini, yang Anda lakukan tidak perlu mengelola sumber daya apa pun secara manual dan masih mendapat manfaat dari alokasi memori dinamis.

Secara khusus, ini menyiratkan bahwa dalam cuplikan ini:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

ada alokasi memori dinamis yang tidak dibutuhkan. Program ini membutuhkan lebih banyak pengetikan (!) Dan memperkenalkan risiko lupa untuk membatalkan alokasi memori. Ini melakukan ini tanpa manfaat nyata.

Mengapa Anda harus menggunakan penyimpanan otomatis sesering mungkin

Pada dasarnya, paragraf terakhir merangkumnya. Menggunakan penyimpanan otomatis sesering mungkin membuat program Anda:

  • lebih cepat mengetik;
  • lebih cepat saat dijalankan;
  • kurang rentan terhadap kebocoran memori / sumber daya.

Poin bonus

Dalam pertanyaan yang dirujuk, ada kekhawatiran tambahan. Secara khusus, kelas berikut:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Sebenarnya jauh lebih berisiko untuk digunakan daripada yang berikut:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

Alasannya adalah bahwa std::stringmendefinisikan konstruktor salinan dengan benar. Pertimbangkan program berikut:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Menggunakan versi asli, program ini kemungkinan akan macet, karena digunakan deletepada string yang sama dua kali. Menggunakan versi yang dimodifikasi, setiap Lineinstance akan memiliki instance string sendiri , masing-masing dengan ingatannya sendiri dan keduanya akan dirilis pada akhir program.

Catatan lain

Penggunaan ekstensif RAII dianggap sebagai praktik terbaik dalam C ++ karena semua alasan di atas. Namun, ada manfaat tambahan yang tidak segera terlihat. Pada dasarnya, ini lebih baik daripada jumlah bagian-bagiannya. Seluruh mekanisme terbentuk . Itu bersisik.

Jika Anda menggunakan Linekelas sebagai blok penyusun :

 class Table
 {
      Line borders[4];
 };

Kemudian

 int main ()
 {
     Table table;
 }

mengalokasikan empat std::stringinstance, empat Lineinstance, satu Tableinstance dan semua konten string dan semuanya dibebaskan secara otomatis .


55
+1 untuk menyebutkan RAII di akhir, tetapi harus ada sesuatu tentang pengecualian dan susun gulungan.
Tobu

7
@Tobu: ya, tapi posting ini sudah cukup panjang, dan saya ingin tetap fokus pada pertanyaan OP. Saya akhirnya akan menulis posting blog atau sesuatu dan menautkannya dari sini.
André Caron

15
Akan menjadi pelengkap yang bagus untuk menyebutkan sisi buruk dari alokasi tumpukan (setidaknya sampai C ++ 1x) - Anda sering perlu menyalin hal-hal yang tidak perlu jika Anda tidak hati-hati. misalnya Monstermeludah keluar Treasureke Worldketika mati. Dalam Die()metodenya itu menambah harta ke dunia. Itu harus digunakan world->Add(new Treasure(/*...*/))di lain untuk melestarikan harta setelah mati. Alternatifnya adalah shared_ptr(mungkin berlebihan), auto_ptr(semantik yang buruk untuk pengalihan kepemilikan), melewati nilai (boros) dan move+ unique_ptr(belum diimplementasikan secara luas).
kizzx2

7
Apa yang Anda katakan tentang variabel lokal yang dialokasikan stack mungkin sedikit menyesatkan. "Tumpukan" mengacu pada tumpukan panggilan, yang menyimpan bingkai tumpukan . Frame tumpukan inilah yang disimpan dalam mode LIFO. Variabel lokal untuk kerangka tertentu dialokasikan seolah-olah mereka adalah anggota struct.
someguy

7
@someguy: Memang, penjelasannya tidak sempurna. Implementasinya memiliki kebebasan dalam kebijakan alokasinya. Namun, variabel-variabel tersebut harus diinisialisasi dan dihancurkan dengan cara LIFO, jadi analoginya berlaku. Saya tidak berpikir itu bekerja mempersulit jawaban lebih jauh.
André Caron

171

Karena tumpukan lebih cepat dan anti bocor

Dalam C ++, hanya diperlukan satu instruksi untuk mengalokasikan ruang - pada stack - untuk setiap objek lingkup lokal dalam fungsi yang diberikan, dan tidak mungkin untuk membocorkan salah satu dari memori itu. Komentar itu dimaksudkan (atau seharusnya dimaksudkan) untuk mengatakan sesuatu seperti "gunakan tumpukan dan bukan tumpukan".


18
"hanya dibutuhkan satu instruksi untuk mengalokasikan ruang" - oh, omong kosong. Tentu hanya membutuhkan satu instruksi untuk menambahkan stack pointer, tetapi jika kelas memiliki struktur internal yang menarik, akan ada lebih banyak daripada menambah stack pointer yang terjadi. Sama validnya dengan mengatakan bahwa di Java tidak memerlukan instruksi untuk mengalokasikan ruang, karena kompiler akan mengelola referensi pada waktu kompilasi.
Charlie Martin

31
@ Charlie benar. Variabel otomatis yang cepat dan sangat mudah akan lebih akurat.
Oliver Charlesworth

28
@Charlie: Internal kelas perlu diatur dengan cara apa pun. Perbandingan sedang dilakukan untuk mengalokasikan ruang yang dibutuhkan.
Oliver Charlesworth

51
batuk int x; return &x;
peterchen

16
cepat ya. Tapi tentu tidak mudah. Tidak ada yang mudah. Anda bisa mendapatkan StackOverflow :)
rxantos

107

Alasannya rumit.

Pertama, C ++ bukan sampah yang dikumpulkan. Karena itu, untuk setiap yang baru, harus ada penghapusan yang sesuai. Jika Anda gagal untuk menghapus ini, maka Anda memiliki kebocoran memori. Sekarang, untuk kasus sederhana seperti ini:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Ini sederhana. Tetapi apa yang terjadi jika "Do stuff" memberikan pengecualian? Ups: kebocoran memori. Apa yang terjadi jika masalah "Lakukan" returnlebih awal? Ups: kebocoran memori.

Dan ini untuk kasus yang paling sederhana . Jika Anda mengembalikan string itu kepada seseorang, sekarang mereka harus menghapusnya. Dan jika mereka melewatinya sebagai argumen, apakah orang yang menerimanya perlu menghapusnya? Kapan mereka harus menghapusnya?

Atau, Anda bisa melakukan ini:

std::string someString(...);
//Do stuff

Tidak ada delete. Objek dibuat pada "tumpukan", dan itu akan dihancurkan begitu keluar dari ruang lingkup. Anda bahkan dapat mengembalikan objek, sehingga mentransfer kontennya ke fungsi panggilan. Anda dapat meneruskan objek ke fungsi (biasanya sebagai referensi atau referensi-konstan:. void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)Dan sebagainya.

Semua tanpa newdan delete. Tidak ada pertanyaan tentang siapa yang memiliki memori atau siapa yang bertanggung jawab untuk menghapusnya. Jika kamu melakukan:

std::string someString(...);
std::string otherString;
otherString = someString;

Hal ini dimengerti bahwa otherStringmemiliki salinan data yang dari someString. Itu bukan pointer; itu adalah objek yang terpisah. Mereka mungkin memiliki konten yang sama, tetapi Anda dapat mengubahnya tanpa mempengaruhi yang lain:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Lihat idenya?


1
Pada catatan itu ... Jika suatu objek dialokasikan secara dinamis main(), ada selama durasi program, tidak dapat dengan mudah dibuat pada tumpukan karena situasi, dan petunjuk ke sana diteruskan ke fungsi apa pun yang memerlukan akses ke sana. , dapatkah ini menyebabkan kebocoran jika terjadi crash pada program, atau apakah aman? Saya akan berasumsi yang terakhir, karena OS yang mendelokasi semua memori program harus secara logis mendelokasinya juga, tetapi saya tidak ingin menganggap apa pun ketika datang ke new.
Justin Time - Pasang kembali Monica

4
@JustinTime Anda tidak perlu khawatir tentang membebaskan memori dari objek yang dialokasikan secara dinamis yang tetap untuk seumur hidup program. Ketika sebuah program dijalankan, OS menciptakan atlas dari memori fisik, atau Virtual Memory, untuk itu. Setiap alamat dalam ruang memori virtual dipetakan ke alamat memori fisik, dan ketika program keluar, semua yang dipetakan ke memori virtual akan dibebaskan. Jadi, selama program keluar sepenuhnya, Anda tidak perlu khawatir tentang memori yang dialokasikan tidak pernah dihapus.
Aiman ​​Al-Eryani

75

Objek yang dibuat newharus pada akhirnya deletejangan sampai bocor. Destruktor tidak akan dipanggil, memori tidak akan dibebaskan, semuanya. Karena C ++ tidak memiliki pengumpulan sampah, itu masalah.

Objek yang dibuat oleh nilai (yaitu pada stack) secara otomatis mati ketika mereka keluar dari ruang lingkup. Panggilan destruktor dimasukkan oleh kompiler, dan memori dibebaskan secara otomatis saat fungsi kembali.

Pointer pintar suka unique_ptr ,shared_ptr memecahkan masalah referensi yang menggantung, tetapi mereka membutuhkan disiplin koding dan memiliki masalah potensial lainnya (copyability, loop referensi, dll.).

Juga, dalam skenario multithreaded, new adalah titik pertikaian antara thread; mungkin ada dampak kinerja karena terlalu banyak menggunakannew . Pembuatan objek tumpukan adalah menurut definisi thread-lokal, karena masing-masing thread memiliki tumpukan sendiri.

Kelemahan dari objek nilai adalah bahwa mereka mati begitu fungsi host kembali - Anda tidak dapat memberikan referensi kepada mereka kembali ke pemanggil, hanya dengan menyalin, mengembalikan atau bergerak berdasarkan nilai.


9
+1. Re "Objek yang dibuat oleh newharus pada akhirnya deletejangan sampai bocor." - lebih buruk lagi, new[]harus dicocokkan dengan delete[], dan Anda mendapatkan perilaku yang tidak terdefinisi jika Anda delete new[]-ed memori atau delete[] new-ed memori - sangat sedikit kompiler memperingatkan tentang ini (beberapa alat seperti Cppcheck lakukan ketika mereka bisa).
Tony Delroy

3
@ TonyDelroy Ada situasi di mana kompiler tidak dapat memperingatkan ini. Jika suatu fungsi mengembalikan sebuah pointer, itu bisa dibuat jika baru (satu elemen) atau baru [].
fbafelipe

32
  • C ++ tidak menggunakan manajer memori apa pun olehnya. Bahasa lain seperti C #, Java memiliki pengumpul sampah untuk menangani memori
  • Implementasi C ++ biasanya menggunakan rutinitas sistem operasi untuk mengalokasikan memori dan terlalu banyak yang baru / hapus dapat memecah-mecah memori yang tersedia
  • Dengan aplikasi apa pun, jika memori sering digunakan, sebaiknya pra-alokasikan dan lepaskan saat tidak diperlukan.
  • Manajemen memori yang tidak tepat dapat menyebabkan kebocoran memori dan sangat sulit untuk dilacak. Jadi menggunakan objek tumpukan dalam ruang lingkup fungsi adalah teknik yang terbukti
  • Kelemahan dari menggunakan objek tumpukan adalah, ia menciptakan banyak salinan objek saat kembali, beralih ke fungsi dll. Namun kompiler pintar sangat menyadari situasi ini dan mereka telah dioptimalkan dengan baik untuk kinerja
  • Sangat membosankan di C ++ jika memori dialokasikan dan dirilis di dua tempat yang berbeda. Tanggung jawab untuk rilis selalu menjadi pertanyaan dan sebagian besar kami mengandalkan beberapa pointer yang dapat diakses secara umum, objek stack (maksimum yang mungkin) dan teknik seperti auto_ptr (objek RAII)
  • Yang terbaik adalah, Anda telah mengontrol memori dan yang terburuk adalah Anda tidak akan memiliki kontrol atas memori jika kami menggunakan manajemen memori yang tidak tepat untuk aplikasi tersebut. Kecelakaan yang disebabkan karena kerusakan memori adalah yang paling buruk dan sulit dilacak.

5
Sebenarnya, bahasa apa pun yang mengalokasikan memori memiliki manajer memori, termasuk c. Kebanyakan hanya sangat sederhana, yaitu int * x = malloc (4); int * y = malloc (4); ... panggilan pertama akan mengalokasikan memori, alias meminta os untuk memori, (biasanya dalam potongan 1k / 4k) sehingga panggilan kedua, tidak benar-benar mengalokasikan memori, tetapi memberi Anda sepotong potongan terakhir yang dialokasikan untuknya. IMO, pengumpul sampah bukan manajer memori, karena hanya menangani deallokasi memori secara otomatis. Untuk disebut manajer memori, itu tidak hanya menangani deallocating tetapi juga mengalokasikan memori.
Rahly

1
Variabel lokal menggunakan stack sehingga kompiler tidak memancarkan panggilan ke malloc()atau teman-temannya untuk mengalokasikan memori yang diperlukan. Namun, tumpukan tidak dapat melepaskan item apa pun di dalam tumpukan, satu-satunya cara tumpukan memori dilepaskan dari atas tumpukan.
Mikko Rantalainen

C ++ tidak "menggunakan rutinitas sistem operasi"; itu bukan bagian dari bahasa, itu hanya implementasi umum. C ++ bahkan dapat berjalan tanpa sistem operasi apa pun.
einpoklum

22

Saya melihat bahwa beberapa alasan penting untuk melakukan hal baru sesedikit mungkin terlewatkan:

Operator newmemiliki waktu pelaksanaan non-deterministik

Memanggil newmungkin atau mungkin tidak menyebabkan OS mengalokasikan halaman fisik baru untuk proses Anda, ini bisa sangat lambat jika Anda sering melakukannya. Atau mungkin sudah memiliki lokasi memori yang sesuai, kita tidak tahu. Jika program Anda perlu memiliki waktu eksekusi yang konsisten dan dapat diprediksi (seperti dalam sistem waktu nyata atau simulasi permainan / fisika), Anda perlu menghindari newdalam putaran kritis waktu Anda.

Operator newadalah sinkronisasi utas implisit

Ya, Anda mendengar saya, OS Anda perlu memastikan tabel halaman Anda konsisten dan dengan panggilan seperti itu newakan menyebabkan utas Anda mendapatkan kunci mutex implisit. Jika Anda secara konsisten menelepon newdari banyak utas Anda sebenarnya membuat serial utas Anda (Saya sudah melakukan ini dengan 32 CPU, masing-masing menekan newuntuk mendapatkan beberapa ratus byte masing-masing, aduh! Itu adalah pita royal untuk debug)

Selebihnya seperti lambat, fragmentasi, rawan kesalahan, dll telah disebutkan oleh jawaban lain.


2
Keduanya dapat dihindari dengan menggunakan penempatan baru / menghapus dan mengalokasikan memori sebelumnya. Atau Anda dapat mengalokasikan / membebaskan memori sendiri dan kemudian memanggil konstruktor / destruktor. Ini adalah cara std :: vector biasanya berfungsi.
rxantos

1
@rxantos Harap baca OP, pertanyaan ini adalah tentang menghindari alokasi memori yang tidak perlu. Juga, tidak ada penghapusan penempatan.
Emily L.

@Emily Inilah yang dimaksud OP, saya pikir:void * someAddress = ...; delete (T*)someAddress
xryl669

1
Menggunakan stack juga tidak deterministik dalam waktu eksekusi. Kecuali Anda sudah menelepon mlock()atau yang serupa. Ini karena sistem mungkin kehabisan memori dan tidak ada halaman memori fisik siap tersedia untuk stack sehingga OS mungkin perlu menukar atau menulis beberapa cache (menghapus memori kotor) ke disk sebelum eksekusi dapat dilanjutkan.
Mikko Rantalainen

1
@mikkorantalainen secara teknis benar tetapi dalam situasi memori rendah semua taruhan tidak aktif lagi karena kinerja Anda mendorong ke disk sehingga tidak ada yang dapat Anda lakukan. Itu tidak dengan cara apa pun membatalkan saran untuk menghindari panggilan baru ketika masuk akal untuk melakukannya.
Emily L.

21

Pra-C ++ 17:

Karena itu rentan terhadap kebocoran halus bahkan jika Anda membungkus hasilnya dengan smart pointer .

Pertimbangkan pengguna "hati-hati" yang ingat untuk membungkus objek dalam pointer pintar:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Kode ini berbahaya karena tidak ada jaminan yang shared_ptrdibangun sebelumT1 atau sesudahnya T2. Oleh karena itu, jika salah satu new T1()atau new T2()gagal setelah yang lain berhasil, maka objek pertama akan bocor karena tidakshared_ptr ada untuk menghancurkan dan membatalkan alokasi itu.

Solusi: gunakan make_shared .

Post-C ++ 17:

Ini bukan lagi masalah: C ++ 17 memberikan batasan pada urutan operasi ini, dalam hal ini memastikan bahwa setiap panggilan new()harus segera diikuti oleh pembangunan smart pointer yang sesuai, tanpa operasi lain di antaranya. Ini menyiratkan bahwa, pada saat yang kedua new()disebut, dijamin bahwa objek pertama telah dibungkus dengan smart pointer, sehingga mencegah kebocoran jika terjadi pengecualian.

Penjelasan lebih rinci dari urutan evaluasi baru yang diperkenalkan oleh C ++ 17 diberikan oleh Barry dalam jawaban lain .

Terima kasih kepada @Remy Lebeau untuk menunjukkan bahwa ini masih menjadi masalah di bawah C ++ 17 (meskipun kurang begitu): shared_ptrkonstruktor dapat gagal mengalokasikan blok kontrol dan melemparnya, dalam hal ini penunjuk yang diteruskan ke sana tidak dihapus.

Solusi: gunakanmake_shared .


5
Solusi lain: Jangan pernah secara dinamis mengalokasikan lebih dari satu objek per baris.
Antimony

3
@Antimony: Ya, jauh lebih menggoda untuk mengalokasikan lebih dari satu objek ketika Anda sudah mengalokasikan satu objek, dibandingkan dengan ketika Anda belum mengalokasikannya.
user541686

1
Saya pikir jawaban yang lebih baik adalah bahwa smart_ptr akan bocor jika pengecualian dipanggil dan tidak ada yang menangkapnya.
Natalie Adams

2
Bahkan dalam kasus pasca-C ++ 17, kebocoran masih dapat terjadi jika newberhasil dan kemudian shared_ptrkonstruksi berikutnya gagal. std::make_shared()akan menyelesaikannya juga
Remy Lebeau

1
@Mehrdad, shared_ptrkonstruktor yang dimaksud mengalokasikan memori untuk blok kontrol yang menyimpan pointer dan deleter yang dibagikan, jadi ya, secara teoritis dapat melempar kesalahan memori. Hanya salin, pindahkan, dan aliasing konstruktor yang tidak membuang. make_sharedmengalokasikan objek yang dibagikan di dalam blok kontrol itu sendiri, jadi hanya ada 1 alokasi alih-alih 2.
Remy Lebeau

17

Untuk sebagian besar, itu seseorang mengangkat kelemahan mereka sendiri ke aturan umum. Tidak ada yang salah per se dengan membuat objek menggunakan newoperator. Apa yang ada beberapa argumen untuk itu adalah bahwa Anda harus melakukannya dengan beberapa disiplin: jika Anda membuat objek Anda perlu memastikan itu akan dihancurkan.

Cara termudah untuk melakukannya adalah dengan membuat objek dalam penyimpanan otomatis, jadi C ++ tahu untuk menghancurkannya ketika keluar dari ruang lingkup:

 {
    File foo = File("foo.dat");

    // do things

 }

Sekarang, perhatikan bahwa ketika Anda jatuh blok itu setelah penjepit-akhir, fooberada di luar jangkauan. C ++ akan memanggil dtornya secara otomatis untuk Anda. Tidak seperti Java, Anda tidak perlu menunggu sampai GC menemukannya.

Anda sudah menulis

 {
     File * foo = new File("foo.dat");

Anda ingin mencocokkannya secara eksplisit dengan

     delete foo;
  }

atau bahkan lebih baik, alokasikan File * sebagai "penunjuk pintar". Jika Anda tidak berhati-hati tentang hal itu dapat menyebabkan kebocoran.

Jawabannya sendiri membuat asumsi yang keliru bahwa jika Anda tidak menggunakan newAnda tidak mengalokasikan pada tumpukan; sebenarnya, di C ++ Anda tidak tahu itu. Paling-paling, Anda tahu bahwa sedikit kehabisan memori, katakan satu pointer, tentu dialokasikan pada stack. Namun, pertimbangkan apakah implementasi File itu seperti

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

maka FileImplakan masih dialokasikan pada stack.

Dan ya, Anda sebaiknya yakin

     ~File(){ delete fd ; }

di kelas juga; tanpanya, Anda akan membocorkan memori dari heap bahkan jika Anda tampaknya tidak mengalokasikan pada heap sama sekali.


4
Anda harus melihat kode dalam pertanyaan yang dirujuk. Pasti ada banyak hal yang salah dalam kode itu.
André Caron

7
Saya setuju tidak ada yang salah dengan menggunakan new per se , tetapi jika Anda melihat kode asli komentar yang newdimaksud , sedang disalahgunakan. Kode ditulis seperti itu Java atau C #, di mana newdigunakan untuk hampir setiap variabel, ketika hal-hal lebih masuk akal untuk berada di tumpukan.
Lukas

5
Titik adil. Tetapi aturan umum biasanya ditegakkan untuk menghindari perangkap umum. Apakah ini merupakan kelemahan individu atau tidak, manajemen memori cukup kompleks untuk menjamin aturan umum seperti ini! :)
Robben_Ford_Fan_boy

9
@Charlie: komentar tidak mengatakan Anda tidak boleh menggunakan new. Dikatakan bahwa jika Anda memiliki pilihan antara alokasi dinamis dan penyimpanan otomatis, gunakan penyimpanan otomatis.
André Caron

8
@ Chlie: tidak ada yang salah dengan menggunakan new, tetapi jika Anda menggunakannya delete, Anda salah melakukannya!
Matthieu M.

16

new()seharusnya tidak digunakan sesedikit mungkin. Ini harus digunakan secermat mungkin. Dan itu harus digunakan sesering yang diperlukan seperti yang ditentukan oleh pragmatisme.

Alokasi objek pada tumpukan, bergantung pada penghancuran implisit mereka, adalah model sederhana. Jika lingkup objek yang diperlukan cocok dengan model itu, maka tidak perlu digunakan new(), dengan terkait delete()dan memeriksa pointer NULL. Dalam kasus di mana Anda memiliki banyak alokasi objek berumur pendek di tumpukan harus mengurangi masalah fragmentasi tumpukan.

Namun, jika masa hidup objek Anda perlu melampaui ruang lingkup saat new()ini maka itu adalah jawaban yang tepat. Pastikan Anda memperhatikan kapan dan bagaimana Anda menelepon delete()dan kemungkinan pointer NULL, menggunakan objek yang dihapus dan semua gotcha lainnya yang datang dengan penggunaan pointer.


9
"jika masa hidup objek Anda perlu melampaui lingkup saat ini maka baru () adalah jawaban yang tepat" ... mengapa tidak preferensi kembali dengan nilai atau menerima variabel lingkup penelepon oleh non- constref atau pointer ...?
Tony Delroy

2
@ Tony: Ya, ya! Saya senang mendengar seseorang menyarankan referensi. Mereka diciptakan untuk mencegah masalah ini.
Nathan Osman

@TonyD ... atau gabungkan mereka: mengembalikan pointer cerdas berdasarkan nilai. Dengan cara itu penelepon dan dalam banyak kasus (yaitu di mana make_shared/_uniquedapat digunakan) callee tidak perlu newatau delete. Jawaban ini meleset dari poin sebenarnya: (A) C ++ menyediakan hal-hal seperti RVO, memindahkan semantik, dan parameter keluaran - yang sering berarti bahwa menangani pembuatan objek dan ekstensi seumur hidup dengan mengembalikan memori yang dialokasikan secara dinamis menjadi tidak perlu dan ceroboh. (B) Bahkan dalam situasi di mana alokasi dinamis diperlukan, stdlib menyediakan pembungkus RAII yang membebaskan pengguna dari detail bagian dalam yang jelek.
underscore_d

14

Saat Anda menggunakan yang baru, objek dialokasikan ke heap. Ini umumnya digunakan ketika Anda mengantisipasi ekspansi. Saat Anda mendeklarasikan objek seperti,

Class var;

ditempatkan di tumpukan.

Anda harus selalu memanggil destroy pada objek yang Anda tempatkan di heap dengan yang baru. Ini membuka potensi kebocoran memori. Objek yang ditempatkan pada tumpukan tidak rentan terhadap kebocoran memori!


2
+1 "[heap] umumnya digunakan saat Anda mengantisipasi ekspansi" - seperti menambahkan ke std::stringatau std::map, ya, wawasan yang tajam. Reaksi awal saya adalah "tetapi juga sangat umum untuk memisahkan seumur hidup suatu objek dari ruang lingkup pembuatan kode", tetapi benar-benar kembali dengan nilai atau menerima nilai-nilai lingkup penelepon dengan non- constreferensi atau pointer lebih baik untuk itu, kecuali ketika ada "ekspansi" yang terlibat terlalu. Ada beberapa kegunaan suara lainnya seperti metode pabrik ....
Tony Delroy

12

Salah satu alasan penting untuk menghindari terlalu banyak tumpukan adalah untuk kinerja - khususnya yang melibatkan kinerja mekanisme manajemen memori default yang digunakan oleh C ++. Sementara alokasi bisa sangat cepat dalam kasus sepele, melakukan banyak newdan deletepada objek ukuran yang tidak seragam tanpa perintah lead yang ketat tidak hanya untuk fragmentasi memori, tetapi juga mempersulit algoritma alokasi dan benar-benar dapat menghancurkan kinerja dalam kasus-kasus tertentu.

Itulah masalah yang dikumpulkan oleh memori yang dibuat untuk diselesaikan, memungkinkan untuk mengurangi kerugian yang melekat pada implementasi tumpukan tradisional, sementara masih memungkinkan Anda untuk menggunakan tumpukan tersebut seperlunya.

Namun, lebih baik tetap menghindari masalah itu sama sekali. Jika Anda bisa meletakkannya di tumpukan, maka lakukanlah.


Anda selalu dapat mengalokasikan jumlah memori yang cukup besar dan kemudian menggunakan penempatan baru / hapus jika kecepatan masalah.
rxantos

Kolam memori adalah untuk menghindari fragmentasi, untuk mempercepat deallokasi (satu deallokasi untuk ribuan objek) dan untuk membuat deallokasi lebih aman.
Lothar

10

Saya pikir poster itu dimaksudkan untuk mengatakan You do not have to allocate everything on theheapdaripada yang stack.

Pada dasarnya objek dialokasikan pada stack (jika ukuran objek memungkinkan, tentu saja) karena biaya alokasi stack yang murah, daripada alokasi berbasis heap yang melibatkan beberapa pekerjaan oleh pengalokasi, dan menambahkan verbositas karena Anda harus mengelola data yang dialokasikan pada heap.


10

Saya cenderung tidak setuju dengan gagasan menggunakan "terlalu banyak" baru. Padahal penggunaan poster asli baru dengan kelas sistem agak konyol. ( int *i; i = new int[9999];? Benarkah? int i[9999];Jauh lebih jelas.) Saya pikir itulah yang mendapatkan kambing komentator.

Ketika Anda bekerja dengan objek sistem, itu sangat jarang Anda membutuhkan lebih dari satu referensi ke objek yang sama persis. Selama nilainya sama, hanya itu yang penting. Dan objek sistem biasanya tidak memakan banyak ruang dalam memori. (satu byte per karakter, dalam sebuah string). Dan jika mereka melakukannya, perpustakaan harus dirancang untuk memperhitungkan manajemen memori tersebut (jika ditulis dengan baik). Dalam kasus ini, (semua kecuali satu atau dua berita dalam kodenya), yang baru praktis tidak ada gunanya dan hanya berfungsi untuk menimbulkan kebingungan dan potensi bug.

Namun, ketika Anda bekerja dengan kelas / objek Anda sendiri (misalnya kelas Line dari poster asli), maka Anda harus mulai memikirkan masalah-masalah seperti jejak memori, ketekunan data, dll. Sendiri. Pada titik ini, memungkinkan beberapa referensi ke nilai yang sama sangat berharga - memungkinkan konstruksi seperti daftar tertaut, kamus, dan grafik, di mana beberapa variabel perlu tidak hanya memiliki nilai yang sama, tetapi referensi objek yang sama persis di memori. Namun, kelas Line tidak memiliki persyaratan tersebut. Jadi kode poster asli sebenarnya sama sekali tidak perlu new.


biasanya baru / delete akan menggunakannya ketika Anda tidak tahu sebelumnya menyerahkan ukuran array. Tentu saja std :: vektor menyembunyikan baru / hapus untuk Anda. Anda masih menggunakannya, tetapi melalui std :: vector. Jadi saat ini akan digunakan ketika Anda tidak tahu ukuran array dan ingin untuk beberapa alasan menghindari overhead dari std :: vector (Yang kecil, tetapi masih ada).
rxantos

When you're working with your own classes/objects... Anda sering tidak punya alasan untuk melakukannya! Sebagian kecil dari Qs ada pada rincian desain wadah oleh coders terampil. Sebaliknya, proporsi yang menyedihkan adalah tentang kebingungan para pemula yang tidak tahu stdlib ada - atau secara aktif diberikan tugas yang mengerikan di 'program' 'kursus', di mana seorang tutor menuntut mereka tanpa sengaja menemukan kembali roda - sebelum mereka bahkan telah belajar apa itu roda dan mengapa itu bekerja. Dengan mempromosikan alokasi yang lebih abstrak, C ++ dapat menyelamatkan kita dari 'segfault dengan daftar tertaut' C yang tak ada habisnya; tolong, mari kita biarkan .
underscore_d

" Penggunaan poster asli dari kelas sistem baru agak konyol. ( int *i; i = new int[9999];? Benarkah? int i[9999];Jauh lebih jelas.)" Ya, itu lebih jelas, tetapi untuk bermain sebagai pendukung iblis, tipe itu tidak selalu merupakan argumen yang buruk. Dengan 9999 elemen, saya bisa membayangkan sistem tertanam yang ketat tidak memiliki cukup tumpukan untuk 9999 elemen: 9999x4 byte adalah ~ 40 kB, x8 ~ 80 kB. Jadi, sistem seperti itu mungkin perlu menggunakan alokasi dinamis, dengan asumsi mereka menerapkannya menggunakan memori alternatif. Namun, itu hanya mungkin dapat membenarkan alokasi dinamis, bukan new; a vectorakan menjadi perbaikan nyata dalam kasus itu
underscore_d

Setuju dengan @underscore_d - itu bukan contoh yang bagus. Saya tidak akan menambahkan 40.000 atau 80.000 byte ke tumpukan saya begitu saja. Saya benar-benar mungkin akan mengalokasikannya di heap ( std::make_unique<int[]>()tentu saja).
einpoklum

3

Dua alasan:

  1. Tidak perlu dalam kasus ini. Anda membuat kode Anda menjadi semakin rumit.
  2. Ini mengalokasikan ruang pada heap, dan itu berarti bahwa Anda harus mengingatnya deletenanti, atau itu akan menyebabkan kebocoran memori.

2

newadalah yang baru goto.

Ingat mengapa gotobegitu dicerca: sementara itu adalah alat yang kuat, tingkat rendah untuk kontrol aliran, orang sering menggunakannya dengan cara rumit yang tidak perlu yang membuat kode sulit untuk diikuti. Selain itu, pola yang paling berguna dan termudah untuk dibaca dikodekan dalam pernyataan pemrograman terstruktur (misalnya foratau while); efek utamanya adalah bahwa kode di mana gotocara yang tepat untuk melakukannya agak jarang, jika Anda tergoda untuk menulis goto, Anda mungkin melakukan hal-hal buruk (kecuali Anda benar-benar - tahu apa yang Anda lakukan).

newserupa - sering digunakan untuk membuat hal-hal yang tidak perlu rumit dan sulit untuk dibaca, dan pola penggunaan yang paling berguna dapat dikodekan telah dikodekan ke dalam berbagai kelas. Selain itu, jika Anda perlu menggunakan pola penggunaan baru yang belum ada kelas standar, Anda dapat menulis kelas Anda sendiri yang menyandikannya!

Aku bahkan akan berpendapat bahwa newadalah lebih buruk daripada goto, karena kebutuhan untuk pasangan newdan deletepernyataan.

Seperti goto, jika Anda pernah berpikir perlu menggunakan new, Anda mungkin melakukan hal-hal buruk - terutama jika Anda melakukannya di luar implementasi kelas yang tujuannya dalam hidup adalah untuk merangkum alokasi dinamis apa pun yang perlu Anda lakukan.


Dan saya akan menambahkan: "Anda pada dasarnya tidak membutuhkannya".
einpoklum

1

Alasan intinya adalah bahwa objek pada heap selalu sulit digunakan dan dikelola daripada nilai-nilai sederhana. Menulis kode yang mudah dibaca dan dipelihara selalu menjadi prioritas pertama dari setiap programmer yang serius.

Skenario lain adalah perpustakaan yang kami gunakan memberikan semantik nilai dan membuat alokasi dinamis tidak perlu. Std::stringadalah contoh yang bagus.

Namun untuk kode berorientasi objek, menggunakan pointer - yang berarti digunakan newuntuk membuatnya sebelumnya - adalah suatu keharusan. Untuk menyederhanakan kompleksitas manajemen sumber daya, kami memiliki lusinan alat untuk membuatnya sesederhana mungkin, seperti smart pointer. Paradigma berbasis objek atau paradigma generik mengasumsikan nilai semantik dan membutuhkan lebih sedikit atau tidak newsama sekali, seperti yang dinyatakan poster di tempat lain.

Pola desain tradisional, terutama yang disebutkan dalam buku GoF , menggunakan newbanyak, karena mereka adalah kode OO khas.


4
Ini adalah jawaban yang sangat buruk . For object oriented code, using a pointer [...] is a must: Omong kosong . Jika Anda mendevaluasi 'OO' dengan merujuk hanya ke sebagian kecil, polimorfisme - juga omong kosong: referensi juga berfungsi. [pointer] means use new to create it beforehand: terutama omong kosong : referensi atau petunjuk dapat diambil untuk objek yang dialokasikan secara otomatis & digunakan secara polimorfik; awasi aku . [typical OO code] use new a lot: mungkin di beberapa buku lama, tapi siapa yang peduli? C ++ eschews new/ pointer mentah yang samar-samar modern sedapat mungkin - & sama sekali tidak kurang OO dengan melakukannya
underscore_d

1

Satu poin lagi untuk semua jawaban yang benar di atas, itu tergantung pada jenis pemrograman yang Anda lakukan. Kernel berkembang di Windows misalnya -> Stack sangat terbatas dan Anda mungkin tidak dapat mengambil kesalahan halaman seperti dalam mode pengguna.

Dalam lingkungan seperti itu, panggilan API baru atau mirip C lebih disukai dan bahkan diperlukan.

Tentu saja, ini hanyalah pengecualian dari aturan.


Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.