webentwicklung-frage-antwort-db.com.de

Ist es gut definiert, einen Zeiger zu verwenden, der auf One-Past-Malloc zeigt?

In C empfiehlt es sich, einen Zeiger zu erstellen, der auf eins nach dem letzten Element eines Arrays zeigt, und ihn in Zeigerarithmetiken zu verwenden, sofern Sie ihn nicht dereferenzieren:

int a[5], *p = a+5, diff = p-a; // Well-defined

Dies sind jedoch UBs:

p = a+6;
int b = *(a+5), diff = p-a; // Dereferencing and pointer arithmetic

Jetzt habe ich eine Frage: Gilt dies für dynamisch zugewiesenen Speicher? Angenommen, ich verwende nur einen Zeiger, der in der Zeigerarithmetik auf vorletzten Zeiger zeigt, ohne ihn zu dereferenzieren, und malloc() ist erfolgreich.

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
// Question:
int *p = a+5;
int diff = p-a; // Use in pointer arithmetic?
47
iBug

Ist es gut definiert, einen Zeiger zu verwenden, der auf One-Past-Malloc zeigt?

Es ist klar definiert, ob p auf eins nach dem zugewiesenen Speicher verweist und nicht dereferenziert wird.

n157 - §6.5.6 (p8):

[...] Wenn das Ergebnis eins nach dem letzten Element des Array-Objekts zeigt, darf es nicht als Operand eines unären * Operator, der ausgewertet wird.

Das Subtrahieren von zwei Zeigern ist nur gültig, wenn sie auf Elemente desselben Array-Objekts oder eines nach dem letzten Element des Array-Objekts zeigen, da dies sonst zu undefiniertem Verhalten führt.

(p9) :

Wenn zwei Zeiger subtrahiert werden, müssen beide auf Elemente desselben Array-Objekts zeigen oder einen nach dem letzten Element des Array-Objekts [...]

Die obigen Anführungszeichen gelten sowohl für dynamisch als auch für statisch zugewiesenen Speicher.

int a[5];
ptrdiff_t diff = &a[5] - &a[0]; // Well-defined

int *d = malloc(5 * sizeof(*d));
assert(d != NULL, "Memory allocation failed");
diff = &d[5] - &d[0];        // Well-defined

Ein weiterer Grund, warum dies für dynamisch zugewiesenen Speicher gilt, wie von Jonathan Leffler in einem Kommentar angegeben:

§7.22.3 (p1) :

Die Reihenfolge und Kontinuität des Speichers, der durch aufeinanderfolgende Aufrufe des aligned_alloc, calloc, malloc und realloc sind nicht spezifiziert. Der bei erfolgreicher Zuweisung zurückgegebene Zeiger ist in geeigneter Weise ausgerichtet, sodass er einem Zeiger auf einen beliebigen Objekttyp mit einer grundlegenden Ausrichtungsanforderung zugewiesen und dann für den Zugriff auf ein solches Objekt oder ein Array von verwendet werden kann solche Objekte im zugewiesenen Speicherplatz (bis der Speicherplatz explizit freigegeben wird).

Der von malloc im obigen Snippet zurückgegebene Zeiger wird d zugewiesen, und der zugewiesene Speicher besteht aus einem Array von 5 int Objekten.

23
haccks

Der Entwurf n4296 für C11 besagt ausdrücklich, dass der Verweis auf eine Stelle hinter einem Array genau definiert ist: 6.5.6 Sprache/Ausdrücke/Additive Operatoren:

§ 8 Wenn ein Ausdruck mit ganzzahligem Typ zu einem Zeiger hinzugefügt oder von diesem subtrahiert wird, hat das Ergebnis den Typ des Zeigeroperanden. ... Wenn der Ausdruck P auf das letzte Element eines Array-Objekts zeigt, zeigt der Ausdruck (P) +1 um eins nach dem letzten Element des Array-Objekts, und wenn der Ausdruck Q um eins nach dem letzten Element eines Arrays zeigt Array-Objekt, der Ausdruck (Q) -1 zeigt auf das letzte Element des Array-Objekts ... Wenn das Ergebnis eins nach dem letzten Element des Array-Objekts zeigt, darf es nicht als Operand eines unären * Operators verwendet werden, der ausgewertet wird.

Da der Speichertyp in der Unterklausel niemals genau angegeben wird, gilt er für jeden Speichertyp, einschließlich des zugewiesenen.

Das bedeutet eindeutig, dass nach:

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");

beide

int *p = a+5;
int diff = p-a;

sind perfekt definiert und da die üblichen Zeigerarithmetikregeln gelten, erhält diff den Wert 5.

26
Serge Ballesta

Ja, die gleichen Regeln gelten für Variablen mit dynamischer und automatischer Speicherdauer. Dies gilt sogar für eine malloc -Anforderung für ein einzelnes Element (ein Skalar entspricht in dieser Hinsicht einem Array mit einem Element).

Zeigerarithmetik ist nur innerhalb von Arrays gültig, einschließlich eines nach dem Ende eines Arrays.

Bei der Dereferenzierung ist es wichtig, eine Überlegung zu beachten: In Bezug auf die Initialisierung int a[5] = {0};, der Compiler darf nicht versuchen dereferenzierena[5] im Ausdruck int* p = &a[5]; es muss dies kompilieren als int* p = a + 5; Auch hier gilt das Gleiche für die dynamische Speicherung.

7
Bathsheba

Ist es gut definiert, einen Zeiger zu verwenden, der auf One-Past-Malloc zeigt?

Ja, es gibt noch einen Eckfall, in dem dies nicht genau definiert ist:

void foo(size_t n) {
  int *a = malloc(n * sizeof *a);
  assert(a != NULL || n == 0, "Memory allocation failed");
  int *p = a+n;
  intptr_t diff = p-a;
  ...
}

Speicherverwaltungsfunktionen ... Wenn die Größe des angeforderten Speicherplatzes Null ist, ist das Verhalten implementierungsdefiniert: Entweder wird ein Nullzeiger zurückgegeben, oder das Verhalten ist so, als wäre die Größe ein Wert ungleich Null. Mit dem zurückgegebenen Zeiger darf jedoch nicht auf ein Objekt zugegriffen werden. C11dr §7.22.3 1

foo(0) -> malloc(0) kann einen NULL oder non-NULL zurückgeben. In der ersten Implementierung ist die Rückgabe von NULL kein "Speicherzuordnungsfehler". Dies bedeutet, dass der Code versucht, int *p = NULL + 0; Mit int *p = a+n; Auszuführen, was die Garantien für die Zeigermathematik verfehlt - oder zumindest diesen Code in Frage stellt.

Vorteile von portablem Code durch Vermeidung von Zuweisungen von 0-Größen.

void bar(size_t n) {
  intptr_t diff;
  int *a;
  int *p;
  if (n > 0) {
    a = malloc(n * sizeof *a);
    assert(a != NULL, "Memory allocation failed");
    p = a+n;
    diff = p-a;
  } else {
    a = p = NULL;
    diff = 0;
  }
  ...
}
7
chux