Es gibt einige Grundkenntnisse, die man wissen muss, bevor man eine C++-Strategie schreibt. Hier ist ein Übertragungsinhalt:
Wenn sich jemand als Programmierer bezeichnet, der aber nichts über Speicher weiß, dann kann ich Ihnen sagen, dass er sich wohl damit anfreut. Wenn man C oder C++ schreibt, sollte man sich mehr mit dem Speicher beschäftigen, nicht nur weil die angemessene Verteilung des Speichers unmittelbar die Effizienz und Leistung des Programms beeinflusst, sondern vor allem, weil es Probleme gibt, die unvorsichtig und oft unsichtbar sind, wenn wir mit dem Speicher arbeiten, wie z.B. Speicherlecks, wie z.B. Hängende Zeiger.
Wir wissen, dass C++ den Speicher in drei logische Bereiche unterteilt: Heap, Heap und Static-Speicher. Ich nenne die Objekte, die sich in ihnen befinden, also Heap-Objekte, Heap-Objekte und Static-Objekte. Was ist also der Unterschied zwischen diesen verschiedenen Speicherobjekten?
1 Grundkonzepte
Zunächst einmal sehen wir, dass
Type stack_object ;
Stack_object ist ein Stack-Objekt, dessen Lebensdauer beginnt am Definitionspunkt und endet, wenn seine Funktion zurückkehrt.
Zusätzlich sind fast alle temporären Objekte Zwergobjekte.
Type fun(Type object);
Die Funktion erzeugt mindestens zwei temporäre Objekte. Zunächst werden die Parameter nach Werten weitergegeben, also wird die Kopie-Konstruktionsfunktion aufgerufen, um einen temporären Objekt object_copy1 zu erzeugen, der innerhalb der Funktion nicht als Object, sondern als Object_copy1 verwendet wird. Natürlich ist Object_copy1 ein Heckobjekt, das bei der Rückkehr der Funktion freigegeben wird.
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
Die Ausführung des zweiten Satzes oben ist, dass man zunächst ein temporäres Objekt generiert, object_copy2, wenn die Funktion fun zurückkehrt, und dann den Assignment-Operator ausführt.
tt = object_copy2 ; //调用赋值运算符
Sehen Sie? Der Compiler erzeugt so viele temporäre Objekte für uns ohne unsere Wahrnehmung, und die Zeit- und Raumausgaben für die Erstellung dieser temporären Objekte können sehr hoch sein, also können Sie vielleicht verstehen, warum es für Konst-Objekte am besten ist, die Funktionsparameter mit Konst-Referenzen zu übertragen, anstatt die Funktionsparameter per Wert zu übertragen.
Als nächstes betrachten wir den Stapel. Der freie Speicherbereich wird dynamisch während der Ausführung des Programms zugewiesen, weshalb seine größte Eigenschaft die Dynamik ist. In C++ sind alle Stapelobjekte vom Programmierer erstellt und zerstört.
Wie wird also ein Stapelobjekt in C++ zugewiesen? Die einzige Möglichkeit ist, den Stapel-Speicher mit new (und natürlich auch mit dem Malloc-Befehl) zu erhalten. Wenn man new verwendet, wird ein Stück Speicher in den Stapel zugewiesen und der Zeiger, der auf den Stapel zeigt, wird zurückgegeben.
Schauen wir uns noch einmal den statischen Speicherbereich an. Alle statischen Objekte und globalen Objekte sind in den statischen Speicherbereichen zugeordnet. In Bezug auf globalen Objekte ist die Allokation vor der Ausführung der Main-Funktion bereits erfolgt.
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Wenn wir also wissen, dass wir einige Tricks daraus ziehen können, wie zum Beispiel, wenn wir vor der Ausführung der main() -Funktion einige Vorbereitungen machen wollen, dann können wir diese Vorbereitungen in die Konstruktionsfunktionen eines bestimmten globalen Objekts schreiben, so dass die Konstruktionsfunktion dieses globalen Objekts vor der Ausführung des ausdrücklichen Codes der main() -Funktion aufgerufen wird, um die vorgesehenen Aktionen auszuführen, und so unser Ziel erreicht wird.
Es gibt noch ein anderes statisches Objekt, das als statisches Mitglied einer Klasse dient.
Das erste Problem ist die Lebensdauer der Objekte, die Static-Mitglieder der Class sind. Die Static-Mitglieder der Class entstehen mit der Entstehung des ersten Class-Objekts und sterben am Ende des gesamten Programms ab.
Das zweite Problem ist, wenn folgende Situationen auftreten:
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 = …… ;
Bitte beachten Sie, dass die drei oben als Schwarzkörper gekennzeichneten S_Objekte das gleiche Objekt sind, das sie aufrufen? Die Antwort lautet ja, sie verweisen tatsächlich auf das gleiche Objekt, was nicht wahr klingt, oder?
Denken wir mal, wenn wir ein Objekt von Typ Derived1 an eine Funktion weiterleiten, die keine Basis-Argumente akzeptiert, wie geschieht das Schneiden?
Alle Objekte der abgeleiteten Klasse, die von der Base-Klasse geerbt werden, enthalten einen Subobjekt des Typs Base (das ist der Schlüssel, der mit dem Base-Pointer auf einen Derived1-Objekt verwiesen werden kann, der natürlich auch ein polymotischer Schlüssel ist), während alle Subobjekte und alle Objekte des Typs Base den gleichen s_object teilen. Natürlich teilen alle Instanzen der Klasse, die aus der Base-Klasse abgeleitet wurden, den gleichen s_object.
2 Vergleich von drei Speicherobjekten
Der Vorteil von Hexobjekten ist, dass sie sich automatisch generieren und dann automatisch zerstören, ohne dass sich Programmierer darum kümmern müssen. Außerdem ist die Erstellung von Hexobjekten im Allgemeinen schneller als bei Stapelobjekten, da bei der Verteilung von Hexobjekten der Operator new aufgerufen wird, der einen Arten von Speicherplatz-Suche-Algorithmen verwendet, die möglicherweise sehr zeitaufwändig ist.
Ein Stapelobjekt, dessen Erstellung und Zerstörung vom Programmierer definiert werden müssen, d.h. der Programmierer hat absolute Kontrolle über das Leben des Stapelobjekts. Wir benötigen häufig solche Objekte. Zum Beispiel, wenn wir ein Objekt erstellen wollen, das von mehreren Funktionen aufgerufen werden kann, aber es nicht global machen wollen, ist es zweifellos eine gute Option, ein Stapelobjekt zu erstellen und dann die Zeiger des Stapelobjekts zwischen den verschiedenen Funktionen zu übertragen, um die gemeinsame Nutzung des Objekts zu ermöglichen.
Wir sehen uns Static-Objekte an.
Zunächst einmal sind es Globale Objekte. Globale Objekte bieten eine einfache Möglichkeit für die Kommunikation zwischen Klassen und zwischen Funktionen, obwohl diese nicht elegant ist. Im Allgemeinen gibt es keine Globale Objekte in einer vollständig objektorientierten Sprache, wie C#, da Globale Objekte unsicher und hochverknüpft sind.
Als nächstes gibt es die statischen Mitglieder der Klassen, die bereits erwähnt wurden. Alle Objekte der Basisklasse und ihrer Ableitklassen teilen diese statischen Mitglieder, so dass sie eine gute Wahl sind, wenn Daten zwischen diesen Klassen oder zwischen diesen Klassenobjekten geteilt oder kommuniziert werden müssen.
Dann gibt es die statischen Lokalobjekte, die hauptsächlich dazu dienen, den Zwischenzustand zu speichern, während die Funktion, auf der sie sich befindet, häufig aufgerufen wird. Ein hervorragendes Beispiel dafür sind die Regressionsfunktionen, von denen wir alle wissen, dass die Regressionsfunktionen ihre eigenen Funktionen aufrufen. Wenn in einer Regressionsfunktion ein nichtstatisches Lokalobjekt definiert wird, dann ist der Aufwand, wenn die Anzahl der Regressionen ziemlich groß ist, auch enorm.
Im Recursive-Funktionsdesign können statische Objekte für nichtstatische lokale Objekte verwendet werden, was nicht nur den Aufwand für die Erstellung und Freisetzung von nichtstatischen Objekten bei jedem Recursive-Call und Rückkehr reduziert, sondern auch den Zwischenzustand für Recursive-Calls speichert und für die einzelnen Aufrufschichten zugänglich macht.
3 Unerwartete Ernte mit einem Objekt
Wie bereits erwähnt, werden die Zwiebelobjekte zum richtigen Zeitpunkt erstellt und dann automatisch freigegeben, d.h. die Zwiebelobjekte verfügen über eine automatische Verwaltungsfunktion. Wo werden die Zwiebelobjekte dann automatisch freigegeben? Erstens, wenn sie am Ende ihrer Lebensdauer sind; zweitens, wenn die Funktion, in der sie sich befinden, abnormal ist.
Das Hex-Objekt ruft bei der automatischen Freisetzung seine eigene Parallellfunktion auf. Wenn wir Ressourcen in einem Hex-Objekt wickeln und die Aktion zum Freisetzen der Ressourcen in der Parallellfunktion des Hex-Objekts ausführen, wird die Wahrscheinlichkeit eines Ressourcenlecks erheblich reduziert, da das Hex-Objekt die Ressourcen automatisch freisetzen kann, auch wenn eine Abweichung in der Funktion auftritt. Der praktische Vorgang ist folgender: Wenn die Funktion eine Abweichung ausführt, tritt so genanntes Stack_unwinding (Stack-Rückrollen) auf, d. h. es wird im Heck ausgebreitet, und da es sich um Hex-Objekte handelt, die von Natur aus im Hex befinden, werden die Parallellfunktionen des Hex-Objekts während des Hecks ausgeführt, wodurch die eingeschränkten Ressourcen freigesetzt werden. Es ist sehr möglich, dass wir diese Art von Parallellösungen wieder ausführen, es
4 Verbot der Erstellung von Stapelobjekten
Wie bereits erwähnt, entscheiden Sie sich, dass die Erstellung eines Stack-Objekts eines bestimmten Typs verboten ist. Dann können Sie selbst eine Ressourcen-Emballage-Klasse erstellen, die nur in der Schachtel erzeugt werden kann, um die verpackten Ressourcen in Ausnahmesituationen automatisch freizusetzen.
Wie verbietet man dann das Erstellen von Stapelobjekten? Wir wissen bereits, dass der einzige Weg, um Stapelobjekte zu erzeugen, die New-Operation ist, und wenn wir New verbieten, ist das nicht möglich. Weiterhin wird der New-Operation bei der Ausführung der Operator new aufgerufen, während der Operator new überladen werden kann. Es gibt eine Möglichkeit, den New-Operator privat zu machen, und um die Symmetrie zu gewährleisten, ist es am besten, den Operator auch als privat zu überladen.
#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 ist jetzt eine Klasse, die Objekte im Stapel verbietet, wenn Sie den folgenden Code schreiben:
NoHashObject* fp = new NoHashObject (()) ; // Kompilierungsfehler!
fp löschen;
Der obige Code erzeugt Kompilaturfehler. Nun, da Sie wissen, wie man eine Klasse entwirft, die ein verbotenes Stapelobjekt verbietet, haben Sie vielleicht wie ich die Frage, ob es nicht möglich ist, ein Stapelobjekt dieses Typs zu erzeugen, wenn die Definition der Klasse NoHashObject nicht geändert werden kann. Nein, es gibt eine Methode, die ich als "Cry-Violence-Breakthrough-Falcon" bezeichne.
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对象所占的堆空间。
}
Die obige Implementierung ist problematisch, und diese Implementierung wird in der Praxis kaum verwendet, aber ich habe den Weg geschrieben, weil es für uns von Vorteil ist, C++-Speicherobjekte zu verstehen.
Die Daten in einem Stück Speicher sind unverändert, und der Typ ist die Brille, die wir tragen. Wenn wir eine Brille tragen, interpretieren wir die Daten in der Speicher mit dem entsprechenden Typ, so dass verschiedene Interpretationen verschiedene Informationen ergeben.
Eine zwangsweise Typübertragung bedeutet, dass man die gleiche Speicherdaten mit einer anderen Brille austauscht und dann wieder anschaut.
Es ist auch zu beachten, dass die Anordnung der Mitgliederdaten für verschiedene Compiler unterschiedlich sein kann. Zum Beispiel ordnen die meisten Compiler die ptr-Pointer-Mitglieder von NoHashObject in den ersten 4 Byte des Objektraums an, um zu gewährleisten, dass die Umwandlung der folgenden Aussage wie erwartet ausgeführt wird:
Ressource* rp = (Resource*) obj_ptr
Das ist jedoch nicht bei allen Kompilatoren der Fall.
Da wir eine Art von Stapelobjekten verbieten können, können wir eine Klasse so gestalten, dass sie keine Steckobjekte erzeugen kann?
5 Verbot der Herstellung von Objekten
Wie bereits erwähnt, wird beim Erstellen eines Kegelobjekts der Kuppelzeiger bewegt, um den Raum für die entsprechende Kegelgröße zu entfernen, und in diesem Raum wird die entsprechende Konstruktionsfunktion direkt aufgerufen, um einen Kegelobjekt zu bilden, und wenn die Funktion zurückkehrt, wird die Objektfreigabe durch die Aufrufung ihrer Dialogenfunktion durchgeführt, um dann den Kuppelzeiger anzupassen, um den Kegel im Speicher zurückzuholen. In diesem Prozess wird kein Operator new/delete benötigt, so dass das Setzen des Operators new/delete auf private nicht erreicht wird.
Das ist möglich, und ich beabsichtige es auch. Aber bevor ich das tue, muss ich noch einmal klarstellen, dass wir keine neue Hebeobjekte direkt erzeugen können, wenn wir die Konstruktionsfunktion als private einstellen, da new ihre Konstruktionsfunktion auch aufruft, nachdem sie den Raum für die Objekte zugewiesen hat.
Wenn eine Klasse nicht als Basisklasse beabsichtigt wird, ist es üblich, ihre Parameterfunktion als private zu deklarieren.
Um die Vererbung zu beschränken, aber nicht die Vererbung zu beschränken, können wir die Dialogen-Funktion als protected deklarieren, so dass beide Dinge gut sind.
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Dann können Sie die NoStackObject-Klasse so verwenden:
NoStackObject* hash_ptr = neues NoStackObject()
...... // Handlungen an Objekten, die auf Hash_ptr verwiesen werden
Hash_ptr->destroy (()); Das ist nicht wahr. Hey, ist es nicht ein bisschen komisch, dass wir ein Objekt mit new erstellen, aber nicht mit delete löschen, sondern mit dem method destruct. Offensichtlich sind die Benutzer nicht an diese seltsame Art der Verwendung gewöhnt. Also habe ich beschlossen, die Konstruktionsfunktion auch auf private oder protected zu setzen.
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
Die NoStackObject-Klasse kann jetzt wie folgt verwendet werden:
NoStackObject* hash_ptr = NoStackObject::creatInstance()
...... // Handlungen an Objekten, die auf Hash_ptr verwiesen werden
Hash_ptr->destroy() ;
Hash_ptr = NULL; // Verhindert den Einsatz von Hangpointern
Jetzt fühlt es sich nicht besser an, dass die Operationen des Erzeugens und des Freigesetzens von Objekten übereinstimmen.
Viele C- oder C++-Programmierer schmälern sich über das Recycling von Müll, weil sie glauben, dass Recycling sicherlich weniger effizient ist als die eigene Verwaltung von dynamischem Speicher und dass das Programm zum Zeitpunkt der Recycling dort zum Stillstand kommt, wozu die Zuweisungs- und Freigabezeiten stabil sind und nicht zum Stillstand führen. Schließlich sind viele C/C++-Programmierer überzeugt, dass der Recycling-Mechanismus in C/C++ nicht realisiert werden kann. Diese falschen Ansichten entstehen aus mangelndem Verständnis der Algorithmen für das Recycling von Müll.
Tatsächlich ist der Müllrecycling-Mechanismus nicht langsamer und sogar effizienter als die Verteilung von dynamischem Speicher. Da wir nur nicht verteilen können, ist es bei der Verteilung von Speicher nur nötig, ständig neue Speicher aus dem Stapel zu erhalten, und die Zeiger des beweglichen Stapels reichen aus; der Prozess der Freigabe wurde weggelassen und ist natürlich beschleunigt. Moderne Müllrecycling-Algorithmen haben sich sehr weiterentwickelt, und die Zuwachs-Sammlung Algorithmen können den Müllrecycling-Prozess in Abschnitten durchführen, um den Ablauf des Abbruchs zu vermeiden.
Die Grundlagen von Algorithmen für das Recycling von Trash basieren in der Regel darauf, alle möglicherweise derzeit verwendeten Speicherblöcke zu scannen und zu markieren, und unmarkierte Speicherblöcke aus allen bereits zugewiesenen Speichern zurückzufordern. Die Ansicht, dass das Recycling von Trash in C/C++ nicht möglich ist, basiert in der Regel darauf, dass alle möglicherweise noch verwendeten Speicherblöcke nicht richtig gescannt werden können. Es ist jedoch nicht kompliziert, was scheinbar unmöglich ist. Zunächst ist es leicht zu erkennen, dass durch das Scannen von Speicherdaten die dynamisch zugewiesenen Speicherdaten auf den Stapel angezeigt werden, und wenn es Fehler gibt, kann man nur einige nicht-Punkt-Daten als Punkte verwenden, und nicht als Nicht-Punkt-Daten.
Beim Recycling muss lediglich der bss-Bereich, der Daten-Bereich und der derzeit genutzte Quadratraum gescannt werden, um die Menge der möglichen Dynamical-Memory-Pointers zu finden, und das referenzierte Gedächtnis wird durch ein Regressions-Scan an alle Dynamical-Memories angezeigt, die gerade genutzt werden.
Es ist möglich, die Geschwindigkeit der Speicherverwaltung zu erhöhen oder sogar den Gesamtspeicherverbrauch zu reduzieren, wenn Sie sich für ein gutes Recycler für Ihr Projekt entscheiden. Wenn Sie interessiert sind, können Sie die bereits vorhandenen Online-Dokumente und Bibliotheken über Recycling durchsuchen, die für einen Programmierer besonders wichtig sind.
Übersetzt vonHK Zhang
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
Sie können nicht nur zugegriffen werden, sie können auch geändert werden, aber dieser Zugriff ist unsicher. Die Adresse der lokalen Variablen befindet sich im Programm-Stapel, und nach dem Ende der Autoritätsvariablen bleibt ihr Wert bestehen, solange die Speicheradresse der lokalen Variablen nicht an eine andere Variable weitergegeben wird. Aber wenn sie geändert wird, ist es gefährlicher, da diese Speicheradresse möglicherweise an andere Variablen des Programms weitergegeben wird, die durch die gezwungene Änderung des Zeichners zum Absturz des Programms führen können.