Existem benefícios em passar por ponteiro em vez de passar por referência em C ++?

225

Quais são os benefícios de passar por ponteiro sobre passar por referência em C ++?

Ultimamente, tenho visto vários exemplos que escolheram passar argumentos de função por ponteiros em vez de passar por referência. Existem benefícios em fazer isso?

Exemplo:

func(SPRITE *x);

com uma chamada de

func(&mySprite);

vs.

func(SPRITE &x);

com uma chamada de

func(mySprite);
Matt Pascoe
fonte
Não se esqueça newde criar um ponteiro e os problemas resultantes de propriedade.
Martin York

Respostas:

216

Um ponteiro pode receber um parâmetro NULL, um parâmetro de referência não. Se houver a chance de você passar "nenhum objeto", use um ponteiro em vez de uma referência.

Além disso, passar pelo ponteiro permite ver explicitamente no site da chamada se o objeto é passado por valor ou por referência:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);
Adam Rosenfield
fonte
18
Resposta incompleta. O uso de ponteiros não autoriza o uso de objetos temporários / promovidos, nem o uso de objetos pontiagudos como objetos do tipo pilha. E isso sugere que o argumento pode ser NULL quando, na maioria das vezes, um valor NULL deve ser proibido. Leia a resposta do litb para obter uma resposta completa.
paercebal
A segunda chamada de função costumava ser anotada func2 passes by reference. Embora eu compreenda que você quis dizer que ele passa "por referência" de uma perspectiva de alto nível, implementada passando um ponteiro na perspectiva de nível de código, isso foi muito confuso (consulte stackoverflow.com/questions/13382356/… ).
Lightness Races in Orbit (
Eu simplesmente não compro isso. Sim, você passa um ponteiro, portanto deve ser um parâmetro de saída, porque o que está apontado não pode ser const?
deworde
Não temos essa referência passageira em C? Estou usando a versão mais recente do codeblock (mingw) e selecionando um projeto em C. Ainda passando por referência (func (int e a)) funciona. Ou está disponível no C99 ou C11 em diante?
quer
1
@ JonWheelock: Não, C não tem passagem por referência. func(int& a)não é C válido em nenhuma versão do padrão. Você provavelmente está compilando seus arquivos como C ++ por acidente.
Adam Rosenfield
245

Passando pelo ponteiro

  • O chamador deve usar o endereço -> não transparente
  • Um valor 0 pode ser fornecido para significar nothing. Isso pode ser usado para fornecer argumentos opcionais.

Passe por referência

  • O chamador passa apenas o objeto -> transparente. Deve ser usado para sobrecarga do operador, pois a sobrecarga para tipos de ponteiros não é possível (os ponteiros são do tipo incorporado). Então você não pode string s = &str1 + &str2;usar ponteiros.
  • Não há 0 valores possíveis -> A função chamada não precisa verificar por eles
  • A referência a const também aceita temporários void f(const T& t); ... f(T(a, b, c));:, ponteiros não podem ser usados ​​assim, pois você não pode usar o endereço de um temporário.
  • Por último, mas não menos importante, as referências são mais fáceis de usar -> menos chance de erros.
Johannes Schaub - litb
fonte
7
Passar pelo ponteiro também gera a mensagem 'A propriedade é transferida ou não?' questão. Este não é o caso com referências.
Frerich Raabe 30/10/09
43
Não concordo com "menos chance de erros". Ao inspecionar o site de chamada e o leitor visualizar "foo (& s)", fica imediatamente claro que s pode ser modificado. Quando você lê "foo (s)", não está totalmente claro se s pode ser modificado. Esta é uma das principais fontes de erros. Talvez exista menos chance de uma determinada classe de erros, mas no geral, a passagem por referência é uma fonte enorme de erros.
precisa
25
O que você quer dizer com "transparente"?
Gbert90
2
@ Gbert90, se você vê foo (& a) em um site de chamadas, sabe que foo () usa um tipo de ponteiro. Se você vê foo (a), não sabe se é necessária uma referência.
Michael J. Davenport
3
@ MichaelJ.Davenport - em sua explicação, você sugere "transparente" para significar algo como "óbvio que o chamador está passando um ponteiro, mas não óbvio que o chamador está passando uma referência". No post de Johannes, ele diz "Passando pelo ponteiro - o chamador tem que pegar o endereço -> não transparente" e "Passar por referência - o chamador passa o objeto -> transparente" - que é quase o oposto do que você diz . Acho que a pergunta de Gbert90 "O que você quer dizer com" transparente "" ainda é válida.
Happy Green Kid Naps
66

Gosto do raciocínio de um artigo de "cplusplus.com:"

  1. Passe por valor quando a função não desejar modificar o parâmetro e o valor for fácil de copiar (ints, doubles, char, bool, etc ... tipos simples. Std :: string, std :: vector e todos os outros STL recipientes NÃO são tipos simples.)

  2. Passe pelo ponteiro const quando o valor é caro para copiar E a função não deseja modificar o valor apontado para AND NULL é um valor esperado e válido que a função manipula.

  3. Passe pelo ponteiro não const quando o valor é caro para copiar E a função deseja modificar o valor apontado para AND NULL é um valor válido e esperado que a função manipula.

  4. Passe pela referência const quando o valor é caro para copiar E a função não deseja modificar o valor referido AND NULL não seria um valor válido se um ponteiro fosse usado.

  5. Passe por referência não contida quando o valor é caro para copiar E a função deseja modificar o valor referido AND NULL não seria um valor válido se um ponteiro fosse usado.

  6. Ao escrever funções de modelo, não há uma resposta clara, pois existem algumas desvantagens que estão além do escopo desta discussão, mas basta dizer que a maioria das funções de modelo toma seus parâmetros por valor ou (const) referência , no entanto, como a sintaxe do iterador é semelhante à dos ponteiros (asterisco para "desreferência"), qualquer função de modelo que espere iteradores como argumentos também por padrão também aceitará ponteiros (e não verificará NULL, pois o conceito de iterador NULL tem uma sintaxe diferente )

http://www.cplusplus.com/articles/z6vU7k9E/

O que retiro disso é que a principal diferença entre optar por usar um ponteiro ou parâmetro de referência é se NULL for um valor aceitável. É isso aí.

Se o valor é de entrada, saída, modificável etc. deve estar na documentação / comentários sobre a função, afinal.

R. Navega
fonte
Sim, para mim os termos relacionados a NULL são as principais preocupações aqui. Thx for
quote
62

Allen Holub, "Bastante Corda para Atirar no Pé", lista as 2 regras a seguir:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

Ele lista vários motivos pelos quais as referências foram adicionadas ao C ++:

  • eles são necessários para definir construtores de cópia
  • eles são necessários para sobrecargas do operador
  • const referências permitem que você tenha semântica de passagem por valor, evitando uma cópia

Seu ponto principal é que as referências não devem ser usadas como parâmetros de 'saída', porque no local da chamada não há indicação se o parâmetro é uma referência ou um valor. Portanto, sua regra é usar apenas constreferências como argumentos.

Pessoalmente, acho que essa é uma boa regra geral, pois fica mais claro quando um parâmetro é um parâmetro de saída ou não. No entanto, embora eu pessoalmente concorde com isso em geral, eu me permito ser influenciado pelas opiniões de outras pessoas da minha equipe se elas argumentarem pelos parâmetros de saída como referências (alguns desenvolvedores gostam imensamente).

Michael Burr
fonte
8
Minha posição nesse argumento é que, se o nome da função tornar totalmente óbvio, sem verificar os documentos, que o parâmetro será modificado, uma referência não-const será OK. Então, pessoalmente, eu permitiria "getDetails (DetailStruct & result)". Um ponteiro ali levanta a feia possibilidade de uma entrada NULL.
Steve Jessop
3
Isso é enganoso. Mesmo que alguns não gostem de referências, eles são uma parte importante da linguagem e devem ser usados ​​como tal. Essa linha de raciocínio é como dizer: não use modelos, você sempre pode usar contêineres de void * para armazenar qualquer tipo. Leia a resposta por litb.
David Rodríguez - dribeas 2/08/08
4
Não vejo como isso é enganoso - há momentos em que as referências são necessárias e há momentos em que as práticas recomendadas podem sugerir não usá-las, mesmo que você possa. O mesmo pode ser dito para qualquer recurso da linguagem - herança, amigos não-membros, sobrecarga de operadores, MI, etc ...
Michael Burr
A propósito, concordo que a resposta do litb é muito boa e certamente é mais abrangente do que esta - acabei de optar por discutir uma lógica para evitar o uso de referências como parâmetros de saída.
Michael Burr
1
Esta regra é usada no guia de estilo do Google c ++: google-styleguide.googlecode.com/svn/trunk/…
Anton Daneyko
9

Esclarecimentos sobre os posts anteriores:


As referências NÃO são uma garantia de obter um ponteiro não nulo. (Embora muitas vezes os tratemos como tal.)

Enquanto código horrivelmente ruim, como o levar para trás do código ruim do depósito de lenha , o seguinte será compilado e executado: (Pelo menos no meu compilador.)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

O verdadeiro problema que tenho com as referências reside em outros programadores, doravante denominados IDIOTS , que alocam no construtor, desalocam no destruidor e não fornecem um construtor ou operador de cópia = ().

De repente, há um mundo de diferença entre foo (BAR bar) e foo (BAR & bar) . (A operação de cópia bit a bit automática é invocada. A desalocação no destruidor é invocada duas vezes.)

Felizmente, os compiladores modernos receberão essa dupla desalocação do mesmo ponteiro. 15 anos atrás, eles não fizeram. (Em gcc / g ++, use setenv MALLOC_CHECK_ 0 para revisar as formas antigas.) Resultando em DEC UNIX, na mesma memória sendo alocada para dois objetos diferentes. Muita diversão na depuração por lá ...


Mais praticamente:

  • As referências ocultam que você está alterando os dados armazenados em outro local.
  • É fácil confundir uma referência com um objeto copiado.
  • Os ponteiros tornam óbvio!
Mr.Ree
fonte
16
esse não é o problema da função ou das referências. você está violando regras de linguagem. desreferenciar um ponteiro nulo por si só já é um comportamento indefinido. "Referências NÃO são garantia de obter um ponteiro não nulo.": O próprio padrão diz que é. outras formas constituem um comportamento indefinido.
Johannes Schaub - litb 3/08/08
1
Eu concordo com litb. Embora verdadeiro, o código que você está nos mostrando é mais sabotagem do que qualquer outra coisa. Existem maneiras de sabotar qualquer coisa, incluindo as notações "referência" e "ponteiro".
paercebal 3/08/08
1
Eu disse que era "levá-lo para trás do código ruim do galpão de madeira"! Na mesma linha, você também pode ter i = new FOO; excluir i; teste (* i); Outra (infelizmente comum) ocorrência de ponteiro / referência pendente.
Mr.Ree
1
Na verdade, não é desreferenciar NULL que é o problema, mas USAR esse objeto desreferenciado (nulo). Como tal, realmente não há diferença (além da sintaxe) entre ponteiros e referências de uma perspectiva de implementação de linguagem. São os usuários que têm expectativas diferentes.
Mr.Ree
2
Independentemente do que você faz com a referência retornada, no momento em que diz *i, seu programa tem um comportamento indefinido. Por exemplo, o compilador pode ver esse código e assumir "OK, esse código tem um comportamento indefinido em todos os caminhos de código, portanto, toda essa função deve estar inacessível". Então, assumirá que todos os ramos que levam a essa função não são utilizados. Essa é uma otimização realizada regularmente.
David Stone
5

A maioria das respostas aqui não aborda a ambiguidade inerente ao ter um ponteiro bruto em uma assinatura de função, em termos de expressar intenção. Os problemas são os seguintes:

  • O chamador não sabe se o ponteiro aponta para um único objeto ou para o início de uma "matriz" de objetos.

  • O chamador não sabe se o ponteiro "possui" a memória para a qual aponta. IE, se a função deve ou não liberar a memória. ( foo(new int)- Isso é um vazamento de memória?).

  • O chamador não sabe se nullptrpode ou não ser passado com segurança para a função.

Todos esses problemas são resolvidos por referências:

  • As referências sempre se referem a um único objeto.

  • As referências nunca possuem a memória a que se referem, são apenas uma visão da memória.

  • As referências não podem ser nulas.

Isso faz das referências um candidato muito melhor para uso geral. No entanto, as referências não são perfeitas - há alguns problemas importantes a serem considerados.

  • Sem indireto explícito. Isso não é um problema com um ponteiro bruto, pois precisamos usar o &operador para mostrar que realmente estamos passando um ponteiro. Por exemplo, int a = 5; foo(a);não está claro aqui que a está sendo passado por referência e pode ser modificado.
  • Anulabilidade. Essa fraqueza dos ponteiros também pode ser uma força, quando realmente queremos que nossas referências sejam anuláveis. Visto que std::optional<T&>não é válido (por boas razões), os ponteiros nos dão a nulidade que você deseja.

Então, parece que quando queremos uma referência nula com indireção explícita, devemos buscar um T*direito? Errado!

Abstrações

Em nosso desespero pela nulidade, podemos buscar T*e simplesmente ignorar todas as deficiências e ambigüidades semânticas listadas anteriormente. Em vez disso, devemos buscar o que o C ++ faz melhor: uma abstração. Se simplesmente escrevermos uma classe que envolve um ponteiro, obteremos a expressividade, bem como a nulidade e a indireta explícita.

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

Essa é a interface mais simples que eu poderia criar, mas faz o trabalho com eficiência. Permite inicializar a referência, verificando se existe um valor e acessando o valor. Podemos usá-lo assim:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

Alcançamos nossos objetivos! Vamos agora ver os benefícios, em comparação com o ponteiro bruto.

  • A interface mostra claramente que a referência deve se referir apenas a um objeto.
  • Claramente, ele não possui a memória a que se refere, pois não possui destruidor definido pelo usuário e não possui método para excluir a memória.
  • O chamador sabe que nullptrpode ser passado, já que o autor da função está solicitando explicitamente umoptional_ref

Poderíamos tornar a interface mais complexa a partir daqui, como adicionar operadores de igualdade, uma interface monádica get_ore mapum método que obtém o valor ou gera uma exceção, constexprsuporte. Isso pode ser feito por você.

Em conclusão, em vez de usar ponteiros brutos, raciocine sobre o que esses ponteiros realmente significam em seu código e aproveite uma abstração de biblioteca padrão ou escreva sua própria. Isso melhorará seu código significativamente.

Tom VH
fonte
3

Na verdade não. Internamente, a passagem por referência é realizada essencialmente passando o endereço do objeto referenciado. Portanto, não há realmente nenhum ganho de eficiência ao passar um ponteiro.

Passar por referência tem um benefício, no entanto. É garantido que você tenha uma instância de qualquer objeto / tipo que esteja sendo passado. Se você passar um ponteiro, corre o risco de receber um ponteiro NULL. Usando a passagem por referência, você está enviando um NULL check implícito um nível acima para o chamador da sua função.

Brian
fonte
1
Isso é uma vantagem e uma desvantagem. Muitas APIs usam ponteiros NULL para significar algo útil (por exemplo, NULL timespec espera para sempre, enquanto o valor significa espera por tanto tempo).
Greg Rogers
1
@Brian: Eu não quero escolher nada, mas: eu não diria que um é garantido para obter uma instância ao obter uma referência. As referências pendentes ainda são possíveis se o chamador de uma função fizer uma referência negativa a um ponteiro pendente, que o destinatário não pode conhecer.
Junid
Às vezes, você pode até obter desempenho usando referências, pois elas não precisam ter armazenamento e não têm endereços atribuídos a si mesmos. nenhum indireção é necessária.
Johannes Schaub - litb 2/08/08
Programas que contêm referências pendentes não são C ++ válidos. Portanto, sim, o código pode assumir que todas as referências são válidas.
Konrad Rudolph
2
Definitivamente, posso desreferenciar um ponteiro nulo e o compilador não será capaz de dizer ... se o compilador não puder dizer que é "C ++ inválido", é realmente inválido?
Rmeador 02/12/08