wake-up-neo.com

Ist es in einer Struktur zulässig, ein Array-Feld für den Zugriff auf ein anderes Feld zu verwenden?

Betrachten Sie als Beispiel die folgende Struktur:

struct S {
  int a[4];
  int b[4];
} s;

Wäre es legal, s.a[6] zu schreiben und zu erwarten, dass es gleich s.b[2] ist? Ich persönlich denke, dass es UB in C++ sein muss, während ich nicht sicher bin, ob C ..__ relevant in den Standards der Sprachen C und C++.


Update

Es gibt mehrere Antworten, die auf Möglichkeiten hinweisen, um sicherzustellen, dass zwischen den Feldern keine Auffüllung. Vorhanden ist, damit der Code zuverlässig funktioniert. Ich möchte hervorheben, dass __, wenn ein solcher Code UB ist, die Abwesenheit von Padding nicht ausreicht. Wenn es UB ist, kann der Compiler davon ausgehen, dass die Zugriffe auf S.a[i] und S.b[j] sich nicht __ überlappen, und der Compiler kann solche Speicherzugriffe neu anordnen. Zum Beispiel,

    int x = s.b[2];
    s.a[6] = 2;
    return x;

kann in umgewandelt werden

    s.a[6] = 2;
    int x = s.b[2];
    return x;

was immer 2 zurückgibt.

51
Nikolai

Wäre es legal, an a [6] zu schreiben und zu erwarten, dass es gleich zu b [2] ist?

Nein. Weil der Zugriff auf ein Array außerhalb von gebundenen undefined Verhalten in C und C++ aufgerufen wurde.

C11 J.2 Undefiniertes Verhalten

  • Das Hinzufügen oder das Subtrahieren eines Zeigers in ein Array-Objekt und einen Integer-Typ oder direkt darüber hinaus führt zu einem Ergebnis, das direkt über .__ hinausgeht. das Array-Objekt und wird als Operand eines unären *-Operators verwendet, der wird ausgewertet (6.5.6).

  • Ein Array-Index befindet sich außerhalb des gültigen Bereichs, auch wenn ein Objekt offensichtlich mit dem angegebenen Index zugänglich ist (wie im lvalue-Ausdruck a[1][7] mit der Deklaration int a[4][5]) (6.5.6)).

C++ - Standard Entwurf Abschnitt 5.7 Additive Operatoren In Absatz 5 heißt es:

Wenn ein Ausdruck mit ganzzahligem Typ hinzugefügt oder abgezogen wird von einem Zeiger hat das Ergebnis den Typ des Zeigeroperanden. Wenn der Der Zeigeroperand zeigt auf ein Element eines Array-Objekts und das Array groß genug ist, zeigt das Ergebnis auf einen Elementversatz von Originalelement so, dass die Differenz der Indizes der Die resultierenden und ursprünglichen Array-Elemente entsprechen dem Integralausdruck . [...] Wenn sowohl der Zeigeroperand als auch das Ergebnis auf die Elemente .__ zeigen. desselben Arrayobjekts oder eines hinter dem letzten Element des Arrays Objekt darf die Bewertung keinen Überlauf erzeugen; ansonsten der Verhalten ist undefiniert.

61
M.S Chaudhari

Abgesehen von der Antwort von @rsp (Undefined behavior for an array subscript that is out of range) kann ich hinzufügen, dass es nicht zulässig ist, über b auf a zuzugreifen, da die Sprache C nicht angibt, wie viel Auffüllraum zwischen dem Ende von a und dem Beginn von b zugewiesen werden darf Auch wenn Sie es für eine bestimmte Implementierung ausführen können, ist es nicht portierbar.

instance of struct:
+-----------+----------------+-----------+---------------+
|  array a  |  maybe padding |  array b  | maybe padding |
+-----------+----------------+-----------+---------------+

Die zweite Auffüllung kann ebenso fehlschlagen wie die Ausrichtung von struct object ist die Ausrichtung von a, die der Ausrichtung von b entspricht, aber die C-Sprache verlangt auch nicht, dass die zweite Auffüllung nicht dort ist.

34
alinsoar

a und b sind zwei verschiedene Arrays, und a enthält 4-Elemente. Daher greift a[6] außerhalb der Grenzen auf das Array zu und ist daher undefiniertes Verhalten. Beachten Sie, dass der Array-Index a[6] als *(a+6) definiert ist, so dass der Nachweis der UB tatsächlich durch den Abschnitt "Additive Operatoren" in Verbindung mit Zeigern "gegeben wird. Siehe den folgenden Abschnitt des C11-Standards (z. B. dies Online-Entwurfsversion). diesen Aspekt beschreiben:

6.5.6 Additive Operatoren

Wenn ein Ausdruck mit ganzzahligem Typ hinzugefügt oder subtrahiert wird von einem Zeiger hat das Ergebnis den Typ des Zeigeroperanden. Wenn der Der Zeigeroperand zeigt auf ein Element eines Array-Objekts und das Array groß genug ist, zeigt das Ergebnis auf einen Elementversatz von Originalelement so, dass die Differenz der Indizes der Die resultierenden und ursprünglichen Array-Elemente entsprechen dem ganzzahligen Ausdruck . Mit anderen Worten, wenn der Ausdruck P auf das i-te Element eines .__ zeigt. Array-Objekt die Ausdrücke (P) + N (äquivalent N + (P)) und (P) -N (wobei N den Wert n hat) zeigen auf i + n-te bzw. i-n-te Elemente des Array-Objekts, sofern sie vorhanden sind. Darüber hinaus, wenn der Ausdruck P zeigt auf das letzte Element eines Array-Objekts, den Ausdruck (P) +1 zeigt eins hinter das letzte Element des Array-Objekts, und wenn der Ausdruck Q einen Punkt hinter das letzte Element eines Arrays zeigt Objekt, der Ausdruck (Q) -1 zeigt auf das letzte Element des Arrays Objekt. Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente zeigen desselben Arrayobjekts oder eines hinter dem letzten Element des Arrays Objekt darf die Bewertung keinen Überlauf erzeugen; ansonsten der Verhalten ist undefiniert. Wenn das Ergebnis einen Punkt hinter das letzte Element zeigt des Array-Objekts darf es nicht als Operand eines unären * .__ verwendet werden. Operator, der ausgewertet wird.

Dasselbe Argument gilt für C++ (obwohl hier nicht zitiert).

Obwohl es eindeutig ein undefiniertes Verhalten ist, weil die Arraygrenzen von a überschritten werden, ist zu beachten, dass der Compiler möglicherweise eine Auffüllung zwischen den Mitgliedern a und b einführt, sodass a+6 nicht notwendigerweise dasselbe ergibt, selbst wenn solche Zeigerarithmetik zulässig wäre Adresse als b+2.

11
Stephan Lechner

Ist es legal Nein. Wie bereits erwähnt, ruft es Undefined Behavior auf.

Wird es funktionieren? Das hängt von deinem Compiler ab. Das ist die Sache über undefiniertes Verhalten: es ist undefined

Bei vielen C- und C++ - Compilern wird die Struktur so angelegt, dass b sofort a im Speicher folgt und es keine Begrenzungen gibt. Der Zugriff auf a [6] ist also tatsächlich mit b [2] identisch und führt nicht zu Ausnahmen. 

Gegeben

struct S {
  int a[4];
  int b[4];
} s

und vorausgesetzt, es wird kein zusätzliches Padding angenommen, ist die Struktur wirklich nur eine Möglichkeit, einen Speicherblock mit 8 Ganzzahlen zu betrachten. Sie könnten es in (int*) umwandeln und ((int*)s)[6] würde auf den gleichen Speicher wie s.b[2] zeigen. 

Sollten Sie sich auf dieses Verhalten verlassen? Absolut nicht. Undefined bedeutet, dass der Compiler dies nicht unterstützen muss. Dem Compiler steht es frei, die Struktur aufzufüllen, die die Annahme annimmt, dass & (s.b [2]) == & (s.a [6]) falsch ist. Der Compiler könnte auch eine Begrenzungsprüfung für den Array-Zugriff hinzufügen (obwohl das Aktivieren von Compiler-Optimierungen eine solche Prüfung wahrscheinlich deaktivieren würde).

Ich habe die Auswirkungen in der Vergangenheit erlebt. Es ist durchaus üblich, eine Struktur wie diese zu haben

struct Bob {
    char name[16];
    char whatever[64];
} bob;
strcpy(bob.name, "some name longer than 16 characters");

Jetzt wird bob.whatever "als 16 Zeichen" sein. (weshalb Sie immer strncpy verwenden sollten)

6
dwilliss

Wenn Sie, wie @MartinJames in einem Kommentar erwähnte, sicherstellen müssen, dass sich a und b im zusammenhängenden Speicher befinden (oder zumindest als solcher behandelt werden können, (editieren), es sei denn, Ihre Architektur/compiler verwendet eine ungewöhnliche Speicherblockgröße/einen ungewöhnlichen Versatz und eine erzwungene Ausrichtung, für die ein Auffüllen erforderlich wäre.) Sie müssen ein union verwenden.

union overlap {
    char all[8]; /* all the bytes in sequence */
    struct { /* (anonymous struct so its members can be accessed directly) */
        char a[4]; /* padding may be added after this if the alignment is not a sub-factor of 4 */
        char b[4];
    };
};

Sie können nicht direkt von b aus auf a zugreifen (z. B. a[6], Wie Sie gefragt haben), aber Sie können greifen auf die Elemente von beiden zu a und b mit all (z. B. all[6] bezieht sich auf denselben Speicherort wie b[2]).

(Bearbeiten: Sie können 8 Und 4 Im obigen Code durch 2*sizeof(int) bzw. sizeof(int) ersetzen, um die Wahrscheinlichkeit zu erhöhen, dass sie mit der Architektur übereinstimmen Ausrichtung, insbesondere, wenn der Code portabler sein muss, Sie jedoch darauf achten müssen, keine Annahmen darüber zu treffen, wie viele Bytes sich in a, b oder all. Dies funktioniert jedoch bei den wahrscheinlich häufigsten (1-, 2- und 4-Byte-) Speicherausrichtungen.)

Hier ist ein einfaches Beispiel:

#include <stdio.h>

union overlap {
    char all[2*sizeof(int)]; /* all the bytes in sequence */
    struct { /* anonymous struct so its members can be accessed directly */
        char a[sizeof(int)]; /* low Word */
        char b[sizeof(int)]; /* high Word */
    };
};

int main()
{
    union overlap testing;
    testing.a[0] = 'a';
    testing.a[1] = 'b';
    testing.a[2] = 'c';
    testing.a[3] = '\0'; /* null terminator */
    testing.b[0] = 'e';
    testing.b[1] = 'f';
    testing.b[2] = 'g';
    testing.b[3] = '\0'; /* null terminator */
    printf("a=%s\n",testing.a); /* output: a=abc */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abc */

    testing.a[3] = 'd'; /* makes printf keep reading past the end of a */
    printf("a=%s\n",testing.a); /* output: a=abcdefg */
    printf("b=%s\n",testing.b); /* output: b=efg */
    printf("all=%s\n",testing.all); /* output: all=abcdefg */

    return 0;
}
5
Jed Schaaf

Nein , da der Zugriff auf ein Array außerhalb der Grenzen Undefined Behavior sowohl in C als auch in C++ aufruft.

3
gsamaras

Kurze Antwort: Nein. Sie befinden sich im Land des undefinierten Verhaltens.

Lange Antwort: Nein. Das bedeutet jedoch nicht, dass Sie nicht auf andere skizzenhafte Arten auf die Daten zugreifen können. Wenn Sie GCC verwenden, können Sie Folgendes tun (Ausarbeitung der Antwort von dwillis):

struct __attribute__((packed,aligned(4))) Bad_Access {
    int arr1[3];
    int arr2[3];
};

und dann könnte über ( Godbolt source + asm ) zugreifen:

int x = ((int*)ba_pointer)[4];

Dieser Cast verstößt gegen striktes Aliasing und ist daher nur mit g++ -fno-strict-aliasing sicher. Sie können einen Strukturzeiger auf einen Zeiger auf das erste Member umwandeln, befinden sich dann jedoch wieder im UB-Boot, da Sie außerhalb des ersten Members zugreifen.

Oder machen Sie das einfach nicht. Speichern Sie einem zukünftigen Programmierer (wahrscheinlich sich selbst) den Schmerz dieses Chaos.

Wenn wir gerade dabei sind, warum nicht std :: vector verwenden? Es ist nicht idiotensicher, aber auf der Rückseite gibt es Wachen, um so schlechtes Verhalten zu verhindern.

Nachtrag:

Wenn Sie wirklich über die Leistung besorgt sind:

Nehmen wir an, Sie haben zwei gleiche Zeiger, auf die Sie zugreifen. Der Compiler geht wahrscheinlich davon aus, dass beide Zeiger die Möglichkeit haben, einzugreifen, und instanziiert zusätzliche Logik, um Sie davor zu schützen, etwas zu tun.

Wenn Sie dem Compiler ernsthaft schwören, dass Sie nicht versuchen, einen Alias ​​zu verwenden, wird der Compiler Sie großzügig belohnen: Bietet das Einschränkungsschlüsselwort erhebliche Vorteile in gcc/g ++

Fazit: Sei nicht böse; Ihr zukünftiges Ich, und der Compiler werden es Ihnen danken.

1
Alex Shirley

Die Antwort von Jed Schaff ist auf dem richtigen Weg, aber nicht ganz richtig. Wenn der Compiler einen Abstand zwischen a und b einfügt, schlägt seine Lösung weiterhin fehl. Wenn Sie jedoch Folgendes erklären:

typedef struct {
  int a[4];
  int b[4];
} s_t;

typedef union {
  char bytes[sizeof(s_t)];
  s_t s;
} u_t;

Sie können jetzt auf (int*)(bytes + offsetof(s_t, b)) Zugreifen, um die Adresse von s.b Abzurufen, unabhängig davon, wie der Compiler die Struktur anordnet. Das Makro offsetof() wird in <stddef.h> Deklariert.

Der Ausdruck sizeof(s_t) ist ein konstanter Ausdruck, der in einer Array-Deklaration in C und C++ zulässig ist. Es wird kein Array mit variabler Länge angegeben. (Entschuldigung, dass Sie den C-Standard falsch verstanden haben. Ich dachte, das klingt falsch.)

In der realen Welt werden jedoch zwei aufeinanderfolgende Arrays von int in einer Struktur so angeordnet, wie Sie es erwarten. (Sie könnten in der Lage sein, ein sehr ausgeklügeltes Gegenbeispiel zu konstruieren, indem Sie die Schranke von a auf 3 oder 5 anstelle von 4 setzen und dann den Compiler veranlassen, beide a auszurichten. und b an einer 16-Byte-Grenze.) Anstatt umständliche Methoden zu verwenden, um zu versuchen, ein Programm zu erhalten, das keinerlei Annahmen über den strengen Wortlaut des Standards hinaus trifft, möchten Sie eine Art defensive Codierung, wie static assert(&both_arrays[4] == &s.b[0], "");. Diese fügen keinen Laufzeit-Overhead hinzu und schlagen fehl, wenn Ihr Compiler etwas tut, das Ihr Programm beschädigen würde, solange Sie UB nicht in der Assertion selbst auslösen.

Wenn Sie auf tragbare Weise sicherstellen möchten, dass beide Sub-Arrays in einen zusammenhängenden Speicherbereich gepackt sind, oder wenn Sie einen Speicherblock auf andere Weise aufteilen möchten, können Sie sie mit memcpy() kopieren.

1
Davislor

Der Standard legt keine Einschränkungen fest, was Implementierungen tun müssen, wenn ein Programm versucht, einen Out-of-Bounds-Array-Index in einem Strukturfeld zu verwenden, um auf ein Member eines anderen zuzugreifen. Zugriffe außerhalb der Grenzen sind somit "illegal" in streng konformen Programmen , und Programme, die solche Zugriffe nutzen, können nicht gleichzeitig zu 100% portabel und fehlerfrei sein. Auf der anderen Seite definieren viele Implementierungen das Verhalten eines solchen Codes, und Programme, die ausschließlich auf solche Implementierungen abzielen, können ein solches Verhalten ausnutzen.

Es gibt drei Probleme mit einem solchen Code:

  1. Während viele Implementierungen Strukturen auf vorhersagbare Weise auslegen, ermöglicht der Standard den Implementierungen das Hinzufügen von willkürlichem Abstand vor jedem anderen Strukturelement als dem ersten. Code könnte sizeof oder offsetof verwenden, um sicherzustellen, dass die Strukturmitglieder wie erwartet platziert werden. Die anderen beiden Probleme bleiben jedoch erhalten.

  2. So etwas wie gegeben:

    if (structPtr->array1[x])
     structPtr->array2[y]++;
    return structPtr->array1[x];
    

    normalerweise wäre es für einen Compiler sinnvoll, anzunehmen, dass die Verwendung von structPtr->array1[x] in der "if" -Bedingung denselben Wert ergibt wie die vorhergehende Verwendung, obwohl dies das Verhalten von Code ändern würde, der auf Aliasing zwischen den beiden Arrays beruht.

  3. Wenn array1[] z. 4 Elemente, ein Compiler wie folgt gegeben:

    if (x < 4) foo(x);
    structPtr->array1[x]=1;
    

daraus könnte der Schluss gezogen werden, dass es keine definierten Fälle gibt, in denen x nicht weniger als 4 ist, und es könnte bedingungslos foo(x) aufgerufen werden.

Während Programme zwar sizeof oder offsetof verwenden können, um sicherzustellen, dass es keine Überraschungen beim Strukturlayout gibt, gibt es keine Möglichkeit, zu testen, ob Compiler versprechen, von den Optimierungen der Typen # 2 oder # 3 abzusehen. Darüber hinaus ist der Standard etwas vage darüber, was in einem Fall gemeint wäre:

struct foo {char array1[4],array2[4]; };

int test(struct foo *p, int i, int x, int y, int z)
{
  if (p->array2[x])
  {
    ((char*)p)[x]++;
    ((char*)(p->array1))[y]++;
    p->array1[z]++;
  }
  return p->array2[x];
}

Der Standard ist ziemlich klar, dass das Verhalten nur definiert werden würde, wenn z im Bereich von 0 bis 3 liegt. Da der Typ von p-> -Array in diesem Ausdruck jedoch char * ist (aufgrund von Zerfall), ist die Umwandlung im Zugriff nicht eindeutig Die Verwendung von y hätte keine Auswirkungen. Da beim Konvertieren des Zeigers auf das erste Element einer Struktur in char* dasselbe Ergebnis erzielt werden soll wie beim Konvertieren eines Strukturzeigers in char*, und der konvertierte Strukturzeiger für den Zugriff auf alle darin enthaltenen Bytes geeignet sein kann, scheint er den Zugriff zu verwenden x sollte für (mindestens) x = 0..7 definiert werden. [Wenn der Versatz von array2 größer als 4 ist, würde dies den Wert von x beeinflussen, der erforderlich ist, um Mitglieder von array2 zu treffen. Einige Werte von x könnten jedoch definiert sein Verhalten].

IMHO wäre eine gute Lösung, den Indexoperator für Array-Typen auf eine Weise zu definieren, die keinen Zeigerzerfall beinhaltet. In diesem Fall könnten die Ausdrücke p->array[x] und &(p->array1[x]) einen Compiler einladen, anzunehmen, dass x 0..3 ist, aber p->array+x und *(p->array+x) würden einen Compiler erfordern, der die Möglichkeit anderer Werte zulässt. Ich weiß nicht, ob Compiler das tun, aber der Standard verlangt es nicht.

0
supercat