¿Por qué vacío en C significa no vacío?

25

En lenguajes fuertemente tipados como Java y C #, void(o Void) como tipo de retorno para un método parecen significar:

Este método no devuelve nada. Nada. Sin retorno. No recibirá nada de este método.

Lo realmente extraño es que en C, voidcomo un tipo de retorno o incluso como un tipo de parámetro de método significa:

Realmente podría ser cualquier cosa. Tendrías que leer el código fuente para averiguarlo. Buena suerte. Si es un puntero, realmente deberías saber lo que estás haciendo.

Considere los siguientes ejemplos 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;
}

Obviamente, el segundo método devuelve un puntero, que por definición podría ser cualquier cosa.

Dado que C es más antiguo que Java y C #, ¿por qué estos lenguajes adoptaron voidcomo "nada" mientras que C lo usó como "nada o nada (cuando un puntero)"?

Naftuli Kay
fuente
138
El título y el texto de su pregunta hablan voidmientras que el ejemplo de código usa void*algo completamente diferente.
44
También tenga en cuenta que Java y C # tienen el mismo concepto, simplemente lo llaman Objecten un caso para desambiguar.
Mooing Duck
44
@Mephy, ¿no están exactamente fuertemente tipados? Por C # que usa la escritura de pato, ¿se refiere al dynamictipo que rara vez se usa?
Axarydax
99
@Mephy: la última vez que verifiqué en Wikipedia figuran once significados mutuamente contradictorios para "fuertemente tipado". Decir que "C # no está fuertemente tipeado" no tiene sentido.
Eric Lippert
8
@LightnessRacesinOrbit: No, el punto es que no hay una definición autorizada del término. Wikipedia no es un recurso prescriptivo que le dice cuál es el significado correcto de un término. Es un recurso descriptivo . El hecho de que describa once significados contradictorios diferentes es evidencia de que el término no puede usarse en una conversación sin definirlo cuidadosamente . Hacer lo contrario significa que las probabilidades son muy buenas, las personas en la conversación estarán hablando más allá, no entre ellas.
Eric Lippert

Respuestas:

58

La palabra clave void(no un puntero) significa "nada" en esos idiomas. Esto es consistente

Como notó, void*significa "puntero a cualquier cosa" en lenguajes que admiten punteros sin formato (C y C ++). Esta es una decisión desafortunada porque, como mencionaste, voidsignifica dos cosas diferentes.

No he podido encontrar la razón histórica detrás de la reutilización voidpara significar "nada" y "nada" en diferentes contextos, sin embargo, C lo hace en varios otros lugares. Por ejemplo, statictiene diferentes propósitos en diferentes contextos. Obviamente, hay un precedente en el lenguaje C para reutilizar palabras clave de esta manera, independientemente de lo que uno pueda pensar de la práctica.

Java y C # son lo suficientemente diferentes como para hacer un corte limpio para corregir algunos de estos problemas. Java y C # "seguro" tampoco permiten punteros sin procesar y no necesitan compatibilidad con C fácil (C # inseguro permite punteros, pero la gran mayoría del código de C # no entra en esta categoría). Esto les permite cambiar un poco las cosas sin preocuparse por la compatibilidad con versiones anteriores. Una forma de hacerlo es introducir una clase Objecten la raíz de la jerarquía de la que heredan todas las clases, por lo que una Objectreferencia cumple la misma función void*sin la maldad de los problemas de tipo y la administración de memoria sin procesar.


fuente
2
Proporcionaré una referencia cuando llegue a casa
44
They also do not allow raw pointers and do not need easy C compatibility.Falso. C # tiene punteros. Usualmente están (y usualmente deberían estar) apagados, pero están ahí.
Magus
8
En cuanto a por qué se reutilizaron void: una razón obvia sería evitar introducir una nueva palabra clave (y potencialmente romper los programas existentes). Si hubieran introducido una palabra clave especial (p unknown. Ej. ), void*Sería una construcción sin sentido (y posiblemente ilegal), y unknownsería legal solo en forma de unknown*.
jamesdlin
20
Incluso en void *, voidsignifica "nada", no "nada". No puede desreferenciar a void *, primero debe convertirlo a otro tipo de puntero.
oefe
2
@oefe Creo que no tiene sentido dividir ese cabello, porque voidy void*son diferentes tipos (en realidad, voides "sin tipo"). Tiene tanto sentido como decir "lo intin int*significa algo". No, una vez que declaras una variable de puntero estás diciendo "esta es una dirección de memoria capaz de contener algo". En el caso de void*, está diciendo que "esta dirección contiene algo pero que algo no puede inferirse del tipo de puntero".
32

voidy void*son dos cosas diferentes. voiden C significa exactamente lo mismo que en Java, la ausencia de un valor de retorno. A void*es un puntero con ausencia de un tipo.

Todos los punteros en C necesitan poder ser desreferenciados. Si desreferenciara a void*, ¿qué tipo esperaría obtener? Recuerde que los punteros en C no llevan ninguna información de tipo de tiempo de ejecución, por lo que el tipo debe conocerse en el momento de la compilación.

Dado ese contexto, lo único que puede hacer lógicamente con un desreferenciado void*es ignorarlo, que es exactamente el comportamiento que voiddenota el tipo.

Karl Bielefeldt
fuente
1
Estoy de acuerdo hasta que llegues al bit de "ignorarlo". Es posible que la cosa devuelta contenga información sobre cómo interpretarla. En otras palabras, podría ser el equivalente en C de una variante BÁSICA. "Cuando obtengas esto, echa un vistazo para descubrir qué es, no puedo decirte por adelantado qué encontrarás".
Floris
1
O para decirlo de otra manera, hipotéticamente podría poner un "descriptor de tipo" definido por el programador en el primer byte en la ubicación de memoria dirigida por el puntero, lo que me diría qué tipo de datos puedo esperar encontrar en los bytes que siguen.
Robert Harvey
1
Por supuesto, pero en C se decidió dejar esos detalles al programador, en lugar de incluirlos en el lenguaje. Desde el punto de vista del lenguaje , no sabe qué más hacer aparte de ignorar. Esto evita la sobrecarga de incluir siempre la información de tipo como el primer byte cuando en la mayoría de los casos de uso puede determinarlo estáticamente.
Karl Bielefeldt
44
@RobertHarvey sí, pero no estás desreferenciando un puntero vacío; está lanzando el puntero vacío a un puntero char y luego desreferenciando el puntero char
user253751
44
@ Floris gccno está de acuerdo contigo. Si intenta usar el valor, gccgenere un mensaje de error que diga error: void value not ignored as it ought to be.
Kasperd
19

Quizás sería más útil pensar voiden el tipo de retorno. Luego, su segundo método sería "un método que devuelve un puntero sin tipo".

Robert Harvey
fuente
Eso ayuda. Como alguien que (¡finalmente!) Está aprendiendo C después de años en lenguajes orientados a objetos, los punteros son un concepto muy nuevo para mí. Parece que al devolver un puntero, voides más o menos equivalente a *lenguajes como ActionScript 3, que simplemente significa "cualquier tipo de retorno".
Naftuli Kay
55
Bueno, voidno es un comodín. Un puntero es solo una dirección de memoria. Poner un tipo antes del *dice "aquí está el tipo de datos que puede esperar encontrar en la dirección de memoria a la que hace referencia este puntero". Poniendo voidantes el dicho *al compilador "No sé el tipo; solo devuélveme el puntero sin procesar y descubriré qué hacer con los datos a los que apunta".
Robert Harvey
2
Incluso diría que un void*apunta a un "vacío". Un puntero desnudo sin nada a lo que apunta. Aún así puedes lanzarlo como cualquier otro puntero.
Robert
11

Vamos a ajustar un poco la terminología.

Del estándar C 2011 en línea :

6.2.5 Tipos
...
19 El voidtipo comprende un conjunto vacío de valores; Es un tipo de objeto incompleto que no se puede completar.
...
6.3 Conversiones
...
6.3.2.2 void

1 El valor (inexistente) de una voidexpresión (una expresión que tiene tipo void) no se utilizará de ninguna manera, y las conversiones implícitas o explícitas (excepto a void) no se aplicarán a Tal expresión. Si una expresión de cualquier otro tipo se evalúa como una void expresión, su valor o designador se descarta. (Una voidexpresión se evalúa por sus efectos secundarios).

6.3.2.3 Punteros

1 Un puntero avoidse puede convertir ao desde un puntero a cualquier tipo de objeto. Un puntero a cualquier tipo de objeto se puede convertir en un puntero para anular y volver; el resultado se comparará igual al puntero original.

Una voidexpresión no tiene valor (aunque puede tener efectos secundarios). Si tengo una función definida para devolver void, así:

void foo( void ) { ... }

entonces la llamada

foo();

no evalúa a un valor; No puedo asignar el resultado a nada, porque no hay resultado.

Un puntero a voides esencialmente un tipo de puntero "genérico"; puede asignar un valor void *a cualquier otro tipo de puntero de objeto sin necesidad de una conversión explícita (razón por la cual todos los programadores de C le gritarán por emitir el resultado demalloc ).

No puede desreferenciar directamente un void * ; primero debe asignarlo a un tipo de puntero de objeto diferente antes de poder acceder al objeto señalado.

voidlos punteros se utilizan para implementar (más o menos) interfaces genéricas; El ejemplo canónico es la qsortfunción de biblioteca, que puede ordenar matrices de cualquier tipo, siempre que proporcione una función de comparación con reconocimiento de tipo.

Sí, usar la misma palabra clave para dos conceptos diferentes (sin valor versus puntero genérico) es confuso, pero no es que no haya precedentes; statictiene múltiples significados distintos en C y C ++.

John Bode
fuente
9

En Java y C #, vacío como tipo de retorno para un método parece significar: este método no devuelve nada. Nada. Sin retorno. No recibirá nada de este método.

Esa afirmación es correcta. También es correcto para C y C ++.

en C, nulo como tipo de retorno o incluso como tipo de parámetro de método significa algo diferente.

Esa afirmación es incorrecta. voidcomo tipo de retorno en C o C ++ significa lo mismo que hace en C # y Java. Estás confundiendo voidcon void*. Son completamente diferentes

¿No es confuso eso voidy void*significa dos cosas completamente diferentes?

Sí.

¿Qué es un puntero? ¿Qué es un puntero vacío?

Un puntero es un valor que puede ser desreferenciado . Anular la referencia a un puntero válido proporciona una ubicación de almacenamiento del tipo apuntado . Un puntero vacío es un puntero que no tiene ningún tipo de apuntamiento particular; debe convertirse a un tipo de puntero más específico antes de desreferenciar el valor para producir una ubicación de almacenamiento .

¿Los punteros nulos son los mismos en C, C ++, Java y C #?

Java no tiene punteros vacíos; C # hace. Son los mismos que en C y C ++: un valor de puntero que no tiene ningún tipo específico asociado, que debe convertirse a un tipo más específico antes de desreferenciarlo para producir una ubicación de almacenamiento.

Dado que C es más antiguo que Java y C #, ¿por qué estos lenguajes adoptaron vacío como significado "nada" mientras que C lo usó como "nada ni nada (cuando un puntero)"?

La pregunta es incoherente porque presupone falsedades. Hagamos algunas mejores preguntas.

¿Por qué Java y C # adoptaron la convención que voides un tipo de retorno válido, en lugar de, por ejemplo, usar la convención de Visual Basic de que las funciones nunca se anulan y las subrutinas siempre se anulan?

Estar familiarizado con los programadores que vienen a Java o C # desde lenguajes donde voides un tipo de retorno.

¿Por qué C # adoptó la convención confusa que void*tiene un significado completamente diferente que void?

Conocer a los programadores que llegan a C # desde lenguajes donde void*hay un tipo de puntero.

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

La primera función no devuelve nada. La segunda función devuelve un puntero vacío. Hubiera declarado esa segunda función como

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

Podría ayudar si piensa en "vacío" como que significa "sin significado". El valor de retorno de describees "sin significado". Devolver un valor de dicha función es ilegal porque le dijo al compilador que el valor devuelto no tiene sentido. No puede capturar el valor de retorno de esa función porque no tiene sentido. No puede declarar una variable de tipo voidporque no tiene sentido. Por ejemplo, no void nonsense;tiene sentido y de hecho es ilegal. También tenga en cuenta que una matriz de vacío void void_array[42];también es una tontería.

Un puntero a void ( void*) es algo diferente. Es un puntero, no una matriz. Leer void*como significado un puntero que apunta a algo "sin significado". El puntero es "sin significado", lo que significa que no tiene sentido desreferenciar dicho puntero. Intentar hacerlo es ilegal. El código que desreferencia un puntero vacío no se compilará.

Entonces, si no puede desreferenciar un puntero vacío, y no puede hacer una serie de vacíos, ¿cómo puede usarlos? La respuesta es que un puntero a cualquier tipo se puede lanzar hacia y desde void*. Al void*lanzar un puntero y volver a un puntero al tipo original, se obtendrá un valor igual al puntero original. El estándar C garantiza este comportamiento. La razón principal por la que ve tantos punteros vacíos en C es que esta capacidad proporciona una forma de implementar la ocultación de información y la programación basada en objetos en un lenguaje muy orientado a objetos.

Finalmente, la razón por la que a menudo se ve movedeclarado como en void *move(...)lugar de void* move(...)porque es poner el asterisco al lado del nombre en lugar del tipo es una práctica muy común en C. La razón es que la siguiente declaración te meterá en un gran problema: int* iptr, jptr;esto te haría cree que está declarando dos punteros a uno int. Usted no. Esta declaración hace jptrun intmás que un puntero a un int. La declaración adecuada es int *iptr, *jptr;: puede fingir que el asterisco pertenece al tipo si se apega a la práctica de declarar solo una variable por declaración de declaración. Pero tenga en cuenta que está fingiendo.

David Hammen
fuente
"El código que desreferencia un puntero nulo no se compilará". Creo que te refieres a un código que desreferencia un puntero vacío no compilará. El compilador no lo protege de desreferenciar un puntero nulo; Ese es un problema de tiempo de ejecución.
Cody Gray
@CodyGray - ¡Gracias! Arreglado eso. El código que desreferencia un puntero nulo se compilará (siempre que el puntero no lo sea void* null_ptr = 0;).
David Hammen
2

En pocas palabras, no hay diferencia . Deja que te dé algunos ejemplos.

Digamos que tengo una clase llamada 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

Creo que aquí la confusión en este punto se basa apagado cómo definiría voidvs void*. Un tipo de retorno voidsignifica "no devuelvo nada", mientras que un tipo de retorno void*significa "devuelvo un puntero de tipo nada".

Esto se debe al hecho de que un puntero es un caso especial. Un objeto puntero siempre apuntará a una ubicación en la memoria, ya sea que haya un objeto válido allí o no. Si mis void* carreferencias a un byte, una palabra, un automóvil o una bicicleta no importan porque mis void*puntos apuntan a algo sin un tipo definido. Depende de nosotros definirlo más adelante.

En lenguajes como C # y Java, la memoria se gestiona y el concepto de punteros se transfiere a la clase Object. En lugar de tener una referencia a un objeto de tipo "nada", sabemos que al menos nuestro objeto es de tipo "Objeto". Déjame explicarte por qué.

En C / C ++ convencional, si crea un objeto y elimina su puntero, entonces es imposible obtener acceso a ese objeto. Esto es lo que se conoce como pérdida de memoria .

En C # / Java, todo es un objeto y esto permite que el tiempo de ejecución realice un seguimiento de cada objeto. No necesariamente necesita saber que mi objeto es un automóvil, simplemente necesita conocer información básica que ya está a cargo de la clase Object.

Para nosotros, los programadores, eso significa que gran parte de la información que tendríamos que cuidar manualmente en C / C ++ nos ocupa la clase Object, eliminando la necesidad del caso especial que es el puntero.

Thebluefish
fuente
"mi vacío * apunta a algo sin un tipo definido", no exactamente. Es más correcto decir que es un puntero a un valor de longitud cero (casi como una matriz con 0 elementos, pero sin siquiera la información de tipo asociada con la matriz).
Scott Whitlock
2

Trataré de decir cómo se podría pensar en estas cosas en C. El lenguaje oficial en el estándar C no está realmente a gusto void, y no trataré de ser completamente consistente con él.

El tipo voidno es un ciudadano de primera clase entre los tipos. Aunque es un tipo de objeto, no puede ser el tipo de ningún objeto (o valor), de un campo o de un parámetro de función; sin embargo, puede ser el tipo de retorno de una función y, por lo tanto, puede ser el tipo de una expresión (básicamente de llamadas de tales funciones, pero las expresiones formadas con el operador condicional también pueden tener un tipo de vacío). Pero incluso el uso mínimo de voidun tipo de valor para el que lo anterior deja la puerta abierta, es decir, finalizar una función que regresa voidcon una declaración de la forma return E;donde Ees una expresión de tipo void, está explícitamente prohibido en C (aunque está permitido en C ++, pero por razones no aplicables a C).

Si voidse permitieran objetos de tipo , tendrían 0 bits (es decir, la cantidad de información en el valor de una expresión de voidtipo); no sería un problema importante permitir tales objetos, pero serían bastante inútiles (los objetos de tamaño 0 darían cierta dificultad para definir la aritmética del puntero, lo que quizás sería mejor prohibir). El conjunto de valores diferentes de dicho objeto tendría un elemento (trivial); su valor podría tomarse, trivialmente porque no tiene ninguna sustancia, pero no puede modificarse (sin ningún valor diferente ). Por lo tanto, el estándar se confunde al decir que voidcomprende un conjunto vacío de valores; dicho tipo podría ser útil para describir expresiones que no puedenser evaluado (como saltos o llamadas que no terminan) aunque el lenguaje C no emplea tal tipo.

Al ser rechazado como tipo de valor, voidse ha utilizado al menos en dos formas no directamente relacionadas para designar "prohibido": especificar (void)como especificación de parámetros en tipos de función significa proporcionar cualquier argumento para ellos (mientras que '()' significaría todo lo contrario está permitido; y sí, incluso proporcionar una voidexpresión como argumento para funciones con (void)especificación de parámetros está prohibido), y declarar void *psignifica que la expresión de desreferenciación *pestá prohibida (pero no es que sea una expresión de tipo válida void). Entonces tiene razón en que estos usos de voidno son realmente consistentesvoid como un tipo válido de expresiones. Sin embargo, un objeto de tipovoid*no es realmente un puntero a valores de ningún tipo, significa un valor que se trata como un puntero, aunque no se puede desreferenciar y se prohíbe la aritmética del puntero. El "tratado como un puntero" en realidad solo significa que puede emitirse desde y hacia cualquier tipo de puntero sin pérdida de información. Sin embargo, también se puede usar para emitir valores enteros hacia y desde, por lo que no tiene que apuntar a nada en absoluto.

Marc van Leeuwen
fuente
Estrictamente hablando, se puede emitir desde y hacia cualquier tipo de puntero de objeto sin pérdida de información, pero no siempre hacia y desde . En general, un lanzamiento desde void*otro tipo de puntero puede perder información (o incluso tener UB, no puedo recordarlo de la mano), pero si el valor del resultado void*proviene de lanzar un puntero del mismo tipo al que ahora está lanzando, entonces no lo hace
Steve Jessop
0

En C # y Java, cada clase se deriva de una Objectclase. Por lo tanto, cada vez que deseamos pasar una referencia a "algo", podemos usar una referencia de tipo Object.

Dado que no necesitamos usar punteros vacíos para ese propósito en estos idiomas, voidno tiene que significar "algo o nada" aquí.

Reg Editar
fuente
0

En C, es posible tener un puntero a cosas de cualquier tipo [los punteros se comportan como un tipo de referencia]. Un puntero puede identificar un objeto de un tipo conocido, o puede identificar un objeto de tipo arbitrario (desconocido). Los creadores de C eligieron usar la misma sintaxis general para "puntero a cosa de tipo arbitrario" que para "puntero a cosa de algún tipo particular". Dado que la sintaxis en el último caso requiere un token para especificar cuál es el tipo, la sintaxis anterior requiere poner algo donde pertenecería ese token si se conociera el tipo. C eligió usar la palabra clave "void" para ese propósito.

En Pascal, como con C, es posible tener punteros a cosas de cualquier tipo; los dialectos más populares de Pascal también permiten "puntero a cosa de tipo arbitrario", pero en lugar de usar la misma sintaxis que usan para "puntero a cosa de algún tipo particular", simplemente se refieren al primero como Pointer.

En Java, no hay tipos de variables definidas por el usuario; todo es una referencia de objeto primitivo o de montón. Se pueden definir cosas de tipo "referencia a un objeto de montón de un tipo particular" o "referencia a un objeto de montón de tipo arbitrario". El primero simplemente usa el nombre del tipo sin puntuación especial para indicar que es una referencia (ya que no puede ser otra cosa); este último usa el nombre del tipo Object.

El uso de void*como nomenclatura para un puntero a algo de tipo arbitrario es, hasta donde yo sé, exclusivo de C y derivados directos como C ++; otros idiomas que conozco manejan el concepto simplemente usando un tipo con un nombre distintivo.

Super gato
fuente
-1

Puede pensar en la palabra clave voidcomo un vacío struct( en C99 las estructuras vacías aparentemente no están permitidas , en .NET y C ++ ocupan más de 0 memoria, y por último recuerdo que todo en Java es una clase):

struct void {
};

... lo que significa que es un tipo con longitud 0. Así es como funciona cuando realiza una llamada de función que devuelve void... en lugar de asignar 4 bytes más o menos en la pila para un valor de retorno entero, no cambia el puntero de la pila (lo incrementa en una longitud de 0 )

Entonces, si declaró algunas variables como:

int a;
void b;
int c;

... y luego tomaste la dirección de esas variables, entonces quizás by cpodría ubicarse en el mismo lugar en la memoria, porque btiene longitud cero. Entonces a void*es un puntero en memoria al tipo incorporado con longitud cero. El hecho de que puedas saber que hay algo inmediatamente después que puede ser útil para ti es otro asunto y requiere que realmente sepas lo que estás haciendo (y es peligroso).

Scott Whitlock
fuente
El único compilador que conozco que asigna una longitud para escribir void es gcc, y gcc le asigna una longitud de 1.
Joshua