Warum erlauben C- und C ++ - Compiler Array-Längen in Funktionssignaturen, wenn sie nie erzwungen werden?

131

Folgendes habe ich während meiner Lernphase gefunden:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

So in den Variablen int dis(char a[1]), das [1]scheint nichts zu tun und nicht funktioniert
alles, weil ich verwenden kann a[2]. Genau wie int a[]oder char *a. Ich weiß, dass der Array-Name ein Zeiger ist und wie man ein Array vermittelt, daher geht es in meinem Puzzle nicht um diesen Teil.

Ich möchte wissen, warum Compiler dieses Verhalten zulassen ( int a[1]). Oder hat es andere Bedeutungen, von denen ich nichts weiß?

Fanl
quelle
6
Das liegt daran, dass Sie Arrays nicht an Funktionen übergeben können.
Ed S.
37
Ich denke, die Frage hier war, warum Sie mit C einen Parameter als Array-Typ deklarieren können, wenn er sich sowieso nur genau wie ein Zeiger verhält.
Brian
8
@Brian: Ich bin nicht sicher, ob dies ein Argument für oder gegen das Verhalten ist, aber es gilt auch, wenn der Argumenttyp ein typedefwith-Array-Typ ist. So den „Zerfall Zeiger“ in Argumenttypen ist nicht nur syntaktischer Zucker ersetzt []mit *, es ist wirklich das Typsystem durchlaufen. Dies hat reale Konsequenzen für einige Standardtypen wie va_listdiesen, die mit Array- oder Nicht-Array-Typ definiert werden können.
R .. GitHub STOP HELPING ICE
4
@songyuanyao Mit einem Zeiger können Sie in C (und C ++) etwas erreichen, das nicht ganz unähnlich ist : int dis(char (*a)[1]). Dann übergeben Sie einen Zeiger auf ein Array : dis(&b). Wenn Sie bereit sind, C-Funktionen zu verwenden, die in C ++ nicht vorhanden sind, können Sie auch Dinge wie void foo(int data[static 256])und sagen int bar(double matrix[*][*]), aber das ist eine ganz andere Dose Würmer.
Stuart Olsen
1
@StuartOlsen Der Punkt ist nicht, welcher Standard was definiert hat. Der Punkt ist, warum derjenige, der es definiert hat, es so definiert hat.
user253751

Antworten:

156

Es ist eine Eigenart der Syntax zum Übergeben von Arrays an Funktionen.

Tatsächlich ist es nicht möglich, ein Array in C zu übergeben. Wenn Sie eine Syntax schreiben, die so aussieht, als ob sie das Array übergeben sollte, passiert tatsächlich, dass stattdessen ein Zeiger auf das erste Element des Arrays übergeben wird.

Da der Zeiger keine Längeninformationen enthält, wird der Inhalt Ihrer []Funktion in der Liste der formalen Funktionsparameter tatsächlich ignoriert.

Die Entscheidung, diese Syntax zuzulassen, wurde in den 1970er Jahren getroffen und hat seitdem viel Verwirrung gestiftet ...

MM
quelle
21
Als Nicht-C-Programmierer finde ich diese Antwort sehr zugänglich. +1
Asteri
21
+1 für "Die Entscheidung, diese Syntax zuzulassen, wurde in den 1970er Jahren getroffen und hat seitdem viel Verwirrung gestiftet ..."
NoSenseEtAl
8
Dies ist wahr, aber es ist auch möglich, ein Array mit genau dieser Größe mithilfe der void foo(int (*somearray)[20])Syntax zu übergeben. In diesem Fall wird 20 auf den Anruferseiten erzwungen.
v.oddou
14
-1 Als C-Programmierer finde ich diese Antwort falsch. []werden in mehrdimensionalen Arrays nicht ignoriert, wie in der Antwort von pat gezeigt. Daher war die Einbeziehung der Array-Syntax erforderlich. Darüber hinaus hindert nichts den Compiler daran, Warnungen auch auf eindimensionalen Arrays auszugeben.
user694733
7
Mit "dem Inhalt Ihres []" spreche ich speziell über den Code in der Frage. Diese Syntax-Eigenart war überhaupt nicht erforderlich. Dasselbe kann durch Verwendung der Zeigersyntax erreicht werden. Wenn also ein Zeiger übergeben wird, muss der Parameter ein Zeigerdeklarator sein. ZB in Pats Beispiel hat void foo(int (*args)[20]);C streng genommen auch keine mehrdimensionalen Arrays; aber es hat Arrays, deren Elemente andere Arrays sein können. Das ändert nichts.
MM
143

Die Länge der ersten Dimension wird ignoriert, aber die Länge der zusätzlichen Dimensionen ist erforderlich, damit der Compiler Offsets korrekt berechnen kann. Im folgenden Beispiel wird der fooFunktion ein Zeiger auf ein zweidimensionales Array übergeben.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

Die Größe der ersten Dimension [10]wird ignoriert. Der Compiler hindert Sie nicht daran, das Ende zu indizieren (beachten Sie, dass das Formal 10 Elemente benötigt, das tatsächliche jedoch nur 2). Die Größe der zweiten Dimension [20]wird jedoch verwendet, um den Schritt jeder Zeile zu bestimmen, und hier muss das Formale mit dem tatsächlichen übereinstimmen. Auch hier hindert Sie der Compiler nicht daran, das Ende der zweiten Dimension zu indizieren.

Der Byte-Offset von der Basis des Arrays zu einem Element args[row][col]wird bestimmt durch:

sizeof(int)*(col + 20*row)

Beachten Sie, dass Sie, wenn col >= 20, dann tatsächlich in eine nachfolgende Zeile (oder am Ende des gesamten Arrays) indizieren.

sizeof(args[0]), kehrt 80auf meiner Maschine wo zurück sizeof(int) == 4. Wenn ich jedoch versuche zu nehmen sizeof(args), erhalte ich die folgende Compiler-Warnung:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

Hier warnt der Compiler, dass er nur die Größe des Zeigers angeben wird, in den das Array zerfallen ist, anstatt die Größe des Arrays selbst.

klopfen
quelle
Sehr nützlich - die Übereinstimmung damit ist auch als Grund für die Eigenart im 1-D-Fall plausibel.
JWG
1
Es ist die gleiche Idee wie im 1-D-Fall. Was in C und C ++ wie ein 2-D-Array aussieht, ist eigentlich ein 1-D-Array, von dem jedes Element ein anderes 1-D-Array ist. In diesem Fall haben wir ein Array mit 10 Elementen, von denen jedes Element "Array von 20 Zoll" ist. Wie in meinem Beitrag beschrieben, wird der Zeiger auf das erste Element von tatsächlich an die Funktion übergeben args. In diesem Fall ist das erste Element von args ein "Array von 20 Zoll". Zeiger enthalten Typinformationen; Was übergeben wird, ist "Zeiger auf ein Array von 20 Zoll".
MM
9
Ja, das ist der int (*)[20]Typ; "Zeiger auf ein Array von 20 Zoll".
Pat
33

Das Problem und wie man es in C ++ löst

Das Problem wurde ausführlich von pat und Matt erklärt . Der Compiler ignoriert grundsätzlich die erste Dimension der Array-Größe und ignoriert effektiv die Größe des übergebenen Arguments.

In C ++ hingegen können Sie diese Einschränkung auf zwei Arten leicht überwinden:

  • unter Verwendung von Referenzen
  • using std::array(seit C ++ 11)

Verweise

Wenn Ihre Funktion nur versucht, ein vorhandenes Array zu lesen oder zu ändern (nicht zu kopieren), können Sie problemlos Referenzen verwenden.

Angenommen, Sie möchten eine Funktion, die ein Array von zehn Sekunden zurücksetzt intund jedes Element auf setzt 0. Sie können dies einfach mit der folgenden Funktionssignatur tun:

void reset(int (&array)[10]) { ... }

Dies funktioniert nicht nur einwandfrei , sondern erzwingt auch die Dimension des Arrays .

Sie können auch Vorlagen verwenden , um den obigen Code generisch zu gestalten :

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

Und schließlich können Sie die constKorrektheit nutzen. Betrachten wir eine Funktion, die ein Array von 10 Elementen druckt:

void show(const int (&array)[10]) { ... }

Durch Anwenden des constQualifikators verhindern wir mögliche Änderungen .


Die Standardbibliotheksklasse für Arrays

Wenn Sie die obige Syntax wie ich für hässlich und unnötig halten, können wir sie in die Dose werfen und std::arraystattdessen verwenden (seit C ++ 11).

Hier ist der überarbeitete Code:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

Ist es nicht wunderbar? Ganz zu schweigen davon, dass der generische Code-Trick, den ich Ihnen zuvor beigebracht habe, immer noch funktioniert:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Darüber hinaus erhalten Sie kostenlos Kopier- und Verschiebungssemantiken. :) :)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Also, worauf wartest Du? Geh und benutze std::array.

Schuh
quelle
2
@kietz, es tut mir leid, dass Ihre vorgeschlagene Bearbeitung abgelehnt wurde, aber wir gehen automatisch davon aus, dass C ++ 11 verwendet wird , sofern nicht anders angegeben.
Schuh
Dies ist wahr, aber wir sollten auch angeben, ob eine Lösung nur C ++ 11 ist, basierend auf dem von Ihnen angegebenen Link.
trlkly
@trlkly, ich stimme zu. Ich habe die Antwort entsprechend bearbeitet. Vielen Dank für den Hinweis.
Schuh
9

Es ist eine unterhaltsame Funktion von C , mit der Sie sich effektiv in den Fuß schießen können, wenn Sie dazu neigen.

Ich denke, der Grund ist, dass C nur einen Schritt über der Assemblersprache liegt. Die Größenprüfung und ähnliche Sicherheitsfunktionen wurden entfernt, um Spitzenleistungen zu ermöglichen. Dies ist keine schlechte Sache, wenn der Programmierer sehr fleißig ist.

Das Zuweisen einer Größe zum Funktionsargument hat außerdem den Vorteil, dass bei Verwendung der Funktion durch einen anderen Programmierer die Möglichkeit einer Größenbeschränkung besteht. Nur die Verwendung eines Zeigers überträgt diese Informationen nicht an den nächsten Programmierer.

Rechnung
quelle
3
Ja. C soll dem Programmierer über den Compiler vertrauen. Wenn Sie das Ende eines Arrays so offensichtlich indizieren, müssen Sie etwas Besonderes und Absichtliches tun.
John
7
Ich habe mir vor 14 Jahren beim Programmieren auf C die Zähne geschnitten. Von allen, die mein Professor sagte, war der eine Satz, der mir mehr als allen anderen in Erinnerung geblieben ist: "C wurde von Programmierern für Programmierer geschrieben." Die Sprache ist extrem mächtig. (Bereite dich auf das Klischee vor) Wie Onkel Ben uns lehrte: "Mit großer Kraft geht große Verantwortung einher."
Andrew Falanga
6

Erstens überprüft C niemals Array-Grenzen. Es spielt keine Rolle, ob es sich um lokale, globale, statische Parameter handelt. Das Überprüfen von Array-Grenzen bedeutet mehr Verarbeitung, und C soll sehr effizient sein, sodass die Überprüfung der Array-Grenzen vom Programmierer bei Bedarf durchgeführt wird.

Zweitens gibt es einen Trick, der es ermöglicht, ein Array als Wert an eine Funktion zu übergeben. Es ist auch möglich, ein Array von einer Funktion nach Wert zurückzugeben. Sie müssen nur einen neuen Datentyp mit struct erstellen. Beispielsweise:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Sie müssen auf folgende Elemente zugreifen: foo.a [1]. Das zusätzliche ".a" mag seltsam aussehen, aber dieser Trick fügt der C-Sprache eine großartige Funktionalität hinzu.

user34814
quelle
7
Sie verwechseln die Überprüfung der Laufzeitgrenzen mit der Überprüfung des Typs zur Kompilierungszeit.
Ben Voigt
@ Ben Voigt: Ich spreche nur von Grenzüberprüfung, wie es die ursprüngliche Frage ist.
user34814
2
@ user34814 Die Überprüfung der Grenzen zur Kompilierungszeit liegt im Rahmen der Typprüfung. Mehrere Hochsprachen bieten diese Funktion.
Leushenko
5

So teilen Sie dem Compiler mit, dass myArray auf ein Array von mindestens 10 Zoll verweist:

void bar(int myArray[static 10])

Ein guter Compiler sollte Sie warnen, wenn Sie auf myArray [10] zugreifen. Ohne das Schlüsselwort "static" würde die 10 überhaupt nichts bedeuten.

gnasher729
quelle
1
Warum sollte ein Compiler warnen, wenn Sie auf das 11. Element zugreifen und das Array mindestens 10 Elemente enthält?
Nwellnhof
Vermutlich liegt dies daran, dass der Compiler nur erzwingen kann, dass Sie mindestens 10 Elemente haben. Wenn Sie versuchen, auf das 11. Element zuzugreifen, kann es nicht sicher sein, ob es vorhanden ist (auch wenn dies möglich ist).
Dylan Watson
2
Ich denke nicht, dass dies eine korrekte Lesart des Standards ist. [static]Ermöglicht dem Compiler zu warnen, wenn Sie mit einem aufrufen . Es gibt nicht vor, auf was Sie innerhalb zugreifen dürfen . Die Verantwortung liegt ganz auf der Anruferseite. barint[5] bar
Tab
3
error: expected primary-expression before 'static'habe diese Syntax noch nie gesehen. Es ist unwahrscheinlich, dass dies Standard C oder C ++ ist.
v.oddou
3
@ v.oddou, es ist in C99 in 6.7.5.2 und 6.7.5.3 angegeben.
Samuel Edwin Ward
5

Dies ist eine bekannte "Funktion" von C, die an C ++ übergeben wird, da C ++ C-Code korrekt kompilieren soll.

Das Problem ergibt sich aus mehreren Aspekten:

  1. Ein Array-Name soll einem Zeiger vollständig entsprechen.
  2. C soll schnell sein, ursprünglich als eine Art "High-Level-Assembler" entwickelt (speziell entwickelt, um das erste "tragbare Betriebssystem" zu schreiben: Unix), also soll es keinen "versteckten" Code einfügen; Die Überprüfung des Laufzeitbereichs ist daher "verboten".
  3. Der Maschinencode, der für den Zugriff auf ein statisches oder ein dynamisches Array (entweder im Stapel oder zugewiesen) generiert wurde, ist tatsächlich unterschiedlich.
  4. Da die aufgerufene Funktion die als Argument übergebene "Art" des Arrays nicht kennen kann, soll alles ein Zeiger sein und als solcher behandelt werden.

Man könnte sagen, dass Arrays in C nicht wirklich unterstützt werden (dies ist nicht wirklich wahr, wie ich bereits sagte, aber es ist eine gute Annäherung); Ein Array wird wirklich als Zeiger auf einen Datenblock behandelt und mithilfe der Zeigerarithmetik aufgerufen. Da C KEINE Form von RTTI hat, müssen Sie die Größe des Array-Elements im Funktionsprototyp deklarieren (um Zeigerarithmetik zu unterstützen). Dies gilt umso mehr für mehrdimensionale Arrays.

Jedenfalls ist alles oben nicht mehr wirklich wahr: p

Die meisten modernen C / C ++ Compiler tun Unterstützung die Überprüfung der Grenzen, sondern Standards erfordern es standardmäßig aus sein (für die Abwärtskompatibilität). In einigermaßen neueren Versionen von gcc wird beispielsweise die Überprüfung des Kompilierungszeitbereichs mit "-O3 -Wall -Wextra" und die Überprüfung der vollständigen Laufzeitgrenzen mit "-fbounds-Checking" durchgeführt.

ZioByte
quelle
Vielleicht C ++ wurde angeblich vor C - Code 20 Jahren zu kompilieren, aber es sicher ist es nicht, und hat nicht für eine lange Zeit (C ++ 98? C99 zumindest, die von jedem neueren C nicht „fixieren“ hat ++ Standard).
Hyde
@hyde Das klingt mir etwas zu hart. Um Stroustrup zu zitieren: "Mit kleinen Ausnahmen ist C eine Teilmenge von C ++." (The C ++ PL 4th ed., Abschnitt 1.2.1). Während sich sowohl C ++ als auch C weiterentwickeln und Funktionen aus der neuesten C-Version existieren, die nicht in der neuesten C ++ - Version enthalten sind, denke ich insgesamt, dass das Stroustrup-Zitat immer noch gültig ist.
MVW
@mvw Der meiste in diesem Jahrtausend geschriebene C-Code, der nicht absichtlich C ++ - kompatibel gehalten wird, indem inkompatible Funktionen vermieden werden, verwendet die von C99 festgelegte Initialisierersyntax ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) zum Initialisieren von Strukturen, da dies eine viel klarere Möglichkeit zum Initialisieren einer Struktur darstellt. Infolgedessen wird der meiste aktuelle C-Code von Standard-C ++ - Compilern abgelehnt, da der meiste C-Code Strukturen initialisiert.
Hyde
@mvw Man könnte vielleicht sagen, dass C ++ mit C kompatibel sein soll, so dass es möglich ist, Code zu schreiben, der sowohl mit C- als auch mit C ++ - Compilern kompiliert wird, wenn bestimmte Kompromisse eingegangen werden. Aber das erfordert eine Teilmenge der Verwendung von sowohl C und C ++, nicht nur von C ++ Teilmenge.
Hyde
@hyde Sie wären überrascht, wie viel C-Code C ++ kompilierbar ist. Vor einigen Jahren war der gesamte Linux-Kernel C ++ - kompilierbar (ich weiß nicht, ob dies noch zutrifft). Ich kompiliere routinemäßig C-Code im C ++ - Compiler, um eine überlegene Warnprüfung zu erhalten. Nur "Produktion" wird im C-Modus kompiliert, um die größtmögliche Optimierung zu erzielen.
ZioByte
3

C transformiert nicht nur einen Parameter vom Typ int[5]in *int; Mit der Deklaration typedef int intArray5[5];wird auch ein Parameter vom Typ intArray5in transformiert *int. Es gibt einige Situationen, in denen dieses Verhalten zwar seltsam, aber nützlich ist (insbesondere bei Dingen wie dem va_listin stdargs.h, die in einigen Implementierungen als Array definiert sind). Es wäre unlogisch, einen als Parameter definierten Typ zuzulassen int[5](die Dimension zu ignorieren), aber nicht int[5]direkt angeben zu lassen.

Ich finde Cs Umgang mit Parametern vom Array-Typ absurd, aber es ist eine Folge der Bemühungen, eine Ad-hoc-Sprache zu verwenden, von der große Teile nicht besonders gut definiert oder durchdacht waren, und zu versuchen, Verhalten zu entwickeln Spezifikationen, die mit den vorhandenen Implementierungen für vorhandene Programme übereinstimmen. Viele der Macken von C sind in diesem Licht sinnvoll, besonders wenn man bedenkt, dass große Teile der Sprache, die wir heute kennen, noch nicht existierten, als viele von ihnen erfunden wurden. Soweit ich weiß, haben Compiler im Vorgänger von C, BCPL genannt, die Variablentypen nicht wirklich gut verfolgt. Eine Erklärung int arr[5];war gleichbedeutend mit int anonymousAllocation[5],*arr = anonymousAllocation;; sobald die Zuteilung aufgehoben wurde. Der Compiler wusste weder, noch kümmerte es ihn, obarrwar ein Zeiger oder ein Array. Beim Zugriff als entweder arr[x]oder *arrwird es als Zeiger betrachtet, unabhängig davon, wie es deklariert wurde.

Superkatze
quelle
1

Eine Sache, die noch nicht beantwortet wurde, ist die eigentliche Frage.

Die bereits gegebenen Antworten erklären, dass Arrays weder in C noch in C ++ als Wert an eine Funktion übergeben werden können. Sie erklären auch, dass ein als deklarierter Parameter so int[]behandelt wird, als hätte er einen Typ int *, und dass eine Variable vom Typ int[]an eine solche Funktion übergeben werden kann.

Sie erklären jedoch nicht, warum es nie zu einem Fehler gekommen ist, explizit eine Array-Länge anzugeben.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Warum ist der letzte nicht ein Fehler?

Ein Grund dafür ist, dass es Probleme mit typedefs verursacht.

typedef int myarray[10];
void f(myarray array);

Wenn es ein Fehler wäre, die Array-Länge in Funktionsparametern anzugeben, könnten Sie den myarrayNamen nicht im Funktionsparameter verwenden. Und da einige Implementierungen Array-Typen für Standardbibliothekstypen verwenden, wie z. B. va_listund alle Implementierungen erforderlich sind, um jmp_bufeinen Array-Typ zu erstellen, wäre es sehr problematisch, wenn es keine Standardmethode zum Deklarieren von Funktionsparametern unter Verwendung dieser Namen gäbe: Ohne diese Fähigkeit wäre dies möglich keine tragbare Implementierung von Funktionen wie vprintf.


quelle
0

Compiler können überprüfen, ob die Größe des übergebenen Arrays der erwarteten entspricht. Compiler können ein Problem warnen, wenn dies nicht der Fall ist.

hamidi
quelle