Dynamische Speicherzuweisung in C für ein Array. Dynamische Speicherzuweisung in C. Standardfunktionen für die dynamische Speicherzuweisung

Die Arbeit mit dynamischem Speicher stellt bei vielen Algorithmen oft einen Flaschenhals dar, sofern keine speziellen Tricks angewendet werden.

In diesem Artikel werde ich einige solcher Techniken betrachten. Die Beispiele im Artikel unterscheiden sich (z. B. von diesem) dadurch, dass eine Überladung der Operatoren new und delete verwendet wird. Dadurch sind die syntaktischen Strukturen minimalistisch und die Überarbeitung des Programms einfach. Auch die dabei auftretenden Fallstricke werden beschrieben (Gurus, die den Standard von Anfang bis Ende lesen, werden natürlich nicht überrascht sein).

0. Brauchen wir manuelle Arbeit mit dem Gedächtnis?

Lassen Sie uns zunächst prüfen, wie sehr ein intelligenter Allokator die Speicherarbeit beschleunigen kann.

Schreiben wir einfache Tests für C++ und C# (C# ist bekannt für seinen hervorragenden Speichermanager, der Objekte in Generationen aufteilt, verschiedene Pools für Objekte unterschiedlicher Größe verwendet usw.).

Klassenknoten ( public: Node* next; ); // ... for (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Class Node ( public Node next; ) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

Trotz des „kugelförmigen Vakuums“-Charakters des Beispiels betrug der Zeitunterschied das Zehnfache (62 ms gegenüber 650 ms). Darüber hinaus ist das C#-Beispiel fertig und gemäß den guten Manieren in C++ müssen die ausgewählten Objekte gelöscht werden, was die Lücke weiter vergrößert (bis zu 2580 ms).

1. Objektpool

Die offensichtliche Lösung besteht darin, einen großen Speicherblock vom Betriebssystem zu nehmen und ihn in gleiche Blöcke der Größe sizeof(Node) aufzuteilen. Wenn Sie Speicher zuweisen, nehmen Sie den Block aus dem Pool und geben Sie ihn beim Freigeben an den Pool zurück. Der einfachste Weg, einen Pool zu organisieren, ist die Verwendung einer einfach verknüpften Liste (Stapel).

Da das Ziel ein minimaler Eingriff in das Programm ist, kann lediglich das BlockAlloc-Mixin zur Node-Klasse hinzugefügt werden:
Klassenknoten: public BlockAlloc

Zunächst benötigen wir einen Pool großer Blöcke (Seiten), die wir aus der OS- oder C-Laufzeit übernehmen. Es kann über den malloc- und free-Funktionen organisiert werden, aber für mehr Effizienz (um die zusätzliche Abstraktionsebene zu überspringen) verwenden wir VirtualAlloc/VirtualFree. Diese Funktionen weisen Speicher in Vielfachen von 4-KByte-Blöcken zu und reservieren außerdem Prozessadressraum in Vielfachen von 64-KByte-Blöcken. Durch die gleichzeitige Angabe der Commit- und Reserve-Optionen springen wir über eine weitere Abstraktionsebene, indem wir Adressraum reservieren und Speicherseiten in einem einzigen Aufruf zuweisen.

PagePool-Klasse

inline size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) Vorlage Klasse PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); seiten.push_back(page); return page; ) ~PagePool() ( for (vector ::iterator i = seiten.begin(); i != seiten.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) privat: Vektor Seiten; );

Dann organisieren wir einen Pool von Blöcken einer bestimmten Größe

BlockPool-Klasse

Vorlage Klasse BlockPool: PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; return tmp; ) void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; head = tmp; ) private: void* head; size_t BlockSize; size_t count; void FormatNewPage() ( void* tmp = GetPage(); head = tmp; for(size_t i = 0; i< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

Kommentar // todo: lock(this) Orte, die eine Inter-Thread-Synchronisierung erfordern, sind markiert (verwenden Sie beispielsweise EnterCriticalSection oder boost::mutex).

Ich erkläre, warum beim „Formatieren“ einer Seite die FreeBlock-Abstraktion nicht verwendet wird, um einen Block zum Pool hinzuzufügen. Wenn so etwas geschrieben würde

Für (size_t i = 0; i< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

Dann würde die Seite nach dem FIFO-Prinzip „invers“ markiert:

Mehrere nacheinander aus dem Pool angeforderte Blöcke hätten absteigende Adressen. Aber der Prozessor mag es nicht, rückwärts zu gehen, das unterbricht Prefetch ( UPD: Nicht relevant für moderne Prozessoren). Wenn Sie Markup in einer Schleife durchführen
for (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
dann würde der Markup-Zyklus rückwärts zu den Adressen gehen.

Nachdem die Vorbereitungen abgeschlossen sind, können wir die Mixin-Klasse beschreiben.
Vorlage class BlockAlloc ( public: static void* Operator new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) static void Operator delete(void * m, size_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( pool.FreeBlock(m); ) ) // todo: implementieren nothrow_t-Überladungen, laut Borisko-Kommentar // http://habrahabr.ru/post/148657/#comment_5020297 // Vermeiden Sie das Verstecken der neuen Platzierung, die von den STL-Containern benötigt wird... static void* Operator new(size_t, void * m) ( return m; ) // ...und die Warnung wegen fehlender Platzierung delete... static void Operator delete(void*, void*) ( ) private: static BlockPool Schwimmbad; ); Vorlage BlockPool BlockAlloc ::Schwimmbad;

Ich erkläre, warum Schecks nötig sind if (s != sizeof(T))
Wann arbeiten sie? Dann wird eine von der Basis T geerbte Klasse erstellt/gelöscht.
Die Erben verwenden das übliche Neu/Löschen, aber BlockAlloc kann auch damit gemischt werden. Auf diese Weise können wir einfach und sicher bestimmen, welche Klassen die Pools verwenden sollen, ohne befürchten zu müssen, dass etwas im Programm kaputt geht. Auch die Mehrfachvererbung funktioniert mit diesem Mixin hervorragend.

Bereit. Wir erben Node von BlockAlloc und führen den Test erneut aus.
Die Testzeit beträgt jetzt 120 ms. 5-mal schneller. Aber in C# ist der Allokator immer noch besser. Es ist wahrscheinlich nicht nur eine verknüpfte Liste. (Wenn wir direkt nach new delete aufrufen und somit nicht viel Speicher verschwenden, indem wir die Daten in den Cache legen, erhalten wir 62 ms. Seltsam. Genau wie die .NET-CLR, als ob sie freigegebene lokale Variablen sofort an die zurückgibt entsprechenden Pool, ohne auf GC zu warten)

2. Behälter und sein farbenfroher Inhalt

Treffen Sie oft auf Klassen, die viele verschiedene untergeordnete Objekte speichern, sodass deren Lebensdauer nicht länger ist als die Lebensdauer des übergeordneten Objekts?

Dies könnte beispielsweise eine XmlDocument-Klasse sein, die mit Node- und Attribute-Klassen sowie C-Strings (char*) gefüllt ist, die aus dem Text innerhalb der Knoten stammen. Oder eine Liste von Dateien und Verzeichnissen im Dateimanager, die beim erneuten Einlesen des Verzeichnisses einmal geladen werden und sich nie wieder ändern.

Wie in der Einleitung gezeigt wurde, ist Löschen teurer als Neu. Die Idee des zweiten Teils des Artikels besteht darin, Speicher für untergeordnete Objekte in einem großen Block zuzuweisen, der mit dem übergeordneten Objekt verknüpft ist. Wenn ein übergeordnetes Objekt gelöscht wird, werden wie üblich die Destruktoren der untergeordneten Objekte aufgerufen, der Speicher muss jedoch nicht zurückgegeben werden, sondern wird in einem großen Block freigegeben.

Erstellen wir eine PointerBumpAllocator-Klasse, die Stücke unterschiedlicher Größe aus einem großen Block abbeißen und einen neuen großen Block zuweisen kann, wenn der alte erschöpft ist.

PointerBumpAllocator-Klasse

Vorlage Klasse PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (block, PageSize); head = GetPage(free); ) void* tmp = head; head = (char*)head + block; free -= block; return tmp; ) ~PointerBumpAllocator() ( für (vector ::iterator i = seiten.begin(); i != seiten.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) private: void* GetPage(size_t size) ( void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); seiten.push_back(page) ; zurück zur Seite; ) Vektor Seiten; leerer* Kopf; size_t frei; ); typedef PointerBumpAllocator<>DefaultAllocator;

Beschreiben wir abschließend ein ChildObject-Mixin mit überladenem Neu- und Löschzugriff auf einen bestimmten Allokator:

Vorlage struct ChildObject ( static void* Operator new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* Operator new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static void-Operator delete(void*, size_t) ( ) // *1 statischer void-Operator delete(void*, A*) ( ) static void-Operator delete(void*, A&) ( ) private: static void*-Operator new(size_t s ); );

In diesem Fall müssen Sie zusätzlich zum Hinzufügen eines Mixins zur untergeordneten Klasse auch alle Aufrufe von new korrigieren (oder das „Factory“-Muster verwenden). Die Syntax des neuen Operators lautet wie folgt:

Neu (...Parameter für den Operator...) ChildObject (...Parameter für den Konstruktor...)

Der Einfachheit halber habe ich zwei neue Operatoren angegeben, die A& oder A* akzeptieren.
Wenn der Allokator als Mitglied zur übergeordneten Klasse hinzugefügt wird, ist die erste Option praktischer:
node = new(allocator) XmlNode(nodename);
Wenn der Allokator als Vorfahr (Mixin) hinzugefügt wird, ist die zweite Methode praktischer:
node = new(this) XmlNode(nodename);

Für den Aufruf von delete gibt es keine spezielle Syntax; der Compiler ruft standardmäßig delete auf (markiert mit *1), unabhängig davon, welcher new-Operator zum Erstellen des Objekts verwendet wurde. Das heißt, die Löschsyntax ist normal:
Knoten löschen;

Wenn im Konstruktor von ChildObject (oder seinem Nachkommen) eine Ausnahme auftritt, wird delete mit einer Signatur aufgerufen, die der Signatur des neuen Operators entspricht, der zum Erstellen dieses Objekts verwendet wurde (der erste Parameter size_t wird durch void* ersetzt).

Das Platzieren des new-Operators im privaten Bereich schützt vor dem Aufruf von new ohne Angabe eines Allokators.

Hier ist ein vollständiges Beispiel für die Verwendung des Allocator-ChildObject-Paares:

Beispiel

Klasse XmlDocument: public DefaultAllocator ( public: ~XmlDocument() ( für (vector ::iterator i = nodes.begin(); i != nodes.end(); ++i) ( delete (*i); ​​​​) ) void AddNode(char* content, char* name) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content ); char* n = (char*)AllocBlock(strlen(name)+1); strcpy(n, content); nodes.push_back(new(this) XmlNode(c, n)); ) class XmlNode: public ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* name; ); privat: Vektor Knoten; );

Abschluss. Der Artikel wurde vor 1,5 Jahren für die Sandbox geschrieben, aber leider gefiel er dem Moderator nicht.

Letzte Aktualisierung: 28.05.2017

Beim Erstellen eines Arrays mit fester Größe wird ihm eine bestimmte Menge Speicher zugewiesen. Nehmen wir zum Beispiel an, wir haben ein Array mit fünf Elementen:

Doppelte Zahlen = (1,0, 2,0, 3,0, 4,0, 5,0);

Für ein solches Array beträgt der zugewiesene Speicher 5 * 8 (doppelte Größe) = 40 Bytes. Auf diese Weise wissen wir genau, wie viele Elemente das Array enthält und wie viel Speicher es beansprucht. Dies ist jedoch nicht immer bequem. Manchmal ist es erforderlich, dass die Anzahl der Elemente und dementsprechend die Größe des zugewiesenen Speichers für ein Array abhängig von bestimmten Bedingungen dynamisch bestimmt wird. Beispielsweise kann der Benutzer die Größe des Arrays selbst eingeben. Und in diesem Fall können wir die dynamische Speicherzuweisung verwenden, um das Array zu erstellen.

Um die dynamische Speicherzuweisung zu steuern, werden eine Reihe von Funktionen verwendet, die in der Header-Datei stdlib.h definiert sind:

    malloc() . Hat einen Prototyp

    Void *malloc(unsigned s);

    Ordnet Speicher mit der Länge s Bytes zu und gibt einen Zeiger auf den Anfang des zugewiesenen Speichers zurück. Gibt NULL zurück, wenn dies nicht erfolgreich ist

    calloc() . Hat einen Prototyp

    Void *calloc(unsigned n, unsigned m);

    Reserviert Speicher für n Elemente mit jeweils m Bytes und gibt einen Zeiger auf den Anfang des zugewiesenen Speichers zurück. Gibt NULL zurück, wenn dies nicht erfolgreich ist

    realloc() . Hat einen Prototyp

    Void *realloc(void *bl, unsigned ns);

    Ändert die Größe des zuvor zugewiesenen Speicherblocks, auf den der Zeiger bl zeigt, auf ns Bytes. Wenn der bl-Zeiger einen NULL-Wert hat, also kein Speicher zugewiesen wurde, ähnelt die Aktion der Funktion der von malloc

    frei() . Hat einen Prototyp

    Void *free(void *bl);

    Gibt einen zuvor zugewiesenen Speicherblock frei, auf dessen Anfang der bl-Zeiger zeigt.

    Wenn wir diese Funktion nicht verwenden, wird der dynamische Speicher beim Beenden des Programms trotzdem automatisch freigegeben. Es empfiehlt sich jedoch immer noch, die Funktion free() aufzurufen, damit Sie so früh wie möglich Speicher freigeben können.

Betrachten wir die Verwendung von Funktionen für ein einfaches Problem. Die Länge des Arrays ist unbekannt und wird während der Programmausführung vom Benutzer eingegeben, außerdem werden die Werte aller Elemente vom Benutzer eingegeben:

#enthalten #enthalten int main(void) ( int *block; // Zeiger auf den Speicherblock int n; // Anzahl der Array-Elemente // Anzahl der Elemente eingeben printf("Size of array="); scanf("%d", &n); // Speicher für das Array zuweisen // Die Malloc-Funktion gibt einen Zeiger vom Typ void* zurück // der automatisch in den Typ int* konvertiert wird block = malloc(n * sizeof(int)); // Geben Sie Zahlen in das ein Array for(int i=0;i

Konsolenausgabe des Programms:

Größe des Arrays = 5 Block = 23 Block = -4 Block = 0 Block = 17 Block = 81 23 -4 0 17 81

Hier wird ein Blockzeiger vom Typ int definiert, um den Speicher für das Array zu verwalten. Die Anzahl der Array-Elemente ist im Voraus unbekannt und wird durch die Variable n dargestellt.

Zunächst gibt der Benutzer die Anzahl der Elemente ein, die in die Variable n fallen. Danach müssen Sie Speicher für eine bestimmte Anzahl von Elementen zuweisen. Um hier Speicher zuzuweisen, könnten wir eine der drei oben beschriebenen Funktionen verwenden: malloc, calloc, realloc. Aber speziell in dieser Situation verwenden wir die malloc-Funktion:

Block = malloc(n * sizeof(int));

Zunächst ist zu beachten, dass alle drei oben genannten Funktionen aus Gründen der Universalität des Rückgabewerts als Ergebnis einen Zeiger vom Typ void * zurückgeben. In unserem Fall wird jedoch ein Array vom Typ int erstellt, das von einem Zeiger vom Typ int * manipuliert wird, sodass das Ergebnis der malloc-Funktion implizit in den Typ int * umgewandelt wird.

Der malloc-Funktion selbst wird die Anzahl der Bytes für den zugewiesenen Block übergeben. Diese Zahl lässt sich ganz einfach berechnen: Multiplizieren Sie einfach die Anzahl der Elemente mit der Größe eines Elements n * sizeof(int) .

Nachdem alle Aktionen abgeschlossen sind, wird der Speicher mit der Funktion free() freigegeben:

Frei(blockieren);

Es ist wichtig, dass wir nach der Ausführung dieser Funktion das Array nicht mehr verwenden können, um beispielsweise seine Werte auf der Konsole anzuzeigen:

Frei(blockieren); for(int i=0;i

Und wenn wir das versuchen, erhalten wir undefinierte Werte.

Anstelle der Funktion malloc könnten wir auch die Funktion calloc() verwenden, die die Anzahl der Elemente und die Größe eines Elements annimmt:

Block = calloc(n, sizeof(int));

Oder Sie können auch die Funktion realloc() verwenden:

Int *block = NULL; block = realloc(block, n * sizeof(int));

Bei der Verwendung von Realloc ist es wünschenswert (in manchen Umgebungen, beispielsweise in Visual Studio, obligatorisch), den Zeiger auf mindestens NULL zu initialisieren.

Aber im Allgemeinen hätten alle drei Aufrufe in diesem Fall einen ähnlichen Effekt:

Block = malloc(n * sizeof(int)); block = calloc(n, sizeof(int)); block = realloc(block, n * sizeof(int));

Schauen wir uns nun ein komplexeres Problem an – die dynamische Zuweisung von Speicher für ein zweidimensionales Array:

#enthalten #enthalten int main(void) ( int **table; // Zeiger auf einen Speicherblock für ein Array von Zeigern int *rows; // Zeiger auf einen Speicherblock zum Speichern von Zeileninformationen int rowscount; // Anzahl der Zeilen int d; / / Zahl eingeben // Anzahl der Zeilen eingeben printf("Rows count="); scanf("%d", &rowscount); // Speicher für ein zweidimensionales Array reservieren table = calloc(rowscount, sizeof(int*) ); rows = malloc(sizeof(int )*rowscount); // Schleife durch Zeilen for (int i = 0; i

Die Tabellenvariable stellt einen Zeiger auf ein Array von Zeigern vom Typ int* dar. Jeder table[i]-Zeiger in diesem Array stellt einen Zeiger auf ein Subarray von int-Elementen dar, also einzelne Tabellenzeilen. Und die Tabellenvariable stellt tatsächlich einen Zeiger auf ein Array von Zeigern auf Tabellenzeilen dar.

Um die Anzahl der Elemente in jedem Subarray zu speichern, wird ein Zeilenzeiger vom Typ int definiert. Es speichert tatsächlich die Anzahl der Spalten für jede Zeile der Tabelle.

Zunächst wird die Anzahl der Zeilen in die Variable rowscount eingetragen. Die Anzahl der Zeilen ist die Anzahl der Zeiger im Array, auf die der Tabellenzeiger zeigt. Darüber hinaus ist die Anzahl der Zeilen die Anzahl der Elemente im dynamischen Array, auf die der Zeilenzeiger zeigt. Daher müssen Sie zunächst allen diesen Arrays Speicher zuweisen:

Table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount);

Als nächstes wird in der Schleife die Anzahl der Spalten für jede Zeile eingegeben. Der eingegebene Wert wird in das Zeilenarray übernommen. Und entsprechend dem eingegebenen Wert wird jeder Zeile die erforderliche Speichergröße zugewiesen:

Scanf("%d", &rows[i]); table[i] = calloc(rows[i], sizeof(int));

Anschließend werden die Elemente für jede Zeile eingegeben.

Am Ende des Programms wird bei der Ausgabe Speicher freigegeben. Im Programm wird Speicher für Tabellenzeilen allokiert, daher muss dieser Speicher freigegeben werden:

Free(table[i]);

Außerdem wird der für die Tabellen- und Zeilenzeiger reservierte Speicher freigegeben:

Frei(Tisch); frei(Zeilen);

Konsolenausgabe des Programms:

Anzahl der Zeilen = 2 Anzahl der Spalten für 1 = 3 Tabelle = 1 Tabelle = 2 Tabelle = 3 Anzahl der Spalten für 2 = 2 Tabelle = 4 Tabelle = 5 1 2 3 4 5

Wir haben die Möglichkeiten der dynamischen Speicherzuweisung entdeckt. Was bedeutet das? Dies bedeutet, dass bei der dynamischen Speicherzuweisung der Speicher nicht in der Kompilierungsphase, sondern in der Programmausführungsphase reserviert wird. Und das gibt uns die Möglichkeit, Speicher effizienter zuzuweisen, hauptsächlich für Arrays. Bei der dynamischen Speicherzuweisung müssen wir die Größe des Arrays nicht im Voraus festlegen, zumal nicht immer bekannt ist, welche Größe das Array haben soll. Schauen wir uns als Nächstes an, wie Speicher zugewiesen werden kann.

Speicherzuweisung in C (Malloc-Funktion)

Die Funktion malloc() ist in der Header-Datei stdlib.h definiert und wird verwendet, um Zeiger mit der erforderlichen Speichermenge zu initialisieren. Der Speicher wird aus dem RAM-Sektor zugewiesen, der für alle auf dem Computer ausgeführten Programme verfügbar ist. Das Argument ist die Anzahl der Bytes des Speichers, die zugewiesen werden müssen; die Funktion gibt einen Zeiger auf den zugewiesenen Block im Speicher zurück. Die Funktion malloc() funktioniert wie jede andere Funktion, nichts Neues.

Da unterschiedliche Datentypen unterschiedliche Speicheranforderungen haben, müssen wir irgendwie lernen, wie wir die Bytegröße für unterschiedliche Datentypen ermitteln. Zum Beispiel benötigen wir einen Speicherabschnitt für ein Array von Werten vom Typ int – dies ist eine Speichergröße, und wenn wir Speicher für ein Array derselben Größe, aber vom Typ char – zuweisen müssen, ist dies ein andere Größe. Daher müssen Sie die Speichergröße irgendwie berechnen. Dies kann mit der Operation sizeof() erfolgen, die einen Ausdruck entgegennimmt und seine Größe zurückgibt. Beispielsweise gibt sizeof(int) die Anzahl der Bytes zurück, die zum Speichern eines int-Werts erforderlich sind. Schauen wir uns ein Beispiel an:

#enthalten int *ptrVar = malloc(sizeof(int));

In diesem Beispiel in Zeile 3 Dem ptrVar-Zeiger wird eine Adresse auf einen Speicherort zugewiesen, dessen Größe dem Datentyp int entspricht. Dieser Speicherbereich wird automatisch für andere Programme unzugänglich. Dies bedeutet, dass der zugewiesene Speicher explizit freigegeben werden muss, nachdem er nicht mehr benötigt wird. Wenn der Speicher nicht explizit freigegeben wird, wird der Speicher nach Abschluss des Programms nicht für das Betriebssystem freigegeben. Dies wird als Speicherverlust bezeichnet. Sie können die Größe des zugewiesenen Speichers auch bestimmen, indem Sie einen Nullzeiger übergeben. Hier ist ein Beispiel:

Int *ptrVar = malloc(sizeof(*ptrVar));

Was ist denn hier los? Die Operation sizeof(*ptrVar) schätzt die Größe des Speicherorts, auf den der Zeiger verweist. Da ptrVar ein Zeiger auf einen int-Speicherort ist, gibt sizeof() die Größe der Ganzzahl zurück. Das heißt, basierend auf dem ersten Teil der Zeigerdefinition wird die Größe für den zweiten Teil berechnet. Warum brauchen wir das also? Dies kann erforderlich sein, wenn wir plötzlich die Definition eines Zeigers ändern müssen, beispielsweise int in float, und wir dann den Datentyp in zwei Teilen der Zeigerdefinition nicht ändern müssen. Es reicht aus, wenn wir den ersten Teil ändern:

Float *ptrVar = malloc(sizeof(*ptrVar));

Wie Sie sehen können, gibt es einen sehr starken Punkt in dieser Notation: Wir sollten die Funktion malloc() nicht mit sizeof(float) aufrufen. Stattdessen haben wir einen Zeiger auf den Float-Typ an malloc() übergeben. In diesem Fall bestimmt sich die Größe des zugewiesenen Speichers automatisch!

Dies ist besonders nützlich, wenn Sie Speicher weit entfernt von der Zeigerdefinition zuweisen müssen:

Float *ptrVar; /* . . . einhundert Codezeilen */ . . . ptrVar = malloc(sizeof(*ptrVar));

Wenn Sie ein Speicherzuweisungskonstrukt mit der Operation sizeof() verwenden würden, müssten Sie die Definition eines Zeigers im Code finden, sich seinen Datentyp ansehen und nur dann könnten Sie den Speicher korrekt zuweisen.

Freigeben von zugewiesenem Speicher

Das Freigeben von Speicher erfolgt mit der Funktion free(). Hier ist ein Beispiel:

Free(ptrVar);

Nach dem Freigeben des Speichers empfiehlt es sich, den Zeiger auf Null zurückzusetzen, d. h. *ptrVar = 0 zuzuweisen. Wenn Sie einem Zeiger den Wert 0 zuweisen, wird der Zeiger null, d. h. er zeigt auf nichts mehr. Weisen Sie dem Zeiger nach dem Freigeben des Speichers immer 0 zu. Andernfalls zeigt der Zeiger auch nach dem Freigeben des Speichers immer noch darauf, was bedeutet, dass Sie versehentlich anderen Programmen schaden können, die diesen Speicher möglicherweise verwenden, von denen Sie jedoch nicht einmal etwas wissen Sie werden feststellen, dass das Programm ordnungsgemäß funktioniert.

P.S.: Jeder, der sich für Videobearbeitung interessiert, könnte an diesem Windows 7-Videoeditor interessiert sein. Der Videoeditor heißt Movavi, vielleicht kennt ihn jemand bereits oder hat sogar damit gearbeitet. Mit diesem Programm auf Russisch können Sie ganz einfach Videos von Ihrer Kamera hinzufügen, die Qualität verbessern und wunderschöne Videoeffekte anwenden.

C++ unterstützt drei Haupttypen Entladung(oder mehr „Verteilungen“) Erinnerung, zwei davon kennen wir bereits:

Statische Speicherzuweisung gilt für und-Variablen. Der Speicher wird einmal beim Programmstart zugewiesen und bleibt während des gesamten Programms erhalten.

Automatische Speicherzuweisung gilt für und . Beim Betreten des Blocks, der diese Variablen enthält, wird Speicher zugewiesen und beim Verlassen des Blocks entfernt.

Dynamische Speicherzuweisung ist das Thema dieser Lektion.

Dynamische Variablenzuordnung

Sowohl die statische als auch die automatische Speicherzuweisung haben zwei gemeinsame Eigenschaften:

Wie funktioniert die dynamische Speicherzuweisung?

Ihr Computer verfügt über Speicher (vielleicht den größten Teil davon), der für die Verwendung durch Programme verfügbar ist. Wenn Sie ein Programm ausführen, lädt Ihr Betriebssystem dieses Programm in einen Teil dieses Speichers. Und dieser von Ihrem Programm verwendete Speicher ist in mehrere Teile unterteilt, von denen jeder eine bestimmte Aufgabe ausführt. Ein Teil enthält Ihren Code, der andere dient zur Ausführung normaler Vorgänge (Verfolgen der aufgerufenen Funktionen, Erstellen und Löschen globaler und lokaler Variablen usw.). Wir werden später darüber sprechen. Allerdings liegt der größte Teil des verfügbaren Speichers einfach da und wartet auf Zuweisungsanfragen von Programmen.

Wenn Sie Speicher dynamisch zuweisen, bitten Sie das Betriebssystem, einen Teil dieses Speichers für die Verwendung durch Ihr Programm zu reservieren. Wenn das Betriebssystem diese Anforderung erfüllen kann, wird die Adresse dieses Speichers an Ihr Programm zurückgegeben. Von nun an kann Ihr Programm diesen Speicher jederzeit nutzen. Wenn Sie mit diesem Speicher bereits alles Notwendige erledigt haben, muss er an das Betriebssystem zurückgegeben werden, um ihn auf andere Anforderungen zu verteilen.

Im Gegensatz zur statischen oder automatischen Speicherzuweisung ist das Programm dafür verantwortlich, dynamisch zugewiesenen Speicher anzufordern und zurückzugeben.

Speicher freigeben

Wenn Sie eine Variable dynamisch zuweisen, können Sie sie auch über eine einheitliche Initialisierung (in C++11) initialisieren:

int *ptr1 = new int (7); // direkte Initialisierung verwenden int *ptr2 = new int ( 8 ); // einheitliche Initialisierung verwenden

Wenn alles Notwendige bereits mit einer dynamisch zugewiesenen Variablen erledigt wurde, müssen Sie C++ explizit anweisen, diesen Speicher freizugeben. Für Variablen geschieht dies mit Operator löschen:

// Angenommen, ptr wurde zuvor mit dem Operator new delete ptr zugewiesen; // den Speicher, auf den ptr zeigt, an das Betriebssystem zurückgeben ptr = 0; // ptr zu einem Nullzeiger machen (nullptr anstelle von 0 in C++11 verwenden)

Der Löschoperator löscht eigentlich nichts. Es gibt einfach den zuvor zugewiesenen Speicher an das Betriebssystem zurück. Das Betriebssystem kann diesen Speicher dann einer anderen Anwendung (oder derselben Anwendung erneut) zuweisen.

Obwohl es den Anschein haben mag, dass wir löschen Variable, aber das ist nicht so! Eine Zeigervariable hat weiterhin den gleichen Gültigkeitsbereich wie zuvor und kann wie jede andere Variable mit einem neuen Wert belegt werden.

Beachten Sie, dass das Löschen eines Zeigers, der nicht auf dynamisch zugewiesenen Speicher zeigt, Probleme verursachen kann.

Hängende Schilder

C++ gibt keine Garantie dafür, was mit dem Inhalt des freigegebenen Speichers oder dem Wert des gelöschten Zeigers geschieht. In den meisten Fällen enthält der an das Betriebssystem zurückgegebene Speicher dieselben Werte wie zuvor. Befreiung, und der Zeiger zeigt weiterhin nur auf den bereits freigegebenen (gelöschten) Speicher.

Ein Zeiger, der auf den freigegebenen Speicher zeigt, wird aufgerufen Hängeschild. Das Dereferenzieren oder Entfernen eines baumelnden Zeigers führt zu unerwarteten Ergebnissen. Betrachten Sie das folgende Programm:

#enthalten int main() ( int *ptr = new int; *ptr = 8; // den Wert am zugewiesenen Speicherort platzieren delete ptr; // den Speicher an das Betriebssystem zurückgeben. ptr ist jetzt ein baumelnder Zeiger std:: cout<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#enthalten

int main()

int * ptr = new int ; // Eine Ganzzahlvariable dynamisch zuweisen

* ptr = 8 ; // Platziere den Wert in der zugewiesenen Speicherzelle

ptr löschen; // Speicher an das Betriebssystem zurückgeben. ptr ist jetzt ein baumelnder Zeiger

std::cout<< * ptr ; // Die Dereferenzierung eines baumelnden Zeigers führt zu unerwarteten Ergebnissen

ptr löschen; // Der erneute Versuch, Speicher freizugeben, führt ebenfalls zu unerwarteten Ergebnissen

return 0 ;

Im obigen Programm kann der Wert 8, der zuvor einer dynamischen Variablen zugewiesen wurde, nach der Freigabe weiterhin vorhanden sein oder auch nicht. Es ist auch möglich, dass der freigegebene Speicher bereits einer anderen Anwendung (oder für die eigene Nutzung durch das Betriebssystem) zugewiesen wurde und der Versuch, darauf zuzugreifen, dazu führt, dass das Betriebssystem Ihr Programm automatisch beendet.

Der Prozess der Speicherfreigabe kann auch zur Erstellung führen mehrere hängende Schilder. Betrachten Sie das folgende Beispiel:

#enthalten int main() ( int *ptr = new int; // Eine Ganzzahlvariable dynamisch zuweisen int *otherPtr = ptr; // otherPtr zeigt jetzt auf denselben zugewiesenen Speicher wie ptr delete ptr; // Speicher an das Betriebssystem zurückgeben. ptr und otherPtr sind jetzt baumelnde Zeiger ptr = 0; // ptr ist jetzt nullptr // OtherPtr ist jedoch immer noch ein baumelnder Zeiger! return 0; )

#enthalten

int main()

int * ptr = new int ; // Eine Ganzzahlvariable dynamisch zuweisen

int * otherPtr = ptr ; // otherPtr zeigt jetzt auf denselben zugewiesenen Speicher wie ptr

ptr löschen; // Speicher an das Betriebssystem zurückgeben. ptr und otherPtr sind jetzt baumelnde Zeiger

ptr = 0 ; // ptr ist jetzt nullptr

// Allerdings ist otherPtr immer noch ein baumelnder Zeiger!

return 0 ;

Versuchen Sie zunächst, Situationen zu vermeiden, in denen mehrere Zeiger auf denselben Teil des zugewiesenen Speichers verweisen. Wenn dies nicht möglich ist, machen Sie klar, welcher Zeiger den Speicher „besitzt“ (und für dessen Löschung verantwortlich ist) und welche Zeiger einfach darauf zugreifen.

Zweitens: Wenn Sie einen Zeiger löschen und er nicht sofort nach dem Löschen beendet wird, muss er auf Null gesetzt werden, d. h. Weisen Sie den Wert 0 zu (oder in C++11). Mit „sofort beim Löschen den Gültigkeitsbereich verlassen“ meinen wir, dass Sie den Zeiger ganz am Ende des Blocks löschen, in dem er deklariert ist.

Regel: Setzen Sie gelöschte Zeiger auf 0 (oder nullptr in C++11), es sei denn, sie verlassen unmittelbar nach dem Löschen den Gültigkeitsbereich.

Betreiber neu

Beim Anfordern von Speicher vom Betriebssystem kann es in seltenen Fällen vorkommen, dass dieser nicht verfügbar ist (d. h., er ist möglicherweise nicht verfügbar).

Wenn der neue Operator nicht funktionierte, wurde standardmäßig kein Speicher zugewiesen Ausnahme bad_alloc. Wenn diese Ausnahme nicht korrekt behandelt wird (und das wird der Fall sein, da wir uns noch nicht mit Ausnahmen und ihrer Behandlung befasst haben), stoppt das Programm einfach die Ausführung (Absturz) mit einem nicht behandelten Ausnahmefehler.

In vielen Fällen ist das Auslösen einer Ausnahme mit dem neuen Operator (sowie der Absturz des Programms) unerwünscht. Daher gibt es eine alternative Form des neuen Operators, die einen Nullzeiger zurückgibt, wenn kein Speicher zugewiesen werden kann. Sie müssen nur hinzufügen Konstante std::nothrow zwischen dem neuen Schlüsselwort und dem Datentyp:

int *value = new (std::nothrow) int; // Der Wertzeiger wird null, wenn die dynamische Zuweisung einer Ganzzahlvariablen fehlschlägt

Wenn new im obigen Beispiel keinen Zeiger mit dynamisch zugewiesenem Speicher zurückgibt, wird ein Nullzeiger zurückgegeben.

Eine Dereferenzierung wird ebenfalls nicht empfohlen, da dies zu unerwarteten Ergebnissen (höchstwahrscheinlich einem Programmabsturz) führen würde. Daher besteht die beste Vorgehensweise darin, alle Speicherzuweisungsanforderungen zu überprüfen, um sicherzustellen, dass die Anforderungen erfolgreich abgeschlossen werden und der Speicher zugewiesen wird:

int *value = new (std::nothrow) int; // Anfrage zur Zuweisung von dynamischem Speicher für einen ganzzahligen Wert if (!value) // Behandeln Sie den Fall, wenn new null zurückgibt (d. h. Speicher ist nicht zugewiesen) ( // Behandeln Sie diesen Fall std::cout<< "Could not allocate memory"; }

Da es äußerst selten vorkommt, dass der neue Operator keinen Speicher zuweist, vergessen Programmierer normalerweise, diese Prüfung durchzuführen!

Nullzeiger und dynamische Speicherzuweisung

Nullzeiger (Zeiger mit dem Wert 0 oder nullptr) sind besonders nützlich während des dynamischen Speicherzuweisungsprozesses. Ihre Anwesenheit informiert uns sozusagen: „Diesem Zeiger ist kein Speicher zugewiesen.“ Und dies wiederum kann verwendet werden, um eine bedingte Speicherzuweisung durchzuführen:

// Wenn ptr noch kein Speicher zugewiesen wurde, dann allozieren Sie ihn if (!ptr) ptr = new int;

Das Entfernen des Nullzeigers hat keine Auswirkung. Folgendes ist also nicht notwendig:

if (ptr) ptr löschen;

if(ptr)

ptr löschen;

Stattdessen können Sie einfach schreiben:

ptr löschen;

Wenn ptr nicht null ist, wird die dynamisch zugewiesene Variable gelöscht. Wenn der Zeigerwert null ist, passiert nichts.

Speicherleck

Dynamisch zugewiesener Speicher hat keinen Gültigkeitsbereich, d. h. Es bleibt zugewiesen, bis es explizit freigegeben wird oder bis die Ausführung Ihres Programms abgeschlossen ist (und das Betriebssystem alle Speicherpuffer selbst löscht). Zeiger, die zum Speichern dynamisch zugewiesener Speicheradressen verwendet werden, folgen jedoch den Gültigkeitsbereichsregeln regulärer Variablen. Diese Diskrepanz kann zu interessantem Verhalten führen. Zum Beispiel:

void doSomething() ( int *ptr = new int; )

Vorlaufzeit Programme. Das Programm reserviert Speicher aus dem Stapelspeicher für lokale Variablen. Lokale Variablen erfordern jedoch eine vorherige Bestimmung der für jede Situation zugewiesenen Speichermenge. Obwohl C++ solche Variablen effizient implementiert, muss der Programmierer im Voraus wissen, wie viel Speicher für jede Situation benötigt wird.

Die zweite Möglichkeit, wie C++ Informationen speichern kann, ist die Verwendung eines dynamischen Zuordnungssystems. Bei dieser Methode wird bei Bedarf Speicher für Informationen aus einem freien Speicherbereich zugewiesen. Der freie Speicherbereich liegt zwischen dem Programmcode mit seinem permanenten Speicherbereich und dem Stack (Abb. 24.1). Die dynamische Zuordnung ist nützlich, wenn Sie nicht wissen, wie viele Datenelemente verarbeitet werden.


Reis. 24.1.

Da das Programm den Stapelbereich nutzt, vergrößert er sich nach unten, d. h. das Programm bestimmt selbst die Größe des Stapelspeichers. Zum Beispiel ein Programm mit einer großen Anzahl rekursive Funktionen benötigt mehr Stapelspeicher als ein Programm ohne rekursive Funktionen, da lokale Variablen und Rücksprungadressen auf Stapeln gespeichert werden. Speicher für das Programm selbst und globale Variablen zeichnet sich durch alles aus Vorlaufzeit Programm und ist für eine bestimmte Umgebung konstant.

Der während der Programmausführung zugewiesene Speicher wird als dynamisch bezeichnet. Nach der Auswahl dynamisch Der Speicher bleibt erhalten, bis er explizit freigegeben wird, was nur mit einer speziellen Operation oder Bibliotheksfunktion möglich ist.

Wenn der dynamische Speicher nicht vor dem Ende des Programms freigegeben wird, wird er automatisch freigegeben, wenn das Programm endet. Allerdings ist es ein Zeichen guten Programmierstils, unnötig gewordenen Speicher explizit freizugeben.

Während der Programmausführung steht ein Abschnitt des dynamischen Speichers überall dort zur Verfügung, wo der Zeiger verfügbar ist, der diesen Abschnitt adressiert. Somit ist Folgendes möglich drei Optionen für die Arbeit mit dynamischem Speicher, in einem bestimmten Block zugewiesen (z. B. im Hauptteil einer Nicht-Hauptfunktion).

  • Ein Zeiger (auf einen Speicherort im dynamischen Speicher) wird als lokales automatisches Speicherobjekt definiert. In diesem Fall ist der zugewiesene Speicher beim Verlassen des Zeigerlokalisierungsblocks nicht verfügbar und muss vor dem Verlassen des Blocks freigegeben werden.
  • Der Zeiger ist als lokales statisches Speicherobjekt definiert. Auf den einmal in einem Block zugewiesenen dynamischen Speicher wird bei jedem erneuten Eintritt in den Block über einen Zeiger zugegriffen. Der Speicher sollte erst freigegeben werden, wenn er nicht mehr benötigt wird.
  • Der Zeiger ist ein globales Objekt in Bezug auf den Block. Dynamischer Speicher ist in allen Blöcken verfügbar, in denen der Zeiger „sichtbar“ ist. Der Speicher sollte erst freigegeben werden, wenn er nicht mehr benötigt wird.

Alle im Programm deklarierten Variablen liegen in einem zusammenhängenden Speicherbereich, der aufgerufen wird Datensegment. Solche Variablen ändern ihre Größe während der Programmausführung nicht und werden aufgerufen statisch. Die Größe des Datensegments reicht möglicherweise nicht aus, um große Informationsmengen aufzunehmen. Der Ausweg aus dieser Situation besteht darin, dynamischen Speicher zu verwenden. Dynamisches Gedächtnis- Dies ist der dem Programm für seine Ausführung zugewiesene Speicher abzüglich des Datensegments, des Stapels, der lokale Variablen von Unterprogrammen und des Programmkörpers selbst enthält.

Zeiger werden verwendet, um mit dynamischem Speicher zu arbeiten. Mit ihrer Hilfe erfolgt der Zugriff auf Bereiche des sogenannten dynamischen Gedächtnisses dynamische Variablen. Zur Aufbewahrung dynamische Variablen Es wird ein spezieller Speicherbereich namens Heap zugewiesen.

Dynamische Variablen werden mithilfe spezieller Funktionen und Operationen erstellt. Sie bleiben entweder bis zum Ende des Programms bestehen oder bis der ihnen zugewiesene Speicher durch spezielle Funktionen oder Operationen freigegeben wird. Das heißt, die Zeit des Lebens dynamische Variablen– vom Zeitpunkt der Erstellung bis zum Ende des Programms oder bis zu einem expliziten Speicher freigeben.

C++ verwendet zwei Möglichkeiten, mit dynamischem Speicher zu arbeiten:

  1. Verwenden der Neu- und Löschvorgänge;
  2. Verwendung der malloc (calloc)-Funktionsfamilie (geerbt von C).

Arbeiten mit dynamischem Speicher mithilfe der Vorgänge „Neu“ und „Löschen“.

In der Programmiersprache C++ für dynamische Speicherzuweisung Es gibt Neu- und Löschvorgänge. Diese Operationen werden verwendet, um Speicherblöcke zuzuweisen und freizugeben. Der Speicherbereich, in dem sich diese Blöcke befinden, wird aufgerufen freier Speicher.

Mit der neuen Operation können Sie einen freien Bereich im Hauptspeicher zuweisen und verfügbar machen, dessen Größe dem durch den Typnamen identifizierten Datentyp entspricht.

Syntax:

neuer Typname;

new TypeName [Initializer];

Der ermittelte Wert wird in den ausgewählten Bereich eingetragen Initialisierer, ein optionales Element. Bei Erfolg gibt new die Adresse des Anfangs des zugewiesenen Speichers zurück. Wenn ein Bereich mit der erforderlichen Größe nicht zugewiesen werden kann (kein Speicher vorhanden), gibt die neue Operation einen Adresswert von Null (NULL) zurück.

Die Syntax zur Verwendung der Operation lautet:

Pointer = new TypeName[Initializer];

Die neue Float-Operation reserviert einen 4-Byte-Speicherblock. Die neue int(15)-Operation weist einen 4-Byte-Speicherblock zu und initialisiert diesen Block mit dem ganzzahligen Wert 15. Die Syntax für die Verwendung der neuen und Löschoperationen beinhaltet die Verwendung von Zeigern. Jeder Zeiger muss im Voraus deklariert werden:

Typ *PointerName;

Zum Beispiel:

float *pi; //Variable pi deklarieren pi=new float; //Speicher für die Variable pi * pi = 2.25; //Wert zuweisen

Als Typ können Sie beispielsweise Standardtypen verwenden int, long, float, double, char.

Der new-Operator wird am häufigsten verwendet, um benutzerdefinierte Datentypen, wie z. B. Strukturen, im Speicher zuzuweisen:

struct Node ( char *Name; int Value; Node *Next ); Knoten *PNode; //Zeiger wird deklariert PNode = new Node; //Speicher wird zugewiesen PNode->Name = "Ata"; //Werte werden zugewiesen PNode->Value = 1; PNode->Next = NULL;

Ein Array kann als Typname in der neuen Operation verwendet werden:

newArrayType

Wenn Sie einem Array dynamischen Speicher zuweisen, müssen dessen Abmessungen vollständig angegeben werden. Zum Beispiel:

ptr = new int ;//10 Elemente vom Typ int oder 40 Bytes ptr = new int ;//falsch, weil Größe nicht bestimmt

Mit diesem Vorgang können Sie einen Bereich im dynamischen Speicher zuweisen, um ein Array des entsprechenden Typs aufzunehmen, es ist jedoch nicht möglich, es zu initialisieren. Als Ergebnis der Ausführung gibt die neue Operation einen Zeiger zurück, dessen Wert die Adresse des ersten Elements des Arrays ist. Zum Beispiel:

int *n = neues int;

Die neue Operation reserviert einen Abschnitt des dynamischen Speichers, der ausreicht, um einen Wert vom Typ int aufzunehmen, und schreibt die Adresse am Anfang dieses Abschnitts in die Variable n. Speicher für die Variable n selbst (mit ausreichender Größe, um den Zeiger aufzunehmen) wird in der Kompilierungsphase zugewiesen.

gastroguru 2017