In che modo questo pezzo di codice determina la dimensione dell'array senza usare sizeof ()?


134

Passando attraverso alcune domande di intervista in C, ho trovato una domanda che diceva "Come trovare la dimensione di un array in C senza usare l'operatore sizeof?", Con la seguente soluzione. Funziona, ma non riesco a capire il perché.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

Come previsto, restituisce 5.

modifica: le persone hanno indicato questa risposta, ma la sintassi differisce un po ', ovvero il metodo di indicizzazione

size = (&arr)[1] - arr;

quindi credo che entrambe le domande siano valide e abbiano un approccio leggermente diverso al problema. Grazie a tutti per l'immenso aiuto e la spiegazione approfondita!


13
Beh, non riesco a trovarlo, ma sembra in senso stretto. L'allegato J.2 afferma esplicitamente: l'operando dell'operatore unario * ha un valore non valido è un comportamento indefinito. Qui &a + 1non punta a nessun oggetto valido, quindi non è valido.
Eugene Sh.



@AlmaDo bene la sintassi differisce un po ', cioè la parte di indicizzazione, quindi credo che questa domanda sia ancora valida da sola, ma potrei sbagliarmi. Grazie per averlo precisato!
janojlic,

1
@janojlicz Sono essenzialmente gli stessi, perché (ptr)[x]è lo stesso di *((ptr) + x).
SS Anne,

Risposte:


135

Quando si aggiunge 1 a un puntatore, il risultato è la posizione dell'oggetto successivo in una sequenza di oggetti di tipo appuntito (ovvero una matrice). Se ppunta a un intoggetto, allora p + 1punterà al successivo intin una sequenza. Se ppunta a un array di 5 elementi di int(in questo caso, l'espressione &a), allora p + 1punterà al successivo array di 5 elementi diint in una sequenza.

Sottraendo due puntatori (a condizione che entrambi puntino nello stesso oggetto array o che uno stia puntando uno oltre l'ultimo elemento dell'array) si ottiene il numero di oggetti (elementi dell'array) tra questi due puntatori.

L'espressione &aproduce l'indirizzo di ae ha il tipo int (*)[5](puntatore alla matrice a 5 elementi di int). L'espressione &a + 1produce l'indirizzo del prossimo array di 5 elementi di intseguito ae ha anche il tipo int (*)[5]. L'espressione *(&a + 1)dereferenzia il risultato di &a + 1, in modo tale da produrre l'indirizzo del primo intdopo l'ultimo elemento di a, e ha tipo int [5], che in questo contesto "decade" in un'espressione di tipo int *.

Allo stesso modo, l'espressione a"decade" in un puntatore al primo elemento dell'array e ha tipo int *.

Un'immagine può aiutare:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

Si tratta di due viste della stessa memoria: a sinistra, la vediamo come una sequenza di matrici a 5 elementi di int, mentre a destra, la vediamo come una sequenza di int. Mostro anche le varie espressioni e i loro tipi.

Attenzione, l'espressione *(&a + 1)provoca un comportamento indefinito :

...
Se il risultato punta oltre l'ultimo elemento dell'oggetto array, non deve essere usato come l'operando di un operatore unario * che viene valutato.

C 2011 Online Draft , 6.5.6 / 9


13
Quel testo "non deve essere usato" è ufficiale: C 2018 6.5.6 8.
Eric Postpischil,

@EricPostpischil: hai un link alla bozza pre-pub 2018 (simile a N1570.pdf)?
John Bode,

1
@JohnBode: questa risposta ha un link alla Wayback Machine . Ho controllato lo standard ufficiale nella mia copia acquistata.
Eric Postpischil,

7
Quindi se uno scrivesse size = (int*)(&a + 1) - a;questo codice sarebbe completamente valido? : o
Gizmo,

@Gizmo probabilmente in origine non l'hanno scritto perché in questo modo devi specificare il tipo di elemento; l'originale è stato probabilmente scritto definito come una macro per uso generico di tipo su diversi tipi di elementi.
Leushenko,

35

Questa linea è di fondamentale importanza:

size = *(&a + 1) - a;

Come puoi vedere, per prima cosa prende l'indirizzo ae ne aggiunge uno. Quindi, dereferenzia quel puntatore e sottrae il valore originale di ada esso.

L'aritmetica del puntatore in C fa sì che questo restituisca il numero di elementi nell'array, oppure 5. Aggiungendo uno ed &aè un puntatore al prossimo array di 5 intsecondi dopo a. Successivamente, questo codice dereferenzia il puntatore risultante e sottrae a(un tipo di array che è decaduto in un puntatore) da quello, fornendo il numero di elementi nell'array.

Dettagli su come funziona l'aritmetica del puntatore:

Supponi di avere un puntatore xyzche punta a un inttipo e contiene il valore (int *)160. Quando si sottrae un numero da xyz, C specifica che l'importo effettivo sottratto xyzè quel numero per la dimensione del tipo a cui punta. Ad esempio, se sottratto 5dalla xyz, il valore della xyzrisultante sarebbe xyz - (sizeof(*xyz) * 5)se l'aritmetica dei puntatori non si applicava.

Come aè un array di 5 inttipi, il valore risultante sarà 5. Tuttavia, questo non funzionerà con un puntatore, ma solo con un array. Se provi questo con un puntatore, il risultato sarà sempre 1.

Ecco un piccolo esempio che mostra gli indirizzi e come questo non è definito. Il lato sinistro mostra gli indirizzi:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Ciò significa che il codice viene sottratto ada &a[5](o a+5), dando 5.

Si noti che questo è un comportamento indefinito e non deve essere utilizzato in nessun caso. Non aspettarti che il comportamento di questo sia coerente su tutte le piattaforme e non utilizzarlo nei programmi di produzione.


27

Hmm, sospetto che questo sia qualcosa che non avrebbe funzionato nei primi giorni di C. È però intelligente.

Prendendo i passaggi uno alla volta:

  • &a ottiene un puntatore a un oggetto di tipo int [5]
  • +1 ottiene il prossimo oggetto del genere supponendo che ci sia un array di quelli
  • * converte efficacemente quell'indirizzo in tipo pointer in int
  • -a sottrae i due puntatori int, restituendo il conteggio delle istanze int tra di loro.

Non sono sicuro che sia completamente legale (in questo intendo il legale-avvocato - non funzionerà in pratica), date alcune delle operazioni di tipo in corso. Ad esempio, si è solo "autorizzati" a sottrarre due puntatori quando puntano a elementi nello stesso array. *(&a+1)è stato sintetizzato accedendo a un altro array, anche se un array genitore, quindi in realtà non è un puntatore nello stesso array di a. Inoltre, mentre ti è permesso sintetizzare un puntatore oltre l'ultimo elemento di un array e puoi trattare qualsiasi oggetto come un array di 1 elemento, l'operazione di dereferencing ( *) non è "consentita" su questo puntatore sintetizzato, anche se non ha alcun comportamento in questo caso!

Ho il sospetto che nei primi giorni di C (sintassi di K&R, chiunque?), Un array decadesse in un puntatore molto più rapidamente, quindi *(&a+1)potrebbe restituire solo l'indirizzo del prossimo puntatore di tipo int **. Le definizioni più rigorose del C ++ moderno consentono sicuramente al puntatore al tipo di array di esistere e conoscere le dimensioni dell'array, e probabilmente gli standard C hanno seguito l'esempio. Tutto il codice funzione C accetta solo i puntatori come argomenti, quindi la differenza tecnica visibile è minima. Ma sto solo indovinando qui.

Questo tipo di domanda dettagliata sulla legalità si applica in genere a un interprete C, oa uno strumento di tipo lanugine, piuttosto che al codice compilato. Un interprete potrebbe implementare un array 2D come un array di puntatori agli array, perché c'è una funzione di runtime in meno da implementare, nel qual caso il dereferenziamento del +1 sarebbe fatale e anche se funzionasse darebbe la risposta sbagliata.

Un'altra possibile debolezza potrebbe essere che il compilatore C potrebbe allineare l'array esterno. Immagina se si trattasse di un array di 5 caratteri ( char arr[5]), quando il programma lo esegue &a+1sta invocando il comportamento "array di array". Il compilatore potrebbe decidere che un array di array di 5 caratteri ( char arr[][5]) sia effettivamente generato come array di array di 8 caratteri ( char arr[][8]), in modo che l'array esterno si allinei correttamente. Il codice che stiamo discutendo ora riporta la dimensione dell'array come 8, non 5. Non sto dicendo che un compilatore particolare lo farebbe sicuramente, ma potrebbe.


Giusto. Tuttavia, per motivi difficili da spiegare, tutti usano sizeof () / sizeof ()?
Gem Taylor,

5
Molte persone lo fanno. Ad esempio, sizeof(array)/sizeof(array[0])fornisce il numero di elementi in un array.
SS Anne,

Al compilatore C è consentito allineare l'array, ma non sono convinto che dopo averlo fatto sia stato permesso di cambiare il tipo di array. L'allineamento verrebbe implementato in modo più realistico inserendo byte di riempimento.
Kevin,

1
La sottrazione di puntatori non si limita a due soli puntatori nello stesso array: i puntatori possono anche trovarsi uno oltre la fine dell'array. &a+1è definito. Come osserva John Bollinger, *(&a+1)non lo è, dal momento che tenta di dereferenziare un oggetto che non esiste.
Eric Postpischil,

5
Un compilatore non può implementare un char [][5]as char arr[][8]. Un array è solo gli oggetti ripetuti in esso; non c'è imbottitura. Inoltre, ciò spezzerebbe l'esempio (non normativo) 2 in C 2018 6.5.3.4 7, che ci dice che possiamo calcolare il numero di elementi in un array con sizeof array / sizeof array[0].
Eric Postpischil,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.