Vor dem Schreiben einer C++-Strategie sind einige Grundkenntnisse notwendig, ohne sich auf Kon-Bei-Sprache zu konzentrieren. Nachfolgend eine Übertragung:
Wenn jemand sich als Programmierer bezeichnet, der nichts über Speicher weiß, dann kann ich Ihnen sagen, dass er sich damit rühmen muss. Wenn man in C oder C++ programmiert, muss man sich mehr auf den Speicher konzentrieren, nicht nur, weil die Verteilung des Speichers auf eine vernünftige Weise die Effizienz und Leistung des Programms direkt beeinflusst, sondern vor allem, weil es Probleme gibt, wenn wir mit dem Speicher reagieren, und oftmals sind diese Probleme nicht leicht zu erkennen, wie z. B. Speicherlecks, wie z. B. Hängen der Fingerzeige.
Wir wissen, dass C++ den Speicher in drei logische Bereiche unterteilt: Stack-, Heck- und Static-Speicherbereiche. Da dies der Fall ist, bezeichne ich die Objekte, die sich in diesen Bereichen befinden, als Stack-, Heck- und Static-Objekte. Was unterscheidet diese verschiedenen Speicherobjekte voneinander? Was sind die Vor- und Nachteile von Stack- und Heckobjekten? Wie kann die Erstellung von Stack- oder Heckobjekten verboten werden?
Zuerst betrachten wir . , das im Allgemeinen für die Speicherung lokaler Variablen oder Objekte verwendet wird, wie zum Beispiel Objekte, die wir in der Funktionsdefinition mit einer ähnlichen Aussage erklären:
Type stack_object ;
Ein stack_object ist ein Stack-Objekt, dessen Lebensdauer von der Definition beginnt und mit der Rückgabe der Funktion endet.
Außerdem sind fast alle Temporary-Objekte Fertigobjekte. Zum Beispiel die folgende Funktionsdefinition:
Type fun(Type object);
Diese Funktion erzeugt mindestens zwei temporäre Objekte, erstens werden die Parameter als Werte übertragen, so dass die Kopie-Konstruktionsfunktion aufgerufen wird, um ein temporäres Objekt object_copy1 zu erzeugen. Innerhalb der Funktion wird nicht object, sondern object_copy1 verwendet, natürlich ist object_copy1 ein Archobjekt, das freigegeben wird, wenn die Funktion zurückgegeben wird; und diese Funktion ist wertgebunden, wenn die Funktion zurückgegeben wird, wenn wir die Rückgabewertoptimierung ((NRV) nicht berücksichtigen, dann wird auch ein temporäres Objekt object_copy2 erzeugt, das innerhalb eines Zeitraums freigegeben wird, nachdem die Funktion zurückgegeben wurde.
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
Die Ausführung des zweiten Statements oben ist folgender: Zuerst wird ein temporäres Objekt object_copy2 erzeugt, wenn die Funktion fun zurückgegeben wird, und dann wird der Assignment-Operator aufgerufen.
tt = object_copy2 ; //调用赋值运算符
Der Kompiler erzeugt so viele temporäre Objekte für uns, ohne dass wir es merken, und die Zeit und der Raum, um diese temporären Objekte zu erzeugen, können sehr teuer sein, so dass Sie vielleicht verstehen, warum es besser ist, die Funktionsparameter für die Objekt-Konserven mit Konst-Referenzen zu übergeben, anstatt nach Werten.
In C++ sind alle Stack-Objekte von Programmierern erstellt und zerstört, so dass, wenn sie schlecht gehandhabt werden, es zu Speicherproblemen kommt. Wenn Sie Stack-Objekte zugewiesen haben, aber vergessen haben, sie freizugeben, wird es zu einem Speicherleck kommen; und wenn Sie ein Objekt freigegeben haben, aber den entsprechenden Zeiger nicht als NULL gesetzt haben, ist dieser Zeiger der sogenannte “Hängende Zeiger”, der bei erneuter Verwendung des Zeigers zu illegalen Zugriffen führt, was im schweren Fall zum Absturz des Programms führt.
Die einzige Möglichkeit, wie man Stackobjekte in C++ verteilt, ist mit new ((Natürlich kann man auch malloc-ähnliche Anweisungen verwenden, um C-ähnliche Stackspeicher zu erhalten), wenn man new verwendet, wird ein Stück Speicher im Stack verteilt und ein Pointer zurückgegeben, der auf dieses Stackobjekt zeigt.
Ein weiterer Aspekt ist der statische Speicherbereich. Alle statischen und globalen Objekte werden in den statischen Speicherbereich verteilt. In Bezug auf globale Objekte werden diese vor der Ausführung der main () -Funktion verteilt. In der Tat wird eine vom Compiler erzeugte_main () -Funktion vor der Ausführung des Anzeigecodes in der main () -Funktion aufgerufen, während die_main () -Funktion die Konstruktion und Initialisierung aller globalen Objekte durchführt.
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Wenn wir das wissen, können wir einige Tricks daraus ableiten, z. B. dass wir vor der Ausführung der main () -Funktion einige Vorbereitungen vornehmen, die wir in die Konstruktionsfunktion eines benutzerdefinierten globalen Objekts schreiben können, so dass die Konstruktionsfunktion dieses globalen Objekts aufgerufen wird, um die gewünschten Bewegungen auszuführen, bevor der offensichtliche Code der main () -Funktion ausgeführt wird. Wenn wir gerade von globalen Objekten in der statischen Speicherzone sprachen, dann ist das ein lokal-statisches Objekt?
Es gibt auch ein statisches Objekt, das als statisches Mitglied einer Klasse fungiert.
Das erste Problem ist die Lebensdauer der statischen Mitglieder einer Klasse, die mit der Entstehung des ersten Classobjekts entstehen und am Ende des gesamten Programms verschwinden. Es gibt also einen Fall, in dem wir eine Klasse definieren, die ein statisches Objekt als Mitglied der Klasse hat, aber während der Ausführung des Programms, wenn wir kein Objekt dieser Klasse erstellt haben, wird kein statisches Objekt erzeugt, das diese Klasse enthält.
Das zweite Problem ist, wenn:
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 = …… ;
Die Antwort ist ja, sie richten sich tatsächlich nach demselben Objekt. Das klingt nicht so, oder? Aber es ist wahr, und Sie können selbst einen einfachen Code schreiben, um zu überprüfen. Was ich tun werde, ist zu erklären, warum das so ist.
Stellen wir uns vor, dass wir ein Derived1-Objekt an eine Funktion übergeben, die nicht auf Basis-Argumente verweist, und diese abgeschnitten wird. Das bedeutet, dass wir einfach das Subobjekt des Derived1-Objekts entfernen und alle anderen Datenmitglieder, die Derived1 benutzt hat, ignorieren, und dieses Subobjekt an die Funktion übergeben (die Funktion benutzt tatsächlich eine Kopie dieses Subobjekts).
Alle Subobjekte der abgeleiteten Klassen, die die Base-Klasse erben, enthalten ein Subobjekt des Typs Base, das den Schlüssel zu einem Derived1-Objekt mit einem Base-Zeiger anzeigt, was natürlich auch ein polymorphischer Schlüssel ist. Alle Subobjekte und alle Base-Typen haben ein s_object, das natürlich alle Instanzen der Klassen der gesamten Ableihungssystemklasse, die von der Base-Klasse abstammen, teilen.
2 Vergleich der drei Speicherobjekte
Der Vorteil von Schattenobjekten besteht darin, dass sie automatisch zum richtigen Zeitpunkt erzeugt und zum richtigen Zeitpunkt automatisch zerstört werden, ohne dass der Programmierer sich darum kümmert; und die Schattenobjekte werden im Allgemeinen schneller als Stapelobjekte erstellt, da beim Verteilen von Stapelobjekten die Operator-New-Operation aufgerufen wird, die eine Art Speicherplatzsuch-Algorithmus verwendet, während die Suche möglicherweise sehr zeitaufwendig ist. Die Schattenobjekte sind jedoch nicht so mühsam, da sie lediglich den Schattenzeiger bewegen müssen.
Wenn wir zum Beispiel ein Objekt erstellen müssen, das von mehreren Funktionen aufgerufen werden kann, aber nicht global sein möchte, ist es eine gute Option, ein Heckobjekt zu erstellen und dann die Pointers für dieses Heckobjekt zwischen den einzelnen Funktionen zu übertragen, um die gemeinsame Nutzung dieses Objekts zu ermöglichen. Außerdem ist die Stackkapazität im Vergleich zum Stackraum viel größer. In der Tat, wenn der physische Speicher nicht ausreicht, wird die Erstellung neuer Heckobjekte, wenn dies erforderlich ist, in der Regel keine Betriebsfehler erzeugen, sondern das virtuelle Speicher wird verwendet, um den tatsächlichen physischen Speicher zu erweitern.
Der nächste Schritt ist das statische Objekt.
Zuerst die globalen Objekte. Globalen Objekte bieten eine der einfachsten Methoden für die Kommunikation zwischen Klassen und Funktionen, obwohl diese nicht elegant sind. In der Regel gibt es keine globalen Objekte in einer vollständig objektorientierten Sprache wie C#, da globalen Objekte unsicher und hoch integriert sind. Die Verwendung von globalen Objekten in einem Programm reduziert die Robustheit, Stabilität, Wartbarkeit und Kompatibilität des Programms erheblich.
Als nächstes ist das statische Mitglied der Klasse, wie oben erwähnt, ein statisches Mitglied, das von allen Objekten der Basisklasse und ihrer Ablegerklasse geteilt wird. Daher ist ein solches statisches Mitglied zweifellos eine gute Wahl, wenn Daten zwischen diesen Klassen oder zwischen diesen Klassenobjekten geteilt oder kommuniziert werden müssen.
Dann gibt es die statischen Oralobjekte, die hauptsächlich dazu dienen, den Zwischensatz zu bewahren, in dem die Funktion, in der sie sich befindet, während der Wiederholung aufgerufen wird, und eines der bemerkenswertesten Beispiele ist die Recursive Funktion. Wir alle wissen, dass eine Recursive Funktion eine Funktion ist, die sich selbst aufruft. Wenn man in einer Recursive Funktion ein nicht-statisches Oralobjekt definiert, dann ist der Aufwand enorm, wenn die Anzahl der Wiederholungen beträchtlich ist.
In der Gestaltung von Recursion-Funktionen können statische Objekte anstelle von nonstatic-lokalen Objekten verwendet werden (z. B. Fermi-Objekte), was nicht nur die Kosten für die Erzeugung und Freisetzung von nonstatic-Objekten bei jedem rekursiven Aufruf und jeder Rückgabe reduziert, sondern auch den Zwischenzustand des rekursiven Aufrufs bewahrt und für die einzelnen Aufrufsebenen zugänglich ist.
3 Unerwartete Ernte von Kühlobjekten
Wie bereits erwähnt, wird ein Schattenobjekt zur richtigen Zeit erstellt und dann automatisch zur richtigen Zeit freigegeben, d.h. das Schattenobjekt wird automatisch verwaltet. Was ist das Schattenobjekt, das automatisch freigegeben wird? Erstens, wenn es am Ende seiner Lebensdauer ist; zweitens, wenn eine Abweichung in der Funktion, in der es sich befindet, auftritt.
Wenn wir die Ressourcen in einem Stack-Objekt verpacken und die Bewegung zum Freigeben der Ressourcen in der Stack-Objekt-Dekonstruktionsfunktion ausführen, wird die Wahrscheinlichkeit eines Ressourcenleckens erheblich verringert, da die Stack-Objekte die Ressourcen automatisch freigeben können, auch wenn die Funktion, in der sie sich befinden, abnormal ist. Der tatsächliche Prozess ist folgender: Wenn die Funktion eine Abweichung auslöst, tritt das so genannte Stack_unwinding auf, d.h. der Stack wird ausgebreitet, da die Stack-Objekte in der natürlichen Stack-Rolle vorhanden sind.
4 Stackobjekte verboten
Wie oben erwähnt, können Sie, wenn Sie die Erzeugung einer bestimmten Art von Stapelobjekten verbieten, selbst eine Ressourcenverpackungsklasse erstellen, die nur in einem Stapel erzeugt werden kann, um die verpackten Ressourcen automatisch zu freisetzen, wenn eine Ausnahme vorliegt.
Wir wissen, dass der einzige Weg, um Stackobjekte zu erzeugen, die new-Operation ist. Wenn wir die new-Operation verbieten, funktioniert das nicht. Weiterhin wird die new-Operation beim Ausführen den Operator new aufgerufen, und der Operator new kann neu geladen werden.
#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 Haufenobjekte verbietet, wenn man den folgenden Code schreibt:
NoHashObject* fp = new NoHashObject (() ; // Kompilierungsfehler!
delete fp ;
Der obige Code erzeugt Kompilierungsfehler. Nun, da Sie nun wissen, wie man eine Klasse entwirft, die Stackobjekte verbietet, haben Sie vielleicht die gleiche Frage wie ich: Kann man diese Art von Stackobjekt nicht erzeugen, wenn die Definition der Klasse NoHashObject nicht geändert werden kann?
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 oben genannte Implementierung ist mühsam und wird in der Praxis kaum verwendet, aber ich habe den Weg geschrieben, weil es nützlich ist, sie zu verstehen, um die C++-Speicherobjekte zu verstehen. Was ist das Grundlegende an so vielen der oben genannten Zwangs-Typen-Umstellungen?
Die Daten in einem Speicherblock bleiben unverändert, und der Typ ist die Brille, die wir tragen. Wenn wir eine Brille tragen, interpretieren wir die Daten im Speicher mit dem entsprechenden Typ, so dass verschiedene Interpretationen unterschiedliche Informationen erhalten.
Die sogenannte Zwangs-Typ-Umstellung bedeutet, dass man die gleiche Speicherdaten wieder ansieht, nachdem man eine andere Brille angezogen hat.
Es ist auch zu beachten, dass die Gliederung der Objektmitglieder in verschiedenen Compilern möglicherweise unterschiedlich ist. Die meisten Compiler platzieren beispielsweise die ptr-Zeigermitglieder von NoHashObject in den ersten vier Bytes des Objektraums, um sicherzustellen, dass die Umwandlung der folgenden Anweisung so ausgeführt wird, wie wir es erwarten:
Resource* rp = (Resource*)obj_ptr ;
Aber nicht alle Kompilatoren sind so.
Wenn wir die Erzeugung von Stackobjekten eines bestimmten Typs verbieten können, können wir dann eine Klasse so entwerfen, dass sie keine Stackobjekte erzeugen kann?
Wie bereits erwähnt, wird beim Erstellen eines Hexobjekts der Domäne-Zeiger bewegt, um aus dem Hex einen Raum der entsprechenden Größe zu entfernen, und dann wird auf diesem Raum eine entsprechende Konstruktionsfunktion direkt aufgerufen, um ein Hexobjekt zu bilden, und wenn die Funktion zurückkommt, wird ihre Kompositionsfunktion aufgerufen, um das Objekt zu entfernen, und dann wird der Domäne-Zeiger angepasst, um das Hex-Speicher zurückzugewinnen. In diesem Prozess wird kein Operator new/delete benötigt, so dass der Operator new/delete als privat eingestellt wird.
Das ist möglich, und ich habe auch vor, dieses Vorgehen zu verwenden. Aber zuvor muss man sich überlegen, ob wir die Konstruktionsfunktion als privat eingestellt haben, und dass wir nicht mit new direkt Stapelobjekte erzeugen können, da new die Konstruktionsfunktion aufruft, nachdem es dem Objekt Platz zugeteilt hat. Also habe ich vor, die Kompositionsfunktion als privat einzustellen.
Wenn eine Klasse nicht als Basisklasse beabsichtigt ist, wird in der Regel die Parametrierung als privat erklärt.
Um die Erbschaft zu beschränken, aber nicht die Erbschaft zu beschränken, können wir die Differentialfunktion als protected deklarieren, und das ist das Beste aus beidem.
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Als nächstes können Sie die NoStackObject-Klasse wie folgt verwenden:
NoStackObject* hash_ptr = new NoStackObject() ;
… … // Handlung mit einem Hash_ptr-gerichteten Objekt
hash_ptr->destroy() ; Oh, ist es nicht etwas seltsam, dass wir ein Objekt mit new erstellen, aber es nicht mit delete löschen, sondern mit destroy. Offensichtlich sind die Benutzer nicht an diese seltsame Art des Gebrauchs gewöhnt. Also habe ich beschlossen, die Konstruktionsfunktion auch als privat oder geschützt zu setzen.
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
Die NoStackObject-Klasse kann nun folgendermaßen verwendet werden:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
… … // Handlung mit einem Hash_ptr-gerichteten Objekt
hash_ptr->destroy() ;
hash_ptr = NULL; // Verhindert die Verwendung von Hängekennzeichen
Jetzt fühlt es sich schon besser an, dass das Generieren von Objekten und das Freisetzen von Objekten übereinstimmen.
Viele C- oder C++-Programmierer lehnen die Trash-Recycling-Methode ab, da sie der Meinung sind, dass Trash-Recycling sicherlich weniger effizient ist als die Verwaltung von dynamischem Speicher selbst, und dass Trash-Recycling das Programm zum Stillstand bringen wird, während die Zuweisung und Freisetzung der Zeit stabil sind und das Programm nicht zum Stillstand bringen, wenn man die Speicherverwaltung selbst kontrolliert. Schließlich sind viele C/C++-Programmierer der festen Überzeugung, dass ein Trash-Recycling-Mechanismus in C/C++ nicht realisiert werden kann. Diese falschen Ansichten sind auf eine Unkenntnis der Algorithmen für Trash-Recycling zurückzuführen.
In der Tat ist der Müll-Recycling-Mechanismus nicht langsam, sondern sogar effizienter als die dynamische Speicherverteilung. Da wir nur verteilen können, ohne freizugeben, ist die Verteilung des Speichers nur erforderlich, um ständig neue Speicher aus dem Stapel zu erhalten, und der Befehl für die Bewegung des Stapels ist ausreichend. Der Freisetzungsprozess wird übersehen, und natürlich wird die Geschwindigkeit beschleunigt.
Die Ansicht, dass es in C/C++ nicht möglich ist, eine Müllwiederherstellung durchzuführen, basiert auf der Unfähigkeit, alle möglichen Speicherblöcke, die noch verwendet werden könnten, richtig zu scannen, aber das, was unmöglich erscheint, wird in Wirklichkeit nicht kompliziert. Zunächst werden die Pointers, die durch das Scannen von Daten aus dem Speicher dynamisch in den Speicher verteilt sind, leicht identifiziert, und wenn ein Fehler erkannt wird, können nur einige nicht-pointierte Daten als Pointers verwendet werden, ohne die Pointers als nicht-pointierte Daten zu verwenden.
Bei der Recycling-Operation wird lediglich der bss-Bereich, der data-Bereich und der derzeit genutzte Speicherplatz gescannt, um zu ermitteln, wie viele Dynamic-Memory-Pointers möglicherweise verwendet werden, und der Referenzdynamic-Memory-Recursion-Scan erhält alle aktuell genutzten Dynamic-Memories.
Wenn Sie einen guten Recycler für Ihre Projekte implementieren wollen, ist es möglich, die Speicherverwaltung zu beschleunigen und sogar den gesamten Speicherverbrauch zu reduzieren. Wenn Sie daran interessiert sind, können Sie sich die bereits vorhandenen Online-Artikel und Bibliotheken über die Recycling-Implementierung durchsuchen.
Es ist ein großes Projekt.HK Zhang
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
Es ist nicht nur zugänglich, sondern auch modifizierbar, nur dass der Zugriff nicht sicher ist. Die Adressen der lokalen Variablen befinden sich im Stack des Programms selbst. Nach dem Ende der Autoritätsvariablen bleiben ihre Werte erhalten, solange die Speicheradresse der lokalen Variablen nicht an eine andere Variablen weitergegeben wird. Eine Änderung ist jedoch gefährlich, da die Speicheradresse möglicherweise an andere Variablen des Programms weitergegeben wird und das Programm möglicherweise zum Zusammenbruch führt, wenn die Änderung durch den Zeiger erzwungen wird.