Warum können wir Arrays löschen, kennen aber die Länge in C/C++ nicht?

Lesezeit: 10 Minuten

Benutzer-Avatar
Fallbash

Wie kommt es, dass es uns möglich ist, dynamisch zugewiesene Arrays zu löschen, aber wir können nicht herausfinden, wie viele Elemente sie haben? Können wir nicht einfach die Größe des Speicherplatzes durch die Größe jedes Objekts teilen?

  • Beachten Sie einen wichtigen Unterschied: Die Größe des zugewiesenen Speicherblocks ist NICHT gleich der Größe des Arrays; Das System kann Blöcke in Einheiten von (sagen wir) 1K zuweisen, aber Sie haben möglicherweise nur nach einem 3-Elemente-Array gefragt.

    – Andreas Jaffe

    22. Oktober 2010 um 8:29 Uhr

  • Das ist irgendwie ähnlich: stackoverflow.com/questions/3886539/…

    – ruslik

    22. Oktober 2010 um 8:39 Uhr

  • Ich denke nicht, dass dies hätte geschlossen werden sollen – das “Mögliche Duplikat” über der Frage und der “irgendwie ähnliche” Link von ruslik decken beide C ab, aber nicht C ++, und verlieren daher die gesamte Diskussion über # Destruktoraufrufe für Arrays.

    – Toni Delroy

    25. Oktober 2010 um 0:53 Uhr

  • @ TonyD Ich stimme zu, ich habe für die Wiedereröffnung nominiert.

    – Jonathan Mei

    26. Februar 2016 um 14:12 Uhr

Benutzer-Avatar
Toni Delroy

In C++ sind beide …

  • die Größe (Bytes), die von einem neuen, neuen angefordert wird[] oder malloc-Aufruf und
  • die Anzahl der angeforderten Array-Elemente in einem neuen[] dynamische Zuordnung

… sind Implementierungsdetails, die der Standard nicht programmgesteuert zur Verfügung stellen muss, obwohl sich die Speicherzuweisungsbibliothek an ersteres und der Compiler an letzteres erinnern muss, damit der Destruktor für die richtige Anzahl von Elementen aufgerufen werden kann.

Manchmal erkennt der Compiler möglicherweise, dass es eine Zuweisung konstanter Größe gibt, und kann sie zuverlässig mit der entsprechenden Aufhebung der Zuweisung verknüpfen, sodass er Code generieren kann, der für diese zur Kompilierzeit bekannten Werte angepasst ist (z. und beim Umgang mit externen Eingaben) muss ein Compiler möglicherweise die #-Elemente zur Laufzeit speichern und abrufen: Genügend Platz für den #element-Zähler könnte z. B. unmittelbar vor oder nach der für den Array-Inhalt zurückgegebenen Adresse mit delete gesetzt werden[] Kenntnis von dieser Konvention. In der Praxis kann sich ein Compiler dafür entscheiden, dies immer zur Laufzeit zu handhaben, nur wegen der Einfachheit, die mit der Konsistenz einhergeht. Es gibt andere Laufzeitmöglichkeiten: Beispielsweise könnten die #-Elemente aus einer gewissen Einsicht in den spezifischen Speicherpool, aus dem die Zuweisung erfüllt wurde, kombiniert mit der Objektgröße abgeleitet werden.

Der Standard bietet keinen programmgesteuerten Zugriff, um sicherzustellen, dass Implementierungen in den Optimierungen (in Bezug auf Geschwindigkeit und/oder Speicherplatz), die sie möglicherweise verwenden, uneingeschränkt sind.

(Die Größe des Speicherplatzes kann größer sein als die genaue Größe, die für die angeforderte Anzahl von Elementen erforderlich ist – diese Größe wird von der Speicherzuweisungsbibliothek gespeichert, die eine vom C++-Compiler unabhängige Blackbox-Bibliothek sein kann).

  • Obwohl ich der Erklärung zustimme, bedeutet dies, dass die Anzahl der Elemente bekannt sein muss, da die Destruktoren ausgeführt werden sollen. Dass es keine Standard-API zum Abrufen gibt, scheint von C geerbt zu sein, wo es keinen Destruktor zum Ausführen gab und daher nur die zugewiesene Größe eine Rolle spielte.

    – Matthias M.

    22. Oktober 2010 um 8:44 Uhr

  • @Matthieu M.: Es stimmt, dass C++ von C geprägt wurde, obwohl sich C++ basierend auf Nachfrage, Gelegenheit, Feedback und Problemlösung davon weg entwickelt hat. Wenn Programmierer konsequent danach gefragt hätten – und den Nutzen beweisen könnten – wäre es schon vor langer Zeit verfügbar gewesen, vorausgesetzt, die Implementierer hätten nicht lauter über die Arbeit oder bestehende Optimierungen gebrüllt. Wie auch immer, es lebe std::vector() :-).

    – Toni Delroy

    22. Oktober 2010 um 8:57 Uhr

  • @Matthieu: “Da die Destruktoren ausgeführt werden sollen, bedeutet dies, dass die Anzahl der Elemente bekannt sein muss” – wenn der enthaltene Typ des Arrays einen Destruktor hat, der irgendetwas tut. Als Optimierung könnte eine C++-Implementierung die Anzahl der angeforderten Elemente für a speichern std::string[]aber nicht für eine speichern int[]. Ich nehme an, dass eine API zum Abrufen der Größe entweder formulieren müsste, wann es funktioniert und wann nicht, oder sonst die Optimierung verbieten würde.

    – Steve Jessop

    22. Oktober 2010 um 12:59 Uhr


Der Speicherzuordner merkt sich die Größe der Zuordnung, gibt sie aber nicht an den Benutzer weiter. Dies gilt in C mit malloc und in C++ mit new.

“Die Größe des Speicherplatzes” kann nicht abgerufen werden. Wenn Sie tun

int *a = new int[N];
std::cout << sizeof(a);

Sie werden feststellen, dass es druckt sizeof(int *)die konstant ist (für eine bestimmte Plattform).

Der übliche Weg in C++ ist die Verwendung std::vector statt Array.

std::vector hat die Methode size was die Anzahl der Elemente zurückgibt.

Wenn möglich, sollten Sie lieber verwenden std::vector anstelle von Array, wo immer möglich.

  • +1 – Bei C++ dreht sich alles darum, dass Probleme von Bibliotheksautoren gelöst werden können.

    – Daniel Earwicker

    22. Oktober 2010 um 8:37 Uhr

Der Grund ist, dass die C-Sprachen Geben Sie diese Informationen nicht preisobwohl es könnte für die spezifische Implementierung verfügbar sein. (Tatsächlich für Array new[] in C++ muss die Größe nachverfolgt werden, um die Destruktoren für jedes Objekt aufzurufen – aber wie dies geschieht, hängt vom jeweiligen Compiler ab.)

Der Grund für diese Geheimhaltung ist, dass Compiler-Autoren und Plattform-Implementierer mehr Freiheit haben wie Sie implementieren Speicherzuweisungen mit variabler Größe. Es ist auch nicht notwendig, diese Informationen im Allgemeinen zu kennen, daher wäre es nicht sinnvoll, sie zu kennen benötigen jede C-Plattform, um diese Informationen verfügbar zu machen.

Auch ein praktischer Grund (z malloc et al.) ist, dass sie gebe dir nicht, worum du gebeten hast: Wenn Sie malloc nach 30 Byte Speicher fragen, erhalten Sie höchstwahrscheinlich 32 Byte (oder eine andere größere Zuordnungsgranularität). Die einzigen intern verfügbaren Informationen sind also die 32 Bytes, und Sie als Programmierer haben für diese Informationen nicht viel Verwendung.

Zwei Dinge sprechen dagegen

  1. Erstens sind Arrays und Zeiger austauschbar – ein Array hat kein zusätzliches Verständnis seiner Länge. ( * Alle klugen Kommentatoren, die versucht sind, die grundlegenden Unterschiede zwischen Arrays und Zeigern zu kommentieren, sollten beachten, dass dies in Bezug auf diesen Punkt keinen Unterschied macht 😉 * )

  2. zweitens, weil das Wissen um die Größe der Zuordnung die Aufgabe des Heaps ist und der Heap keine Standardmethode zum Ermitteln der Größe der Zuordnung bietet.

Symbian hat jedoch eine AllocSize() Funktion, aus der Sie ableiten können, wie viele Elemente sich im Array befinden. Jedochsind Zuweisungen manchmal größer als angefordert, da der Speicher in wortausgerichteten Blöcken verwaltet wird.

Sie können ganz einfach eine Klasse erstellen, um die Anzahl der Zuweisungen zu verfolgen.

der Grund, warum wir die Länge nicht kennen, ist, weil sie es hat stets war ein Implementierungsdetail (afaik). Der Compiler kennt die Ausrichtung der Elemente, und das abi beeinflusst auch, wie es implementiert wird.

Beispielsweise speichert itanium 64 abi das Cookie (Elementanzahl) in den führenden Bytes der Zuordnung (insbesondere Nicht-POD) und füllt es dann bei Bedarf auf die natürliche Ausrichtung der Objekte auf. Sie werden dann zurückgegeben (von new[]) die Adresse des ersten verwendbar -Element und nicht die Adresse der tatsächlichen Zuordnung. Es gibt also eine Menge nicht tragbarer Buchhaltung.

eine Wrapper-Klasse ist der einfache Weg, dies zu verwalten.

Es ist tatsächlich eine interessante Übung, Allokatoren zu schreiben, außer Kraft zu setzen Objekt::new/delete, Platzierungsoperatoren und schauen Sie sich an, wie das alles zusammenpasst (obwohl es keine besonders triviale Übung ist, wenn Sie möchten, dass der Allocator im Produktionscode verwendet wird).

Kurz gesagt, wir kennen die Größe der Speicherzuweisung nicht, und es ist aufwändiger, die Zuweisungsgröße (neben anderen erforderlichen Variablen) konsistent über mehrere Plattformen hinweg herauszufinden, als eine benutzerdefinierte Vorlagenklasse zu verwenden, die einen Zeiger und enthält a size_t.

Darüber hinaus gibt es keine Garantie dafür, dass der Zuordner genau die Anzahl der angeforderten Bytes zugewiesen hat (daher könnten Ihre Zählungen falsch sein, wenn Sie die Anzahl basierend auf der Zuweisungsgröße bestimmen). Wenn Sie malloc-Schnittstellen durchlaufen, sollten Sie in der Lage sein, Ihre Zuordnung zu finden … aber das ist immer noch nicht sehr nützlich, portabel oder sicher für jeden nicht trivialen Fall.

Aktualisieren:

@Default Es gibt viele Gründe, eine eigene Schnittstelle zu erstellen. wie Toni schon erwähnt hat, std::vector ist eine bekannte Implementierung. Die Basis für einen solchen Wrapper ist einfach (Schnittstelle entlehnt von std::vector:

/* holds an array of @a TValue objects which are created at construction and destroyed at destruction. interface borrows bits from std::vector */
template<typename TValue>
class t_array {
    t_array(const t_array&); // prohibited
    t_array operator=(const t_array&); // prohibited
    typedef t_array<TValue>This;
public:
    typedef TValue value_type;
    typedef value_type* pointer;
    typedef const value_type* const_pointer;
    typedef value_type* const pointer_const;
    typedef const value_type* const const_pointer_const;
    typedef value_type& reference;
    typedef const value_type& const_reference;

    /** creates @a count objects, using the default ctor */
    t_array(const size_t& count) : d_objects(new value_type[count]), d_count(count) {
        assert(this->d_objects);
        assert(this->d_count);
    }

    /** this owns @a objects */
    t_array(pointer_const objects, const size_t& count) : d_objects(objects), d_count(count) {
        assert(this->d_objects);
        assert(this->d_count);
    }

    ~ t_array() {
        delete[] this->d_objects;
    }

    const size_t& size() const {
        return this->d_count;
    }

    bool empty() const {
        return 0 == this->size();
    }

    /* element access */
    reference at(const size_t& idx) {
        assert(idx < this->size());
        return this->d_objects[idx];
    }

    const_reference at(const size_t& idx) const {
        assert(idx < this->size());
        return this->d_objects[idx];
    }

    reference operator[](const size_t& idx) {
        assert(idx < this->size());
        return this->d_objects[idx];
    }

    const_reference operator[](const size_t& idx) const {
        assert(idx < this->size());
        return this->d_objects[idx];
    }

    pointer data() {
        return this->d_objects;
    }

    const_pointer data() const {
        return this->d_objects;
    }

private:
    pointer_const d_objects;
    const size_t d_count;
};

so nützlich wie std::vector Es gibt einige Fälle, in denen es nützlich sein kann, eigene Basen zu erstellen:

  • um ein Objekt mit einer kleineren Schnittstelle zu erstellen. Minimalismus ist gut.
  • um ein Objekt zu erstellen, das keinen Zuordner benötigt. zum Beispiel: t_array führt zu weniger exportierten Symbolen sowie zu kürzeren Namen für diese Symbole (durch Entfernen des allocator-Arguments).
  • um eine Variante zu erstellen, die zusätzliche const-Fälle behandelt. Im obigen Beispiel gibt es oft wenig Grund, das zu ändern, worauf der Container zeigt. also oben t_array verwendet 2 konstante Mitglieder, die jeweils weniger Variationen gewährleisten als std::vector. Ein guter Optimierer sollte sich diese Details zunutze machen. Es verhindert auch, dass Benutzer versehentlich Fehler machen.
  • Bauzeiten zu verkürzen. wenn Ihre Bedürfnisse so einfach sind wie t_arrayoder noch einfacher, Sie können Ihre Build-Zeiten verkürzen, indem Sie eine minimale Schnittstelle verwenden.

Andere Fälle:

  • um ein Objekt mit einer größeren Schnittstelle oder mehr Funktionen zu erstellen
  • um ein Objekt mit zusätzlichen Debugging-Möglichkeiten zu erstellen
  • um ein Objekt zu erstellen, das in Unterklassen unterteilt werden kann (die meisten Implementierungen von std::vector sollen nicht unterklassiert werden)
  • um ein Objekt Thread-sicher zu machen

Benutzer-Avatar
Lorenzo Stella

Es ist alles perfekt in der “Keep it simple”-Philosophie von C: Sie MÜSSEN an einem Punkt entschieden haben, welche Größe das Array / der Puffer / was auch immer benötigt wird; Also behalte diesen Wert und das war’s. Warum einen Funktionsaufruf verschwenden, um bereits vorhandene Informationen abzurufen?

  • Weil es auch mühsam ist, etwas in der Nähe zu behalten, und wenn es zweimal gespeichert wird, verschwendet es Speicherplatz, was ebenfalls nicht C/C++-ähnlich ist. Sie müssen zugeben, dass es einige Vorteile hat, es von dort abzurufen, wo die Implementierung es gespeichert hat. Der “Funktionsaufruf” kann trotzdem Inline-fähig sein – je nach Implementierung.

    – Toni Delroy

    22. Oktober 2010 um 10:31 Uhr

1176160cookie-checkWarum können wir Arrays löschen, kennen aber die Länge in C/C++ nicht?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy