Pourquoi vide en C signifie-t-il non nul?

25

Dans les langages fortement typés comme Java et C #, void(ou Void) comme type de retour pour une méthode semble 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, voidcomme type de retour ou même comme type de paramètre de méthode, cela 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é voidcomme signifiant "rien" alors que C l'a utilisé comme "rien ou rien (quand un pointeur)"?

Naftuli Kay
la source
138
Le titre et le texte de votre question en parlent voidpendant que l'exemple de code utilise void*ce qui est quelque chose de complètement différent.
4
Notez également que Java et C # ont le même concept, ils l'appellent simplement Objectdans un cas pour lever l'ambiguïté.
Mooing Duck
4
@Mephy, ils ne sont pas exactement typés? Par C # en utilisant la frappe de canard, voulez-vous dire le dynamictype qui est rarement utilisé?
Axarydax
9
@Mephy: La dernière fois que j'ai vérifié Wikipédia, il y avait onze significations mutuellement contradictoires pour "fortement typé". Dire que "C # n'est pas fortement typé" n'a donc aucun sens.
Eric Lippert
8
@LightnessRacesinOrbit: Non, le fait est qu'il n'y a pas de définition faisant autorité du terme. Wikipédia n'est pas une ressource normative qui vous dit quelle est la bonne signification d'un terme. Il s'agit d'une ressource descriptive . Le fait qu'il décrit onze significations contradictoires différentes prouve que le terme ne peut pas être utilisé dans une conversation sans le définir avec soin . Faire autrement signifie que les chances sont très bonnes que les personnes dans la conversation parleront au-delà les unes des autres.
Eric Lippert

Réponses:

58

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 voidsignifie deux choses différentes.

Je n'ai pas été en mesure de trouver la raison historique derrière la réutilisation voidpour signifier «rien» et «quoi que ce soit» dans différents contextes, mais C le fait à plusieurs autres endroits. Par exemple, statica 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 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, de sorte qu'une Objectréférence remplit la même fonction void*sans les problèmes de type et la gestion de la mémoire brute.


la source
2
Je fournirai une référence quand je
4
They also do not allow raw pointers and do not need easy C compatibility.Faux. C # a des pointeurs. Ils sont généralement (et devraient généralement être) éteints, mais ils sont là.
Magus
8
Concernant pourquoi ils ont réutilisé void: une raison évidente serait d'éviter d'introduire un nouveau mot-clé (et potentiellement de casser les programmes existants). S'ils avaient introduit un mot-clé spécial (par exemple unknown), alors ce void*serait une construction dénuée de sens (et peut-être illégale), et unknownne serait légale que sous la forme de unknown*.
jamesdlin
20
Même en void *, voidsignifie «rien», pas «rien». Vous ne pouvez pas déréférencer un void *, vous devez d'abord le convertir en un autre type de pointeur.
oefe
2
@oefe Je pense que cela n'a aucun sens de diviser ces cheveux, car voidet void*sont de types différents (en fait, voidc'est "pas de type"). Il a autant de sens que de dire « le intau int*moyen de quelque chose. » Non, une fois que vous déclarez une variable de pointeur, vous dites "c'est une adresse mémoire capable de contenir quelque chose". Dans le cas de void*, vous dites que "cette adresse contient quelque chose mais que quelque chose ne peut pas être déduit du type de pointeur".
32

voidet void*sont deux choses différentes. voiden C signifie exactement la même chose qu'en Java, une absence de valeur de retour. A 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 a void*, quel type vous attendriez-vous à obtenir? N'oubliez pas que les pointeurs C ne contiennent aucune information sur le 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 déréférencé void*est de l'ignorer, ce qui correspond exactement au comportement indiqué par le voidtype.

Karl Bielefeldt
la source
1
Je suis d'accord jusqu'à ce que vous arriviez au bit "ignorer". Il est possible que la chose retournée contienne des informations sur la façon de l'interpréter. En d'autres termes, il pourrait s'agir de l'équivalent C d'une variante BASIC. "Lorsque vous obtenez ceci, jetez un œil pour comprendre ce que c'est - je ne peux pas vous dire tout de suite ce que vous trouverez".
Floris
1
Ou pour le dire autrement, je pourrais hypothétiquement mettre un "descripteur de type" défini par le programmeur dans le premier octet à l'emplacement de mémoire adressé par le pointeur, ce qui me dirait quel type de données je peux m'attendre à trouver dans les octets qui suivent.
Robert Harvey
1
Bien sûr, mais en C, il a été décidé de laisser ces détails au programmeur, plutôt que de les cuire dans le langage. Du point de vue de la langue , il ne sait pas quoi faire d'autre qu'ignorer. Cela évite la surcharge de toujours inclure les informations de type comme premier octet alors que dans la plupart des cas d'utilisation, vous pouvez les déterminer statiquement.
Karl Bielefeldt
4
@RobertHarvey oui, mais alors vous ne déréférencez pas un pointeur vide; vous transformez le pointeur void en un pointeur char, puis vous déréférencer le pointeur char.
user253751
4
@Floris gccn'est pas d'accord avec vous. Si vous essayez d'utiliser la valeur, gccgénérez un message d'erreur indiquant error: void value not ignored as it ought to be.
kasperd
19

Il serait peut-être plus utile de penser voidau type de retour. Ensuite, votre deuxième méthode se lirait «une méthode qui renvoie un pointeur non typé».

Robert Harvey
la source
Qui aide. En tant que quelqu'un qui (enfin!) Apprend le C après des années passées dans des langages orientés objet, les pointeurs sont quelque chose de très nouveau pour moi. Il semble que lorsque vous retournez un pointeur, voidc'est à peu près l'équivalent *de langages comme ActionScript 3 qui signifie simplement "n'importe quel type de retour".
Naftuli Kay
5
Eh bien, ce voidn'est pas un caractère générique. Un pointeur n'est qu'une adresse mémoire. Mettre un type avant le *dit "voici le type de données que vous pouvez vous attendre à trouver à l'adresse mémoire référencée par ce pointeur". Mettre voidavant le *dit au compilateur "Je ne connais pas le type; renvoyez-moi simplement le pointeur brut, et je saurai quoi faire avec les données vers lesquelles il pointe."
Robert Harvey
2
Je dirais même que ça void*pointe vers un "vide". Un pointeur nu avec rien qu'il pointe. Vous pouvez toujours le lancer comme n'importe quel autre pointeur.
Robert
11

Resserrons un peu la terminologie.

De la norme en ligne C 2011 :

6.2.5 Types
...
19 Le voidtype 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 void

1 La valeur (inexistante) d'une voidexpression (une expression qui a du type void) ne doit en aucun cas être utilisée, et les conversions implicites ou explicites (sauf vers void) ne doivent pas être appliquées à une telle expression. Si une expression de tout autre type est évaluée en tant void qu'expression, sa valeur ou son indicatif est ignoré. (Une voidexpression est évaluée pour ses effets secondaires.)

6.3.2.3 Pointeurs

1 Un pointeur versvoidpeut être converti en ou à partir d'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 voidexpression 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 voidest 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 a void *; vous devez d'abord l'affecter à un type de pointeur d'objet différent avant de pouvoir accéder à l'objet pointé.

voidles pointeurs sont utilisés pour implémenter (plus ou moins) des interfaces génériques; l'exemple canonique est la qsortfonction de bibliothèque, 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; statica plusieurs significations distinctes en C et C ++.

John Bode
la source
9

En Java et C #, void comme type de retour pour une méthode semble signifier: cette méthode ne retourne rien. Rien. Non-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. voidcomme type de retour en C ou C ++ signifie la même chose qu'en C # et Java. Vous confondez voidavec void*. Ils sont complètement différents.

N'est-ce pas confus voidet void*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ée . 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 de déréférencer la valeur 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 n'importe quoi (quand un pointeur)"?

La question est incohérente car elle suppose des mensonges. Posons de meilleures questions.

Pourquoi Java et C # ont-ils adopté la convention qui voidest un type de retour valide, au lieu, par exemple, d'utiliser la convention Visual Basic selon laquelle les fonctions ne sont jamais renvoyées nulles et les sous-routines sont toujours retournées nulles?

Être familier aux programmeurs venant en Java ou C # à partir de langages où voidest un type de retour.

Pourquoi C # a-t-il adopté la convention déroutante qui void*a une signification complètement différente de celle void?

Pour être familier aux programmeurs arrivant en C # à partir de langages où void*est un type de pointeur.

Eric Lippert
la source
5
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 describeest "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 voidcar elle est sans signification. Par exemple void nonsense;est un non-sens et est en fait illégal. Notez également qu'un tableau de vide void void_array[42];est également un non-sens.

Un pointeur sur void ( void*) est quelque chose de différent. C'est un pointeur, pas un tableau. A lire 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 void*et la conversion en un pointeur vers le type d'origine produiront une valeur égale au pointeur d'origine. La norme C garantit ce comportement. La principale raison pour laquelle vous voyez tant 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 essentiellement un langage non orienté objet.

Enfin, la raison pour laquelle vous voyez souvent movedéclaré comme étant void *move(...)plutôt que void* move(...)parce que mettre l'astérisque à côté du nom plutôt que du type est une pratique très courante en C. La raison en est que la déclaration suivante vous posera de gros problèmes: int* iptr, jptr;cela vous rendrait pensez que vous déclarez deux pointeurs vers un int. Tu n'es pas. Cette déclaration fait jptrun intplutôt qu'un pointeur sur 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.

David Hammen
la source
"Le code qui déréférence un pointeur nul ne sera pas compilé." Je pense que vous voulez dire du code qui déréférence un pointeur vide ne compilera pas. Le compilateur ne vous protège pas contre le déréférencement d'un pointeur nul; c'est un problème d'exécution.
Cody Gray
@CodyGray - Merci! Correction de ça. Le code qui déréférence un pointeur nul sera compilé (tant que le pointeur ne l'est pas void* null_ptr = 0;).
David Hammen
2

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 ici la confusion est basé sur la façon dont vous définiriez ici voidvs void*. Un type de retour voidsignifie "je ne retourne rien", tandis qu'un type de retour void*signifie "je ne renvoie aucun 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 mes void* carréférences fassent référence à un octet, à un mot, à une voiture ou à un vélo, cela n'a pas d'importance parce que mon void*pointage vers 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'à tout le 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 une 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.

Thebluefish
la source
"mon vide * pointe vers quelque chose sans type défini" - pas exactement. Il est plus correct de dire que c'est un pointeur vers une valeur de longueur nulle (presque comme un tableau avec 0 éléments, mais sans même les informations de type associées au tableau).
Scott Whitlock du
2

Je vais essayer de dire comment on pourrait penser à ces choses en C. La langue officielle de la norme C n'est pas vraiment à l'aise void, et je n'essaierai pas d'être tout à fait cohérente avec elle.

Le type voidn'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 telles fonctions, mais les expressions formées avec l'opérateur conditionnel peuvent également avoir un type void). Mais même l'utilisation minimale de voidcomme type de valeur pour laquelle ce qui précède laisse la porte ouverte, à savoir terminer une fonction retournant voidavec une déclaration de la forme return E;Eest une expression de type void, est explicitement interdite en C (elle est cependant autorisée en C ++, mais pour des raisons non applicables à C).

Si les 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 voidtype); 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 préférable 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 valeur différente ). La norme est donc confuse en disant qu'elle voidcomprend un ensemble vide de valeurs; un tel type pourrait être utile pour décrire des expressions qui ne peuvent pasêtre évalué (comme les sauts ou les appels sans terminaison) bien que le langage C n'emploie pas réellement un tel type.

Le fait d'être interdit comme type de valeur voida été utilisé d'au moins deux façons non directement liées pour désigner «interdit»: la spécification (void)comme spécification de paramètre dans les types de fonction signifie qu'il est interdit de leur fournir des arguments (tandis que «()» signifie au contraire tout est autorisé; et oui, même fournir une voidexpression comme argument aux fonctions avec (void)spécification de paramètre est interdit), et déclarer void *psignifie que l'expression de déréférencement *pest interdite (mais pas qu'il s'agit d'une expression valide de type void). Vous avez donc raison de dire que ces utilisations de voidne sont pas vraiment compatibles avec voidun type d'expressions valide. Cependant un objet de typevoid*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.

Marc van Leeuwen
la source
À strictement parler, il peut être converti depuis et vers n'importe quel type de pointeur d'objet sans perte d'informations, mais pas toujours vers et depuis . En général, une conversion de void*vers un autre type de pointeur peut perdre des informations (ou même avoir UB, je ne m'en souviens pas), mais si la valeur de la void*provenait de la conversion d'un pointeur du même type vers lequel vous effectuez la conversion, alors ce n'est pas le cas.
Steve Jessop
0

En C # et Java, chaque classe est dérivée d'une Objectclasse. 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, voidcela ne signifie pas nécessairement «quelque chose ou rien» ici.

Reg Edit
la source
0

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 en 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 qu'ils utilisent 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 aucune 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 du type Object.

L'utilisation de void*comme nomenclature pour 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.

supercat
la source
-1

Vous pouvez en quelque sorte penser le mot-clé voidcomme un vide struct( en C99, les structures vides sont apparemment interdites , en .NET et C ++, elles occupent plus de 0 mémoire, et enfin je rappelle que tout en Java est une classe):

struct void {
};

... ce qui signifie que c'est un type de longueur 0. Voici comment cela fonctionne lorsque vous effectuez un appel de fonction qui renvoie void... au lieu d'allouer environ 4 octets à la pile pour une valeur de retour entière, cela ne change pas du tout le pointeur de la pile (l'incrémente d'une longueur de 0 ).

Donc, si vous avez déclaré certaines variables comme:

int a;
void b;
int c;

... et puis vous avez pris l'adresse de ces variables, alors peut b- être et cpeut-être situé au même endroit en mémoire, car ba une longueur nulle. Donc, a void*est un pointeur en mémoire vers le type intégré de longueur nulle. Le fait que vous sachiez qu'il y a quelque chose immédiatement après cela qui pourrait vous être utile est une autre affaire et vous oblige à vraiment savoir ce que vous faites (et c'est dangereux).

Scott Whitlock
la source
Le seul compilateur que je connaisse qui attribue une longueur au type void est gcc, et gcc lui attribue une longueur de 1.
Joshua