Qual é a vantagem de retornar um ponteiro para uma estrutura em vez de retornar toda a estrutura na return
instrução da função?
Eu estou falando sobre funções como fopen
e outras funções de baixo nível, mas provavelmente existem funções de nível superior que retornam ponteiros para estruturas também.
Acredito que isso seja mais uma escolha de design do que apenas uma questão de programação e estou curioso para saber mais sobre as vantagens e desvantagens dos dois métodos.
Uma das razões pelas quais pensei que seria uma vantagem em retornar um ponteiro para uma estrutura é poder saber com mais facilidade se a função falhou ao retornar o NULL
ponteiro.
Devolver uma estrutura completaNULL
seria mais difícil, suponho, ou menos eficiente. Este é um motivo válido?
fonte
gets()
função totalmente mal projetada fosse removida. Alguns programadores ainda têm aversão a copiar estruturas, velhos hábitos morrem com dificuldade.FILE*
é efetivamente um identificador opaco. O código do usuário não deve se importar com a estrutura interna.&
e acessar um membro.
".Respostas:
Existem várias razões práticas pelas quais funções como
fopen
retornar ponteiros para, em vez de instâncias destruct
tipos:struct
tipo do usuário;No caso de tipos como
FILE *
, é porque você não deseja expor detalhes da representação do tipo para o usuário - umFILE *
objeto serve como um identificador opaco e você apenas passa esse identificador para várias rotinas de E / S (e emboraFILE
muitas vezes seja implementado como umstruct
tipo, não precisa ser).Portanto, você pode expor um tipo incompleto
struct
em um cabeçalho em algum lugar:Embora não seja possível declarar uma instância de um tipo incompleto, você pode declarar um ponteiro para ela. Portanto, posso criar um
FILE *
e atribuir a ele por meio defopen
,freopen
etc., mas não posso manipular diretamente o objeto para o qual ele aponta.Também é provável que a
fopen
função esteja alocando umFILE
objeto dinamicamente, usandomalloc
ou similar. Nesse caso, faz sentido retornar um ponteiro.Finalmente, é possível que você esteja armazenando algum tipo de estado em um
struct
objeto e precise disponibilizá-lo em vários locais diferentes. Se você retornasse instâncias dostruct
tipo, essas instâncias seriam objetos separados na memória uns dos outros e, eventualmente, ficariam fora de sincronia. Ao retornar um ponteiro para um único objeto, todos estão se referindo ao mesmo objeto.fonte
Existem duas maneiras de "retornar uma estrutura". Você pode retornar uma cópia dos dados ou retornar uma referência (ponteiro) para eles. Geralmente, é preferível retornar (e distribuir em geral) um ponteiro, por alguns motivos.
Primeiro, copiar uma estrutura leva muito mais tempo da CPU do que copiar um ponteiro. Se isso é algo que seu código faz com frequência, pode causar uma diferença perceptível no desempenho.
Segundo, não importa quantas vezes você copie um ponteiro, ele ainda está apontando para a mesma estrutura na memória. Todas as modificações nele serão refletidas na mesma estrutura. Mas se você copiar a estrutura em si e fizer uma modificação, a alteração será exibida apenas nessa cópia . Qualquer código que contenha uma cópia diferente não verá a alteração. Às vezes, muito raramente, é isso que você deseja, mas na maioria das vezes não é, e pode causar erros se você errar.
fonte
Além de outras respostas, às vezes vale a pena retornar um valor pequeno
struct
. Por exemplo, pode-se retornar um par de dados e algum código de erro (ou êxito) relacionado a ele.Por exemplo,
fopen
retorna apenas um dado (o abertoFILE*
) e, em caso de erro, fornece o código de erro através daerrno
variável pseudo-global. Mas talvez seja melhor retornar umstruct
dos dois membros: oFILE*
identificador e o código de erro (que seria definido se o identificador do arquivo forNULL
). Por razões históricas, não é o caso (e os erros são relatados através doerrno
global, que hoje é uma macro).Observe que o idioma Go possui uma notação interessante para retornar dois (ou alguns) valores.
Observe também que no Linux / x86-64 as convenções ABI e de chamada (consulte a página x86-psABI ) especificam que um
struct
dos dois membros escalares (por exemplo, um ponteiro e um número inteiro, ou dois ponteiros ou dois números inteiros) é retornado através de dois registros (e isso é muito eficiente e não passa pela memória).Portanto, no novo código C, retornar um C pequeno
struct
pode ser mais legível, mais fácil de encadear e mais eficiente.fonte
rdx:rax
. Assim,struct foo { int a,b; };
é devolvido embalado emrax
(por exemplo, com shift / ou) e deve ser descompactado com shift / mov. Aqui está um exemplo no Godbolt . Mas o x86 pode usar os baixos 32 bits de um registro de 64 bits para operações de 32 bits sem se preocupar com os altos, por isso é sempre muito ruim, mas definitivamente pior do que usar 2 registradores na maioria das vezes para estruturas de 2 membros.std::optional<int>
retorna o booleano na metade superiorrax
, então você precisa de uma constante de máscara de 64 bits para testá-lotest
. Ou você poderia usarbt
. Mas é chato comparar o chamador e o destinatário com o usodl
, o que os compiladores devem fazer para funções "particulares". Também relacionado: libstdc ++std::optional<T>
não é trivialmente copiável, mesmo quando T é, portanto, sempre retorna por meio de ponteiro oculto: stackoverflow.com/questions/46544019/… . (libc ++ 's é trivialmente-copiável)struct { int a; _Bool b; };
em C, se o interlocutor queria testar o booleano, porque estruturas trivialmente-copyable C ++ usam a mesma ABI como C.div_t div()
Você está no caminho certo
Os dois motivos mencionados são válidos:
Se você tem uma textura (por exemplo) em algum lugar da memória e deseja fazer referência a essa textura em vários locais do seu programa; não seria prudente fazer uma cópia sempre que você quisesse fazer referência a ela. Em vez disso, se você simplesmente passar um ponteiro para fazer referência à textura, seu programa será executado muito mais rápido.
O maior motivo é a alocação dinâmica de memória. Muitas vezes, quando um programa é compilado, você não tem certeza da quantidade exata de memória necessária para determinadas estruturas de dados. Quando isso acontece, a quantidade de memória que você precisa usar será determinada em tempo de execução. Você pode solicitar memória usando 'malloc' e liberá-lo quando terminar de usar 'free'.
Um bom exemplo disso é a leitura de um arquivo especificado pelo usuário. Nesse caso, você não tem idéia do tamanho do arquivo ao compilar o programa. Você só pode descobrir quanta memória precisa quando o programa está realmente sendo executado.
Malloc e ponteiros de retorno gratuitos para locais na memória. Portanto, as funções que fazem uso da alocação dinâmica de memória retornam os ponteiros para onde eles criaram suas estruturas na memória.
Além disso, nos comentários, vejo que há uma dúvida sobre se você pode retornar uma estrutura de uma função. Você pode realmente fazer isso. O seguinte deve funcionar:
fonte
struct incomplete* foo(void)
. Dessa forma, posso declarar funções em um cabeçalho, mas apenas definir as estruturas dentro de um arquivo C, permitindo assim o encapsulamento.Algo como a
FILE*
não é realmente um ponteiro para uma estrutura no que diz respeito ao código do cliente, mas é uma forma de identificador opaco associado a alguma outra entidade, como um arquivo. Quando um programa é chamadofopen
, geralmente ele não se importa com o conteúdo da estrutura retornada - tudo o que importa é que outras funçõesfread
façam o que for necessário.Se uma biblioteca padrão mantém dentro de uma
FILE*
informação sobre, por exemplo, a posição atual de leitura dentro desse arquivo, uma chamada parafread
precisa atualizar essa informação. Terfread
recebido um ponteiro para oFILE
facilita isso. Se, emfread
vez disso, recebesse umFILE
, não haveria como atualizar oFILE
objeto mantido pelo chamador.fonte
Esconder informações
O mais comum é a ocultação de informações . C não tem, digamos, a capacidade de tornar campos
struct
particulares, e muito menos fornecer métodos para acessá-los.Portanto, se você deseja impedir forçadamente os desenvolvedores de ver e mexer no conteúdo de um pontapé, por exemplo,
FILE
a única maneira é impedir que eles sejam expostos à sua definição, tratando o ponteiro como opaco, cujo tamanho e ponta definição são desconhecidas para o mundo exterior. A definição deFILE
será visível apenas para aqueles que implementam as operações que requerem sua definição, comofopen
, enquanto apenas a declaração da estrutura será visível para o cabeçalho público.Compatibilidade binária
Ocultar a definição da estrutura também pode ajudar a fornecer espaço para respirar para preservar a compatibilidade binária nas APIs do dylib. Ele permite que os implementadores da biblioteca alterem os campos na estrutura opaca sem quebrar a compatibilidade binária com aqueles que usam a biblioteca, uma vez que a natureza de seu código precisa apenas saber o que eles podem fazer com a estrutura, não o tamanho ou o tamanho dos campos. tem.
Como exemplo, eu posso realmente executar alguns programas antigos criados durante a era do Windows 95 hoje (nem sempre perfeitamente, mas surpreendentemente muitos ainda funcionam). Provavelmente, parte do código desses binários antigos usava ponteiros opacos para estruturas cujo tamanho e conteúdo foram alterados desde a era do Windows 95. No entanto, os programas continuam funcionando em novas versões do Windows, pois não foram expostos ao conteúdo dessas estruturas. Ao trabalhar em uma biblioteca onde a compatibilidade binária é importante, o que o cliente não está exposto geralmente pode mudar sem quebrar a compatibilidade com versões anteriores.
Eficiência
Normalmente, é menos eficiente presumir que o tipo possa praticamente caber e ser alocado na pilha, a menos que exista um alocador de memória muito menos generalizado sendo usado nos bastidores do que
malloc
, como uma memória de pool de alocadores de tamanho fixo e não variável já alocada. É uma troca de segurança nesse caso, provavelmente, permitir que os desenvolvedores da biblioteca mantenham invariantes (garantias conceituais) relacionados aFILE
.Não é um motivo tão válido, pelo menos do ponto de vista do desempenho, para fazer
fopen
retornar um ponteiro, pois o único motivo pelo qual ele retornariaNULL
é a falha ao abrir um arquivo. Isso seria otimizar um cenário excepcional em troca da lentidão de todos os caminhos de execução de casos comuns. Em alguns casos, pode haver um motivo válido de produtividade para tornar os projetos mais diretos e fazer com que retornem ponteiros para permitir oNULL
retorno em alguma pós-condição.Para operações de arquivo, a sobrecarga é relativamente trivial em comparação com as próprias operações de arquivo, e o manual
fclose
não pode ser evitado de qualquer maneira. Portanto, não é possível poupar ao cliente o aborrecimento de liberar (fechar) o recurso, expondo a definiçãoFILE
e devolvendo-o por valorfopen
ou esperar muito aumento de desempenho, considerando o custo relativo das operações de arquivo para evitar uma alocação de heap .Pontos de acesso e correções
Em outros casos, porém, eu criei um perfil de muitos códigos C desperdiçados em bases de código herdadas com pontos de acesso
malloc
e falhas desnecessárias de cache obrigatórias como resultado do uso frequente dessa prática com ponteiros opacos e da alocação desnecessária de coisas desnecessárias na pilha, às vezes em grandes laços.Uma prática alternativa que eu uso é expor as definições de estrutura, mesmo que o cliente não as adultere, usando um padrão de convenção de nomenclatura para comunicar que ninguém mais deve tocar nos campos:
Se houver preocupações de compatibilidade binária no futuro, achei bom o suficiente reservar espaço supérfluo para propósitos futuros, como:
Esse espaço reservado é um pouco inútil, mas pode salvar vidas se descobrirmos no futuro que precisamos adicionar mais alguns dados
Foo
sem quebrar os binários que usam nossa biblioteca.Na minha opinião, ocultação de informações e compatibilidade binária geralmente são a única razão decente para permitir apenas a alocação de estruturas de heap além de estruturas de comprimento variável (o que sempre exigiria isso, ou pelo menos seria um pouco estranho de usar caso contrário, se o cliente tivesse que alocar memória na pilha de maneira VLA para alocar o VLS). Mesmo grandes estruturas costumam ser mais baratas para retornar por valor, se isso significa que o software trabalha muito mais com a memória quente na pilha. E mesmo que não fosse mais barato retornar pelo valor na criação, alguém poderia simplesmente fazer o seguinte:
... para inicializar
Foo
da pilha sem a possibilidade de uma cópia supérflua. Ou o cliente ainda tem a liberdade de alocarFoo
na pilha, se desejar, por algum motivo.fonte