Dans les langages fortement typés comme Java et C #, void
(ou Void
) comme type de retour pour une méthode semblent signifier:
Cette méthode ne renvoie rien. Rien. Non-retour. Vous ne recevrez rien de cette méthode.
Ce qui est vraiment étrange, c'est qu'en C, void
comme type de retour ou même comme type de paramètre de méthode signifie:
Ça pourrait vraiment être n'importe quoi. Il faudrait lire le code source pour le savoir. Bonne chance. Si c'est un pointeur, vous devriez vraiment savoir ce que vous faites.
Considérez les exemples suivants en 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;
}
De toute évidence, la deuxième méthode renvoie un pointeur, qui par définition pourrait être n'importe quoi.
Puisque C est plus ancien que Java et C #, pourquoi ces langages ont-ils adopté void
comme signifiant "rien" alors que C l'a utilisé comme "rien ou rien (quand un pointeur)"?
Le mot clé void
(pas un pointeur) signifie "rien" dans ces langues. C'est cohérent.
Comme vous l'avez noté, void*
signifie "pointeur vers n'importe quoi" dans les langages qui prennent en charge les pointeurs bruts (C et C++). C'est une décision malheureuse car, comme vous l'avez mentionné, cela signifie void
deux choses différentes.
Je n'ai pas pu trouver la raison historique derrière la réutilisation de void
pour signifier "rien" et "quoi que ce soit" dans différents contextes, cependant C le fait à plusieurs autres endroits. Par exemple, static
a des objectifs différents dans des contextes différents. Il existe évidemment un précédent dans le langage C pour réutiliser des mots clés de cette façon, indépendamment de ce que l'on peut penser de la pratique.
Java et C # sont suffisamment différents pour faire une pause nette pour corriger certains de ces problèmes. Java et C # "sûr" n'autorisent pas non plus les pointeurs bruts et n'ont pas besoin d'une compatibilité C facile (C # non sûr le fait autorise les pointeurs mais la grande majorité du code C # ne tombe pas dans cette catégorie. Cela leur permet de changer un peu les choses sans se soucier de la compatibilité descendante. Une façon de procéder consiste à introduire une classe Object
à la racine de la hiérarchie dont toutes les classes héritent, donc une référence Object
remplit la même fonction que void*
sans les problèmes de type et la gestion de la mémoire brute.
void
et void*
sont deux choses différentes. void
en C signifie exactement la même chose qu'en Java, une absence de valeur de retour. Un void*
Est un pointeur avec une absence de type.
Tous les pointeurs en C doivent pouvoir être déréférencés. Si vous déréférenciez un void*
, Quel type vous attendriez-vous? N'oubliez pas que les pointeurs C ne contiennent aucune information de type d'exécution, donc le type doit être connu au moment de la compilation.
Dans ce contexte, la seule chose que vous pouvez logiquement faire avec un void*
Déréférencé est de l'ignorer, ce qui est exactement le comportement que le type void
désigne.
Il serait peut-être plus utile de penser à void
comme type de retour. Ensuite, votre deuxième méthode se lirait "une méthode qui renvoie un pointeur non typé."
Resserrons un peu la terminologie.
De la norme C 2011 en ligne :
6.2.5 Types
...
19 Le typevoid
comprend un ensemble vide de valeurs; il s'agit d'un type d'objet incomplet qui ne peut pas être complété.
...
6.3 Conversions
...
6.3.2.2 nul
1 La valeur (inexistante) d'une expressionvoid
(une expression qui a le typevoid
) ne doit en aucun cas être utilisée et les conversions implicites ou explicites (sauf versvoid
) ne doit pas être appliqué à une telle expression. Si une expression de tout autre type est évaluée en tant qu'expressionvoid
, sa valeur ou son indicatif est ignoré. (Une expressionvoid
est évaluée pour ses effets secondaires.)
6.3.2.3 Pointeurs
1 Un pointeur versvoid
peut être converti vers ou depuis un pointeur vers n'importe quel type d'objet. Un pointeur vers n'importe quel type d'objet peut être converti en un pointeur pour annuler et inverser; le résultat doit être égal au pointeur d'origine.
Une expression void
n'a aucune valeur (bien qu'elle puisse avoir des effets secondaires). Si j'ai une fonction définie pour retourner void
, comme ceci:
void foo( void ) { ... }
puis l'appel
foo();
n'évalue pas une valeur; Je ne peux assigner le résultat à rien, car il n'y a pas de résultat.
Un pointeur vers void
est essentiellement un type de pointeur "générique"; vous pouvez attribuer une valeur de void *
à tout autre type de pointeur d'objet sans avoir besoin d'une conversion explicite (c'est pourquoi tous les programmeurs C vous crieront après avoir casté le résultat de malloc
).
Vous ne pouvez pas déréférencer directement un void *
; vous devez d'abord l'affecter à un type de pointeur d'objet différent avant de pouvoir accéder à l'objet pointé.
void
les pointeurs sont utilisés pour implémenter (plus ou moins) des interfaces génériques; l'exemple canonique est la fonction de bibliothèque qsort
, qui peut trier des tableaux de n'importe quel type, tant que vous fournissez une fonction de comparaison sensible au type.
Oui, utiliser le même mot-clé pour deux concepts différents (pas de valeur par rapport au pointeur générique) est source de confusion, mais ce n'est pas comme s'il n'y avait pas de précédent; static
a plusieurs significations distinctes en C et C++.
Dans Java et C #, void comme type de retour pour une méthode semble signifier: cette méthode ne retourne rien. Rien. Aucun retour. Vous ne recevrez rien de cette méthode.
Cette affirmation est correcte. Il est également correct pour C et C++.
en C, void comme type de retour ou même comme type de paramètre de méthode signifie quelque chose de différent.
Cette déclaration est incorrecte. void
comme type de retour en C ou C++ signifie la même chose qu'en C # et Java. Vous confondez void
avec void*
. Ils sont complètement différents.
N'est-il pas déroutant que
void
etvoid*
signifie deux choses complètement différentes?
Oui.
Qu'est-ce qu'un pointeur? Qu'est-ce qu'un pointeur vide?
Un pointeur est une valeur qui peut être déréférencé . Le déréférencement d'un pointeur valide donne un emplacement de stockage du type pointé . Un pointeur vide est un pointeur qui n'a pas de type pointé particulier; il doit être converti en un type de pointeur plus spécifique avant que la valeur ne soit déréférencée pour produire un emplacement de stockage .
Les pointeurs void sont-ils les mêmes en C, C++, Java et C #?
Java n'a pas de pointeurs vides; C # le fait. Ils sont les mêmes qu'en C et C++ - une valeur de pointeur à laquelle aucun type spécifique n'est associé, qui doit être converti en un type plus spécifique avant de le déréférencer pour produire un emplacement de stockage.
Puisque C est plus ancien que Java et C #, pourquoi ces langages ont-ils adopté void comme signifiant "rien" alors que C l'a utilisé comme "rien ou rien (quand un pointeur)"?
La question est incohérente car elle suppose des mensonges. Posons de meilleures questions.
Pourquoi Java et C # ont adopté la convention que
void
est un type de retour valide, au lieu, par exemple, d'utiliser la convention Visual Basic que les fonctions ne sont jamais nulles et les sous-routines sont toujours nul retour?
Pour être familier aux programmeurs venant à Java ou C # à partir de langages où void
est un type de retour.
Pourquoi C # a-t-il adopté la convention déroutante selon laquelle
void*
a une signification complètement différente devoid
?
Pour être familier aux programmeurs arrivant en C # à partir de langages où void*
est un type de pointeur.
void describe(void *thing);
void *move(void *location, Direction direction);
La première fonction ne renvoie rien. La deuxième fonction renvoie un pointeur vide. J'aurais déclaré cette deuxième fonction comme
void* move(void* location, Direction direction);
Cela pourrait aider si vous considérez "vide" comme signifiant "sans signification". La valeur de retour de describe
est "sans signification". Le retour d'une valeur à partir d'une telle fonction est illégal car vous avez dit au compilateur que la valeur de retour est sans signification. Vous ne pouvez pas capturer la valeur de retour de cette fonction car elle est sans signification. Vous ne pouvez pas déclarer une variable de type void
car elle est sans signification. Par exemple, void nonsense;
Est un non-sens et est en fait illégal. Notez également qu'un tableau de void void void_array[42];
Est également un non-sens.
Un pointeur vers void (void*
) Est quelque chose de différent. C'est un pointeur, pas un tableau. Lisez void*
Comme signifiant un pointeur qui pointe vers quelque chose "sans signification". Le pointeur est "sans signification", ce qui signifie qu'il n'est pas logique de déréférencer un tel pointeur. Essayer de le faire est en fait illégal. Le code qui déréférence un pointeur vide ne sera pas compilé.
Donc, si vous ne pouvez pas déréférencer un pointeur de vide et que vous ne pouvez pas créer un tableau de vides, comment pouvez-vous les utiliser? La réponse est qu'un pointeur vers n'importe quel type peut être converti vers et depuis void*
. La conversion d'un pointeur en void*
Et la conversion en un pointeur vers le type d'origine produira une valeur égale au pointeur d'origine. La norme C garantit ce comportement. La principale raison pour laquelle vous voyez autant de pointeurs vides en C est que cette capacité fournit un moyen d'implémenter le masquage d'informations et la programmation basée sur des objets dans ce qui est en grande partie un langage non orienté objet.
Enfin, la raison pour laquelle vous voyez souvent move
déclaré comme void *move(...)
plutôt que void* move(...)
est parce que mettre l'astérisque à côté du nom plutôt que du type est une pratique très courante dans C. La raison en est que la déclaration suivante vous posera de gros problèmes: int* iptr, jptr;
Cela vous ferait penser que vous déclarez deux pointeurs vers un int
. Tu n'es pas. Cette déclaration fait jptr
un int
plutôt qu'un pointeur vers un int
. La déclaration appropriée est int *iptr, *jptr;
Vous pouvez prétendre que l'astérisque appartient au type si vous vous en tenez à ne déclarer qu'une seule variable par instruction de déclaration. Mais gardez à l'esprit que vous faites semblant.
Autrement dit, il n'y a pas de différence. Laisse moi te donner quelques exemples.
Disons que j'ai une classe appelée 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
Je crois que la confusion ici est basée sur la façon dont vous définiriez void
vs void*
. Un type de retour de void
signifie "Je ne retourne rien", tandis qu'un type de retour de void*
signifie "Je retourne un pointeur de type rien".
Cela est dû au fait qu'un pointeur est un cas particulier. Un objet pointeur pointe toujours vers un emplacement en mémoire, qu'il y ait ou non un objet valide. Que mon void* car
fait référence à un octet, un mot, une voiture ou un vélo n'a pas d'importance car mon void*
pointe sur quelque chose sans type défini. C'est à nous de le définir plus tard.
Dans des langages tels que C # et Java, la mémoire est gérée et le concept de pointeurs est reporté dans la classe Object. Au lieu d'avoir une référence à un objet de type "rien", nous savons qu'au moins notre objet est de type "Object". Laissez-moi vous expliquer pourquoi.
En C/C++ conventionnel, si vous créez un objet et supprimez son pointeur, il est impossible d'accéder à cet objet. C'est ce qu'on appelle un fuite de mémoire.
En C #/Java, tout est un objet et cela permet au runtime de garder une trace de chaque objet. Il n'a pas nécessairement besoin de savoir que mon objet est une voiture, il a simplement besoin de connaître certaines informations de base qui sont déjà prises en charge par la classe Object.
Pour nous programmeurs, cela signifie qu'une grande partie des informations que nous aurions à traiter manuellement en C/C++ sont prises en charge par la classe Object, ce qui supprime la nécessité du cas spécial qu'est le pointeur.
Je vais essayer de dire comment on pourrait penser à ces choses en C. La langue officielle dans la norme C n'est pas vraiment à l'aise avec void
, et je n'essaierai pas d'être tout à fait cohérente avec elle.
Le type void
n'est pas un citoyen de première classe parmi les types. Bien qu'il s'agisse d'un type d'objet, il ne peut s'agir du type d'aucun objet (ou valeur), d'un champ ou d'un paramètre de fonction; cependant, il peut s'agir du type de retour d'une fonction, et donc du type d'une expression (essentiellement des appels de ces fonctions, mais les expressions formées avec l'opérateur conditionnel peuvent également avoir un type void). Mais même l'utilisation minimale de void
comme type de valeur pour lequel ce qui précède laisse la porte ouverte, à savoir terminer une fonction renvoyant void
avec une déclaration de la forme return E;
où E
est une expression de type void
, est explicitement interdit en C (il est cependant autorisé en C++, mais pour des raisons non applicables à C).
Si des objets de type void
étaient autorisés, ils auraient 0 bits (c'est-à-dire la quantité d'informations dans la valeur d'une expression de type void
); ce ne serait pas un problème majeur d'autoriser de tels objets, mais ils seraient plutôt inutiles (les objets de taille 0 rendraient difficile la définition de l'arithmétique des pointeurs, ce qui serait peut-être mieux d'être interdit). L'ensemble des différentes valeurs d'un tel objet aurait un élément (trivial); sa valeur pourrait être prise, trivialement car elle n'a pas de substance, mais elle ne peut pas être modifiée (sans aucune valeur différente). La norme est donc confuse en disant que void
comprend un ensemble vide de valeurs; un tel type pourrait être utile pour décrire des expressions qui ne peuvent pas être évaluées (comme des sauts ou des appels sans terminaison) bien que le langage C n'emploie pas réellement un tel type.
N'étant pas autorisé comme type de valeur, void
a été utilisé d'au moins deux manières non directement liées pour désigner "interdit": en spécifiant (void)
comme spécification de paramètre dans les types de fonction signifie que leur fournir des arguments est interdit (tandis que '()' signifierait au contraire que tout est autorisé; et oui, même fournir une expression void
comme argument aux fonctions avec (void)
la spécification des paramètres est interdite), et déclarer void *p
signifie que l'expression de déréférencement *p
est interdit (mais pas que ce soit une expression valide de type void
). Vous avez donc raison de dire que ces utilisations de void
ne sont pas vraiment cohérentes avec void
en tant que type d'expressions valides. Cependant un objet de type void*
n'est pas vraiment un pointeur vers des valeurs de n'importe quel type, cela signifie une valeur qui est traitée comme un pointeur bien qu'elle ne puisse pas être déréférencée et l'arithmétique du pointeur avec elle est interdite. Le "traité comme un pointeur" signifie alors uniquement qu'il peut être converti depuis et vers n'importe quel type de pointeur sans perte d'informations. Cependant, il peut également être utilisé pour convertir des valeurs entières vers et depuis, il n'a donc pas à pointer du tout.
En C, il est possible d'avoir un pointeur vers des objets de tout type [les pointeurs se comportent comme un type de référence]. Un pointeur peut identifier un objet d'un type connu, ou il peut identifier un objet de type arbitraire (inconnu). Les créateurs de C ont choisi d'utiliser la même syntaxe générale pour "pointeur vers une chose de type arbitraire" que pour "pointeur vers une chose d'un type particulier". Étant donné que la syntaxe dans ce dernier cas nécessite un jeton pour spécifier le type, l'ancienne syntaxe nécessite de placer quelque chose à l'endroit où ce jeton appartiendrait si le type était connu. C a choisi d'utiliser le mot clé "void" à cette fin.
En Pascal, comme avec C, il est possible d'avoir des pointeurs vers des choses de tout type; les dialectes plus populaires de Pascal permettent également de "pointer vers une chose de type arbitraire", mais plutôt que d'utiliser la même syntaxe que pour "pointer vers une chose d'un type particulier", ils se réfèrent simplement à la première comme Pointer
.
En Java, il n'y a pas de types de variables définis par l'utilisateur; tout est soit une primitive soit une référence d'objet de tas. On peut définir des choses de type "référence à un objet tas d'un type particulier" ou "référence à un objet tas de type arbitraire". Le premier utilise simplement le nom du type sans ponctuation spéciale pour indiquer qu'il s'agit d'une référence (car il ne peut pas être autre chose); ce dernier utilise le nom de type Object
.
L'utilisation de void*
car la nomenclature d'un pointeur vers quelque chose de type arbitraire est pour autant que je sache unique au C et aux dérivés directs comme C++; d'autres langues que je connais gèrent le concept en utilisant simplement un type au nom distinct.
En C # et Java, chaque classe est dérivée d'une classe Object
. Par conséquent, chaque fois que nous souhaitons passer une référence à "quelque chose", nous pouvons utiliser une référence de type Object
.
Comme nous n'avons pas besoin d'utiliser des pointeurs void à cet effet dans ces langues, void
n'a pas à signifier "quelque chose ou rien" ici.