Puntatori C: puntano a un array di dimensioni fisse


120

Questa domanda va ai guru del C là fuori:

In C, è possibile dichiarare un puntatore come segue:

char (* p)[10];

.. che fondamentalmente afferma che questo puntatore punta a un array di 10 caratteri. La cosa bella della dichiarazione di un puntatore come questo è che si otterrà un errore in fase di compilazione se si tenta di assegnare un puntatore di un array di dimensioni diverse a p. Ti darà anche un errore in fase di compilazione se provi ad assegnare il valore di un semplice puntatore char a p. L'ho provato con gcc e sembra funzionare con ANSI, C89 e C99.

Mi sembra che dichiarare un puntatore come questo sarebbe molto utile, in particolare, quando si passa un puntatore a una funzione. Di solito, le persone scriverebbero il prototipo di una tale funzione come questa:

void foo(char * p, int plen);

Se ti aspettavi un buffer di una dimensione specifica, dovresti semplicemente testare il valore di plen. Tuttavia, non è possibile garantire che la persona che ti passa p ti fornirà davvero piene posizioni di memoria valide in quel buffer. Devi fidarti che la persona che ha chiamato questa funzione sta facendo la cosa giusta. D'altro canto:

void foo(char (*p)[10]);

.. forza il chiamante a fornire un buffer della dimensione specificata.

Questo sembra molto utile ma non ho mai visto un puntatore dichiarato in questo modo in nessun codice che abbia mai incontrato.

La mia domanda è: c'è qualche motivo per cui le persone non dichiarano puntatori come questo? Non vedo qualche ovvia trappola?


3
nota: poiché C99 l'array non deve essere di dimensione fissa come suggerito dal titolo, 10può essere sostituito da qualsiasi variabile nell'ambito
MM

Risposte:


174

Quello che stai dicendo nel tuo post è assolutamente corretto. Direi che ogni sviluppatore C arriva esattamente alla stessa scoperta e alla stessa conclusione quando (se) raggiunge un certo livello di competenza con il linguaggio C.

Quando le specifiche dell'area di applicazione richiedono un array di dimensioni fisse specifiche (la dimensione dell'array è una costante del tempo di compilazione), l'unico modo corretto per passare tale array a una funzione è usare un parametro pointer-to-array

void foo(char (*p)[10]);

(nel linguaggio C ++ questo viene fatto anche con i riferimenti

void foo(char (&p)[10]);

).

Ciò abiliterà il controllo del tipo a livello di lingua, che assicurerà che l'array di dimensioni esattamente corrette sia fornito come argomento. Infatti in molti casi le persone usano questa tecnica implicitamente, senza nemmeno rendersene conto, nascondendo il tipo di array dietro un nome typedef

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Nota inoltre che il codice sopra è invariante in relazione al Vector3dtipo che è un array o un file struct. Puoi cambiare la definizione di Vector3din qualsiasi momento da un array a un filestruct e viceversa, e non dovrai cambiare la dichiarazione della funzione. In entrambi i casi le funzioni riceveranno un oggetto aggregato "per riferimento" (ci sono eccezioni a questo, ma nel contesto di questa discussione questo è vero).

Tuttavia, non vedrai questo metodo di passaggio di array usato esplicitamente troppo spesso, semplicemente perché troppe persone sono confuse da una sintassi piuttosto contorta e semplicemente non sono abbastanza a proprio agio con tali caratteristiche del linguaggio C per usarle correttamente. Per questo motivo, nella vita reale media, passare un array come puntatore al suo primo elemento è un approccio più popolare. Sembra solo "più semplice".

Ma in realtà, usare il puntatore al primo elemento per il passaggio di array è una tecnica molto di nicchia, un trucco, che ha uno scopo ben preciso: il suo unico scopo è quello di facilitare il passaggio di array di dimensioni diverse (es. Dimensione del runtime) . Se hai davvero bisogno di essere in grado di elaborare array di dimensioni runtime, il modo corretto per passare tale array è un puntatore al suo primo elemento con la dimensione concreta fornita da un parametro aggiuntivo

void foo(char p[], unsigned plen);

In realtà, in molti casi è molto utile essere in grado di elaborare array di dimensioni runtime, il che contribuisce anche alla popolarità del metodo. Molti sviluppatori C semplicemente non incontrano mai (o non riconoscono mai) la necessità di elaborare un array a dimensione fissa, rimanendo così ignari della corretta tecnica a dimensione fissa.

Tuttavia, se la dimensione dell'array è fissa, passarla come puntatore a un elemento

void foo(char p[])

è un grave errore a livello di tecnica, che purtroppo è piuttosto diffuso in questi giorni. Una tecnica pointer-to-array è un approccio molto migliore in questi casi.

Un altro motivo che potrebbe ostacolare l'adozione della tecnica di passaggio di array a dimensione fissa è il predominio dell'approccio ingenuo alla tipizzazione di array allocati dinamicamente. Ad esempio, se il programma richiede array di tipo fissi char[10](come nel tuo esempio), uno sviluppatore medio utilizzerà mallocarray come

char *p = malloc(10 * sizeof *p);

Questo array non può essere passato a una funzione dichiarata come

void foo(char (*p)[10]);

il che confonde lo sviluppatore medio e gli fa abbandonare la dichiarazione del parametro a dimensione fissa senza pensarci ulteriormente. In realtà, però, la radice del problema sta nell'approccio ingenuo malloc. Il mallocformato mostrato sopra dovrebbe essere riservato per array di dimensioni runtime. Se il tipo di array ha una dimensione in fase di compilazione, un modo migliore per mallocottenerlo sarebbe il seguente

char (*p)[10] = malloc(sizeof *p);

Questo, ovviamente, può essere facilmente passato a quanto sopra dichiarato foo

foo(p);

e il compilatore eseguirà il controllo del tipo appropriato. Ma ancora una volta, questo è eccessivamente confuso per uno sviluppatore C non preparato, motivo per cui non lo vedrai troppo spesso nel codice "tipico" medio di tutti i giorni.


2
La risposta fornisce una descrizione molto concisa e informativa di come sizeof () ha successo, come spesso fallisce e come fallisce sempre. le tue osservazioni della maggior parte degli ingegneri C / C ++ che non capiscono, e quindi fare qualcosa che pensano di capire invece è una delle cose più profetiche che ho visto da un po ', e il velo non è niente in confronto alla precisione che descrive. seriamente, signore. Bella risposta.
WhozCraig

Ho appena refactoring del codice basato su questa risposta, bravo e grazie sia per la Q che per la A.
Perry

1
Sono curioso di sapere come gestisci la constproprietà con questa tecnica. Un const char (*p)[N]argomento non sembra compatibile con un puntatore a. char table[N];Al contrario, un semplice char*ptr rimane compatibile con un const char*argomento.
Ciano

4
Potrebbe essere utile notare che per accedere a un elemento del tuo array, devi farlo (*p)[i]e non farlo *p[i]. Quest'ultimo salterà in base alla dimensione dell'array, che quasi certamente non è quello che vuoi. Almeno per me, l'apprendimento di questa sintassi ha causato, piuttosto che impedito, un errore; Avrei ottenuto il codice corretto più velocemente semplicemente passando un float *.
Andrew Wagner

1
Sì @mickey, quello che hai suggerito è un constpuntatore a un array di elementi mutabili. E sì, questo è completamente diverso da un puntatore a una serie di elementi immutabili.
Ciano

11

Vorrei aggiungere alla risposta di AndreyT (nel caso qualcuno si imbattesse in questa pagina alla ricerca di maggiori informazioni su questo argomento):

Quando comincio a giocare di più con queste dichiarazioni, mi rendo conto che c'è un handicap maggiore ad esse associato in C (apparentemente non in C ++). È abbastanza comune avere una situazione in cui si desidera fornire a un chiamante un puntatore const a un buffer in cui si è scritto. Sfortunatamente, questo non è possibile quando si dichiara un puntatore come questo in C.In altre parole, lo standard C (6.7.3 - Paragrafo 8) è in contrasto con qualcosa del genere:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Questo vincolo non sembra essere presente in C ++, rendendo questo tipo di dichiarazioni molto più utili. Ma nel caso di C, è necessario tornare a una normale dichiarazione di puntatore ogni volta che si desidera un puntatore const al buffer a dimensione fissa (a meno che il buffer stesso non sia stato dichiarato const per cominciare). Puoi trovare maggiori informazioni in questo thread di posta: testo del collegamento

Questo è un vincolo severo secondo me e potrebbe essere uno dei motivi principali per cui le persone di solito non dichiarano puntatori come questo in C. L'altro è il fatto che la maggior parte delle persone non sa nemmeno che puoi dichiarare un puntatore come questo come AndreyT ha sottolineato.


2
Sembra essere un problema specifico del compilatore. Sono stato in grado di duplicare utilizzando gcc 4.9.1, ma clang 3.4.2 è stato in grado di passare da una versione non-const a una const senza problemi. Ho letto le specifiche C11 (p 9 nella mia versione ... la parte che parla di due tipi qualificati compatibili) e sono d'accordo sul fatto che sembra dire che queste conversioni sono illegali. Tuttavia, sappiamo in pratica che puoi sempre convertire automaticamente da char * a char const * senza preavviso. IMO, clang è più coerente nel consentire questo rispetto a gcc, anche se sono d'accordo con te che le specifiche sembrano vietare qualsiasi di queste conversioni automatiche.
Doug Richardson,

4

L'ovvia ragione è che questo codice non viene compilato:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

La promozione predefinita di un array è a un puntatore non qualificato.

Vedi anche questa domanda , l'uso foo(&p)dovrebbe funzionare.


3
Ovviamente foo (p) non funzionerà, foo sta chiedendo un puntatore a un array di 10 elementi, quindi devi passare l'indirizzo del tuo array ...
Brian R. Bondy

9
Come è questa la "ragione ovvia"? Ovviamente è chiaro che il modo corretto di chiamare la funzione è foo(&p).
AnT

3
Immagino che "ovvio" sia la parola sbagliata. Intendevo "il più semplice". La distinzione tra p e & p in questo caso è abbastanza oscura per il programmatore C medio. Qualcuno che cerca di fare ciò che il poster ha suggerito scriverà ciò che ho scritto, riceverà un errore in fase di compilazione e si arrenderà.
Keith Randall,

2

Voglio anche usare questa sintassi per abilitare un maggiore controllo del tipo.

Ma sono anche d'accordo sul fatto che la sintassi e il modello mentale dell'uso dei puntatori sia più semplice e facile da ricordare.

Ecco alcuni altri ostacoli che ho incontrato.

  • L'accesso all'array richiede l'utilizzo di (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    Si è tentati di usare invece un puntatore a carattere locale:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Ma questo annullerebbe parzialmente lo scopo di utilizzare il tipo corretto.

  • Bisogna ricordarsi di usare l'operatore address-of quando si assegna l'indirizzo di un array a un pointer-to-array:

    char a[10];
    char (*p)[10] = &a;

    L'operatore address-of ottiene l'indirizzo dell'intero array &a, con il tipo corretto a cui assegnarlo p. Senza l'operatore, aviene automaticamente convertito nell'indirizzo del primo elemento dell'array, lo stesso di &a[0], che ha un tipo diverso.

    Poiché questa conversione automatica è già in corso, sono sempre perplesso che &sia necessario. È coerente con l'uso di &su variabili di altri tipi, ma devo ricordare che un array è speciale e che ho bisogno di &per ottenere il tipo corretto di indirizzo, anche se il valore dell'indirizzo è lo stesso.

    Una ragione del mio problema potrebbe essere che ho imparato K&R C negli anni '80, che non consentiva ancora di utilizzare l' &operatore su interi array (sebbene alcuni compilatori lo ignorassero o tollerassero la sintassi). Il che, a proposito, potrebbe essere un altro motivo per cui i puntatori agli array hanno difficoltà a essere adottati: funzionano correttamente solo a partire da ANSI C e la &limitazione dell'operatore potrebbe essere stata un'altra ragione per considerarli troppo scomodi.

  • Quando nontypedef viene utilizzato per creare un tipo per il puntatore all'array (in un file di intestazione comune), un puntatore globale all'array necessita di una dichiarazione più complicata per condividerlo tra i file:extern

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];

1

Beh, in poche parole, C non fa le cose in questo modo. Un array di tipo Tviene passato come puntatore al primo Tdell'array, e questo è tutto ciò che ottieni.

Ciò consente alcuni algoritmi interessanti ed eleganti, come il ciclo attraverso l'array con espressioni come

*dst++ = *src++

Lo svantaggio è che la gestione delle dimensioni dipende da te. Sfortunatamente, l'incapacità di farlo coscienziosamente ha anche portato a milioni di bug nella codifica C e / o opportunità di sfruttamento malevolo.

Ciò che si avvicina a ciò che chiedi in C è passare intorno a struct(per valore) o un puntatore a uno (per riferimento). Finché lo stesso tipo di struttura viene utilizzato su entrambi i lati di questa operazione, sia il codice che distribuisce il riferimento sia il codice che lo utilizza concordano sulla dimensione dei dati gestiti.

La tua struttura può contenere tutti i dati che desideri; potrebbe contenere il tuo array di dimensioni ben definite.

Tuttavia, nulla impedisce a te oa un programmatore incompetente o malevolo di usare i cast per ingannare il compilatore facendogli trattare la tua struttura come una di dimensioni diverse. La capacità quasi illimitata di fare questo genere di cose fa parte del design di C.


1

Puoi dichiarare un array di caratteri in diversi modi:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Il prototipo di una funzione che accetta un array per valore è:

void foo(char* p); //cannot modify p

o per riferimento:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

o dalla sintassi dell'array:

void foo(char p[]); //same as char*

2
Non dimenticare che un array di dimensioni fisse può anche essere allocato dinamicamente come char (*p)[10] = malloc(sizeof *p).
AnT

Vedi qui per una discussione più dettagliata tra le differenze di char array [] e char * ptr qui. stackoverflow.com/questions/1807530/...
t0mm13b

1

Non consiglierei questa soluzione

typedef int Vector3d[3];

poiché oscura il fatto che Vector3D ha un tipo che devi conoscere. I programmatori di solito non si aspettano che variabili dello stesso tipo abbiano dimensioni diverse. Tener conto di :

void foo(Vector3d a) {
   Vector3D b;
}

dove sizeof a! = sizeof b


Non lo stava suggerendo come soluzione. Stava semplicemente usando questo come esempio.
figurassa

Hm. Perché sizeof(a)non è lo stesso di sizeof(b)?
sherrellbc

0

Forse mi manca qualcosa, ma ... poiché gli array sono puntatori costanti, in pratica ciò significa che non ha senso passare i puntatori ad essi.

Non potresti semplicemente usare void foo(char p[10], int plen);?


4
Gli array NON sono puntatori costanti. Leggi alcune FAQ sugli array, per favore.
AnT

2
Per ciò che conta qui (array unidimensionali come parametri), il fatto è che decadono in puntatori costanti. Leggi una FAQ su come essere meno pedante, per favore.
Fortran

-2

Sul mio compilatore (vs2008) tratta char (*p)[10]come un array di puntatori di caratteri, come se non ci fossero parentesi, anche se compilo come file C. Il supporto del compilatore per questa "variabile"? Se è così, questa è una delle ragioni principali per non usarla.


1
-1 Sbagliato. Funziona bene su vs2008, vs2010, gcc. In particolare, questo esempio funziona bene: stackoverflow.com/a/19208364/2333290
kotlomoy
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.