Perché il vuoto in C significa non vuoto?


25

In linguaggi fortemente tipizzati come Java e C #, void(o Void) come tipo restituito per un metodo sembra significare:

Questo metodo non restituisce nulla. Niente. Senza ritorno. Non riceverai nulla da questo metodo.

La cosa davvero strana è che in C, voidcome tipo di ritorno o anche come tipo di parametro del metodo significa:

Potrebbe davvero essere qualsiasi cosa. Dovresti leggere il codice sorgente per scoprirlo. In bocca al lupo. Se è un puntatore, dovresti davvero sapere cosa stai facendo.

Considera i seguenti esempi in C:

void describe(void *thing)
{
    Object *obj = thing;
    printf("%s.\n", obj->description);
}

void *move(void *location, Direction direction)
{
    void *next = NULL;

    // logic!

    return next;
}

Ovviamente, il secondo metodo restituisce un puntatore, che per definizione potrebbe essere qualsiasi cosa.

Dato che C è più vecchio di Java e C #, perché questi linguaggi hanno adottato voidil significato di "niente" mentre C lo ha usato come "niente o niente (quando un puntatore)"?


138
Il titolo della domanda e il testo parlano voidmentre l'esempio di codice usa void*qualcosa di completamente diverso.

4
Si noti inoltre che Java e C # hanno lo stesso concetto, semplicemente lo chiamano Objectin un caso per chiarire le ambiguità.
Mooing Duck,

4
@Mephy non sono esattamente tipizzati fortemente? Con C # usando la tipizzazione duck, intendi il dynamictipo che viene usato raramente?
Axarydax,

9
@Mephy: L'ultima volta che ho controllato Wikipedia ha elencato undici significati reciprocamente contraddittori per "fortemente tipizzato". Dire che "C # non è fortemente tipizzato" non ha quindi senso.
Eric Lippert,

8
@LightnessRacesinOrbit: No, il punto è che non esiste una definizione autorevole del termine. Wikipedia non è una risorsa prescrittiva che ti dice qual è il giusto significato di un termine. È una risorsa descrittiva . Il fatto che descriva undici diversi significati contraddittori è la prova che il termine non può essere usato nella conversazione senza definirlo attentamente . Fare diversamente significa che le probabilità sono molto buone che le persone nella conversazione parleranno tra loro, non tra di loro.
Eric Lippert,

Risposte:


58

La parola chiave void(non un puntatore) significa "niente" in quelle lingue. Questo è coerente.

Come hai notato, void*significa "puntatore a qualsiasi cosa" nei linguaggi che supportano i puntatori non elaborati (C e C ++). Questa è una decisione sfortunata perché come hai detto, voidsignifica due cose diverse.

Non sono stato in grado di trovare la ragione storica dietro il riutilizzo voidper significare "niente" e "nulla" in contesti diversi, tuttavia C lo fa in molti altri luoghi. Ad esempio, staticha scopi diversi in contesti diversi. Esiste ovviamente un precedente nel linguaggio C per riutilizzare le parole chiave in questo modo, indipendentemente da cosa si possa pensare della pratica.

Java e C # sono abbastanza diversi da fare una bella pausa per correggere alcuni di questi problemi. Java e C # "sicuro" inoltre non consentono puntatori non elaborati e non richiedono una facile compatibilità C (C # non sicuro consente i puntatori ma la stragrande maggioranza del codice C # non rientra in questa categoria). Ciò consente loro di cambiare un po 'le cose senza preoccuparsi della retrocompatibilità. Un modo per farlo è introdurre una classe Objectalla radice della gerarchia da cui ereditano tutte le classi, quindi un Objectriferimento ha la stessa funzione void*senza la cattiveria dei problemi di tipo e la gestione della memoria grezza.


2
Fornirò un riferimento quando torno a casa

4
They also do not allow raw pointers and do not need easy C compatibility.Falso. C # ha puntatori. Di solito sono (e di solito dovrebbero essere) spenti, ma sono lì.
Magus,

8
Per quanto riguarda il motivo per cui sono stati riutilizzati void: una ragione ovvia sarebbe quella di evitare di introdurre una nuova parola chiave (e di interrompere potenzialmente i programmi esistenti). Se avessero introdotto una parola chiave speciale (ad es. unknown), void*Sarebbero stati un costrutto insignificante (e forse illegale) e unknownsarebbero stati legali solo nella forma di unknown*.
jamesdlin,

20
Anche in void *, voidsignifica "niente", non "niente". Non puoi dedurre a void *, devi prima lanciarlo su un altro tipo di puntatore.
oefe,

2
@oefe Penso che non abbia senso dividere quei capelli, perché voide void*sono tipi diversi (in realtà, voidè "nessun tipo"). Ha lo stesso senso che dire "l' intin in int*significa qualcosa". No, una volta dichiarata una variabile puntatore stai dicendo "questo è un indirizzo di memoria in grado di contenere qualcosa". Nel caso di void*, stai dicendo "questo indirizzo contiene qualcosa ma non si può dedurre qualcosa dal tipo di puntatore".

32

voide void*sono due cose diverse. voidin C significa esattamente la stessa cosa che fa in Java, l'assenza di un valore di ritorno. A void*è un puntatore con un'assenza di un tipo.

Tutti i puntatori in C devono poter essere dereferenziati. Se hai indicato a void*, quale tipo ti aspetteresti di ottenere? Ricorda che i puntatori C non contengono alcuna informazione sul tipo di runtime, quindi il tipo deve essere noto al momento della compilazione.

Dato quel contesto, l'unica cosa che puoi logicamente fare con un dereferenziato void*è ignorarlo, che è esattamente il comportamento indicato dal voidtipo.


1
Sono d'accordo fino a quando non si arriva al bit "ignoralo". È possibile che la cosa restituita contenga informazioni su come interpretarla. In altre parole, potrebbe essere l'equivalente C di una variante BASIC. "Quando ottieni questo, dai un'occhiata per capire di cosa si tratta - non posso dirti in anticipo cosa troverai".
Floris,

1
O per dirla in altro modo, ipoteticamente potrei inserire un "descrittore di tipo" definito dal programmatore nel primo byte nella posizione di memoria indirizzata dal puntatore, che mi direbbe quale tipo di dati posso aspettarmi di trovare nei byte che seguono.
Robert Harvey,

1
Certo, ma in C è stato deciso di lasciare quei dettagli al programmatore, piuttosto che inserirli nella lingua. Dal punto di vista linguistico , non sa cos'altro fare se non ignorare. Ciò evita il sovraccarico di includere sempre le informazioni sul tipo come primo byte quando nella maggior parte dei casi è possibile determinarle staticamente.
Karl Bielefeldt,

4
@RobertHarvey sì, ma non stai dereferenziando un puntatore vuoto; stai lanciando il puntatore vuoto su un puntatore carattere e quindi dereferenziando il puntatore carattere.
user253751

4
@Floris gccnon è d' accordo con te. Se si tenta di utilizzare il valore, gccprodurre un messaggio di errore che dice error: void value not ignored as it ought to be.
Kasperd,

19

Forse sarebbe più utile pensare voidal tipo di ritorno. Quindi il tuo secondo metodo dovrebbe leggere "un metodo che restituisce un puntatore non tipizzato".


Questo aiuta. Come qualcuno che sta (finalmente!) Imparando C dopo anni trascorsi in linguaggi orientati agli oggetti, i puntatori sono qualcosa che è un concetto molto nuovo per me. Sembra che quando si restituisce un puntatore, voidè all'incirca l'equivalente *di linguaggi come ActionScript 3 che significa semplicemente "qualsiasi tipo di ritorno".
Naftuli Kay,

5
Bene, voidnon è un jolly. Un puntatore è solo un indirizzo di memoria. Mettere un tipo prima che *dice "ecco il tipo di dati che ci si può aspettare di trovare all'indirizzo di memoria a cui fa riferimento questo puntatore". Mettendo voidprima il *detto al compilatore "Non conosco il tipo; restituiscimi solo il puntatore non elaborato e capirò cosa fare con i dati a cui punta".
Robert Harvey,

2
Direi anche che un void*punto in un "vuoto". Un puntatore nudo con nulla a cui punta. Tuttavia puoi lanciarlo come qualsiasi altro puntatore.
Robert,

11

Rafforziamo un po 'la terminologia.

Dallo standard online C 2011 :

6.2.5 Tipi
...
19 Il voidtipo comprende un insieme di valori vuoto; è un tipo di oggetto incompleto che non può essere completato.
...
6.3 Conversioni
...
6.3.2.2 void

1 Il valore (inesistente) di voidun'espressione (un'espressione che ha tipo void) non deve essere utilizzato in alcun modo e le conversioni implicite o esplicite (tranne che per void) non devono essere applicate a tale espressione. Se un'espressione di qualsiasi altro tipo viene valutata come void espressione, il suo valore o designatore viene scartato. ( voidUn'espressione viene valutata per i suoi effetti collaterali.)

6.3.2.3 Puntatori

1 Un puntatore avoidpuò essere convertito in o da un puntatore a qualsiasi tipo di oggetto. Un puntatore a qualsiasi tipo di oggetto può essere convertito in un puntatore in vuoto e viceversa; il risultato deve essere uguale al puntatore originale.

voidUn'espressione ha valore (anche se può avere effetti collaterali). Se ho una funzione definita da restituire void, in questo modo:

void foo( void ) { ... }

quindi la chiamata

foo();

non valuta un valore; Non riesco ad assegnare il risultato a nulla, perché non c'è risultato.

Un puntatore a voidè essenzialmente un tipo di puntatore "generico"; puoi assegnare un valore void *a qualsiasi altro tipo di puntatore oggetto senza la necessità di un cast esplicito (motivo per cui tutti i programmatori C ti urleranno per lanciare il risultato di malloc).

Non è possibile dereferenziare direttamente a void *; devi prima assegnarlo a un diverso tipo di puntatore oggetto prima di poter accedere all'oggetto puntato.

voidi puntatori vengono utilizzati per implementare interfacce generiche (più o meno); l'esempio canonico è la qsortfunzione di libreria, che può ordinare array di qualsiasi tipo, purché si fornisca una funzione di confronto sensibile al tipo.

Sì, l'uso della stessa parola chiave per due concetti diversi (nessun valore rispetto a un puntatore generico) è confuso, ma non è come se non ci fosse un precedente; staticha molteplici significati distinti in C e C ++.


9

In Java e C #, void come tipo restituito per un metodo sembra significare: questo metodo non restituisce nulla. Niente. Senza ritorno. Non riceverai nulla da questo metodo.

Questa affermazione è corretta. È corretto anche per C e C ++.

in C, vuoto come tipo di ritorno o anche come tipo di parametro del metodo significa qualcosa di diverso.

Questa affermazione non è corretta. voidcome tipo restituito in C o C ++ significa la stessa cosa che fa in C # e Java. Si confonde voidcon void*. Sono completamente diversi.

Non lo confonde voide void*significa due cose completamente diverse?

Sì.

Che cos'è un puntatore? Che cos'è un puntatore vuoto?

Un puntatore è un valore che può essere dereferenziato . Dereferenziare un puntatore valido fornisce una posizione di archiviazione del tipo puntato . Un puntatore vuoto è un puntatore che non ha un particolare tipo appuntito; deve essere convertito in un tipo di puntatore più specifico prima che il valore venga dereferenziato per produrre un percorso di archiviazione .

I puntatori void sono uguali in C, C ++, Java e C #?

Java non ha puntatori vuoti; C # fa. Sono gli stessi di C e C ++ - un valore di puntatore che non ha alcun tipo specifico associato ad esso, che deve essere convertito in un tipo più specifico prima di dereferenziarlo per produrre un percorso di archiviazione.

Dato che C è più vecchio di Java e C #, perché questi linguaggi hanno adottato il vuoto come "niente" mentre C lo ha usato come "niente o niente (quando un puntatore)"?

La domanda è incoerente perché presuppone falsità. Facciamo alcune domande migliori.

Perché Java e C # hanno adottato la convenzione che voidè un tipo di ritorno valido, anziché, ad esempio, usando la convenzione di Visual Basic secondo cui le funzioni non annullano mai la restituzione e le subroutine sono sempre annullate?

Conoscere i programmatori che arrivano a Java o C # dalle lingue in cui voidè un tipo restituito.

Perché C # ha adottato la convenzione confusa che void*ha un significato completamente diverso rispetto a void?

Conoscere i programmatori che arrivano a C # da linguaggi in cui void*è presente un tipo di puntatore.


5
void describe(void *thing);
void *move(void *location, Direction direction);

La prima funzione non restituisce nulla. La seconda funzione restituisce un puntatore vuoto. Avrei dichiarato quella seconda funzione come

void* move(void* location, Direction direction);

Potrebbe essere utile se pensi a "vuoto" come significato "senza significato". Il valore restituito di describeè "senza significato". Restituire un valore da tale funzione è illegale perché hai detto al compilatore che il valore restituito è privo di significato. Non è possibile acquisire il valore restituito da quella funzione perché è privo di significato. Non puoi dichiarare una variabile di tipo voidperché è senza significato. Ad esempio void nonsense;è una sciocchezza ed è infatti illegale. Si noti inoltre che una serie di vuoti void void_array[42];è anche senza senso.

Un puntatore a void ( void*) è qualcosa di diverso. È un puntatore, non un array. Leggi void*come significato un puntatore che punta a qualcosa "senza significato". Il puntatore è "senza significato", il che significa che non ha senso dereferenziare un tale puntatore. Cercare di farlo è in effetti illegale. Il codice che dereferenzia un puntatore vuoto non verrà compilato.

Quindi se non riesci a riconoscere un puntatore vuoto e non puoi creare una matrice di vuoti, come puoi usarli? La risposta è che un puntatore a qualsiasi tipo può essere trasmesso da e verso void*. Se si void*esegue il cast di un puntatore e si ricollega a un puntatore al tipo originale, si otterrà un valore uguale al puntatore originale. Lo standard C garantisce questo comportamento. Il motivo principale per cui si vedono così tanti puntatori vuoti in C è che questa capacità fornisce un modo per implementare il nascondimento di informazioni e la programmazione basata su oggetti in quello che è molto un linguaggio non orientato agli oggetti.

Infine, il motivo che vedi spesso movedichiarato void *move(...)anziché piuttosto che void* move(...)perché mettere l'asterisco accanto al nome anziché al tipo è una pratica molto comune in C. Il motivo è che la seguente dichiarazione ti metterà nei guai: int* iptr, jptr;questo ti farebbe pensa di dichiarare due puntatori a un int. Non sei. Questa dichiarazione rende jptruna intpiuttosto che un puntatore ad un int. La dichiarazione corretta è int *iptr, *jptr;Puoi far finta che l'asterisco appartenga al tipo se ti attieni alla pratica di dichiarare solo una variabile per dichiarazione di dichiarazione. Ma tieni presente che stai fingendo.


"Il codice che dereferenzia un puntatore null non verrà compilato." Penso che intendi codice che dereferenzia un puntatore vuoto non verrà compilato. Il compilatore non ti protegge dal dereferenziare un puntatore nullo; questo è un problema di runtime.
Cody Grey,

@CodyGray - Grazie! Risolto questo. Il codice che dereferenzia un puntatore null verrà compilato (purché il puntatore non lo sia void* null_ptr = 0;).
David Hammen,

2

In poche parole, non c'è differenza . Lascia che ti faccia alcuni esempi.

Di 'che ho una classe chiamata Car.

Car obj = anotherCar; // obj is now of type Car
Car* obj = new Car(); // obj is now a pointer of type Car
void obj = 0; // obj has no type
void* = new Car(); // obj is a pointer with no type

Credo che qui la confusione qui è basato come definirebbe voidvs void*. Un tipo di ritorno voidsignifica "non restituisco nulla", mentre un tipo di ritorno void*significa "restituisco un puntatore di tipo niente".

Ciò è dovuto al fatto che un puntatore è un caso speciale. Un oggetto puntatore punterà sempre a una posizione in memoria, indipendentemente dal fatto che ci sia o meno un oggetto valido. Che i miei void* carriferimenti a un byte, una parola, un'automobile o una bicicletta non contano perché void*indica qualcosa senza un tipo definito. Sta a noi definirlo in seguito.

In linguaggi come C # e Java, la memoria viene gestita e il concetto di puntatore viene inserito nella classe Object. Invece di avere un riferimento a un oggetto di tipo "niente" sappiamo che almeno il nostro oggetto è di tipo "oggetto". Lasciami spiegare perché.

Nel C / C ++ convenzionale se si crea un oggetto e si elimina il suo puntatore, è impossibile ottenere l'accesso a quell'oggetto. Questo è ciò che è noto come perdita di memoria .

In C # / Java, tutto è un oggetto e questo consente al runtime di tenere traccia di ogni oggetto. Non ha necessariamente bisogno di sapere che il mio oggetto è un'auto, deve semplicemente conoscere alcune informazioni di base che sono già gestite dalla classe Object.

Per noi programmatori, ciò significa che gran parte delle informazioni di cui dovremmo occuparci manualmente in C / C ++ vengono gestite dalla classe Object, eliminando la necessità del caso speciale che è il puntatore.


"Il mio vuoto * indica qualcosa senza un tipo definito" - non esattamente. È più corretto dire che è un puntatore a un valore di lunghezza zero (quasi come un array con 0 elementi, ma senza nemmeno le informazioni sul tipo associate all'array).
Scott Whitlock,

2

Proverò a dire come si potrebbe pensare a queste cose in C. La lingua ufficiale nello standard C non è davvero a suo agio voide non cercherò di essere del tutto coerente con essa.

Il tipo voidnon è un cittadino di prima classe tra i tipi. Sebbene sia un tipo di oggetto, non può essere il tipo di alcun oggetto (o valore), di un campo o di un parametro di funzione; tuttavia può essere il tipo restituito di una funzione, e quindi il tipo di un'espressione (fondamentalmente delle chiamate di tali funzioni, ma anche le espressioni formate con l'operatore condizionale possono avere un tipo vuoto). Ma anche l'uso minimo di voidcome tipo di valore per cui quanto sopra lascia aperta la porta, vale a dire la fine di una funzione che ritorna voidcon un'istruzione della forma in return E;cui Eè un'espressione di tipo void, è esplicitamente vietato in C (è consentito in C ++, tuttavia ma per motivi non applicabili a C).

Se gli oggetti di tipo voidfossero ammessi, avrebbero 0 bit (ovvero la quantità di informazioni nel valore di un'espressione di voidtipo); non sarebbe un grosso problema consentire tali oggetti, ma sarebbero piuttosto inutili (gli oggetti di dimensione 0 darebbero qualche difficoltà a definire l'aritmetica del puntatore, che forse sarebbe meglio solo vietare). L'insieme di valori diversi che un tale oggetto avrebbe un elemento (banale); il suo valore potrebbe essere preso, banalmente perché non ha alcuna sostanza, ma non può essere modificato (privo di alcun valore diverso ). Lo standard è quindi confuso nel dire che voidcomprende un insieme vuoto di valori; un tale tipo potrebbe essere utile per descrivere espressioni che non possonoessere valutato (come salti o chiamate non terminanti) sebbene il linguaggio C non utilizzi effettivamente tale tipo.

Essendo vietato come tipo di valore, voidè stato utilizzato in almeno due modi non direttamente correlati per designare "proibito": specificare (void)come specifica di parametro nei tipi di funzione significa vietare fornire loro argomenti (mentre "()" significherebbe al contrario tutto è consentito; e sì, anche fornire voidun'espressione come argomento alle funzioni con la (void)specifica dei parametri è proibito), e dichiarare void *psignifica che l'espressione di dereferenziazione *pè proibita (ma non che sia una valida espressione di tipo void). Quindi hai ragione sul fatto che questi usi di voidnon sono realmente coerenti con voidcome un tipo valido di espressioni. Tuttavia un oggetto di tipovoid*non è in realtà un puntatore a valori di qualsiasi tipo, significa un valore che viene trattato come un puntatore sebbene non possa essere dereferenziato e l'aritmetica del puntatore con esso è vietata. Il "trattato come un puntatore" significa quindi solo che può essere trasmesso da e verso qualsiasi tipo di puntatore senza perdita di informazioni. Tuttavia, può anche essere utilizzato per trasmettere valori interi da e verso, quindi non deve puntare a nulla.


A rigor di termini, può essere trasmesso da e verso qualsiasi tipo di puntatore oggetto senza perdita di informazioni ma non sempre da e verso . In generale un cast da void*un altro tipo di puntatore potrebbe perdere informazioni (o anche avere UB, non ricordo di mano), ma se il valore di void*proviene dal cast di un puntatore dello stesso tipo in cui stai lanciando, allora non lo fa.
Steve Jessop,

0

In C # e Java, ogni classe deriva da una Objectclasse. Quindi ogni volta che desideriamo passare un riferimento a "qualcosa", possiamo usare un riferimento di tipo Object.

Dal momento che non abbiamo bisogno di usare i puntatori vuoti a tale scopo in queste lingue, voidnon significa qui "qualcosa o niente".


0

In C, è possibile avere un puntatore a cose di qualsiasi tipo [i puntatori si comportano come un tipo di riferimento]. Un puntatore può identificare un oggetto di un tipo noto o può identificare un oggetto di tipo arbitrario (sconosciuto). I creatori di C hanno scelto di usare la stessa sintassi generale per "puntatore a cosa di tipo arbitrario" come per "puntatore a cosa di un tipo particolare". Poiché la sintassi in quest'ultimo caso richiede un token per specificare quale sia il tipo, la prima sintassi richiede di inserire qualcosa a cui quel token appartiene se il tipo fosse noto. C ha scelto di utilizzare la parola chiave "void" a tale scopo.

In Pascal, come in C, è possibile avere puntatori a cose di qualsiasi tipo; i dialetti più popolari di Pascal consentono anche "puntatore a qualcosa di tipo arbitrario", ma piuttosto che usare la stessa sintassi che usano per "puntatore a qualcosa di un tipo particolare", si riferiscono semplicemente al primo come Pointer.

In Java non esistono tipi di variabili definiti dall'utente; tutto è un riferimento all'oggetto heap o primitivo. Si possono definire cose di tipo "riferimento a un oggetto heap di un tipo particolare" o "riferimento a un oggetto heap di tipo arbitrario". Il primo usa semplicemente il nome del tipo senza punteggiatura speciale per indicare che è un riferimento (poiché non può essere nient'altro); quest'ultimo usa il nome del tipo Object.

L'uso di void*come nomenclatura per un puntatore a qualcosa di tipo arbitrario è per quanto ne so unico per C e derivati ​​diretti come C ++; altre lingue che conosco gestiscono il concetto semplicemente usando un tipo con un nome distinto.


-1

Puoi pensare alla parola chiave voidcome a un vuoto struct( in C99 apparentemente non sono consentite le strutture vuote , in .NET e C ++ occupano più di 0 memoria e per ultimo ricordo che tutto in Java è una classe):

struct void {
};

... il che significa che è un tipo con lunghezza 0. Funziona così quando si esegue una chiamata di funzione che restituisce void... invece di allocare circa 4 byte nello stack per un valore di ritorno intero, non cambia affatto il puntatore dello stack (lo incrementa di una lunghezza di 0 ).

Quindi, se hai dichiarato alcune variabili come:

int a;
void b;
int c;

... e poi hai preso l'indirizzo di quelle variabili, quindi forse be cpotresti trovarti nello stesso posto in memoria, perché bha lunghezza zero. Quindi a void*è un puntatore in memoria al tipo incorporato con lunghezza zero. Il fatto che tu possa sapere che c'è qualcosa subito dopo che potrebbe esserti utile è un'altra cosa e richiede che tu sappia davvero cosa stai facendo (ed è pericoloso).


L'unico compilatore che conosco che assegna una lunghezza per digitare void è gcc e gcc gli assegna una lunghezza di 1.
Giosuè
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.