Haufen freigeben c. Statische und dynamische Speicherzuweisung. Dynamische Arrays erstellen

Die Arbeit mit dynamischem Speicher ist bei vielen Algorithmen oft ein Flaschenhals, wenn Sie keine speziellen Tricks anwenden.

Ich werde ein paar dieser Techniken in diesem Artikel behandeln. Die Beispiele in dem Artikel unterscheiden sich (zum Beispiel von diesem) darin, dass das Überladen der new- und delete-Operatoren verwendet wird, wodurch die Syntax minimalistisch und die Programmüberarbeitung einfach ist. Auch die dabei gefundenen Fallstricke werden beschrieben (Gurus, die den Standard von vorne bis hinten gelesen haben, werden natürlich nicht überrascht sein).

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

Lassen Sie uns zunächst prüfen, wie ein intelligenter Allokator die Arbeit mit dem Speicher beschleunigen kann.

Lassen Sie uns einfache Tests für C ++ und C # schreiben (C # ist bekannt für einen hervorragenden Speichermanager, der Objekte nach Generationen aufteilt, verschiedene Pools für Objekte unterschiedlicher Größe verwendet usw.).

Klasse Node (öffentlich: Node * next;); // ... für (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 "sphärischen Vakuums" des Beispiels stellte sich heraus, dass der Zeitunterschied das 10-fache betrug (62 ms vs. 650 ms). Außerdem ist das c#-Beispiel vollständig, und nach den Regeln der guten Form 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 (Knoten-)Größe aufzuteilen, beim Zuweisen von Speicher einen Block aus dem Pool zu nehmen und ihn beim Freigeben an den Pool zurückzugeben. Der einfachste Weg, einen Pool zu organisieren, besteht darin, eine einzeln verknüpfte Liste (Stack) zu verwenden.

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

Zunächst benötigen wir einen Pool von großen Blöcken (Seiten), die wir aus dem OS oder der C-Runtime nehmen. Es kann zusätzlich zu malloc und freien Funktionen organisiert werden, aber für mehr Effizienz (um eine unnötige Abstraktionsebene zu überspringen) verwenden wir VirtualAlloc / VirtualFree. Diese Funktionen weisen Speicher in Blöcken zu, die ein Vielfaches von 4 KB sind, und reservieren außerdem den Prozessadressraum in Blöcken, die ein Vielfaches von 64 KB sind. Durch die gleichzeitige Angabe der Commit- und Reserve-Optionen überspringen wir eine weitere Abstraktionsebene, reservieren Adressraum und zuweisen Speicherseiten in einem Aufruf.

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 class PagePool (public: void * GetPage () (void * page = VirtualAlloc (NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back (page); return page;) ~ PagePool () (for (vector :: Iterator i = Seiten.begin (); i! = Seiten.Ende (); ++ i) (VirtualFree (* i, 0, MEM_RELEASE);)) privat: Vektor Seiten; );

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

BlockPool-Klasse

Schablone 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: sperren (das) Stellen, die eine Interthread-Synchronisation erfordern, sind markiert (verwenden Sie beispielsweise EnterCriticalSection oder boost :: mutex).

Lassen Sie mich erklären, warum die "Formatierung" der Seite nicht die FreeBlock-Abstraktion verwendet, um dem Pool einen Block hinzuzufügen. Wenn sowas

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

Dann würde die Seite nach dem FIFO-Prinzip "umgekehrt" beschriftet:

Mehrere nacheinander aus dem Pool angeforderte Blöcke hätten absteigende Adressen. Und der Prozessor geht nicht gerne zurück, davon bricht er Prefetch ( UPD: Nicht relevant für moderne Prozessoren). Wenn Sie das Markup in einer Schleife machen
for (size_t i = PageSize- (BlockSize- (PageSize% BlockSize)); i! = 0; i - = BlockSize) FreeBlock ...
dann würde die Markup-Schleife rückwärts gehen.

Nachdem nun die Vorbereitungen getroffen sind, kann die Mixin-Klasse definiert werden.
Schablone 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 überlädt laut borisko "comment // http://habrahabr.ru/post/148657/#comment_5020297 // Vermeiden Sie das Ausblenden von Platzierungen neu, die von den stl-Containern benötigt werden ... 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; ); Schablone BlockPool BlockAlloc :: Schwimmbad;

Ich werde erklären, warum Kontrollen erforderlich sind if (s! = Größe von (T))
Wann feuern sie? Dann beim Erstellen / Löschen einer Klasse, die von der Basis T geerbt wurde.
Nachkommen verwenden das übliche new / delete, aber BlockAlloc kann auch mit ihnen 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 Allocator noch besser. Es ist wahrscheinlich nicht nur eine verlinkte Liste. (Wenn man jedoch sofort nach new sofort delete aufruft und dadurch nicht viel Speicher verschwendet, indem man die Daten in den Cache einfügt, bekommen wir 62 ms. Seltsam. Genau wie die .NET CLR, als ob sie das freigegebene lokale zurückliefert Variablen sofort in den entsprechenden Pool, ohne auf GC zu warten)

2. Der Behälter und sein bunter Inhalt

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

Dies kann beispielsweise eine XmlDocument-Klasse sein, die mit Node- und Attributklassen gefüllt ist, sowie C-Strings (char *), die aus dem Text in den Nodes stammen. Oder eine Liste von Dateien und Verzeichnissen im Dateimanager, die beim erneuten Lesen eines Verzeichnisses einmal geladen wird und sich nicht mehr ändert.

Wie in der Einleitung gezeigt, 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 dem übergeordneten Objekt zugeordnet ist. Wenn das übergeordnete Objekt gelöscht wird, werden die Destruktoren des untergeordneten Objekts wie üblich aufgerufen, aber der Speicher muss nicht zurückgegeben werden - er wird in einem großen Block freigegeben.

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

PointerBumpAllocator-Klasse

Schablone 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 (Vektor .) :: Iterator i = Seiten.begin (); i! = Seiten.Ende (); ++ i) (VirtualFree (* i, 0, MEM_RELEASE);)) private: void * GetPage (size_t size) (void * page = VirtualAlloc (NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back (page) ; Seite zurück;) Vektor Seiten; Leere * Kopf; size_t frei; ); typedef PointerBumpAllocator<>DefaultAllocator;

Schließlich beschreiben wir das ChildObject-Mixin mit new und delete überladen mit Bezug auf den angegebenen Allocator:

Schablone 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 ( Größe_t s ););

In diesem Fall müssen Sie nicht nur ein Mixin zur untergeordneten Klasse hinzufügen, sondern auch alle Aufrufe auf neu setzen (oder das "Factory" -Muster verwenden). Die Syntax für den neuen Operator sieht wie folgt aus:

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

Der Einfachheit halber habe ich zwei neue Operatoren definiert, die entweder A & oder A * annehmen.
Wenn der Allocator der übergeordneten Klasse als Member hinzugefügt wird, ist die erste Option bequemer:
Knoten = new (Zuordner) XmlNode (Knotenname);
Wenn der Allocator als Vorfahr (Mixin) hinzugefügt wird, ist der zweite bequemer:
node = new (this) XmlNode (Knotenname);

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

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

Das Platzieren des neuen Operators im privaten Abschnitt schützt davor, new ohne Angabe eines Zuweisers anzurufen.

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

Beispiel

Klasse XmlDocument: public DefaultAllocator (public: ~ XmlDocument () (für (Vektor :: Iterator i = node.begin (); i! = Knoten.Ende (); ++ 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); node.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 der Moderator mochte ihn leider nicht.

Statischer Speicher wird bereits vor dem Programmstart, in der Phase der Kompilierung und Montage, zugewiesen. Statische Variablen haben eine feste Adresse, die vor dem Programmstart bekannt ist und sich während des Betriebs nicht ändert. Statische Variablen werden erstellt und initialisiert, bevor die Hauptfunktion aufgerufen wird, die die Programmausführung startet.

Es gibt zwei Arten von statischen Variablen:

  • globale Variablen sind Variablen definiert keine Funktionen, in deren Beschreibung das Wort statisch fehlt. In der Regel Beschreibungen globale Variablen, einschließlich des Wortes extern, werden in Header-Dateien (h-Dateien) verschoben. Das Wort extern bedeutet, dass die Variable an dieser Stelle im Programm beschrieben, aber noch nicht angelegt ist. Definitionen globale Variablen, d.h. Beschreibungen ohne das Wort extern werden in Implementierungsdateien (c-Dateien oder cpp-Dateien) platziert. Beispiel: Die globale Variable maxind wird zweimal beschrieben:
    • in h-Datei mit Zeile

      extern int maxind;

      diese Beschreibung meldet die Existenz einer solchen Variablen, erzeugt diese Variable aber nicht!
    • in der cpp-Datei mit der Zeile

      int maxind = 1000;

      diese Beschreibung schafft Variable maxind und weist ihr einen Anfangswert von 1000 zu. Beachten Sie, dass der Sprachstandard keine obligatorische Zuweisung von Anfangswerten an globale Variablen vorschreibt, dies jedoch immer besser ist, da sonst die Variable einen unvorhersehbaren Wert enthält (Müll, wie Programmierer sagen). Es ist sinnvoll, alle globalen Variablen zu initialisieren, wenn sie definiert sind.
    Globale Variablen werden so genannt, weil sie überall im Programm in all seinen Dateien verfügbar sind. Daher müssen die Namen globaler Variablen lang genug sein, um eine versehentliche Überlappung der Namen zweier verschiedener Variablen zu vermeiden. Beispielsweise sind die Namen x oder n für eine globale Variable nicht geeignet;
  • statische Variablen- dies sind Variablen, in deren Beschreibung das Wort statisch vorkommt. Typischerweise werden statische Variablen beschrieben keine Funktionen... Solche statischen Variablen ähneln in allem globalen, mit einer Ausnahme: Der Geltungsbereich einer statischen Variablen ist auf eine Datei beschränkt, in der sie definiert ist - und außerdem kann sie erst nach ihrer Beschreibung verwendet werden, d.h. unten im Text. Aus diesem Grund werden statische Variablendeklarationen normalerweise am Anfang der Datei platziert. Im Gegensatz zu globalen Variablen sind statische Variablen noch nie werden nicht in h-Dateien beschrieben (externe und statische Modifikatoren kollidieren miteinander). Tipp: Verwenden Sie statische Variablen, wenn diese nur für die darin beschriebenen Funktionen verfügbar sein sollen die gleiche Datei... Verwenden Sie in solchen Situationen möglichst keine globalen Variablen, um Namenskonflikte bei der Umsetzung großer Projekte mit Hunderten von Dateien zu vermeiden.
    • Eine statische Variable kann auch innerhalb einer Funktion deklariert werden, obwohl dies normalerweise niemand tut. Die Variable wird nicht auf dem Stack, sondern im statischen Speicher, d.h. er kann nicht in Rekursion verwendet werden, und sein Wert wird zwischen verschiedenen Eingaben der Funktion gespeichert. Der Gültigkeitsbereich einer solchen Variablen wird durch den Rumpf der Funktion begrenzt, in der sie definiert ist. Ansonsten ähnelt es einer statischen oder globalen Variablen. Beachten Sie, dass das Schlüsselwort static in C für zwei verschiedene Zwecke verwendet wird:
      • als Hinweis auf den Speichertyp: die Variable befindet sich im statischen Speicher, nicht auf dem Stack;
      • um den Gültigkeitsbereich einer Variablen auf den Gültigkeitsbereich einer Datei zu beschränken (im Fall einer Variablendeklaration außerhalb einer Funktion).
  • Das Wort statisch kann auch in der Kopfzeile einer Funktion vorkommen. Es wird jedoch nur verwendet, um den Geltungsbereich des Funktionsnamens auf den Geltungsbereich einer Datei zu beschränken. Beispiel:

    statisches int gcd (int x, int y); // Prototyp der Funktion. ... ... static int gcd (int x, int y) (// Implementierung...)

    Tipp: Verwenden Sie den statischen Modifikator im Funktionsheader, wenn Sie wissen, dass die Funktion nur innerhalb einer einzelnen Datei aufgerufen wird. Das Wort static muss sowohl in der Beschreibung des Funktionsprototyps als auch im Header der Funktion während ihrer Implementierung vorhanden sein.

Stack oder lokaler Speicher

Lokale oder Stapelvariablen sind Variablen, die beschrieben werden durch Innenfunktion... Speicher für solche Variablen wird auf dem Hardware-Stack zugewiesen, siehe Abschnitt 2.3.2. Speicher wird beim Betreten einer Funktion oder eines Blocks zugewiesen und beim Verlassen einer Funktion oder eines Blocks wieder freigegeben. In diesem Fall erfolgt die Erfassung und Freigabe des Speichers fast sofort, da der Computer ändert nur das Register, das die Adresse des oberen Endes des Stapels enthält.

Lokale Variablen können rekursiv verwendet werden, da beim erneuten Aufrufen der Funktion ein neuer Satz lokaler Variablen auf dem Stack erstellt wird und der vorherige Satz nicht zerstört wird. Aus dem gleichen Grund sind lokale Variablen in der parallelen Programmierung Thread-sicher (siehe Abschnitt 2.6.2). Programmierer nennen diese Eigenschaft einer Funktion Wiedereintritt, aus dem Englischen. re-enterable - die Fähigkeit, wieder einzutreten. Dies ist eine sehr wichtige Eigenschaft im Hinblick auf die Programmzuverlässigkeit und -sicherheit! Ein Programm, das mit statischen Variablen arbeitet, hat diese Eigenschaft nicht, also müssen Sie zum Schutz statischer Variablen verwenden Synchronisationsmechanismen(siehe 2.6.2), und die Logik des Programms wird viel komplizierter. Globale und statische Variablen sollten Sie immer vermeiden, wenn Sie mit lokalen Variablen auskommen.

Die Nachteile lokaler Variablen sind eine Erweiterung ihrer Stärken. Lokale Variablen werden beim Eintritt in eine Funktion erstellt und verschwinden beim Verlassen, sodass sie nicht als Daten verwendet werden können, die von mehreren Funktionen gemeinsam genutzt werden. Darüber hinaus ist die Größe des Hardware-Stacks nicht unendlich, der Stapel kann in einem bestimmten Moment (z. B. während einer tiefen Rekursion) überlaufen, was zu einer katastrophalen Programmbeendigung führt. Daher sollten lokale Variablen nicht groß sein. Insbesondere große Arrays können nicht als lokale Variablen verwendet werden.

Dynamischer Speicher oder Heap

Neben statischem und Stack-Speicher gibt es auch eine fast unbegrenzte Speicherressource namens dynamisch, oder Haufen(Haufen). Das Programm kann Teile des dynamischen Speichers der gewünschten Größe erfassen. Nach der Verwendung sollte der zuvor erfasste Teil des Haufens freigegeben werden.

Dynamischer Speicher ist der virtuelle Speicherbereich eines Prozesses zwischen statischem Speicher und dem Stack. (Der Mechanismus des virtuellen Speichers wurde in Abschnitt 2.6 besprochen.) Typischerweise befindet sich der Stack an höheren Adressen des virtuellen Speichers und wächst in Richtung abnehmender Adressen (siehe Abschnitt 2.3). Die Programm- und Konstantendaten befinden sich in unteren Adressen, statische Variablen befinden sich darüber. Der Platz über statischen Variablen und unter dem Stack ist Heap-Speicher:

die Anschrift Speicherinhalt

Programmcode und Daten,

vor Veränderung geschützt

...

statische Variablen

Programme

dynamisches Gedächtnis

max. Adresse (2 32 -4)

Stapel

Die Heap-Struktur wird automatisch vom C- oder C++-Laufzeitsystem gepflegt. Der dynamische Speicher besteht aus erfassten und freien Segmenten, denen jeweils ein Segmentdeskriptor vorangestellt ist. Beim Ausführen einer Speichererfassungsanforderung sucht das ausführende System nach einem freien Segment ausreichender Größe und greift darin ein Segment der erforderlichen Länge. Beim Freigeben eines Speichersegments wird dieses als frei markiert, ggf. werden mehrere aufeinanderfolgende freie Segmente zusammengefasst.

In der Sprache C werden die Standardfunktionen malloc und free verwendet, um Heap-Speicher zu erfassen und freizugeben, Beschreibungen ihrer Prototypen sind in der Standard-Header-Datei "stdlib.h" enthalten. (Der Name Malloc ist die Abkürzung für Speicher zuweisen- "Memory Capture".) Die Prototypen dieser Funktionen sind wie folgt:

void * malloc (size_t n); // Besorgen Sie sich ein Stück Speicher // n Byte groß void free (void * p); // Geben Sie ein Stück // Speicher mit der Adresse p . frei

Dabei ist n die Größe des zu erfassenden Bereichs in Bytes, size_t ist der Name eines der Integer-Typen, die die maximale Größe des zu erfassenden Bereichs bestimmen. Der Typ size_t wird in der Standard-Header-Datei "stdlib.h" mit dem Operator typedef angegeben (siehe Seite 117). Dadurch wird sichergestellt, dass der Text des C-Programms unabhängig von der verwendeten Architektur ist. Auf einer 32-Bit-Architektur ist der Typ size_t als Ganzzahl ohne Vorzeichen definiert:

typedef unsigned int size_t;

Die malloc-Funktion gibt die Adresse des erfassten Speicherbereichs zurück oder Null im Fehlerfall (wenn kein ausreichend großer freier Bereich vorhanden ist). Die free-Funktion gibt ein Stück Speicher an der angegebenen Adresse frei. Zum Setzen der Adresse wird ein Zeiger vom allgemeinen Typ void * verwendet. Nach dem Aufruf der Funktion malloc muss diese mit der Cast-Operation in einen Zeiger auf einen bestimmten Typ umgewandelt werden, siehe Abschnitt 3.4.11. Im folgenden Beispiel wird beispielsweise ein 4000-Byte-Block des Heap-Speichers erfasst und seine Adresse einem Zeiger auf ein Array von 1000 ganzen Zahlen zugewiesen:

int * a; // Zeiger auf ein Array von ganzen Zahlen. ... ... a = (int *) malloc (1000 * sizeof (int));

Der Ausdruck im Argument für malloc ist 4000, da sizeof (int) vier Byte beträgt. Um einen Zeiger zu konvertieren, wird die Operation der Typumwandlung (int *) von einem Zeiger eines generischen Typs zu einem Zeiger auf eine ganze Zahl verwendet.

Beispiel: Drucken der ersten n Primzahlen

Sehen wir uns ein Beispiel mit Heap-Capture an. Sie müssen eine ganze Zahl n eingeben und die ersten n Primzahlen ausgeben. (Eine Primzahl ist eine Zahl, die keine nichttrivialen Teiler hat.) Wir verwenden den folgenden Algorithmus: Wir prüfen sequentiell alle ungeraden Zahlen, beginnend mit drei (wir betrachten zwei getrennt). Teilen Sie die nächste Zahl durch alle Primzahlen, die in den vorherigen Schritten des Algorithmus gefunden wurden und die Quadratwurzel der zu prüfenden Zahl nicht überschreiten. Wenn es durch keine dieser Primzahlen teilbar ist, dann ist es selbst prim; es wird gedruckt und dem Array der gefundenen Primzahlen hinzugefügt.

Da die erforderliche Anzahl von Primzahlen n vor dem Programmstart nicht bekannt ist, ist es unmöglich, ein Array zu erstellen, um sie im statischen Speicher zu speichern. Die Lösung besteht darin, nach Eingabe der Zahl n Platz für das Array im Heap zu schaffen. Hier der vollständige Programmtext:

#enthalten #enthalten #enthalten int main () (int n; // Erforderliche Anzahl Primzahlen int k; // Aktuelle Anzahl gefundener Primzahlen int * a; // Zeiger auf ein Array gefundener Primzahlen int p; // Nächste geprüfte Zahl int r ; // Integer-Teil Quadratwurzel von p int i; // Index des Primteilers bool prime; // Zeichen der Einfachheit printf ("Geben Sie die Zahl der Primzahl ein:"); scanf ("% d", & n); if (n<= 0) // Некорректное значение =>1 zurückgeben; // mit dem Fehlercode beenden // Speicher für ein Array von Primzahlen holen a = (int *) malloc (n * sizeof (int)); a = 2; k = 1; // Zwei zum Array hinzufügen printf ("% d", a); // und drucke es aus p = 3; während (k< n) { // Проверяем число p на простоту r = (int)(// Целая часть корня sqrt((double) p) + 0.001); i = 0; prime = true; while (i < k && a[i] <= r) { if (p % a[i] == 0) { // p делится на a[i] prime = false; // =>p ist nicht einfach, brechen; // die Schleife verlassen) ++ i; // zum nächsten Primfaktor) if (prime) (// Wenn du eine Primzahl findest, a [k] = p; // dann füge sie zum Array hinzu ++ k; // Erhöhe die Anzahl der Primzahlen printf ("% d", p ); // Gib eine Primzahl aus if (k% 5 == 0) (// Gehe in eine neue Zeile printf ("\ n"); // nach jeweils fünf Zahlen)) p + = 2; // Gehe zur nächsten ungeraden Zahl) if (k% 5! = 0) (printf ("\ n"); // Übersetze den String) // Freier dynamischer Speicher frei (a); 0 zurückgeben; )

Ein Beispiel für die Funktionsweise dieses Programms:

Geben Sie die Anzahl der Primzahlen ein: 50 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229

C++ neue und Löschoperatoren

C++ verwendet die Operatoren new und delete, um dynamischen Speicher zu erfassen und freizugeben. Sie sind Teil der Sprache C++, im Gegensatz zu den malloc- und freien Funktionen, die Teil der C-Bibliothek der Standardfunktionen sind.

Sei T eine Art von C- oder C++-Sprache, p - ein Zeiger auf ein Objekt vom Typ T. Um dann den Speicher eines Elements vom Typ T zu erfassen, wird der neue Operator verwendet:

T*p; p = neues T;

Um beispielsweise acht Bytes für eine reelle Zahl vom Typ double zu erfassen, verwenden Sie das Fragment

doppelt * p; p = neues Doppel;

Bei der Verwendung von new ist es im Gegensatz zu malloc nicht erforderlich, einen Zeiger vom Typ void * in den gewünschten Typ umzuwandeln: Der new-Operator gibt einen Zeiger auf den Typ zurück, der nach dem Wort new geschrieben wurde. Vergleichen Sie zwei gleichwertige C- und C++-Schnipsel.

C++ unterstützt drei Haupttypen Ausscheidungen(oder mehr "Verteilungen") Erinnerung, von denen wir zwei bereits kennen:

Statische Speicherzuweisung wird für und Variablen ausgeführt. Der Speicher wird einmalig beim Programmstart zugewiesen und bleibt während des gesamten Programms erhalten.

Automatische Speicherzuweisung wird für und durchgeführt. Speicher wird beim Betreten des Blocks mit diesen Variablen zugewiesen und beim Verlassen entfernt.

Dynamische Speicherzuweisung ist das Thema dieses Tutorials.

Dynamische Zuordnung von Variablen

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 von Programmen verwendet werden kann. 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 erfüllt. Ein Teil enthält Ihren Code, der andere wird verwendet, um normale Operationen auszuführen (Verfolgen, welche Funktionen aufgerufen werden, Erstellen und Löschen globaler und lokaler Variablen usw.). Wir werden später darüber sprechen. Der größte Teil des verfügbaren Speichers sitzt jedoch nur da und wartet auf Zuweisungsanforderungen 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 verwenden, sobald es möchte. Wenn Sie mit diesem Speicher bereits alles erledigt haben, was benötigt wurde, muss er zur Verteilung auf andere Anforderungen an das Betriebssystem zurückgegeben werden.

Im Gegensatz zur statischen oder automatischen Speicherzuweisung ist das Programm selbst für das Anfordern und Zurückgeben von dynamisch zugewiesenem Speicher verantwortlich.

Speicher freigeben

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

int * ptr1 = neue 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. Bei Variablen geschieht dies mit Operator löschen:

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

Der Löschoperator löscht eigentlich nichts. Es gibt einfach den Speicher zurück, der zuvor dem Betriebssystem zugewiesen wurde. Das Betriebssystem kann diesen Speicher dann einer anderen Anwendung (oder wieder der gleichen) zuweisen.

Obwohl es so aussehen mag, als würden wir löschen Variable, aber das ist nicht so! Eine Zeigervariable hat immer noch den gleichen Gültigkeitsbereich wie zuvor und kann wie jeder anderen Variablen einen neuen Wert zugewiesen bekommen.

Beachten Sie, dass das Löschen eines Zeigers, der nicht auf dynamisch zugewiesenen Speicher zeigt, zu Problemen führen kann.

Baumelnde Zeiger

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

Der Zeiger, der auf den freigegebenen Speicher zeigt, heißt baumelnder Zeiger... 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; // setze den Wert in den zugewiesenen Speicherplatz delete ptr; // bringe den Speicher zurück zum Betriebssystem.ptr ist jetzt ein baumelnder Zeiger std :: cout<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#enthalten

int main ()

int * ptr = neues int; // eine Integer-Variable dynamisch zuweisen

* ptr = 8; // setze den Wert in den zugewiesenen Speicherplatz

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

std :: cout<< * ptr ; // Die Dereferenzierung des Dangling-Pointers führt zu unerwarteten Ergebnissen

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

0 zurückgeben;

Im obigen Programm kann der Wert 8, der zuvor einer dynamischen Variable zugewiesen wurde, nach der Freigabe dort verbleiben oder nicht. Es ist auch möglich, dass der frei gewordene Speicher bereits einer anderen Anwendung (oder für den eigenen Gebrauch des Betriebssystems) zugewiesen wurde und ein Zugriffsversuch dazu führt, dass das Betriebssystem die Ausführung Ihres Programms automatisch beendet.

Der Prozess des Freigebens von Speicher kann auch zur Erstellung von mehrere baumelnde Zeiger. Betrachten Sie das folgende Beispiel:

#enthalten int main() (int * ptr = new int; // eine Integer-Variable dynamisch zuweisen int * otherPtr = ptr; // otherPtr zeigt jetzt auf denselben zugewiesenen Speicher wie ptr delete ptr; // Speicher zurück zum Betriebssystem. ptr und otherPtr sind jetzt baumelnde Zeiger ptr = 0; // ptr ist jetzt nullptr // Aber otherPtr ist immer noch ein baumelnder Zeiger! return 0;)

#enthalten

int main ()

int * ptr = neues int; // eine Integer-Variable dynamisch zuweisen

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

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

ptr = 0; // ptr ist jetzt nullptr

// otherPtr ist jedoch immer noch ein baumelnder Zeiger!

0 zurückgeben;

Versuchen Sie zunächst, Situationen zu vermeiden, in denen mehrere Zeiger auf denselben Teil des zugewiesenen Speichers zeigen. Wenn dies nicht möglich ist, klären Sie, welcher von allen Zeigern den Speicher "besitzt" (und für das Löschen verantwortlich ist) und welche Zeiger einfach darauf zugreifen.

Zweitens, wenn Sie einen Zeiger löschen und dieser nicht sofort nach dem Löschen beendet wird, muss er auf null gesetzt werden, d. weisen Sie den Wert 0 (oder in C++ 11) zu. Mit "unmittelbar nach dem 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 entfernte Zeiger auf 0 (oder nullptr in C++ 11), wenn sie beim Löschen nicht sofort den Gültigkeitsbereich verlassen.

Neuer Betreiber

Wenn Speicher vom Betriebssystem angefordert wird, ist dieser in seltenen Fällen möglicherweise nicht verfügbar (d. h. er ist möglicherweise nicht verfügbar).

Wenn der neue Operator nicht funktioniert hat, wurde der Speicher standardmäßig nicht zugewiesen, dann wird er generiert eine Ausnahme bad_alloc... Wenn diese Ausnahme falsch behandelt wird (und genau das wird es sein, da wir Ausnahmen und ihre Behandlung noch nicht betrachtet haben), dann beendet das Programm einfach seine Ausführung (es stürzt ab) mit einem unbehandelten Ausnahmefehler.

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

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

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

Es wird auch nicht empfohlen, es zu dereferenzieren, da dies zu unerwarteten Ergebnissen (höchstwahrscheinlich zu einem Programmabsturz) führt. Daher empfiehlt es sich, alle Anforderungen zur Speicherzuweisung zu überprüfen, um sicherzustellen, dass diese Anforderungen erfolgreich sind und Speicher zugewiesen wird:

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

Da es sehr selten vorkommt, dass kein Speicher mit dem neuen Operator zugewiesen wird, vergessen Programmierer diese Überprüfung normalerweise!

Nullzeiger und dynamische Speicherzuweisung

Nullzeiger (Zeiger mit dem Wert 0 oder nullptr) sind besonders nützlich bei der dynamischen Speicherzuweisung. Ihre Anwesenheit informiert uns in gewisser Weise: "Diesem Zeiger wurde 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 Auswirkungen. Somit ist folgendes unnötig:

if (ptr) ptr löschen;

wenn (ptr)

ptr löschen;

Stattdessen können Sie einfach schreiben:

ptr löschen;

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

Speicherleck

Dynamisch zugewiesener Speicher ist nicht bereichsbezogen, d. h. es bleibt zugewiesen, bis es explizit freigegeben wird oder bis Ihr Programm seine Ausführung abgeschlossen hat (und das Betriebssystem alle Speicherpuffer von selbst löscht). Zeiger, die zum Speichern dynamisch zugewiesener Speicheradressen verwendet werden, folgen jedoch den Bereichsregeln regulärer Variablen. Diese Diskrepanz kann zu interessantem Verhalten führen. Beispielsweise:

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

Die dynamische Speicherzuweisung ist für eine effiziente Nutzung des Computerspeichers unerlässlich. Wir haben zum Beispiel eine Art Programm geschrieben, das ein Array verarbeitet. Beim Schreiben dieses Programms war es notwendig, ein Array zu deklarieren, dh ihm eine feste Größe zu geben (zB von 0 bis 100 Elemente). Dann ist dieses Programm nicht universell, da es ein Array von nicht mehr als 100 Elementen verarbeiten kann. Und wenn wir nur 20 Elemente benötigen, aber Platz für 100 Elemente im Speicher zugewiesen wird, weil die Array-Deklaration statisch war und diese Speichernutzung extrem ineffizient ist.

In C++ dienen die Operationen new und delete zum dynamischen Zuweisen von Computerspeicher. Die neue Operation weist Speicher aus dem freien Speicherbereich zu, und die Löschoperation gibt den zugewiesenen Speicher frei. Der zugewiesene Speicher muss nach seiner Verwendung freigegeben werden, sodass die Operationen new und delete paarweise verwendet werden. Auch wenn Sie Speicher nicht explizit freigeben, wird dieser bei der Programmbeendigung von den Betriebssystemressourcen freigegeben. Ich empfehle dennoch, den Löschvorgang nicht zu vergessen.

// Beispiel für die Verwendung der Operation new int * ptrvalue = new int; // wobei ptrvalue ein Zeiger auf den zugewiesenen Speicherbereich vom Typ int ist // new ist die Operation zum Zuweisen von freiem Speicher für das zu erstellende Objekt.

Die neue Operation erstellt ein Objekt des angegebenen Typs, weist ihm Speicher zu und gibt einen Zeiger des richtigen Typs auf die angegebene Speicherstelle zurück. Kann Speicher nicht allokiert werden, z. B. wenn keine freien Bereiche vorhanden sind, wird ein Null-Zeiger zurückgegeben, dh der Zeiger liefert den Wert 0. Die Speicherbelegung ist für jeden Datentyp möglich: int, schweben,doppelt,verkohlen usw.

// Beispiel für die Verwendung des Löschvorgangs: delete ptrvalue; // wobei ptrvalue ein Zeiger auf den zugewiesenen Speicherbereich vom Typ int ist // delete ist eine Operation zum Freigeben von Speicher

Lassen Sie uns ein Programm entwickeln, in dem eine dynamische Variable erstellt wird.

// new_delete.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #include "stdafx.h" #include << "ptrvalue = "<< * ptrwert << endl; ptrvalue löschen; // freies Speichersystem ("pause"); return 0; }

// Code Code :: Blöcke Block

// Dev-C++-Code

// new_delete.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #enthalten Verwenden des Namensraums std; int main (int argc, char * argv) (int * ptrvalue = new int; // dynamische Speicherzuweisung für ein Objekt vom Typ int * ptrvalue = 9; // Objektinitialisierung über einen Zeiger // int * ptrvalue = new int ( 9); Initialisierung kann sofort durchgeführt werden, wenn ein dynamisches cout-Objekt deklariert wird<< "ptrvalue = "<< * ptrwert << endl; ptrvalue löschen; // freier Speicher Rückgabe 0; )

(! SPRACHE: B Linie 10 zeigt eine Möglichkeit, ein dynamisches Objekt mit einer Neun zu deklarieren und zu initialisieren, Sie müssen lediglich den Wert in Klammern hinter dem Datentyp angeben. Das Ergebnis des Programms ist in Abbildung 1 dargestellt.

Ptrvalue = 9 Drücken Sie eine beliebige Taste, um fortzufahren. ... ...

Abbildung 1 - Dynamische Variable

Dynamische Arrays erstellen

Wie bereits erwähnt, können Arrays auch dynamisch sein. In den meisten Fällen werden die Operationen new und delete verwendet, um dynamische Arrays zu erstellen und nicht um dynamische Variablen zu erstellen. Sehen wir uns einen Codeausschnitt zum Erstellen eines eindimensionalen dynamischen Arrays an.

// Deklaration eines eindimensionalen dynamischen Arrays mit 10 Elementen: float * ptrarray = new float; // wobei ptrarray ein Zeiger auf den zugewiesenen Speicherbereich für ein Array von reellen Zahlen vom Typ Float ist // in eckigen Klammern geben wir die Größe des Arrays an

Nachdem das dynamische Array überflüssig geworden ist, müssen Sie den dafür zugewiesenen Speicherbereich freigeben.

// Speicher freigeben für ein eindimensionales dynamisches Array: delete ptrarray;

Nach dem Löschoperator werden eckige Klammern gesetzt, die angeben, dass ein für ein eindimensionales Array reservierter Speicherbereich freigegeben wird. Lassen Sie uns ein Programm entwickeln, in dem wir ein eindimensionales dynamisches Array erstellen, das mit Zufallszahlen gefüllt ist.

// new_delete_array.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #enthalten"stdafx.h" #include !} // in Header-Datei // in Header-Datei < 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

// Code Code :: Blöcke Block

// Dev-C++-Code

// new_delete_array.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #enthalten // in Header-Datei enthält den Prototyp der time()-Funktion #include // in Header-Datei enthält den Prototyp der setprecision()-Funktion #include #enthalten Verwenden des Namensraums std; int main (int argc, char * argv) (srand (time (0)); // Generiere Zufallszahlen float * ptrarray = new float; // erzeuge ein dynamisches Array von reellen Zahlen für zehn Elemente für (int count = 0; zählen< 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

Das erstellte eindimensionale dynamische Array wird mit zufälligen reellen Zahlen gefüllt, die mit den Funktionen zum Generieren von Zufallszahlen erhalten wurden, und die Zahlen werden im Bereich von 1 bis 10 generiert, das Intervall wird wie folgt festgelegt - rand ()% 10 + 1 . Um zufällige reelle Zahlen zu erhalten, wird eine Divisionsoperation unter Verwendung einer expliziten Umwandlung in den reellen Typ des Nenners durchgeführt - float ((rand ()% 10 + 1)). Um nur zwei Dezimalstellen anzuzeigen, verwenden Sie die Funktion setprecision (2) , Der Prototyp dieser Funktion befindet sich in der Header-Datei . Die Funktion time (0) setzt dem Zufallszahlengenerator einen Zeitwert zu und reproduziert somit die Zufälligkeit des Auftretens von Zahlen (siehe Abbildung 2).

Array = 0,8 0,25 0,86 0,5 2,2 10 1,2 0,33 0,89 3,5 Drücken Sie eine beliebige Taste, um fortzufahren. ... ...

Abbildung 2 - Dynamisches Array in C ++

Nach Abschluss der Arbeit mit dem Array wird es gelöscht, wodurch der für seine Speicherung zugewiesene Speicher freigegeben wird.

Wir haben gelernt, wie man eindimensionale dynamische Arrays erstellt und damit arbeitet. Sehen wir uns nun den Codeausschnitt an, der zeigt, wie ein zweidimensionales dynamisches Array deklariert wird.

// Deklaration eines zweidimensionalen dynamischen Arrays mit 10 Elementen: float ** ptrarray = new float *; // zwei Zeilen im Array für (int count = 0; count< 2; count++) ptrarray = new float ; // и пять столбцов // где ptrarray – массив указателей на выделенный участок памяти под массив вещественных чисел типа float

Zuerst wird ein Zeiger zweiter Ordnung float ** ptrarray deklariert, der sich auf ein Array von float * Zeigern bezieht, wobei die Größe des Arrays zwei beträgt . Dann wird in der for-Schleife jede Zeile des Arrays deklariert in Zeile 2 Speicher für fünf Elemente wird zugewiesen. Das Ergebnis ist ein zweidimensionales dynamisches Array-Ptraarray.Betrachten Sie ein Beispiel für das Freigeben von Speicher, der einem zweidimensionalendynamischen Array zugewiesen ist.

// Speicher freigeben für ein zweidimensionales dynamisches Array: for (int count = 0; count< 2; count++) delete ptrarray; // где 2 – количество строк в массиве

Das Deklarieren und Löschen eines zweidimensionalen dynamischen Arrays erfolgt mit einer Schleife, wie oben gezeigt. Sie müssen verstehen und sich daran erinnern, wie dies geschieht. Lassen Sie uns ein Programm entwickeln, in dem wir ein zweidimensionales dynamisches Array erstellen.

// new_delete_array2.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #include "stdafx.h" #include #enthalten #enthalten < 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

// Code Code :: Blöcke Block

// Dev-C++-Code

// new_delete_array2.cpp: definiert den Einstiegspunkt für die Konsolenanwendung. #enthalten #enthalten #enthalten #enthalten Verwenden des Namensraums std; int main (int argc, char * argv) (srand (time (0)); // Zufallszahlen generieren // dynamisch ein zweidimensionales Array reeller Zahlen für zehn Elemente erstellen float ** ptrarray = new float *; // zwei Strings im Array für (int count = 0; count< 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

Bei der Anzeige des Arrays wurde die Funktion setw() verwendet, wenn Sie es nicht vergessen haben, dann weist sie den Ausgabedaten einen Speicherplatz einer bestimmten Größe zu. In unserem Fall gibt es für jedes Element des Arrays vier Positionen. Dies ermöglicht es Ihnen, Nummern unterschiedlicher Länge nach Spalten auszurichten (siehe Abbildung 3).

2,7 10 0,33 3 1,4 6 0,67 0,86 1,2 0,44 Drücken Sie eine beliebige Taste, um fortzufahren. ... ...

Abbildung 3 - Dynamisches Array in C ++

Fortsetzung des Themas:
Geräte

Freie Sonnenstrahlen effizient in Energie umzuwandeln, mit der Haushalte und andere Einrichtungen mit Strom versorgt werden können, ist der gehegte Traum vieler Verteidiger grüner ...