Pertanyaan Mengapa programmer C ++ harus meminimalkan penggunaan 'baru'?


Saya menemukan pertanyaan Stack Overflow Memori bocor dengan std :: string saat menggunakan std :: list <std :: string>, dan salah satu komentar mengatakan ini:

Berhenti menggunakan new banyak. Saya tidak bisa melihat alasan Anda menggunakan baru di mana saja   kamu lakukan. Anda dapat membuat objek berdasarkan nilai di C ++ dan itu salah satu dari   keuntungan besar menggunakan bahasa. Anda tidak harus mengalokasikan   semuanya di heap. Berhenti berpikir seperti programmer Java.

Saya tidak begitu yakin apa yang dia maksud dengan itu. Mengapa objek harus dibuat berdasarkan nilai dalam C ++ sesering mungkin, dan apa bedanya secara internal? Apakah saya salah menafsirkan jawabannya?


750
2018-06-28 00:08


asal


Jawaban:


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

Tumpukan

Tumpukan selalu mengalokasikan memori secara berurutan. Ini dapat dilakukan 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 mengalokasikan secara implisit.

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

Tumpukan

Heap 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 delete atau delete[] (free di C). Namun, ketiadaan titik rilis implisit adalah kunci fleksibilitas tumpukan.

Alasan untuk menggunakan alokasi dinamis

Bahkan jika menggunakan heap lebih lambat dan berpotensi menyebabkan kebocoran memori atau fragmentasi memori, ada kasus penggunaan yang sangat baik untuk alokasi dinamis, karena itu 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 apa ukuran file itu, sehingga Anda tidak dapat memutuskan berapa banyak memori untuk mengalokasikan hingga Anda menjalankan program.

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

Mengapa alokasi dinamis sering tidak diperlukan

Dalam C + + ada konstruksi rapi yang disebut a perusak. Mekanisme ini memungkinkan Anda mengelola sumber daya dengan menyelaraskan sumber daya seumur hidup dengan masa pakai variabel. Teknik ini disebut RAII dan merupakan titik pembeda C ++. Ini "membungkus" sumber daya menjadi objek. std::string adalah contoh sempurna. Potongan ini:

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

sebenarnya mengalokasikan sejumlah variabel memori. Itu std::string objek mengalokasikan memori menggunakan heap dan melepaskannya di destructor-nya. Dalam hal ini, Anda melakukannya tidak perlu mengelola sumber daya 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 diperlukan. Program ini membutuhkan lebih banyak pengetikan (!) Dan memperkenalkan risiko lupa untuk membatalkan alokasi memori. Itu melakukan ini tanpa manfaat jelas.

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 direferensikan, ada kekhawatiran tambahan. Khususnya, 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 itu std::string mendefinisikan dengan benar konstruktor salinan. Pertimbangkan program berikut:

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

Menggunakan versi asli, program ini kemungkinan akan crash, karena menggunakan delete pada senar yang sama dua kali. Menggunakan versi modifikasi, masing-masing Line instance akan memiliki string sendiri contoh, masing-masing dengan memori sendiri dan keduanya akan dirilis pada akhir program.

Catatan lainnya

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

Jika Anda menggunakan Line kelas sebagai blok bangunan:

 class Table
 {
      Line borders[4];
 };

Kemudian

 int main ()
 {
     Table table;
 }

mengalokasikan empat std::string contoh, empat Line contoh, satu Table contoh dan semua isi string dan semuanya dibebaskan secara otomatis.


898
2018-06-28 00:47



Karena tumpukannya cepat dan sangat mudah

Dalam C ++, dibutuhkan tetapi instruksi tunggal 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 msgstr "gunakan stack dan bukan heap".


155
2018-06-28 00:14



Ini rumit.

Pertama, C ++ bukan sampah yang dikumpulkan. Oleh 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. Tapi apa yang terjadi jika "Lakukan hal-hal" melempar pengecualian? Ups: kebocoran memori. Apa yang terjadi jika masalah "Lakukan hal-hal" return awal? Ups: kebocoran memori.

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

Atau, Anda bisa melakukan ini:

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

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

Semua tanpa new dan 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;

Sudah dimengerti itu otherString memiliki salinan data dari someString. Itu bukan penunjuk; itu adalah objek terpisah. Mereka mungkin memiliki konten yang sama, tetapi Anda dapat mengubahnya tanpa memengaruhi yang lain:

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

Lihat idenya?


94
2018-06-28 00:17



Objek yang dibuat oleh new harus akhirnya deletejangan sampai mereka bocor. Destructor tidak akan dipanggil, memori tidak akan dibebaskan, seluruh bit. Karena C ++ tidak memiliki koleksi sampah, itu masalah.

Objek yang dibuat oleh nilai (i. E. Di stack) secara otomatis mati ketika mereka keluar dari ruang lingkup. Panggilan destruktor disisipkan oleh kompilator, dan memori otomatis dibebaskan setelah pengembalian fungsi.

Smart pointer menyukai auto_ptr, shared_ptr memecahkan masalah referensi yang menggantung, tetapi mereka membutuhkan disiplin pengkodean dan memiliki masalah lain (copyability, referensi loop, dll.).

Juga, dalam skenario yang sangat multithread, new adalah titik pertentangan antara utas; bisa ada dampak kinerja karena terlalu sering menggunakan new. Pembuatan objek stack adalah berdasarkan definisi thread-local, karena setiap thread memiliki stack-nya sendiri.

Kelemahan dari objek nilai adalah bahwa mereka mati setelah fungsi host kembali - Anda tidak bisa melewatkan referensi ke kembali ke pemanggil, hanya dengan menyalin atau mengembalikan nilai.


65
2018-06-28 00:11



  • C ++ tidak menggunakan pengelola memori apa pun dengan sendirinya. Bahasa lain seperti C #, Java memiliki garbage collector untuk menangani memori
  • C + + menggunakan rutin sistem operasi untuk mengalokasikan memori dan terlalu banyak / hapus baru dapat memecah memori yang tersedia
  • Dengan aplikasi apa pun, jika memori sering digunakan, sebaiknya pra-mengalokasikannya dan lepaskan bila tidak diperlukan.
  • Manajemen memori yang tidak tepat dapat menyebabkan kebocoran memori dan sangat sulit untuk dilacak. Jadi menggunakan objek stack dalam ruang lingkup fungsi adalah teknik yang telah terbukti
  • Kelemahan menggunakan objek stack adalah, ia membuat banyak salinan objek saat kembali, beralih ke fungsi, dll. Namun, kompiler cerdas sangat menyadari situasi ini dan mereka telah dioptimalkan dengan baik untuk kinerja.
  • Ini benar-benar membosankan di C + + jika memori dialokasikan dan dirilis di dua tempat yang berbeda. Tanggung jawab untuk rilis selalu menjadi pertanyaan dan kebanyakan kita bergantung pada beberapa petunjuk yang dapat diakses, menumpuk objek (semaksimal mungkin) dan teknik seperti auto_ptr (objek RAII)
  • Yang terbaik adalah, Anda telah mengontrol memori dan yang terburuk adalah Anda tidak akan memiliki kendali atas memori jika kami menggunakan manajemen memori yang tidak tepat untuk aplikasi. Kerusakan yang disebabkan karena kerusakan memori adalah yang paling menjijikkan dan sulit dilacak.

27
2018-06-28 02:59



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

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

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

    // do things

 }

Sekarang, amati bahwa ketika Anda jatuh dari blok itu setelah pengikatan ujung, foo berada di luar ruang lingkup. C ++ akan memanggil dtornya secara otomatis untuk Anda. Tidak seperti Java, Anda tidak perlu menunggu GC untuk menemukannya.

Sudahkah Anda menulis

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

Anda ingin mencocokkannya secara eksplisit dengan

     delete foo;
  }

atau bahkan lebih baik, mengalokasikan Anda File * sebagai "penunjuk pintar". Jika Anda tidak berhati-hati, itu bisa menyebabkan kebocoran.

Jawabannya sendiri membuat asumsi keliru bahwa jika Anda tidak menggunakannya new Anda tidak mengalokasikan pada heap; sebenarnya, di C ++ Anda tidak tahu itu. Paling-paling, Anda tahu bahwa sebagian kecil memori, katakanlah satu penunjuk, tentu dialokasikan pada tumpukan. Namun, pertimbangkan apakah implementasi File adalah sesuatu seperti

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

kemudian FileImpl akan masih dialokasikan pada tumpukan.

Dan ya, Anda sebaiknya yakin untuk memilikinya

     ~File(){ delete fd ; }

di kelas juga; tanpa itu, Anda akan membocorkan memori dari tumpukan meskipun Anda tidak melakukannya tampaknya mengalokasikan pada heap sama sekali.


17
2018-06-28 00:11



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

Operator new memiliki waktu eksekusi non-deterministik

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

Operator new adalah sinkronisasi thread implisit

Ya Anda mendengar saya, OS Anda perlu memastikan tabel halaman Anda konsisten dan sebagai panggilan tersebut new akan menyebabkan utas Anda memperoleh kunci mutex implisit. Jika Anda secara konsisten menelepon new dari banyak utas Anda sebenarnya membuat serial utas Anda (saya telah melakukan ini dengan 32 CPU, masing-masing dipukul new untuk mendapatkan beberapa ratus byte masing-masing, aduh! itu adalah royal p.i.t.a. untuk debug)

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


16
2018-02-12 17:57



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

Class var;

itu ditempatkan di tumpukan.

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


13
2018-06-28 00:10



new() tidak boleh digunakan sebagai sedikit mungkin. Itu harus digunakan sebagai hati-hati mungkin. Dan itu harus digunakan sesering yang diperlukan sebagaimana didiktekan oleh pragmatisme.

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

Namun, jika seumur hidup objek Anda perlu melampaui ruang lingkup saat itu new() adalah jawaban yang benar. Pastikan Anda memperhatikan kapan dan bagaimana Anda menelepon delete() dan kemungkinan pointer NULL, menggunakan objek yang dihapus dan semua getchas lain yang datang dengan menggunakan pointer.


13
2018-06-28 00:38