Sebelum menulis strategi C++, ada beberapa hal dasar yang perlu Anda ketahui, setidaknya Anda harus tahu aturan ini. Berikut ini adalah informasi untuk ditransklusi:
Jika seseorang menyebut dirinya seorang programmer yang baik tetapi tidak tahu apa-apa tentang memori, maka saya dapat memberi tahu Anda bahwa dia pasti membanggakan diri. Menulis program C atau C++, perlu lebih memperhatikan memori, bukan hanya karena apakah pembagian memori yang wajar secara langsung mempengaruhi efisiensi dan kinerja program, tetapi lebih penting lagi, ketika kita mengoperasikan memori, masalah akan muncul dengan tidak sengaja, dan banyak kali, masalah ini tidak mudah dideteksi, seperti kebocoran memori, seperti kurung petunjuk. Saya ingin berbicara di sini hari ini bukan untuk membahas bagaimana menghindari masalah ini, tetapi untuk memahami objek memori C++ dari sudut pandang lain.
Kita tahu bahwa C++ membagi memori ke dalam tiga area logis: heap, heap, dan static. Karena itu, saya menyebut objek yang berada di dalamnya sebagai heap, heap, dan static. Jadi apa perbedaan antara objek memori yang berbeda?
1 Konsep dasar
Pertama-tama mari kita lihat...........................
Type stack_object ;
stack_object adalah objek yang hidup pada titik definisi dan berakhir pada saat fungsi yang bersangkutan kembali.
Selain itu, hampir semua obyek sementara adalah obyek tang. Misalnya, definisi fungsi berikut:
Type fun(Type object);
Fungsi ini menghasilkan setidaknya dua objek sementara, pertama, parameter yang diteruskan dengan nilai, sehingga akan memanggil fungsi copy constructor untuk menghasilkan objek sementara object_copy1, yang digunakan di dalam fungsi bukan object, tetapi object_copy1, secara alami, object_copy1 adalah obyek tangkas, yang dilepaskan ketika fungsi kembali; dan fungsi ini adalah nilai yang dikembalikan, ketika fungsi kembali, maka juga akan menghasilkan obyek sementara object_copy2, yang akan dilepaskan beberapa waktu setelah fungsi kembali. Misalnya, fungsi memiliki kode sebagai berikut:
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
Implementasi dari kalimat kedua di atas adalah, pertama-tama, ketika fungsi fun kembali, object_copy2 akan dibuat sebagai obyek sementara, kemudian operator penugasan akan dieksekusi.
tt = object_copy2 ; //调用赋值运算符
Anda lihat? Kompiler menghasilkan begitu banyak obyek sementara untuk kita tanpa kita sadari, dan biaya waktu dan ruang untuk menghasilkan obyek sementara ini mungkin sangat besar, jadi, Anda mungkin mengerti mengapa untuk obyek yang lebih baik untuk diarahkan dengan referensi const daripada dengan parameter fungsi yang diarahkan dengan nilai.
Selanjutnya, perhatikan heap. Heap, juga disebut area penyimpanan bebas, yang secara dinamis dialokasikan selama program dijalankan, sehingga karakteristik terbesarnya adalah dinamika. Dalam C++, semua objek heap dibuat dan dihancurkan oleh programmer, sehingga masalah memori akan terjadi jika diproses dengan buruk. Jika objek heap dialokasikan, tetapi lupa untuk dilepaskan, maka akan terjadi kebocoran memori; dan jika objek telah dilepaskan, tetapi tidak menetapkan pointer yang sesuai sebagai NULL, pointer ini disebut pendingin pendingin, yang digunakan lagi, maka akan terjadi akses ilegal, yang menyebabkan keruntuhan program.
Jadi, bagaimana cara mengalokasikan objek tumpukan di C++? Satu-satunya cara adalah dengan menggunakan new (yang tentu saja juga dapat memperoleh memori tumpukan C dengan perintah Malloc), yang hanya mengalokasikan sebuah memori dalam tumpukan dengan menggunakan new dan mengembalikan pointer ke objek tumpukan tersebut.
Kembali ke ruang penyimpanan statis. Semua objek statis dan global ditugaskan ke ruang penyimpanan statis. Untuk objek global, semua objek global ditugaskan sebelum fungsi main () dijalankan. Sebenarnya, sebelum kode tampilan dalam fungsi main () dijalankan, fungsi main () yang dihasilkan oleh kompilator akan dipanggil, sedangkan fungsi main () akan melakukan pekerjaan konstruksi dan inisialisasi semua objek global.
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Jadi, setelah mengetahui ini, kita dapat mengambil beberapa trik dari ini, misalnya, misalkan kita ingin melakukan beberapa persiapan sebelum fungsi main() dijalankan, maka kita dapat menulis persiapan ini ke dalam fungsi pembangun objek global yang telah ditentukan, sehingga sebelum kode eksplisit fungsi main() dijalankan, fungsi pembangun objek global ini akan dipanggil dan melakukan tindakan yang diharapkan, sehingga mencapai tujuan kita. Jika kita baru saja berbicara tentang objek global di area penyimpanan statis, maka apakah itu objek statis lokal?
Ada juga objek statis, yaitu yang merupakan anggota statis dari kelas. Dalam mempertimbangkan hal ini, beberapa masalah yang lebih rumit timbul.
Masalah pertama adalah masa hidup dari objek anggota statik kelas, yang terbentuk dengan terbentuknya objek kelas pertama, dan mati pada akhir program; yaitu, ada situasi di mana dalam program kita mendefinisikan sebuah kelas yang memiliki satu objek statik sebagai anggota, tetapi dalam proses pelaksanaan program, jika kita tidak membuat salah satu dari objek kelas itu, maka tidak akan ada objek statik yang terkandung dalam kelas itu; jika ada beberapa objek kelas yang dibuat, maka semua objek tersebut berbagi anggota objek statik.
Masalah kedua adalah ketika terjadi hal-hal berikut:
class Base
{
public:
static Type s_object ;
}
class Derived1 : public Base / / 公共继承
{
... // other data
}
class Derived2 : public Base / / 公共继承
{
... // other data
}
Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;
Perhatikan bahwa tiga kalimat di atas yang ditandai sebagai blackbody, apakah s_object yang mereka kunjungi adalah objek yang sama? Jawabannya adalah ya, mereka memang menunjuk ke objek yang sama, yang tidak terdengar seperti benar, kan? Tapi itu benar, Anda dapat menulis beberapa kode sederhana untuk memverifikasi sendiri. Yang akan saya lakukan adalah menjelaskan mengapa ini terjadi.
Mari kita bayangkan bahwa ketika kita menyampaikan suatu objek tipe Derived1 ke sebuah fungsi yang menerima parameter tipe Base yang tidak berreferensi, maka bagaimana pemotongan itu terjadi?
Semua obyek dari kelas turunan yang mewarisi BASE memiliki subobjek tipe BASE (yang merupakan kunci yang dapat diarahkan ke objek Derived1 dengan pointer tipe BASE, dan tentu saja juga merupakan kunci multi-mode), sedangkan semua subobjek dan semua objek tipe BASE berbagi objek s_object yang sama, dan tentu saja, semua contoh kelas dalam seluruh sistem mewarisi yang berasal dari kelas BASE akan berbagi objek s_object yang sama.
2 Perbandingan tiga jenis objek memori
Keuntungan dari obyek kerucut adalah secara otomatis dihasilkan pada saat yang tepat, dan juga di saat yang tepat dihancurkan secara otomatis, tanpa perlu programmer khawatir; dan obyek kerucut umumnya dibuat lebih cepat daripada obyek tumpukan, karena ketika mendistribusikan obyek tumpukan, operator new akan memanggil operasi, operator new akan menggunakan beberapa algoritma pencarian ruang memori, dan proses pencarian ini mungkin sangat memakan waktu, dan menghasilkan objek kerucut tidak terlalu banyak kerumitan, hanya perlu memindahkan kursor kerucut. Namun perlu dicatat bahwa biasanya kapasitas ruang kerucut relatif kecil, umumnya 1 MB / 2 MB, sehingga obyek bervolume lebih besar tidak cocok dalam distribusi kerucut.
Objek tumpukan, yang saat diciptakannya dan saat dihancurkan harus didefinisikan oleh programmer, yaitu programmer memiliki kendali penuh atas kehidupan objek tumpukan. Kami sering membutuhkan objek seperti itu, misalnya, kami ingin membuat sebuah objek yang dapat diakses oleh beberapa fungsi, tetapi tidak ingin membuatnya global, maka saat ini membuat objek tumpukan pasti merupakan pilihan yang baik, dan kemudian menyampaikan petunjuk objek tumpukan ini di antara berbagai fungsi, sehingga dapat mencapai pembagian objek tersebut. Selain itu, kapasitas tumpukan jauh lebih besar dibandingkan dengan ruang kerucut.
Kita lihat objek statis selanjutnya.
Pertama adalah objek global. Objek global memberikan cara yang paling sederhana untuk komunikasi antar kelas dan antar fungsi, meskipun cara ini tidak elegan. Secara umum, dalam bahasa berorientasi objek sepenuhnya, tidak ada objek global, seperti C#, karena objek global berarti tidak aman dan tinggi perpaduan, dan terlalu banyak menggunakan objek global dalam program akan sangat mengurangi ketahanan, stabilitas, pemeliharaan, dan repeatabilitas program.
Selanjutnya adalah anggota statik kelas, yang telah disebutkan di atas. Semua objek kelas dasar dan kelas turunan mereka berbagi objek anggota statik ini, jadi anggota statik seperti ini pasti merupakan pilihan yang baik ketika perlu berbagi data atau komunikasi antara kelas ini atau antara objek kelas ini.
Selanjutnya adalah objek lokal statis, yang terutama digunakan untuk menyimpan keadaan tengah selama fungsi di mana objek itu berada dipanggil berulang kali, salah satu contoh yang paling menonjol adalah fungsi recursive, yang kita ketahui adalah fungsi recursive yang memanggil fungsinya sendiri, jika objek lokal nonstatis didefinisikan dalam fungsi recursive, maka jika jumlah recursive yang cukup besar, maka biaya yang dihasilkan juga besar. Ini karena objek lokal nonstatis adalah objek yang terikat, setiap panggilan recursive menghasilkan objek seperti ini, setiap kembalinya melepaskan objek ini, dan, objek seperti ini hanya terbatas pada lapisan panggilan saat ini, tidak terlihat untuk lapisan yang lebih dalam dan lapisan yang lebih dangkal.
Dalam desain fungsi recursive, objek statis dapat digunakan untuk menggantikan objek lokal nonstatis (misalnya, objek kerucut), yang tidak hanya mengurangi biaya untuk menghasilkan dan melepaskan objek nonstatis setiap kali panggilan recursive dan kembali, tetapi juga objek statis dapat menyimpan status tengah panggilan recursive dan dapat diakses oleh setiap lapisan panggilan.
3 Kesempatan untuk menggunakan benda yang tidak diinginkan
Seperti yang telah kami sebutkan sebelumnya, obyek kerucut dibuat pada waktu yang tepat dan kemudian dilepaskan secara otomatis pada waktu yang tepat, yang berarti obyek kerucut memiliki fungsi manajemen otomatis. Jadi di mana obyek kerucut akan dilepaskan secara otomatis? Pertama, pada akhir hidupnya; kedua, ketika fungsi di mana ia berada terjadi kelainan.
Obyek kerucut, ketika dilepaskan secara otomatis, akan memanggil fungsi analitiknya sendiri. Jika kita mengunggah sumber daya di dalam kerucut, dan melakukan tindakan melepaskan sumber daya dalam fungsi analitik objek kerucut, maka kemungkinan kebocoran sumber daya akan sangat berkurang, karena objek kerucut dapat melepaskan sumber daya secara otomatis, bahkan ketika fungsinya terjadi kelainan. Proses praktisnya adalah sebagai berikut: ketika fungsi dilepaskan secara abnormal, terjadi apa yang disebut stack_unwinding (mengalir kembali kerucut), yang akan berlangsung di dalam tumpukan, karena objek kerucut, secara alami ada di kerucut, maka fungsi analitik objek kerucut akan dijalankan dalam proses pengunggahan kembali, sehingga melepaskan sumber daya kecil yang dikemas.
4 Melarang membuat objek tumpukan
Seperti yang telah disebutkan di atas, Anda memutuskan untuk melarang pembuatan objek heap dari jenis tertentu, di mana Anda dapat membuat kelas kemasan sumber daya sendiri, yang hanya dapat dihasilkan dalam heap, sehingga sumber daya yang dikemas dapat secara otomatis dilepaskan dalam situasi yang luar biasa.
Jadi bagaimana cara melarang membuat benda tumpukan? Kita sudah tahu, satu-satunya cara untuk membuat benda tumpukan adalah dengan menggunakan new, jika kita melarang menggunakan new tidak akan berhasil. Selanjutnya, new akan memanggil operator new saat dijalankan, sementara operator new dapat dimuat. Ada cara untuk membuat new operator private, untuk simetri, sebaiknya operator juga di ulang menjadi private. Sekarang, Anda mungkin bertanya-tanya lagi, apakah membuat benda tumpukan tidak perlu memanggil new?
#include <stdlib.h> //需要用到C式内存分配函数
class Resource ; //代表需要被封装的资源类
class NoHashObject
{
private:
Resource* ptr ;//指向被封装的资源
... ... //其它数据成员
void* operator new(size_t size) //非严格实现,仅作示意之用
{
return malloc(size) ;
}
void operator delete(void* pp) //非严格实现,仅作示意之用
{
free(pp) ;
}
public:
NoHashObject()
{
//此处可以获得需要封装的资源,并让ptr指针指向该资源
ptr = new Resource() ;
}
~NoHashObject()
{
delete ptr ; //释放封装的资源
}
};
NoHashObject sekarang adalah kelas yang melarang objek tumpukan jika Anda menulis kode berikut:
NoHashObject* fp = new NoHashObject (()) ; // Kesalahan kompilasi!
menghapus fp;
Kode di atas akan menghasilkan kesalahan pada periode kompilasi. Baiklah, sekarang Anda tahu cara merancang kelas yang melarang objek tumpukan, Anda mungkin memiliki pertanyaan seperti saya, apakah tidak mungkin untuk menghasilkan objek tumpukan tipe ini ketika definisi kelas NoHashObject tidak dapat diubah? Tidak, apakah ada cara, yang saya sebut dengan penyalahgunaan kekerasan.
void main(void)
{
char* temp = new char[sizeof(NoHashObject)] ;
//强制类型转换,现在ptr是一个指向NoHashObject对象的指针
NoHashObject* obj_ptr = (NoHashObject*)temp ;
temp = NULL ; //防止通过temp指针修改NoHashObject对象
//再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
Resource* rp = (Resource*)obj_ptr ;
//初始化obj_ptr指向的NoHashObject对象的ptr成员
rp = new Resource() ;
//现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
... ...
delete rp ;//释放资源
temp = (char*)obj_ptr ;
obj_ptr = NULL ;//防止悬挂指针产生
delete [] temp ;//释放NoHashObject对象所占的堆空间。
}
Implementasi di atas adalah rumit, dan implementasi ini hampir tidak digunakan dalam praktik, tetapi saya tetap menulis jalan karena memahaminya bermanfaat bagi pemahaman kita tentang objek memori C++.
Data dalam memori tidak berubah, dan tipe adalah kacamata yang kita kenakan, dan ketika kita mengenakan kacamata, kita akan menggunakan tipe yang sesuai untuk menafsirkan data dalam memori, sehingga interpretasi yang berbeda menghasilkan informasi yang berbeda.
Konversi tipe yang dipaksakan pada dasarnya adalah mengganti kacamata lain dan melihat data memori yang sama lagi.
Perlu juga dicatat bahwa komposer yang berbeda dapat mengatur tata letak data anggota objek yang berbeda, misalnya, sebagian besar komposer mengatur anggota ptr pointer NoHashObject pada 4 byte pertama ruang objek untuk memastikan bahwa tindakan konversi dari pernyataan di bawah ini dilakukan seperti yang kita harapkan:
Sumber* rp = (Sumber*) obj_ptr ;
Namun, tidak semua kompilator harus seperti itu.
Jika kita bisa melarang suatu jenis objek tumpukan, apakah kita bisa merancang sebuah kelas sehingga tidak menghasilkan objek kerucut? Tentu saja bisa.
5 Membatalkan pembuatan objek kerucut
Seperti yang telah disebutkan sebelumnya, ketika membuat sebuah obyek kerucut, kursor akan memindahkan kursor untuk memindahkan kerucut ke ruang dengan ukuran kerucut yang tepat, dan kemudian memanggil fungsi konstruksi yang sesuai untuk membentuk objek kerucut secara langsung di ruang ini, dan ketika fungsi kembali, kursor akan memanggil fungsi pembongkaran untuk membebaskan objek tersebut, dan kemudian mengubah kursor untuk mengambil kembali memori kerucut tersebut. Operator baru / hapus tidak diperlukan dalam proses ini, jadi pengaturan operator baru / hapus sebagai pribadi tidak dapat dicapai. Tentu saja dari narasi di atas, Anda mungkin sudah berpikir: kursor mengatur fungsi pembongkaran atau fungsi pembongkaran sebagai pribadi, sehingga sistem tidak dapat menggunakan fungsi pembongkaran / pembongkaran, tentu saja tidak dapat membuat objek kerucut.
Itu bisa, dan saya juga berencana untuk menggunakan ini. Tetapi sebelum itu, satu hal yang perlu dipertimbangkan adalah bahwa jika kita mengatur fungsi pembangun sebagai pribadi, maka kita tidak dapat menggunakan new untuk menghasilkan objek tumpukan secara langsung, karena new akan memanggil fungsi pembangunnya setelah memberi ruang untuk objek. Jadi, saya hanya akan mengatur fungsi pembangun analisis sebagai pribadi.
Jika suatu kelas tidak dimaksudkan sebagai kelas dasar, yang biasanya digunakan adalah untuk menyatakan fungsi analitiknya sebagai pribadi.
Untuk membatasi obyek-objek yang terikat, tetapi tidak membatasi pewarisan, kita dapat menyatakan fungsi analisis sebagai protected, sehingga keduanya baik-baik saja; seperti yang ditunjukkan dalam kode berikut:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Kemudian, Anda dapat menggunakan kelas NoStackObject seperti ini:
NoStackObject* hash_ptr = new NoStackObject() ;
...... // melakukan operasi pada objek yang diarahkan ke hash_ptr
hash_ptr->destroy (); Apa yang terjadi? Oh, tidakkah itu terasa sedikit aneh, kita membuat sebuah objek dengan new, tetapi tidak menggunakan delete untuk menghapusnya, tetapi menggunakan metode destruct. Jelas, pengguna tidak terbiasa dengan cara aneh ini. Jadi, saya memutuskan untuk mengatur fungsi pembangun juga menjadi pribadi atau dilindungi. Ini kembali ke pertanyaan yang saya coba hindari di atas, yaitu bagaimana cara membuat sebuah objek tanpa menggunakan new?
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
Sekarang kita bisa menggunakan kelas NoStackObject seperti ini:
NoStackObject* hash_ptr = NoStackObject::creatInstance()
...... // melakukan operasi pada objek yang diarahkan ke hash_ptr
hash_ptr->menghancurkan() ;
hash_ptr = NULL; // Menghindari penggunaan penunjuk gantung
Sekarang rasanya tidak lebih baik, operasi untuk membuat dan melepaskan objek sama.
Banyak programer C atau C++ yang tidak peduli dengan recycling sampah, karena mereka berpikir bahwa recycling sampah pasti lebih tidak efisien daripada mereka untuk mengelola memori dinamis, dan pada saat recycling program pasti akan berhenti di sana, sedangkan jika mereka mengendalikan manajemen memori, waktu alokasi dan pelepasan stabil dan tidak menyebabkan program berhenti. Akhirnya, banyak programmer C/C++ percaya bahwa tidak ada mekanisme recycling sampah yang dapat dilaksanakan di C/C++.
Pada kenyataannya, mesin pengolahan sampah tidak lambat, bahkan lebih efisien daripada distribusi memori dinamis. Karena kita hanya dapat mendistribusikan tanpa melepaskan, maka ketika mendistribusikan memori hanya perlu mendapatkan memori baru dari tumpukan secara terus menerus, cukup dengan menunjuk tumpukan bergerak; dan proses pelepasan dihilangkan, dan secara alami dipercepat. Algoritma pengolahan sampah modern telah berkembang banyak, dan algoritma pengumpulan tambahan telah memungkinkan proses pengolahan sampah dilakukan secara bertahap, menghindari proses yang terganggu.
Algorithm pengembalian sampah biasanya didasarkan pada pemindaian dan penandaan semua blok memori yang mungkin digunakan saat ini, dan pengembalian memori yang tidak ditandai dari semua memori yang telah dialokasikan. Dalam C/C++, pandangan bahwa pengembalian sampah tidak dapat dilakukan biasanya didasarkan pada ketidakmampuan untuk memindai semua blok memori yang mungkin masih digunakan dengan benar, namun hal yang tampaknya mustahil sebenarnya tidak rumit. Pertama, dengan memindai data memori, data yang dialokasikan secara dinamis ke dalam tumpukan memori dapat dengan mudah diidentifikasi, dan jika ada kesalahan pengidentifikasian, hanya dapat menunjuk beberapa data yang bukan pointer sebagai pointer, dan tidak menunjuk sebagai data non-pointer. Dengan demikian, proses pengembalian sampah hanya akan melewatkan pengembalian kembali dan menghapus fungsi memori yang tidak seharusnya.
Pada saat pengolahan sampah, hanya perlu memindai segmen bss, segmen data, dan ruang disk yang saat ini digunakan untuk menemukan jumlah yang mungkin merupakan pointer memori dinamis. Memindai memori yang dirujuk dapat mendapatkan semua memori dinamis yang saat ini digunakan.
Jika Anda ingin membangun recycle bin yang bagus untuk proyek Anda, maka Anda dapat meningkatkan kecepatan manajemen memori, atau bahkan mengurangi konsumsi memori keseluruhan. Jika Anda tertarik, carilah makalah yang sudah ada di internet tentang recycling bin dan perpustakaan yang telah dilaksanakan.
Dikirim olehHK Zhang
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
Tidak hanya dapat diakses, tetapi juga dapat dimodifikasi, hanya saja akses tersebut tidak pasti. Alamat variabel lokal berada di dalam tumpukan program sendiri, dan setelah variabel otoritas berakhir, nilainya tetap ada selama alamat memori variabel lokal tidak diberikan kepada variabel lain. Tetapi jika diubah, itu lebih berbahaya karena alamat memori ini mungkin diberikan kepada variabel lain dalam program, yang dapat menyebabkan program crash jika diubah secara paksa dengan pointer.