Sebelum menulis strategi C ++, anda perlu mengetahui beberapa asas, dan sekurang-kurangnya anda perlu mengetahui peraturan-peraturan ini. Berikut adalah terjemahan:
Jika seseorang mengatakan bahawa dia seorang pengaturcara tetapi tidak tahu apa-apa tentang memori, saya boleh memberitahu anda bahawa dia pasti berbangga. Menulis program dalam C atau C ++ memerlukan perhatian yang lebih besar terhadap memori, bukan hanya kerana alokasi memori yang wajar secara langsung mempengaruhi kecekapan dan prestasi program, tetapi lebih penting lagi, apabila kita mengendalikan memori, masalah akan timbul secara tidak sengaja, dan sering kali, masalah ini tidak dapat dikesan, seperti kebocoran memori, seperti pin penunjuk jari.
Kita tahu bahawa C++ membahagikan memori kepada tiga kawasan logik: stack, heap, dan static storage area. Oleh kerana itu, saya menyebut objek yang terletak di antara mereka sebagai objek stack, heap, dan static. Jadi apa perbezaan antara objek memori yang berbeza?
1 Konsep asas
Mari kita lihat . , yang biasanya digunakan untuk menyimpan pembolehubah atau objek tempatan, seperti objek yang kita nyatakan dalam definisi fungsi dengan pernyataan seperti berikut:
Type stack_object ;
stack_object adalah objek yang berumput, yang mempunyai jangka hayat yang bermula dari titik definisi, dan berakhir apabila fungsi yang terletak di sana dikembalikan.
Di samping itu, hampir semua objek sementara adalah objek gelung. Sebagai contoh, fungsi berikut ditakrifkan:
Type fun(Type object);
Fungsi ini menghasilkan sekurang-kurangnya dua objek sementara, pertama, argumen yang dihantar mengikut nilai, jadi fungsi penciptaan salinan akan dipanggil untuk menghasilkan objek sementara object_copy1, yang digunakan dalam fungsi ini bukan object, tetapi object_copy1, semula jadi, object_copy1 adalah objek gelung, yang akan dilepaskan apabila fungsi kembali; dan fungsi ini adalah nilai yang dikembalikan, apabila fungsi kembali, jika kita tidak mempertimbangkan pengoptimuman nilai kembali ((NRV), maka objek sementara object_copy2 akan dihasilkan, yang akan dilepaskan dalam jangka masa selepas fungsi kembali. Sebagai contoh, fungsi mempunyai kod berikut:
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
Ini adalah pelaksanaan ayat kedua di atas, dengan membuat objek sementara object_copy2 apabila fungsi fun dikembalikan, dan kemudian memanggil pengendali nilai untuk melaksanakan
tt = object_copy2 ; //调用赋值运算符
Perhatikan? Penyusun menghasilkan begitu banyak objek sementara untuk kita tanpa kita menyedarinya, dan pengeluaran masa dan ruang untuk menghasilkan objek sementara ini mungkin besar, jadi, anda mungkin dapat memahami mengapa lebih baik menggunakan rujukan konstan untuk objek yang lebih besar daripada menggunakan parameter fungsi untuk nilai.
Kemudian, lihat tumpukan. Tumpukan, juga dikenali sebagai kawasan penyimpanan bebas, ia secara dinamik diedarkan semasa pelaksanaan program, jadi ciri utamanya adalah dinamik. Dalam C ++, semua objek tumpukan harus dibuat dan dimusnahkan oleh pengaturcara, jadi jika tidak dikendalikan dengan baik, masalah memori akan berlaku. Jika objek tumpukan didistribusikan, tetapi lupa untuk melepaskan, maka akan ada kebocoran memori; dan jika objek telah dibebaskan, tetapi tidak menetapkan penunjuk yang sesuai sebagai NULL, penunjuk itu adalah apa yang dipanggil penunjuk gantung gantung, penggunaan penunjuk ini lagi akan menyebabkan akses haram, yang menyebabkan program runtuh jika serius.
Jadi, bagaimana untuk membahagikan objek-objek dalam C++? Satu-satunya cara ialah dengan menggunakan new ((tentu saja, arahan kelas malloc juga boleh digunakan untuk mendapatkan memori C-jenis stack), dengan menggunakan new, anda akan membahagikan sepotong memori dalam stack dan mengembalikan penunjuk yang mengarah ke objek dalam stack tersebut.
Lihatlah lagi static storage area. Semua objek statik, objek global, dibagikan di static storage area. Berkenaan dengan objek global, ia dibagikan sebelum main () fungsi dijalankan. Sebenarnya, sebelum kod paparan dalam fungsi main () dijalankan, fungsi _main () yang dihasilkan oleh pengumpul akan dipanggil, sementara fungsi _main () akan melakukan pembinaan dan inisialisasi semua objek global, dan sebelum fungsi main () berakhir, fungsi exit yang dihasilkan oleh pengumpul akan dipanggil, untuk melepaskan semua objek global.
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Oleh itu, setelah mengetahui ini, kita dapat mengambil beberapa teknik, seperti, jika kita ingin melakukan beberapa persiapan sebelum main () dilaksanakan, maka kita boleh menulis persiapan ini ke dalam fungsi pembinaan objek global yang disesuaikan, sehingga, sebelum kod eksplisit fungsi main () dijalankan, fungsi pembinaan objek global ini akan dipanggil dan melakukan tindakan yang diharapkan, sehingga mencapai tujuan kita. Jika kita bercakap tentang objek global dalam static storage area, maka, objek statik tempatan? Objek statik tempatan juga biasanya didefinisikan dalam fungsi, seperti objek hantu, hanya saja, ia mempunyai lebih banyak kata kunci statik di hadapan.
Terdapat juga objek statik, iaitu ia sebagai ahli statik kelas. Apabila mempertimbangkan keadaan ini, terdapat beberapa masalah yang lebih rumit.
Masalah pertama ialah jangka hayat objek ahli statik kelas, objek ahli statik kelas yang dihasilkan dengan penciptaan objek kelas pertama, dan hilang pada akhir keseluruhan program. Yaitu, terdapat keadaan di mana dalam program kita menentukan kelas yang mempunyai objek statik sebagai ahli, tetapi dalam proses pelaksanaan program, jika kita tidak membuat objek kelas, maka tidak akan dihasilkan objek statik yang termasuk dalam kelas tersebut.
Masalah kedua ialah apabila:
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 tiga pernyataan di atas yang ditandakan sebagai Blackbody, adakah s_object yang mereka lawati adalah objek yang sama? Jawapannya adalah ya, mereka memang merujuk kepada objek yang sama, dan ia tidak terdengar seperti benar, bukan? Tetapi ia adalah fakta, anda boleh menulis kod mudah untuk mengesahkan sendiri.
Mari kita fikirkan, apabila kita memberikan objek Derived1 kepada fungsi yang menerima argumen bukan rujukan Base, bagaimana ia akan dipotong? Sekarang anda sudah tahu, ia hanya mengambil subobject dari objek Derived1 dan mengabaikan semua anggota data lain yang disesuaikan oleh Derived1 dan kemudian menyerahkan subobject ini kepada fungsi (yang sebenarnya menggunakan salinan subobject ini dalam fungsi).
Semua objek derivatif yang mewarisi jenis Base mengandungi subobject jenis Base (yang merupakan kunci yang boleh digunakan untuk mengarahkan objek Derived1 dengan penunjuk jenis Base, yang juga merupakan kunci polymorphic), dan semua subobject dan semua objek jenis Base berkongsi objek s_object yang sama, dan secara semula jadi, setiap contoh dari seluruh sistem pembiakan yang berasal dari jenis Base akan berkongsi objek s_object yang sama. Susunan objek dalam contoh, contoh 1, contoh 2 yang disebutkan di atas adalah seperti yang ditunjukkan dalam rajah berikut:
2 Perbandingan tiga objek memori
Kelebihan objek gelung ialah ia dihasilkan secara automatik pada masa yang sesuai dan dimusnahkan secara automatik pada masa yang sesuai, tanpa memerlukan perhatian pengaturcara; dan penciptaan objek gelung biasanya lebih cepat daripada objek tumpukan, kerana apabila mengedarkan objek tumpukan, ia akan memanggil operasi operator new, operator new akan menggunakan algoritma pencarian ruang ingatan, dan proses pencarian mungkin memakan masa, menghasilkan objek gelung tidak begitu banyak masalah, ia hanya perlu menggerakkan penunjuk penutup gelung. Tetapi perlu diingat bahawa kapasiti ruang gelung biasanya lebih kecil, biasanya 1MB / 2MB, jadi objek yang lebih besar tidak sesuai untuk diedarkan dalam gelung.
Objek tumpukan, masa penciptaan dan pemusnahan mesti ditentukan oleh pengaturcara, iaitu, pengaturcara mempunyai kawalan penuh terhadap kehidupan objek tumpukan. Kita sering memerlukan objek seperti ini, misalnya, kita perlu membuat objek yang dapat diakses oleh banyak fungsi, tetapi tidak mahu menjadikannya global, maka ketika ini membuat objek tumpukan adalah pilihan yang baik, dan kemudian menyampaikan penunjuk objek tumpukan ini di antara setiap fungsi, untuk mencapai perkongsian objek tersebut. Selain itu, berbanding dengan ruang kosong, kapasiti tumpukan jauh lebih besar.
Kemudian, lihat objek statik.
Pertama, objek global. Objek global menyediakan cara yang paling mudah untuk berkomunikasi antara kelas dan antara fungsi, walaupun ia tidak elegan. Secara amnya, objek global tidak wujud dalam bahasa yang berorientasikan objek sepenuhnya, seperti C #, kerana objek global bermaksud tidak selamat dan berkolaborasi tinggi, menggunakan objek global terlalu banyak dalam program akan mengurangkan kekuatan, kestabilan, kebolehperluan dan kebolehgunaan program.
Kemudian adalah ahli statik kelas, seperti yang telah disebutkan di atas, semua objek kelas induk dan anak kelasnya berkongsi objek ahli statik ini, jadi anggota statik seperti itu pasti merupakan pilihan yang baik apabila anda perlu berkongsi data atau berkomunikasi antara kelas atau antara objek kelas.
Kemudian adalah objek-objek tempatan statik, yang digunakan untuk menyimpan objek-objek tersebut dalam keadaan pertengahan semasa fungsi yang dipanggil berulang kali, salah satu contoh yang paling ketara adalah fungsi berulang, kita semua tahu bahawa fungsi berulang adalah fungsi yang memanggil dirinya sendiri, jika fungsi berulang didefinisikan sebagai objek tempatan yang tidak statik, maka apabila jumlah berulang yang cukup besar, pengeluaran yang dihasilkan juga besar. Ini kerana objek tempatan yang tidak statik adalah objek titisan, setiap panggilan berulang sekali, akan menghasilkan objek seperti itu, setiap kali kembali, akan melepaskan objek ini, dan, objek seperti itu hanya terhad kepada lapisan panggilan semasa, tidak dapat dilihat untuk lapisan yang lebih mendalam dan lapisan yang lebih cetek.
Dalam reka bentuk fungsi regresi, objek statik boleh digunakan untuk menggantikan objek tempatan yang tidak statik (iaitu objek gelung), yang tidak hanya dapat mengurangkan perbelanjaan untuk menghasilkan dan melepaskan objek nonstatik setiap kali dipanggil dan dikembalikan secara regresi, tetapi objek statik juga dapat menyimpan keadaan pertengahan panggilan regresi dan dapat diakses oleh setiap lapisan panggilan.
3 Hasil yang tidak dijangka daripada penggunaan titanium
Seperti yang telah dibincangkan di atas, objek-objek yang dicipta pada masa yang tepat dan kemudian dilepaskan secara automatik pada masa yang tepat, iaitu objek-objek yang dicipta secara automatik mempunyai fungsi pengurusan automatik. Jadi, di mana objek yang dicipta secara automatik dilepaskan? Pertama, pada akhir hayatnya; kedua, apabila terdapat kecacatan dalam fungsi yang di dalamnya. Anda mungkin berkata, semua ini adalah normal, tidak ada masalah besar.
Jika kita membungkus sumber di dalam objek penyu dan melakukan tindakan untuk melepaskan sumber dalam fungsi penyu, maka kemungkinan kebocoran sumber akan berkurangan kerana objek penyu dapat melepaskan sumber secara automatik walaupun terdapat kecacatan dalam fungsi penyu. Prosesnya sebenarnya adalah seperti ini: apabila fungsi menyalurkan kecacatan, apa yang disebut stack_unwinding berlaku, iaitu, tumpukan terbuka, kerana objek penyu berada di dalam tumpukan semulajadi, fungsi penyu akan dijalankan dalam proses penyu berputar dan melepaskan sumber yang dibungkus.
4 Tidak boleh menghasilkan objek tumpukan
Seperti yang telah disebutkan di atas, jika anda memutuskan untuk melarang penciptaan objek tumpukan jenis tertentu, anda boleh membuat kelas kemasan sumber anda sendiri, yang hanya boleh dihasilkan dalam tumpukan, yang akan melepaskan sumber yang terbungkus secara automatik dalam keadaan yang tidak normal.
Jadi bagaimana untuk melarang ciptaan objek tumpukan? Kita sudah tahu bahawa satu-satunya cara untuk menghasilkan objek tumpukan adalah dengan menggunakan operasi new, jika kita melarang penggunaan new tidak akan berfungsi. Lebih jauh lagi, operasi new akan memanggil operator new, dan operator new boleh dimuat semula.
#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 kini merupakan kelas yang melarang objek tumpukan, jika anda menulis kod berikut:
NoHashObject* fp = new NoHashObject (() ; // Kesilapan pengkompilan!
delete fp ;
Kod di atas akan menghasilkan kesilapan pengkompilan. Baiklah, sekarang anda sudah tahu bagaimana merancang kelas yang melarang objek timbunan, anda mungkin mempunyai soalan seperti saya, adakah tidak mungkin untuk menghasilkan objek timbunan jenis ini jika definisi NoHashObject tidak dapat diubah? Tidak, atau ada cara, saya memanggilnya sebagai penembusan keganasan yang kuat. C ++ sangat kuat, anda boleh melakukannya dengan apa sahaja yang anda mahu.
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 bermasalah, dan ia hampir tidak digunakan dalam praktiknya, tetapi saya tetap menulisnya, kerana memahaminya adalah baik untuk memahami objek memori C ++. Apa yang paling mendasar dari banyak penukaran jenis paksa di atas? Kita boleh memahaminya sebagai berikut:
Data dalam satu blok memori tidak berubah, dan jenisnya adalah cermin mata yang kita pakai. Apabila kita memakai satu jenis cermin mata, kita akan menggunakan jenis yang sesuai untuk mentafsirkan data dalam memori, sehingga tafsiran yang berbeza akan mendapat maklumat yang berbeza.
Apa yang dipanggil penukaran jenis paksa sebenarnya adalah menukarkan mata dengan mata yang lain dan melihat data yang sama dalam ingatan.
Perlu diingatkan juga bahawa susunan data ahli objek mungkin berbeza antara pengumpul yang berbeza, contohnya, kebanyakan pengumpul meletakkan ahli penunjuk ptr NoHashObject di empat bait pertama ruang objek, untuk memastikan tindakan penukaran seperti yang kita harapkan:
Resource* rp = (Resource*)obj_ptr ;
Namun, tidak semestinya semua penyusun adalah seperti itu.
Oleh kerana kita boleh melarang untuk menghasilkan objek-objek jenis tertentu, bolehkah kita merancang sebuah kelas yang tidak boleh menghasilkan objek-objek yang berlainan?
5 Larangan untuk menghasilkan objek yang tidak bercahaya
Seperti yang telah disebutkan di atas, apabila mencipta objek timah, anda akan menggerakkan penunjuk timah untuk mengikis ruang yang sesuai dengan saiz timah, kemudian memanggil fungsi pembina yang sesuai secara langsung di ruang ini untuk membentuk objek timah, dan apabila fungsi tersebut kembali, anda akan memanggil fungsi penyusunnya untuk melepaskan objek ini, dan kemudian menyesuaikan penunjuk timah untuk mengambil semula blok timah tersebut. Memori dalam proses ini tidak memerlukan operasi new / delete, jadi menetapkan operator new / delete sebagai swasta tidak dapat mencapai tujuan.
Ini boleh dilakukan, dan saya juga bercadang untuk menggunakan kaedah ini. Tetapi sebelum itu, ada satu perkara yang perlu dipertimbangkan, iaitu, jika kita menetapkan fungsi pembinaan sebagai peribadi, maka kita tidak boleh menggunakan new untuk menghasilkan objek heap secara langsung, kerana new akan memanggil fungsi pembinaannya setelah memberi ruang kepada objek. Jadi, saya hanya akan menetapkan fungsi penyusunan sebagai swasta.
Jika sebuah kelas tidak bertujuan untuk menjadi kelas asas, penyelesaian yang biasa digunakan adalah untuk mengisytiharkan fungsi penyusunannya sebagai peribadi.
Untuk mengehadkan objek yang terhad, tetapi tidak mengehadkan pewarisan, kita boleh mendeklarasikan fungsi pemisahan sebagai dilindungi, dan kedua-duanya adalah baik. Seperti yang ditunjukkan oleh kod berikut:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Kemudian, anda boleh menggunakan kelas NoStackObject seperti ini:
NoStackObject* hash_ptr = new NoStackObject() ;
… … // melakukan tindakan terhadap objek yang diarahkan oleh hash_ptr
hash_ptr->destroy() ; Nah, adakah agak pelik, kita membuat objek dengan menggunakan new dan bukannya menghapusnya dengan menggunakan delete, kita menggunakan destroy. Jelas sekali, pengguna tidak terbiasa dengan penggunaan yang pelik ini. Jadi, saya memutuskan untuk menetapkan fungsi binaan sebagai peribadi atau dilindungi. Ini kembali kepada soalan yang cuba dielakkan di atas, iaitu tanpa menggunakan new, bagaimana untuk menghasilkan objek?
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
Kelas NoStackObject boleh digunakan dengan cara ini:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
… … // melakukan tindakan terhadap objek yang diarahkan oleh hash_ptr
hash_ptr->destroy() ;
hash_ptr = NULL ; // mencegah penggunaan penunjuk gantung
Sekarang ia berasa lebih baik kerana ia menghasilkan objek dan melepaskan objek dengan cara yang sama.
Banyak pengaturcara C atau C++ yang tidak suka dengan pengumpulan sampah, berpendapat bahawa pengumpulan sampah pasti kurang berkesan daripada menguruskan memori dinamik sendiri, dan pada masa pemulihan pasti akan membuat program berhenti di sana, dan jika anda mengawal pengurusan memori, peruntukan dan masa pelepasan adalah stabil, tidak akan menyebabkan program berhenti. Akhirnya, banyak pengaturcara C / C ++ yakin bahawa mekanisme pengumpulan sampah tidak dapat dilaksanakan di C / C ++.
Sebenarnya mekanisme pemulihan sampah tidak lambat, bahkan lebih cekap daripada peruntukan memori dinamik. Oleh kerana kita hanya boleh membahagikan tanpa melepaskan, maka pembahagian memori hanya perlu mendapatkan memori baru dari timbunan, pengarah tumpukan bergerak sudah cukup; dan proses pelepasan diabaikan, dan secara semula jadi juga mempercepatkan. Algoritma pemulihan sampah moden telah berkembang banyak, algoritma pengumpulan tambahan telah dapat membuat proses pemulihan sampah dilakukan secara beransur-ansur, untuk mengelakkan gangguan program.
Algoritma pemulihan sampah biasanya berdasarkan pengimbas dan penandaan semua blok memori yang mungkin digunakan pada masa ini, memulihkan memori yang tidak ditandai dari semua memori yang telah diperuntukkan. Pandangan bahawa pemulihan sampah tidak dapat dicapai dalam C / C ++ biasanya didasarkan pada ketidakupayaan untuk mengimbas dengan betul semua blok memori yang mungkin digunakan, tetapi apa yang nampaknya mustahil sebenarnya tidak rumit. Pertama, dengan mengimbas data dalam memori, penunjuk yang dialokasikan ke dalam memori secara dinamik sangat mudah dikenali, dan jika terdapat kesalahan pengiktirafan, penunjuk hanya boleh menjadi penunjuk pada beberapa data yang bukan penunjuk, dan tidak akan menjadi penunjuk pada data yang bukan penunjuk.
Pada masa pemulihan sampah, hanya perlu mengimbas segmen bss, segmen data dan ruang cadang yang sedang digunakan untuk mencari jumlah yang mungkin menjadi penunjuk memori dinamik, mengimbas berulang memori rujukan untuk mendapatkan semua memori dinamik yang sedang digunakan.
Jika anda ingin mewujudkan pengumpul sampah yang baik untuk projek anda, anda boleh meningkatkan kelajuan pengurusan memori, dan bahkan mengurangkan penggunaan memori keseluruhan. Jika anda berminat, anda boleh mencari kertas kerja yang ada di internet mengenai pengumpulan sampah dan perpustakaan yang telah dilaksanakan, yang penting bagi pengaturcara untuk membuka perspektif.
Dipetik dari:HK Zhang
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
Ia bukan sahaja boleh diakses, tetapi juga diubah suai, tetapi ia tidak pasti. Alamat pembolehubah tempatan berada di dalam timbunan program itu sendiri, dan nilai pembolehubah pihak berkuasa akan tetap wujud selagi alamat memori pembolehubah tempatan itu tidak diberikan kepada pembolehubah lain. Tetapi jika ia diubah, ia adalah lebih berbahaya, kerana alamat memori ini mungkin diberikan kepada pembolehubah lain dalam program, dan jika ia diubah secara paksa melalui penunjuk, ia boleh menyebabkan program itu runtuh.