Arraygröße bestimmen lassen



  • Hallo ihr Lieben,
    ich meine, im Buch gelesen zu haben, dass ich die Arraygröße bestimmen kann, in dem ich wie folgt vorgehe:

    // Online IDE - Code Editor, Compiler, Interpreter

    #include<iostream>
    using namespace std;

    double foo(int a[])
    {
    double erg = 0;
    int n = sizeof(a) / sizeof(a[0]); // Um diese Zeile geht es mir
    for(int i=0; i<n; i++)
    {
    erg += a[i];
    }
    return erg/n;
    }

    int main()
    {
    int array[] = {8, 5, 9, 3, 10, 4};

    cout << foo(array);

    return 0;
    

    }

    Wenn ich diese Zeile in der main-Funktion eingebe, dann passt das und es kommen vier Arrayplätze raus. Aber in der Funktion gibt er mir gerade eine komische Rückmeldung:
    "warning: sizeof an array function parameter 'a' will return size of 'int*' [-Wsizeof-array-argument]

    Was genau ist mein Fehler?



  • Das int a[] zerfällt bei der Übergabe als Parameter zu einem int*, und daraus lässt sich die Länge des Arrays nicht mehr bestimmen. Mit einem Funktionstemplate geht das allerdings wieder:

    template<typename T, unsigned int N>
    std::size_t array_length( T(&)[N] )
    {
       return N;
    }
    
    int main()
    {
       int a[4];
       std::size_t s = array_length( a );
    }
    

    Bei der Übergabe an die template Funktion zerfällt das Array nicht, sondern wird als Referenz auf ein int-Array mit 4 Elementen übergeben. Anhand des N-Parameters weiß die template-Funktion, dass das Array 4 Elemente lang ist.



  • In der Funktion ist int a[] kein Array mehr, sondern nur noch ein Zeiger (d.h. äquivalent zu int* a - wie es auch in der Warnung steht) und somit kommt dann bei der sizeof-Berechnung nicht das von dir erwartete Ergebnis heraus.
    Übergebe explizit die Arraygröße als weiteren Parameter.

    Oder benutze besser std::array<T, N> - diese kennt die Größe.


  • Mod

    @DocShoe sagte in Arraygröße bestimmen lassen:

    Mit einem Funktionstemplate geht das allerdings wieder:

    @Th69 sagte in Arraygröße bestimmen lassen:

    Oder benutze besser std::array<T, N> - diese kennt die Größe.

    Warum schlechte Ansätze retten? Richtig und üblich wäre doch, nicht die Daten zu übergeben, sondern einen Iterator auf Anfang und Ende der Daten. Dann kann die gleiche Funktion auch den Durchschnittswert eines jeden beliebigen Containers berechnen. Wenn DocShoe schon die Templates zückt (und std::array benötigt ja auch Templatesyntax), dann ist das auch nicht schwieriger als die hier gezeigten Lösungen.



  • Oder wenns erstmal einfacher sein soll: schmeiß das array weg und nimm einen vector:

    #include<iostream>
    #include<vector>
    using namespace std;
    
    double foo(const std::vector<int> &a) {
        double erg = 0;
        for(int i=0; i<a.size(); i++) {
            erg += a[i];
        }
        return erg/a.size();
    }
    
    int main() {
        std::vector<int> array = {8, 5, 9, 3, 10, 4};
        std::cout << foo(array);
    }
    

    Das ist vielleicht einfacher als Templates. (es verwendet sie nur, macht aber keine eigenen).

    Es sei noch erwähnt, dass die Summe auch mit std::accumulate gebildet werden könnte.

    Und es sei auch noch erwähnt, dass diese Art schleifen mit i als Index auch durch range-Based-Loops ersetzt werden könnten:

    double foo(const std::vector<int> &a) {
        double erg = 0;
        for (int value : a) {
            erg += value;
        }
        return erg/a.size();
    }
    

    Aber generell schau dir mal an, wie die standardlib-Funktionen wie accumulate das machen (wie @SeppJ schon sagte).


  • Mod

    @wob sagte in Arraygröße bestimmen lassen:

    Oder wenns erstmal einfacher sein soll: schmeiß das array weg und nimm einen vector:

    Gleiches Problem: Wenn wir sowieso Templates nutzen, warum dann nicht gleich universell?

    #include<iostream>
    #include<iterator>
    
    template <typename Iterator>
    double foo(Iterator begin, Iterator end)
    {
      // Besser und universeller wäre es noch, die Typen für die Rückgabe und Zwischenergebnisse per decltype zu bestimmen
      double erg = 0;
      int n = 0;
      for(Iterator it = begin; it != end; ++it)
      {
        erg += *it;
        ++n;
      }
      return erg/n;
    }
    
    int main()
    {
      int array[] = {8, 5, 9, 3, 10, 4};
      std::cout << foo(std::begin(array), std::end(array));
      return 0;
    }
    


  • @SeppJ sagte in Arraygröße bestimmen lassen:

    Gleiches Problem: Wenn wir sowieso Templates nutzen, warum dann nicht gleich universell?

    weil einfacher. Es ist ein massiver Schwierigkeitsunterschied zwischen "Template benutzen" und "Template selber schreiben". Sowas wie vector<int> kann man als "einfachen" Typen ansehen und muss sich keine Gedanken machen, dass das ein Template ist.

    Nur als Beispiele: du musst das Iterator-Konzept kennen, du musst den * kennen, du hast keine Vervollständigung im Editor, wenn du it. schreibst etc. Du musst die Template-Syntax verstehen. Du musst die Funktion sichtbar in der aktuellen Übersetzungseinheit haben. Für Anfänger sind das gleich sehr viele Hürden.



  • Der C++-Kompilierer macht aus Deinem Array-Argument array einen Zeiger, wenn Du es lokal an eine Funktion übergibst(1).
    Der Zeiger zeigt dann auf das erste Element von Deinem Array, die 8.

    In C++ nennt man lokale Variablen auch "automatische Variablen", da sie zum Beispiel vom Stack auf und ab gestapelt werden von alleine.
    Automatische Variablen sind nur sichtbar innerhalb eines {...}-Klammerpaars.
    Man spricht auch hier von "Gültigkeitsbereich" oder Scope.

    Jetzt hast Du angegeben in Deiner foo-Funktion, dass Du int a[] erwartest.
    Die Groesse wird damit nicht mitgegeben, weil Du sie unspezifiziert gelassen hast.
    int a[] oder int a[10] versteht der C++-Kompilierer als int* a.
    Kein Unterschied.
    Obschon man doch die Groesse angibt mit int a[10].
    Das mag etwas unintuitiv sein, aber auch nicht verwunderlich(1).

    Was Du machen kannst, ist (eine Möglichkeit von vielen):

    #include <iostream>
    using namespace std;
    
    // Lies: "Zeiger auf ein Feld/Array von 6 ints".
    double foo(int (*a)[6]) {
        double erg = 0;
        // Hier musst Du nun auf das Array mit dem Addresszugriffs- oder Dereferenzierungsoperator `*` vorgehen:
        int n = sizeof(*a) / sizeof((*a)[0]);
        for (int i = 0; i < n; i++) {
            erg += (*a)[i];
        }
        return erg / n;
    }
    
    int main() {
        int array[] = {8, 5, 9, 3, 10, 4};
        cout << foo(&array);
        return 0;
    }
    
    

    Also mit dem Zeiger auf das Array int (*zeiger_auf_array)[42], sagst Du dem Kompilierer, dass ich nur auf Typen zeigen möchte, die vom Typ int [42] sind.
    Du kannst also nicht einfach auf int [41] ohne weiteres (zum Beispiel explizite Typumwandlung) zeigen.

    Ich hoffe, ich war verständlich. 🙂

    Referenz:

    1. C++98-Standard
       4 Standard conversions                                                                                                [conv]
    1   Standard conversions are implicit conversions defined for built-in types. Clause 4 enumerates the full set of
       such conversions. A standard conversion sequence is a sequence of standard conversions in the following
       order:
       -- Zero or one conversion from the following set: lvalue-to-rvalue conversion, array-to-pointer conversion,
          and function-to-pointer conversion.
    
       4.2 Array-to-pointer conversion                                                                                      [conv.array]
    1   An lvalue or rvalue of type "array of N T" or "array of unknown bound of T" can be converted to an rvalue
       of type "pointer to T." The result is a pointer to the first element of the array.
    
    -- Parameter declarations that differ only in a pointer * versus an array [] are equivalent. That is, the
       array declaration is adjusted to become a pointer declaration (8.3.5). Only the second and subsequent
       array dimensions are significant in parameter types (8.3.4).
    


  • Alles richtig, aber:
    Wenn meine Funktionssignatur:

    double foo(int (*a)[6])
    

    lautet, dann benötige ich innerhalb der Funktion nicht mehr

    int n = sizeof(*a) / sizeof((*a)[0]);
    

    um die Elementanzahl zu bestimmen, oder?


  • Mod

    @Belli sagte in Arraygröße bestimmen lassen:

    Alles richtig, aber:
    Wenn meine Funktionssignatur:

    double foo(int (*a)[6])
    

    lautet, dann benötige ich innerhalb der Funktion nicht mehr

    int n = sizeof(*a) / sizeof((*a)[0]);
    

    um die Elementanzahl zu bestimmen, oder?

    Ja, es ist praktisch sinnlos, es zeigt nur, wie man es prinzipiell machen würde, wenn man wollte. Kann man natürlich auch mit Templates kombinieren (der Templateparameter N wird beim Aufruf so auch automatisch bestimmt und braucht nicht angegeben werden):

    template <size_t N> double foo(int (&array)[N])  // ....
    

    Aber dann ist man halt wieder bei "Wenn man sowieso schon Templates nutzt…" von oben



  • @Hawaiihemd, falls Du magst, kannst Du auch mal sehen wie der C++-Kompilierer Deinen geschriebenen Code sieht:

    https://cppinsights.io/lnk?code=I2luY2x1ZGU8aW9zdHJlYW0+CnVzaW5nIG5hbWVzcGFjZSBzdGQ7Cgpkb3VibGUgZm9vKGludCBhW10pCnsKZG91YmxlIGVyZyA9IDA7CmludCBuID0gc2l6ZW9mKGEpIC8gc2l6ZW9mKGFbMF0pOyAvLyBVbSBkaWVzZSBaZWlsZSBnZWh0IGVzIG1pcgpmb3IoaW50IGk9MDsgaTxuOyBpKyspCnsKZXJnICs9IGFbaV07Cn0KcmV0dXJuIGVyZy9uOwp9CgppbnQgbWFpbigpCnsKaW50IGFycmF5W10gPSB7OCwgNSwgOSwgMywgMTAsIDR9OwoKY291dCA8PCBmb28oYXJyYXkpOwoKcmV0dXJuIDA7Cgp9&insightsOptions=cpp17&std=cpp17&rev=1.0

    Ausgabe ist:

    #include<iostream>
    using namespace std;
    
    double foo(int * a)
    {
      double erg = 0;
      int n = static_cast<int>(sizeof(a) / sizeof(a[0]));
      for(int i = 0; i < n; i++) {
        erg = erg + static_cast<double>(a[i]);
      }
      
      return erg / static_cast<double>(n);
    }
    
    
    int main()
    {
      int array[6] = {8, 5, 9, 3, 10, 4};
      std::cout.operator<<(foo(array));
      return 0;
    }
    

    Musst auf "Abspielen" oder "Play" drücken. 😉



  • @SeppJ sagte in Arraygröße bestimmen lassen:

    Richtig und üblich wäre doch, nicht die Daten zu übergeben, sondern einen Iterator auf Anfang und Ende der Daten.

    In Zeiten von C++20 (sofern man das verwenden kann), lässt sich dieser universelle Ansatz sogar noch weiter treiben. Da kann man sogar auf die Template-Funktion verzichten. Nur so als Anregung:

    #include <iostream>
    #include <ranges>
    #include <vector>
    #include <array>
    #include <span>
    
    void print(const std::ranges::input_range auto& range)
    {
        for (auto value : range)
            std::cout << value << "\n";
    }
    
    auto main() -> int
    {
        int a[] = { 1, 2, 3 };
        std::vector<int> b = { 4, 5, 6 };
        std::array<int, 3> c = { 7, 8, 9 };
    
        print(a);
        print(b);
        print(c);
    
        auto begin = std::begin(a);
        auto end = std::end(a);
    
        print(std::span{ begin, end });
    }
    

  • Mod

    @Finnegan print ist ein (abbreviated) Funktionstemplate (das, was Du als Template Funktion bezeichnest). Und inwiefern auto main() -> int eine stilistische Verbesserung sein soll, das musst Du mal erklaeren... sonst stimme ich zu, dass diese Art es zu schreiben moderner ist. Ich wuerde uebrigens value als Referenz definieren.



  • @Columbo sagte in Arraygröße bestimmen lassen:

    @Finnegan print ist ein (abbreviated) Funktionstemplate (das, was Du als Template Funktion bezeichnest).

    Ja, das ist schon klar, das da unter der Haube ein Template ist, aber es sieht auf den ersten Blick nicht so aus und hat weniger Code-Text. Daher halte ich es für geeigneter für Anfänger, da man das über die Schiene "implementiert das Input Range Konzept" erklären kann, ohne gleich Templates, deren Argumente und Deduktion einführen zu müssen.

    Und inwiefern auto main() -> int eine stilistische Verbesserung sein soll, das musst Du mal erklaeren...

    Interessant, dass du dich daran stösst 😁 ... das kann ich erklären: Die auto f() -> type-Syntax hat den grossen Vorteil, dass alle Funktionsnamen in der selben Editorspalte beginnen. Die Namen sind m.E. die wichtigste Information, und das konsequent zu machen macht meiner Meinung nach den Code besser lesbar. Spätestens dann, wenn es umfangreicher wird. Eine Ausnahme mache ich bei void, weil auto f() -> void finde selbst ich dann auch ein wenig affig 🙂

    sonst stimme ich zu, dass diese Art es zu schreiben moderner ist. Ich wuerde uebrigens value als Referenz definieren.

    Ist ein simples Beispiel. Je mehr solche Feinheiten, umso verwirrender für jemanden, der da zum ersten mal sieht IMHO. Aber stimmt natürlich.


Log in to reply