Perché i compilatori C e C ++ consentono lunghezze di array nelle firme di funzioni quando non vengono mai applicate?


131

Questo è quello che ho trovato durante il mio periodo di apprendimento:

#include<iostream>
using namespace std;
int dis(char a[1])
{
    int length = strlen(a);
    char c = a[2];
    return length;
}
int main()
{
    char b[4] = "abc";
    int c = dis(b);
    cout << c;
    return 0;
}  

Quindi nella variabile int dis(char a[1]), [1]sembra non fare nulla e non funziona
affatto, perché posso usarlo a[2]. Proprio come int a[]o char *a. So che il nome dell'array è un puntatore e come comunicare un array, quindi il mio puzzle non riguarda questa parte.

Quello che voglio sapere è perché i compilatori consentono questo comportamento ( int a[1]). O ha altri significati che non conosco?


6
Questo perché in realtà non è possibile passare array a funzioni.
Ed S.

37
Penso che la domanda qui sia perché C ti consente di dichiarare un parametro di tipo array quando si comporterà comunque esattamente come un puntatore.
Brian,

8
@Brian: non sono sicuro che questo sia un argomento a favore o contro il comportamento, ma si applica anche se il tipo di argomento è un typedeftipo array. Così il "decadimento puntatore" in tipi di argomenti non è solo zucchero sintattico sostituzione []con *, è davvero passare attraverso il sistema di tipi. Ciò ha conseguenze nel mondo reale per alcuni tipi standard come quelli va_listche possono essere definiti con tipo array o non array.
R .. GitHub smette di aiutare ICE il

4
@songyuanyao È possibile realizzare qualcosa di non del tutto dissimile in C (e C ++) con un puntatore: int dis(char (*a)[1]). Poi, si passa un puntatore a un array: dis(&b). Se sei disposto a utilizzare funzionalità C che non esistono in C ++, puoi anche dire cose come void foo(int data[static 256])e int bar(double matrix[*][*]), ma questa è un'intera lattina di worm.
Stuart Olsen,

1
@StuartOlsen Il punto non è quale standard ha definito cosa. Il punto è il motivo per cui chiunque lo abbia definito in questo modo.
user253751

Risposte:


156

È una stranezza della sintassi per il passaggio di matrici alle funzioni.

In realtà non è possibile passare un array in C. Se si scrive una sintassi che sembra che dovrebbe passare l'array, ciò che accade effettivamente è che viene invece passato un puntatore al primo elemento dell'array.

Poiché il puntatore non include alcuna informazione sulla lunghezza, il contenuto []dell'elenco dei parametri formale della funzione viene effettivamente ignorato.

La decisione di consentire questa sintassi è stata presa negli anni '70 e da allora ha creato molta confusione ...


21
Come programmatore non C, trovo questa risposta molto accessibile. +1
asteri,

21
+1 per "La decisione di consentire questa sintassi è stata presa negli anni '70 e ha causato molta confusione da allora ..."
NoSenseEtAl

8
questo è vero ma è anche possibile passare un array di quelle dimensioni usando la void foo(int (*somearray)[20])sintassi. in questo caso 20 viene applicato sui siti del chiamante.
v

14
-1 Come programmatore C, trovo questa risposta errata. []non sono ignorati negli array multidimensionali come mostrato nella risposta di pat. Quindi era necessaria l'inclusione della sintassi dell'array. Inoltre, nulla impedisce al compilatore di emettere avvisi anche su array monodimensionali.
user694733

7
Con "i contenuti del tuo []", sto parlando specificamente del codice nella domanda. Questa stranezza della sintassi non era affatto necessaria, la stessa cosa può essere raggiunta usando la sintassi del puntatore, cioè se viene passato un puntatore, è necessario che il parametro sia un dichiaratore di puntatore. Ad esempio nell'esempio di pat, void foo(int (*args)[20]);inoltre, in senso stretto C non ha matrici multidimensionali; ma ha matrici i cui elementi possono essere altre matrici. Questo non cambia nulla.
MM

143

La lunghezza della prima dimensione viene ignorata, ma la lunghezza delle dimensioni aggiuntive è necessaria per consentire al compilatore di calcolare correttamente gli offset. Nel seguente esempio, alla foofunzione viene passato un puntatore a un array bidimensionale.

#include <stdio.h>

void foo(int args[10][20])
{
    printf("%zd\n", sizeof(args[0]));
}

int main(int argc, char **argv)
{
    int a[2][20];
    foo(a);
    return 0;
}

La dimensione della prima dimensione [10]viene ignorata; il compilatore non ti impedirà di indicizzare alla fine (nota che il formale vuole 10 elementi, ma l'effettivo fornisce solo 2). Tuttavia, la dimensione della seconda dimensione [20]viene utilizzata per determinare il passo di ciascuna riga e, qui, il formale deve corrispondere a quello reale. Anche in questo caso, il compilatore non ti impedirà di indicizzare la fine della seconda dimensione.

L'offset di byte dalla base dell'array a un elemento args[row][col]è determinato da:

sizeof(int)*(col + 20*row)

Si noti che col >= 20, quindi, verrà effettivamente indicizzato in una riga successiva (o alla fine dell'intero array).

sizeof(args[0]), ritorna 80sulla mia macchina dove sizeof(int) == 4. Tuttavia, se provo a prendere sizeof(args), ricevo il seguente avviso del compilatore:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
    printf("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

Qui, il compilatore avverte che fornirà solo la dimensione del puntatore in cui è decaduto l'array anziché la dimensione dell'array stesso.


Molto utile - la coerenza con questo è anche plausibile come la ragione della stranezza nel caso 1-d.
jwg

1
È la stessa idea del caso 1-D. Quello che sembra un array 2-D in C e C ++ è in realtà un array 1-D, ogni cui elemento è un altro array 1-D. In questo caso abbiamo un array con 10 elementi, ognuno dei quali è "array di 20 in". Come descritto nel mio post, ciò che viene effettivamente passato alla funzione è il puntatore al primo elemento di args. In questo caso, il primo elemento di args è un "array di 20 in". I puntatori includono informazioni sul tipo; ciò che viene passato è "puntatore a un array di 20 in".
MM

9
Sì, questo è il int (*)[20]tipo; msgstr "puntatore a un array di 20 in".
pat

33

Il problema e come risolverlo in C ++

Il problema è stato ampiamente spiegato da Pat e Matt . Il compilatore sta sostanzialmente ignorando la prima dimensione della dimensione dell'array ignorando effettivamente la dimensione dell'argomento passato.

In C ++, d'altra parte, puoi facilmente superare questa limitazione in due modi:

  • usando riferimenti
  • utilizzo std::array(dal C ++ 11)

Riferimenti

Se la tua funzione sta solo cercando di leggere o modificare un array esistente (non copiandolo), puoi facilmente usare i riferimenti.

Ad esempio, supponiamo che tu voglia avere una funzione che reimposta una matrice di dieci ints impostando ogni elemento su 0. Puoi farlo facilmente usando la seguente firma della funzione:

void reset(int (&array)[10]) { ... }

Non solo funzionerà bene , ma imporrà anche la dimensione dell'array .

Puoi anche utilizzare i modelli per rendere generico il codice sopra :

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

E infine puoi sfruttare la constcorrettezza. Consideriamo una funzione che stampa un array di 10 elementi:

void show(const int (&array)[10]) { ... }

Applicando il constqualificatore stiamo impedendo possibili modifiche .


La classe di libreria standard per gli array

Se consideri la sintassi sopra sia brutta e non necessaria, come faccio io, possiamo buttarla nella lattina e usarla std::arrayinvece (dal C ++ 11).

Ecco il codice refactored:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

Non è meraviglioso? Per non parlare del fatto che il trucco del codice generico che ti ho insegnato in precedenza, funziona ancora:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }

template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

Non solo, ma ottieni copia e spostamento semantico gratis. :)

void copy(std::array<Type, N> array) {
    // a copy of the original passed array 
    // is made and can be dealt with indipendently
    // from the original
}

Allora, cosa stai aspettando? Vai a usare std::array.


2
@kietz, mi dispiace che la tua modifica suggerita sia stata respinta, ma assumiamo automaticamente che venga utilizzato C ++ 11 , se non diversamente specificato.
Scarpa

questo è vero, ma dovremmo anche specificare se una soluzione è solo C ++ 11, in base al link che hai fornito.
truscly

@trlkly, sono d'accordo. Ho modificato la risposta di conseguenza. Grazie per segnalarlo.
Scarpa

9

È una caratteristica divertente di C che ti consente di spararti efficacemente nel piede se sei così incline.

Penso che il motivo sia che C è solo un gradino sopra il linguaggio assembly. Il controllo delle dimensioni e caratteristiche di sicurezza simili sono state rimosse per consentire le massime prestazioni, il che non è un male se il programmatore è molto diligente.

Inoltre, assegnare una dimensione all'argomento della funzione ha il vantaggio che quando la funzione viene utilizzata da un altro programmatore, è possibile che notino una limitazione della dimensione. Il solo utilizzo di un puntatore non trasmette tali informazioni al prossimo programmatore.


3
Sì. C è progettato per fidarsi del programmatore sul compilatore. Se stai indicizzando in modo così palese la fine di un array, devi fare qualcosa di speciale e intenzionale.
Giovanni,

7
Mi sono tagliato i denti durante la programmazione su C 14 anni fa. Di tutte le parole del mio professore, l'unica frase che mi è rimasta impressa più di tutte le altre, "C è stata scritta da programmatori, per programmatori". La lingua è estremamente potente. (Preparati al cliché) Come ci ha insegnato lo zio Ben, "Con grande potenza, derivano grandi responsabilità".
Andrew Falanga,

6

Innanzitutto, C non controlla mai i limiti dell'array. Non importa se sono parametri locali, globali, statici, qualunque cosa. Il controllo dei limiti dell'array implica una maggiore elaborazione e C dovrebbe essere molto efficiente, quindi il controllo dei limiti dell'array viene eseguito dal programmatore quando necessario.

In secondo luogo, esiste un trucco che consente di passare un valore per array a una funzione. È anche possibile restituire per valore un array da una funzione. Devi solo creare un nuovo tipo di dati usando struct. Per esempio:

typedef struct {
  int a[10];
} myarray_t;

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}

Devi accedere agli elementi in questo modo: foo.a [1]. Il ".a" in più potrebbe sembrare strano, ma questo trucco aggiunge una grande funzionalità al linguaggio C.


7
Stai confondendo il controllo dei limiti di runtime con il controllo del tipo in fase di compilazione.
Ben Voigt,

@Ben Voigt: sto solo parlando del controllo dei limiti, come è la domanda originale.
user34814

2
@ user34814 il controllo dei limiti di compilazione rientra nell'ambito del controllo del tipo. Diverse lingue di alto livello offrono questa funzione.
Leushenko,

5

Per dire al compilatore che myArray punta a un array di almeno 10 pollici:

void bar(int myArray[static 10])

Un buon compilatore dovrebbe darti un avviso se accedi a myArray [10]. Senza la parola chiave "statica", il 10 non significherebbe nulla.


1
Perché un compilatore dovrebbe avvisare se si accede all'undicesimo elemento e l'array contiene almeno 10 elementi?
nwellnhof,

Presumibilmente ciò è dovuto al fatto che il compilatore può solo imporre di avere almeno 10 elementi. Se si tenta di accedere all'11 ° elemento, non si può essere sicuri che esista (anche se può).
Dylan Watson,

2
Non credo sia una lettura corretta dello standard. [static]consente al compilatore di avvisare se si chiama bar con un int[5]. Essa non dettare quello che si può accedere all'interno bar . L'onere è interamente dal lato del chiamante.
tab

3
error: expected primary-expression before 'static'mai visto questa sintassi. è improbabile che questo sia C o C ++ standard.
v

3
@ v.oddou, è specificato in C99, in 6.7.5.2 e 6.7.5.3.
Samuel Edwin Ward,

5

Questa è una "caratteristica" ben nota di C, passata a C ++ perché C ++ dovrebbe compilare correttamente il codice C.

Il problema sorge da diversi aspetti:

  1. Un nome di array dovrebbe essere completamente equivalente a un puntatore.
  2. C si suppone che sia veloce, originariamente developerd di essere una sorta di "alto livello Assembler" (appositamente progettato di scrivere il primo "Portable Operating System": Unix), quindi è non dovrebbe inserire "nascosto" del codice; il controllo dell'intervallo di runtime è quindi "proibito".
  3. Il codice macchina generato per accedere a un array statico o dinamico (nello stack o allocato) è in realtà diverso.
  4. Poiché la funzione chiamata non può conoscere il "tipo" di array passato come argomento, tutto dovrebbe essere un puntatore e trattato come tale.

Si potrebbe dire che le matrici non sono realmente supportate in C (questo non è proprio vero, come dicevo prima, ma è una buona approssimazione); un array viene realmente trattato come un puntatore a un blocco di dati e vi si accede utilizzando l'aritmetica del puntatore. Poiché C NON ha alcuna forma di RTTI È necessario dichiarare la dimensione dell'elemento array nel prototipo della funzione (per supportare l'aritmetica del puntatore). Questo è persino "più vero" per gli array multidimensionali.

Comunque tutto quanto sopra non è più vero: p

La maggior parte dei compilatori moderna C / C ++ fanno limiti di supporto controllo, ma gli standard richiedono che sia disabilitata di default (per compatibilità all'indietro). Versioni abbastanza recenti di gcc, ad esempio, eseguono il controllo dell'intervallo in fase di compilazione con "-O3 -Wall -Wextra" e i limiti di runtime completi controllano con "-fbounds-check".


Forse il C ++ avrebbe dovuto compilare il codice C 20 anni fa, ma certamente non lo è , e non lo è da molto tempo (almeno C ++ 98? C99, che non è stato "riparato" da nessun nuovo standard C ++).
hyde,

@hyde Mi sembra un po 'troppo duro. Per citare Stroustrup "Con piccole eccezioni, C è un sottoinsieme di C ++." (Il C ++ PL 4a ed., Sec. 1.2.1). Mentre sia C ++ che C si evolvono ulteriormente e esistono funzionalità dell'ultima versione C che non sono nell'ultima versione C ++, nel complesso penso che la citazione di Stroustrup sia ancora valida.
mvw

@mvw La maggior parte del codice C scritto in questo millennio, che non viene mantenuto intenzionalmente compatibile con C ++ evitando caratteristiche incompatibili, utilizzerà la sintassi degli inizializzatori designati C99 ( struct MyStruct s = { .field1 = 1, .field2 = 2 };) per inizializzare le strutture, perché è un modo molto più chiaro di inizializzare una struttura. Di conseguenza, la maggior parte del codice C corrente verrà rifiutata dai compilatori C ++ standard, poiché la maggior parte del codice C inizializzerà le strutture.
hyde,

@mvw Si potrebbe forse dire che C ++ dovrebbe essere compatibile con C, quindi è possibile scrivere codice che verrà compilato con compilatori sia C che C ++, se vengono fatti alcuni compromessi. Ma che richiede l'utilizzo di un sottoinsieme di entrambi C e C ++, non solo sottoinsieme del C ++.
hyde,

@hyde Sareste sorpresi di quanto codice C sia compilabile in C ++. Alcuni anni fa l'intero kernel Linux era compilabile in C ++ (non so se sia ancora vero). Compilo regolarmente il codice C nel compilatore C ++ per ottenere un controllo di avviso superiore, solo "produzione" viene compilata in modalità C per ottenere la massima ottimizzazione.
ZioByte,

3

C non trasformerà solo un parametro di tipo int[5]in *int; data la dichiarazione typedef int intArray5[5];, trasformerà un parametro di tipo intArray5a *intpure. Ci sono alcune situazioni in cui questo comportamento, sebbene dispari, è utile (specialmente con cose come quelle va_listdefinite in stdargs.h, che alcune implementazioni definiscono come un array). Sarebbe illogico consentire come parametro un tipo definito come int[5](ignorando la dimensione) ma non consentire int[5]di essere specificato direttamente.

Trovo assurda la gestione dei parametri di tipo array da parte di C, ma è una conseguenza degli sforzi per prendere un linguaggio ad hoc, gran parte dei quali non erano particolarmente ben definiti o pensati, e provare a trovare un comportamento specifiche coerenti con quanto fatto dalle implementazioni esistenti per i programmi esistenti. Molte delle stranezze di C hanno senso se viste sotto quella luce, in particolare se si considera che quando molte di esse sono state inventate, gran parte del linguaggio che conosciamo oggi non esisteva ancora. Da quello che ho capito, nel predecessore di C, chiamato BCPL, i compilatori non tenevano molto bene traccia dei tipi di variabili. Una dichiarazione int arr[5];era equivalente a int anonymousAllocation[5],*arr = anonymousAllocation;; una volta che l'allocazione è stata accantonata. il compilatore non sapeva né importava searrera un puntatore o un array. Se acceduto come uno arr[x]o*arr, sarebbe considerato un puntatore indipendentemente da come è stato dichiarato.


1

Una cosa a cui non è stata ancora data risposta è la vera domanda.

Le risposte già fornite spiegano che le matrici non possono essere passate per valore a una funzione in C o C ++. Spiegano anche che un parametro dichiarato come int[]viene trattato come se avesse tipo int *e che una variabile di tipo int[]può essere passata a tale funzione.

Ma non spiegano perché non sia mai stato fatto un errore nel fornire esplicitamente una lunghezza di array.

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

Perché l'ultimo di questi non è un errore?

Un motivo è che causa problemi con i typedef.

typedef int myarray[10];
void f(myarray array);

Se fosse un errore specificare la lunghezza dell'array nei parametri della funzione, non si sarebbe in grado di usare il myarraynome nel parametro della funzione. E poiché alcune implementazioni usano tipi di array per tipi di librerie standard come va_list, e tutte le implementazioni sono necessarie per creare jmp_bufun tipo di array, sarebbe molto problematico se non ci fosse un modo standard di dichiarare i parametri delle funzioni usando quei nomi: senza quell'abilità, ci sarebbe non essere un'implementazione portatile di funzioni come vprintf.


0

È consentito ai compilatori di verificare se la dimensione dell'array passato è uguale a quella prevista. I compilatori possono avvertire un problema se non è il caso.

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.