Por que vazio em C significa não vazio?

25

Em linguagens fortemente tipadas como Java e C #, void(ou Void) como um tipo de retorno para um método parece significar:

Este método não retorna nada. Nada. Sem retorno. Você não receberá nada deste método.

O que é realmente estranho é que em C, voidcomo um tipo de retorno ou mesmo como um tipo de parâmetro de método, significa:

Realmente poderia ser qualquer coisa. Você precisaria ler o código fonte para descobrir. Boa sorte. Se é um ponteiro, você realmente deve saber o que está fazendo.

Considere os seguintes exemplos em 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, o segundo método retorna um ponteiro, que por definição pode ser qualquer coisa.

Como C é mais antigo que Java e C #, por que essas linguagens adotaram voidcomo significando "nada" enquanto C usava como "nada ou nada (quando um ponteiro)"?

Naftuli Kay
fonte
138
O título e o texto da sua pergunta falam sobre voidenquanto o exemplo de código usa void*algo completamente diferente.
4
Observe também que Java e C # têm o mesmo conceito, eles simplesmente o chamam Objectem um caso para desambiguar.
Mooing Duck
4
@ Mephy eles não são exatamente de tipo forte? Por C # usando digitação de pato, você quer dizer o dynamictipo que raramente é usado?
precisa saber é o seguinte
9
@ Mephy: A última vez que verifiquei a Wikipedia listou onze significados mutuamente contraditórios para "fortemente tipado". Dizer que "C # não é fortemente digitado" não faz sentido.
Eric Lippert
8
@LightnessRacesinOrbit: Não, o ponto é que não há uma definição autorizada do termo. A Wikipedia não é um recurso prescritivo que informa qual é o significado correto de um termo. É um recurso descritivo . O fato de descrever onze significados contraditórios diferentes é evidência de que o termo não pode ser usado na conversa sem defini-lo com cuidado . Agir de outra forma significa que as chances são muito boas de que as pessoas na conversa falem do passado, e não do outro.
Eric Lippert

Respostas:

58

A palavra-chave void(não um ponteiro) significa "nada" nesses idiomas. Isso é consistente.

Como você observou, void*significa "ponteiro para qualquer coisa" em idiomas que suportam ponteiros brutos (C e C ++). Essa é uma decisão infeliz, porque, como você mencionou, ela voidsignifica duas coisas diferentes.

Não consegui encontrar a razão histórica por trás da reutilização voidpara significar "nada" e "qualquer coisa" em contextos diferentes, no entanto, C faz isso em vários outros lugares. Por exemplo, statictem finalidades diferentes em contextos diferentes. Obviamente, existe um precedente na linguagem C para reutilizar palavras-chave dessa maneira, independentemente do que se possa pensar da prática.

Java e C # são diferentes o suficiente para fazer uma pausa limpa para corrigir alguns desses problemas. Java e C # "seguro" também não permitem ponteiros brutos e não precisam de compatibilidade fácil com C (o C # não seguro permite ponteiros, mas a grande maioria do código C # não se enquadra nessa categoria). Isso permite que eles mudem um pouco as coisas sem se preocupar com a compatibilidade com versões anteriores. Uma maneira de fazer isso é introduzir uma classe Objectna raiz da hierarquia da qual todas as classes são herdadas; portanto, uma Objectreferência serve à mesma função void*sem a desagradabilidade dos problemas de tipo e gerenciamento de memória não processada.


fonte
2
Vou fornecer uma referência quando chegar em casa
4
They also do not allow raw pointers and do not need easy C compatibility.Falso. C # tem ponteiros. Eles geralmente estão (e geralmente deveriam estar) desligados, mas estão lá.
Magus
8
Quanto ao motivo de sua reutilização void: uma razão óbvia seria evitar a introdução de uma nova palavra-chave (e potencialmente interromper os programas existentes). Se eles tivessem introduzido uma palavra-chave especial (por exemplo unknown), então void*seria uma construção sem sentido (e possivelmente ilegal) e unknownseria legal apenas na forma de unknown*.
precisa saber é o seguinte
20
Mesmo em void *, voidsignifica "nada", não "nada". Você não pode desreferenciar a void *, é necessário convertê-lo primeiro em outro tipo de ponteiro.
oefe 23/08/14
2
@oefe Eu acho que não faz sentido dividir esse cabelo, porque voide void*são tipos diferentes (na verdade, voidé "nenhum tipo"). Faz tanto sentido quanto dizer "o intno int*meio de algo." Não, depois de declarar uma variável de ponteiro, você está dizendo "este é um endereço de memória capaz de armazenar algo". No caso de void*, você está dizendo "esse endereço contém algo, mas que algo não pode ser deduzido do tipo de ponteiro".
32

voide void*são duas coisas diferentes. voidem C significa exatamente a mesma coisa que em Java, uma ausência de um valor de retorno. A void*é um ponteiro com uma ausência de um tipo.

Todos os ponteiros em C precisam poder ser desreferenciados. Se você referenciasse a void*, que tipo você esperaria obter? Lembre-se de que os ponteiros C não carregam nenhuma informação de tipo de tempo de execução, portanto, o tipo deve ser conhecido em tempo de compilação.

Dado esse contexto, a única coisa que você pode fazer logicamente com um desreferenciado void*é ignorá-lo, que é exatamente o comportamento que o voidtipo denota.

Karl Bielefeldt
fonte
1
Eu concordo até você chegar ao ponto "ignorar". É possível que a coisa retornada contenha informações sobre como interpretá-la. Em outras palavras, poderia ser o equivalente C de uma variante BASIC. "Quando você conseguir isso, dê uma olhada para descobrir o que é - não posso lhe dizer com antecedência o que você encontrará".
Floris
1
Ou, dito de outra forma, hipoteticamente eu poderia colocar um "descritor de tipo" definido pelo programador no primeiro byte no local da memória endereçado pelo ponteiro, o que me diria qual tipo de dados posso esperar encontrar nos bytes a seguir.
Robert Harvey
1
Obviamente, mas em C foi decidido deixar esses detalhes para o programador, em vez de inseri-los no idioma. Do ponto de vista do idioma , ele não sabe mais o que fazer além de ignorar. Isso evita a sobrecarga de sempre incluir informações de tipo como o primeiro byte quando, na maioria dos casos de uso, você pode determiná-las estaticamente.
Karl Bielefeldt
4
@RobertHarvey sim, mas você não está referenciando um ponteiro nulo; você está convertendo o ponteiro nulo em um ponteiro de char e, em seguida, desreferenciando o ponteiro de char.
user253751
4
@Floris gccdiscorda de você. Se você tentar usar o valor, gccproduza uma mensagem de erro dizendo error: void value not ignored as it ought to be.
kasperd
19

Talvez seja mais útil pensar voidcomo o tipo de retorno. Em seguida, seu segundo método seria "um método que retorna um ponteiro sem tipo".

Robert Harvey
fonte
Isso ajuda. Como alguém que está (finalmente!) Aprendendo C depois de anos em linguagens orientadas a objetos, ponteiros são algo que é um conceito muito novo para mim. Parece que, ao retornar um ponteiro, voidé aproximadamente equivalente a *idiomas como o ActionScript 3, que significa simplesmente "qualquer tipo de retorno".
Naftuli Kay
5
Bem, voidnão é um curinga. Um ponteiro é apenas um endereço de memória. Colocando um tipo antes de o *texto diz "aqui está o tipo de dados que você pode encontrar no endereço de memória referido por esse ponteiro". Colocando voidantes do que *diz ao compilador "Eu não sei o tipo; basta retornar o ponteiro bruto e vou descobrir o que fazer com os dados para os quais ele aponta".
Robert Harvey
2
Eu diria mesmo que um void*aponta para um "vazio". Um ponteiro vazio com nada para o qual aponta. Ainda assim, você pode convertê-lo como qualquer outro ponteiro.
Robert
11

Vamos reforçar um pouco a terminologia.

No padrão C 2011 online :

6.2.5 Tipos
...
19 O voidtipo compreende um conjunto de valores vazio; é um tipo de objeto incompleto que não pode ser concluído.
...
6.3 Conversões
...
6.3.2.2 void

1 O valor (inexistente) de uma voidexpressão (uma expressão que possui tipo void) não deve ser usado de forma alguma, e conversões implícitas ou explícitas (exceto para void) não devem ser aplicadas a essa expressão. Se uma expressão de qualquer outro tipo for avaliada como void expressão, seu valor ou designador será descartado. (Uma voidexpressão é avaliada por seus efeitos colaterais.)

6.3.2.3 Ponteiros

1 Um ponteiro paravoidpode ser convertido para ou de um ponteiro para qualquer tipo de objeto. Um ponteiro para qualquer tipo de objeto pode ser convertido em um ponteiro para anular e voltar; o resultado deve comparar igual ao ponteiro original.

Uma voidexpressão não tem valor (embora possa ter efeitos colaterais). Se eu tiver uma função definida para retornar void, assim:

void foo( void ) { ... }

então a ligação

foo();

não avalia como um valor; Não posso atribuir o resultado a nada, porque não há resultado.

Um ponteiro para voidé essencialmente um tipo de ponteiro "genérico"; você pode atribuir um valor void *a qualquer outro tipo de ponteiro de objeto sem precisar de uma conversão explícita (e é por isso que todos os programadores C gritarão com você por transmitir o resultado de malloc).

Você não pode desreferenciar diretamente a void *; você deve primeiro atribuí-lo a um tipo de ponteiro de objeto diferente antes de poder acessar o objeto apontado.

voidponteiros são usados ​​para implementar (mais ou menos) interfaces genéricas; o exemplo canônico é a qsortfunção de biblioteca, que pode classificar matrizes de qualquer tipo, desde que você forneça uma função de comparação com reconhecimento de tipo.

Sim, usar a mesma palavra-chave para dois conceitos diferentes (sem valor versus ponteiro genérico) é confuso, mas não é como se não houvesse precedente; statictem vários significados distintos em C e C ++.

John Bode
fonte
9

Em Java e C #, nulo como um tipo de retorno para um método parece significar: esse método não retorna nada. Nada. Sem retorno. Você não receberá nada deste método.

Essa afirmação está correta. Também está correto para C e C ++.

em C, nulo como um tipo de retorno ou mesmo como um tipo de parâmetro de método significa algo diferente.

Essa afirmação está incorreta. voidcomo um tipo de retorno em C ou C ++ significa a mesma coisa que em C # e Java. Você está confundindo voidcom void*. Eles são completamente diferentes.

Não é confuso voide void*significa duas coisas completamente diferentes?

Sim.

O que é um ponteiro? O que é um ponteiro nulo?

Um ponteiro é um valor que pode ser desreferenciado . A exclusão de um ponteiro válido fornece um local de armazenamento do tipo apontado . Um ponteiro nulo é um ponteiro que não possui um tipo apontado específico; ele deve ser convertido em um tipo de ponteiro mais específico antes que o valor seja desreferenciado para produzir um local de armazenamento .

Os ponteiros nulos são os mesmos em C, C ++, Java e C #?

Java não possui ponteiros nulos; C # faz. Eles são os mesmos que em C e C ++ - um valor de ponteiro que não possui nenhum tipo específico associado a ele, que deve ser convertido em um tipo mais específico antes de retirá-lo da referência para produzir um local de armazenamento.

Como C é mais antigo que Java e C #, por que essas linguagens adotaram nulo como significando "nada" enquanto C o usava como "nada ou qualquer coisa (quando um ponteiro)"?

A questão é incoerente porque pressupõe falsidades. Vamos fazer algumas perguntas melhores.

Por que Java e C # adotaram a convenção que voidé um tipo de retorno válido, em vez de, por exemplo, usar a convenção do Visual Basic de que funções nunca retornam nulas e sub-rotinas sempre retornam nulas?

Familiarizar-se com os programadores que vêm para Java ou C # de linguagens onde voidé um tipo de retorno.

Por que o C # adotou a convenção confusa que void*tem um significado completamente diferente void?

Familiarizar-se com os programadores que chegam ao C # a partir de idiomas onde void*é um tipo de ponteiro.

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

A primeira função não retorna nada. A segunda função retorna um ponteiro nulo. Eu teria declarado essa segunda função como

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

Se puder ajudar, se você pensar em "vazio" como significado "sem significado". O valor de retorno de describeé "sem significado". Retornar um valor dessa função é ilegal porque você disse ao compilador que o valor de retorno é sem significado. Você não pode capturar o valor de retorno dessa função porque é sem significado. Você não pode declarar uma variável do tipo voidporque é sem significado. Por exemplo, void nonsense;é um absurdo e é de fato ilegal. Observe também que uma matriz de nulos void void_array[42];também é um absurdo.

Um ponteiro para void ( void*) é algo diferente. É um ponteiro, não uma matriz. Leia void*como significando um ponteiro que aponta para algo "sem significado". O ponteiro é "sem sentido", o que significa que não faz sentido desreferenciar esse ponteiro. Tentar fazer isso é de fato ilegal. Código que desreferencia um ponteiro nulo não será compilado.

Portanto, se você não pode desreferenciar um ponteiro de nulo e não pode fazer uma série de vazios, como pode usá-los? A resposta é que um ponteiro para qualquer tipo pode ser convertido de e para void*. Converter um ponteiro para void*e converter um ponteiro para o tipo original produzirá um valor igual ao ponteiro original. O padrão C garante esse comportamento. A principal razão pela qual você vê tantos ponteiros nulos em C é que esse recurso fornece uma maneira de implementar a ocultação de informações e a programação baseada em objetos em uma linguagem que não é orientada a objetos.

Finalmente, a razão muitas vezes você vê movedeclarada como void *move(...)mais do que void* move(...)é porque colocar o asterisco ao lado do nome em vez do tipo é uma prática muito comum em C. A razão é que a seguinte declaração vai chegar em apuros: int* iptr, jptr;Isto faria com que você acho que você está declarando dois ponteiros para um int. Você não é. Esta declaração faz jptrum em intvez de um ponteiro para um int. A declaração adequada é int *iptr, *jptr;Você pode fingir que o asterisco pertence ao tipo se você seguir a prática de declarar apenas uma variável por declaração. Mas tenha em mente que você está fingindo.

David Hammen
fonte
"Código que desreferencia um ponteiro nulo não será compilado." Eu acho que você quer dizer código que desreferencia um ponteiro nulo não será compilado. O compilador não protege você de remover a referência de um ponteiro nulo; isso é um problema de tempo de execução.
Cody Grey
@CodyGray - Obrigado! Corrigido isso. Código que desreferencia um ponteiro nulo será compilado (desde que o ponteiro não seja void* null_ptr = 0;).
David Hammen
2

Simplificando, não há diferença . Deixe-me dar alguns exemplos.

Digamos que eu tenha uma turma chamada Carro.

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

Eu acredito que aqui a confusão aqui é baseado fora como você definiria voidvs void*. Um tipo de retorno voidsignifica "Eu não retorno nada", enquanto um tipo de retorno void*significa "Retorno um ponteiro do tipo nada".

Isso se deve ao fato de um ponteiro ser um caso especial. Um objeto ponteiro sempre apontará para um local na memória, independentemente de existir ou não um objeto válido. Se minhas void* carreferências a um byte, uma palavra, um carro ou uma bicicleta não importam porque meus void*pontos apontam para algo sem tipo definido. Cabe a nós defini-lo mais tarde.

Em linguagens como C # e Java, a memória é gerenciada e o conceito de ponteiros é transferido para a classe Object. Em vez de ter uma referência a um objeto do tipo "nada", sabemos que, no mínimo, nosso objeto é do tipo "Objeto". Deixe-me explicar o porquê.

No C / C ++ convencional, se você criar um objeto e excluir seu ponteiro, será impossível obter acesso a esse objeto. Isso é conhecido como vazamento de memória .

Em C # / Java, tudo é um objeto e isso permite que o tempo de execução acompanhe cada objeto. Ele não precisa necessariamente saber que meu objeto é um carro, ele simplesmente precisa conhecer algumas informações básicas que já são tratadas pela classe Object.

Para nós, programadores, isso significa que muitas das informações que precisaríamos cuidar manualmente em C / C ++ são tratadas por nós pela classe Object, eliminando a necessidade do caso especial que é o ponteiro.

Thebluefish
fonte
"my void * aponta para algo sem tipo definido" - não exatamente. É mais correto dizer que é um ponteiro para um valor de comprimento zero (quase como uma matriz com 0 elementos, mas sem as informações de tipo associadas à matriz).
Scott Whitlock
2

Tentarei dizer como alguém poderia pensar sobre essas coisas em C. O idioma oficial no padrão C não está muito à vontade voide não tentarei ser totalmente consistente com ele.

O tipo voidnão é um cidadão de primeira classe entre os tipos. Embora seja um tipo de objeto, não pode ser o tipo de qualquer objeto (ou valor), de um campo ou de um parâmetro de função; no entanto, pode ser o tipo de retorno de uma função e, portanto, o tipo de uma expressão (basicamente de chamadas de tais funções, mas as expressões formadas com o operador condicional também podem ter o tipo nulo). Mas mesmo o uso mínimo de voidcomo um tipo de valor pelo qual o item acima deixa a porta aberta, ou seja, encerrar uma função retornando voidcom uma declaração da forma em return E;que Eé uma expressão do tipo void, é explicitamente proibido em C (embora seja permitido em C ++, mas por razões não aplicáveis ​​a C).

Se objetos do tipo voidfossem permitidos, eles teriam 0 bits (que é a quantidade de informações no valor de uma expressão do voidtipo); não seria um grande problema permitir esses objetos, mas eles seriam bastante inúteis (objetos de tamanho 0 dificultariam a definição da aritmética dos ponteiros, o que talvez fosse melhor apenas proibido). O conjunto de valores diferentes desse objeto teria um elemento (trivial); seu valor pode ser obtido, trivialmente, porque não possui substância, mas não pode ser modificado (sem valor diferente ). Portanto, o padrão é confuso em dizer que voidcompreende um conjunto vazio de valores; esse tipo pode ser útil para descrever expressões que não podemser avaliado (como saltos ou chamadas não terminadas), embora a linguagem C não use esse tipo.

Ser desaprovado como tipo de valor, voidfoi usado em pelo menos duas maneiras não diretamente relacionadas para designar "proibido": especificar (void)como especificação de parâmetro em tipos de função significa fornecer qualquer argumento a eles é proibido (enquanto '()', pelo contrário, significaria tudo é permitido e, sim, mesmo fornecendo uma voidexpressão como argumento para funções com (void)especificação de parâmetro é proibido), e declarar void *psignifica que a expressão de exclusão de referência *pé proibida (mas não que seja uma expressão válida do tipo void). Portanto, você está certo de que esses usos de voidnão são realmente consistentes voidcomo um tipo válido de expressões. No entanto, um objeto do tipovoid*não é realmente um ponteiro para valores de qualquer tipo, significa um valor que é tratado como um ponteiro, embora não possa ser desreferenciado e a aritmética do ponteiro seja proibida. O "tratado como um ponteiro" significa apenas que pode ser convertido de e para qualquer tipo de ponteiro sem perda de informações. No entanto, também pode ser usado para converter valores inteiros de e para, para que não precise apontar nada.

Marc van Leeuwen
fonte
A rigor, ele pode ser convertido de e para qualquer tipo de ponteiro de objeto sem perda de informações, mas nem sempre para e de . Em geral, uma conversão de void*para outro tipo de ponteiro pode perder informações (ou até ter UB, não me lembro de imediato), mas se o valor de void*veio de converter um ponteiro do mesmo tipo para o qual você está convertendo agora, isso não acontece.
Steve Jessop
0

Em C # e Java, toda classe é derivada de uma Objectclasse. Portanto, sempre que desejamos passar uma referência a "alguma coisa", podemos usar uma referência do tipo Object.

Como não precisamos usar ponteiros nulos para esse fim nesses idiomas, voidnão precisa significar "algo ou nada" aqui.

Reg Editar
fonte
0

Em C, é possível ter um ponteiro para itens de qualquer tipo [os ponteiros se comportam como um tipo de referência]. Um ponteiro pode identificar um objeto de um tipo conhecido ou um objeto de tipo arbitrário (desconhecido). Os criadores de C optaram por usar a mesma sintaxe geral para "ponteiro para coisa de tipo arbitrário" como para "ponteiro para coisa de algum tipo específico". Como a sintaxe no último caso requer um token para especificar qual é o tipo, a sintaxe anterior exige colocar algo onde esse token pertenceria se o tipo fosse conhecido. C optou por usar a palavra-chave "nulo" para esse fim.

Em Pascal, como em C, é possível ter ponteiros para coisas de qualquer tipo; dialetos mais populares de Pascal também permitem "apontador para coisa de tipo arbitrário", mas, em vez de usar a mesma sintaxe usada para "apontador para coisa de algum tipo específico", eles simplesmente se referem ao primeiro como Pointer.

Em Java, não há tipos de variáveis ​​definidos pelo usuário; tudo é uma referência de objeto primitivo ou heap. Pode-se definir itens do tipo "referência a um objeto de pilha de um tipo específico" ou "referência a um objeto de pilha de tipo arbitrário". O primeiro simplesmente usa o nome do tipo sem nenhuma pontuação especial para indicar que é uma referência (já que não pode ser outra coisa); o último usa o nome do tipo Object.

O uso de void*como nomenclatura para um ponteiro para algo do tipo arbitrário é, até onde sei, exclusivo para C e derivadas diretas como C ++; outras línguas que conheço lidam com o conceito simplesmente usando um tipo de nome distinto.

supercat
fonte
-1

Você pode pensar na palavra-chave voidcomo uma vazia struct( no C99, estruturas vazias são aparentemente proibidas , no .NET e C ++ elas ocupam mais de 0 de memória e, por último, lembro que tudo em Java é uma classe):

struct void {
};

... o que significa que é um tipo com 0 comprimento. É assim que funciona quando você executa uma chamada de função que retorna void... em vez de alocar 4 bytes ou mais na pilha para um valor de retorno inteiro, ele não altera o ponteiro da pilha (incrementa-o em um comprimento de 0 )

Portanto, se você declarou algumas variáveis ​​como:

int a;
void b;
int c;

... e então você tomou o endereço dessas variáveis, então talvez be cpode estar localizado no mesmo lugar na memória, porque btem comprimento zero. Portanto, a void*é um ponteiro na memória para o tipo interno com comprimento zero. O fato de você saber que há algo imediatamente a seguir que possa ser útil para você é outra questão e exige que você realmente saiba o que está fazendo (e é perigoso).

Scott Whitlock
fonte
O único compilador que eu saiba que atribui um comprimento para o tipo void é gcc, e cessionários GCC um comprimento de 1.
Joshua