Le variabili in C sono dichiarate come un tipo seguito da un identificatore seguito da una dimensione facoltativa. (Tralascio i modificatori facoltativi, come static
perché non sono pertinenti.) Inizierò con gli interi poiché sono facili da digerire:
Se non ci sono dimensioni presenti, la variabile è del tipo immediatamente a sinistra dell'identificatore. La dichiarazione di int x
significa che x
è di tipo int
. Lo stesso con un puntatore: int *x
è di tipo int *
.
L'aggiunta di una dimensione ( int x[5]
) modifica la dichiarazione in una matrice, che ha alcuni effetti collaterali:
-
La matrice stessa (cioè il suo contenuto ) è la variabile; l'identificatore diventa semplicemente un modo per riferirsi ad esso, e in C che lo squalifica dall'essere usato come un lvalue. Si tratta di un residuo del linguaggio assembly, in cui è possibile definire un blocco di spazio nella sorgente, assegnargli un'etichetta e fare in modo che la posizione finale dell'etichetta venga riempita ovunque sia stata utilizzata durante il collegamento. Non è possibile modificare l'etichetta; se volevi una variabile, doveva essere in un registro o in una memoria.
-
Il compilatore allocerà abbastanza spazio per contenere l'array (sia esso in pila per un automatico, spazio nell'oggetto per una statica o come parte di una struttura) e prendere nota di dove si trova lo spazio.
-
L'uso dell'identificatore dell'array come valore di rvalore ne viene trattato come se fosse un puntatore al primo elemento * in tutti i contesti tranne due (quelli che sono gli operatori &
e sizeof
). Ai fini della comprensione di ciò che è stato dichiarato, trattalo come un puntatore: aggiungi una stella alla fine del tipo e non preoccuparti della dimensione (ad es., int x[5]
si comporta come int *x
quando x
è usato come valore di rvalore ).
Applicandolo al tuo esempio, lo suddividiamo nelle sue parti costitutive:
char ** p [5]
È presente una dimensione, quindi la sminuzziamo e aggiungiamo una stella al tipo:
char *** p
Utilizzato come valore di rvalore in tutti tranne i casi eccezionali, p
equivale a un puntatore a un puntatore a un puntatore a char
.
Il problema con i puntatori è che non hanno alcuna nozione di dove possono essere puntati senza superare i limiti dell'array. Questo è un altro holdover da assembly, in cui un indirizzo potrebbe trovarsi in un registro senza altri contesti e potrebbe non puntare a un array; potrebbe essere solo un'istanza casuale di quel tipo.
L'operatore sizeof
ti dà un modo per determinare la dimensione di un array, perché il suo operando è uno di quei casi eccezionali in cui l'identificatore non valuta un puntatore:
int x[5];
size_t x_elements = sizeof x / sizeof *x
Un'altra nota: chiedi delle precedenze relative di *
e []
. In una dichiarazione, non sono operatori perché non stai valutando un'espressione. Ergo, la precedenza non si applica.
* Ad un certo punto nei 30 anni da quando ho imparato C, qualcuno ha iniziato a chiamare questo decadimento . Come programmatore di assemblaggi di lunga data, penso che sia un termine terribile perché l'implicazione è che il sottoprodotto è in qualche modo inferiore. Ci sono, sfortunatamente, molti puristi del linguaggio che non riescono a capire che qualcuno deve scrivere tutte queste cose di basso livello per far funzionare le loro belle astrazioni. K & R lo ha reso abbastanza chiaro nella prefazione alla prima edizione di The C Programming Language che questa è l'applicazione per cui è stato sviluppato C.