Eu sei que as referências são açúcar sintático, então o código é mais fácil de ler e escrever.
Mas quais são as diferenças?
int &x = *(int*)0;
no gcc. Referência pode realmente apontar para NULL.
Eu sei que as referências são açúcar sintático, então o código é mais fácil de ler e escrever.
Mas quais são as diferenças?
int &x = *(int*)0;
no gcc. Referência pode realmente apontar para NULL.
Respostas:
Um ponteiro pode ser redesignado:
int x = 5;
int y = 6;
int *p;
p = &x;
p = &y;
*p = 10;
assert(x == 5);
assert(y == 10);
Uma referência não pode e deve ser atribuída na inicialização:
int x = 5;
int y = 6;
int &r = x;
Um ponteiro possui seu próprio endereço de memória e tamanho na pilha (4 bytes em x86), enquanto uma referência compartilha o mesmo endereço de memória (com a variável original), mas também ocupa algum espaço na pilha. Como uma referência tem o mesmo endereço da variável original, é seguro pensar em uma referência como outro nome para a mesma variável. Nota: O que um ponteiro aponta para pode estar na pilha ou na pilha. Idem uma referência. Minha afirmação nesta declaração não é que um ponteiro deva apontar para a pilha. Um ponteiro é apenas uma variável que contém um endereço de memória. Esta variável está na pilha. Como uma referência tem seu próprio espaço na pilha e como o endereço é o mesmo que a variável que ela faz referência. Mais sobre stack vs heap. Isso implica que existe um endereço real de uma referência que o compilador não informará.
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2);
Você pode ter ponteiros para ponteiros, oferecendo níveis extras de indireção. Enquanto as referências oferecem apenas um nível de indireção.
int x = 0;
int y = 0;
int *p = &x;
int *q = &y;
int **pp = &p;
pp = &q;//*pp = q
**pp = 4;
assert(y == 4);
assert(x == 0);
Um ponteiro pode ser atribuído nullptr
diretamente, enquanto a referência não. Se você se esforçar o suficiente, e você souber como, poderá fazer o endereço de uma referência nullptr
. Da mesma forma, se você se esforçar o suficiente, poderá ter uma referência a um ponteiro e essa referência poderá conter nullptr
.
int *p = nullptr;
int &r = nullptr; <--- compiling error
int &r = *p; <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
Os ponteiros podem iterar sobre uma matriz; você pode usar ++
para ir para o próximo item que um ponteiro está apontando e + 4
para o quinto elemento. Não importa qual o tamanho do objeto para o qual o ponteiro aponta.
Um ponteiro precisa ser desreferenciado *
para acessar o local da memória para o qual aponta, enquanto uma referência pode ser usada diretamente. Um ponteiro para uma classe / estrutura usa ->
para acessar seus membros, enquanto uma referência usa a .
.
As referências não podem ser inseridas em uma matriz, enquanto os ponteiros podem ser (Mencionado pelo usuário @litb)
As referências Const podem ser vinculadas a temporários. Os ponteiros não podem (não sem algum indireção):
const int &x = int(12); //legal C++
int *y = &int(12); //illegal to dereference a temporary.
Isso torna const&
mais seguro o uso em listas de argumentos e assim por diante.
Uma referência pode ser pensada como um ponteiro constante (não deve ser confundido com um ponteiro para um valor constante!) Com indireção automática, ou seja, o compilador aplicará o *
operador para você.
Todas as referências devem ser inicializadas com um valor não nulo ou a compilação falhará. Não é possível obter o endereço de uma referência - o operador de endereço retornará o endereço do valor referenciado - nem é possível fazer aritmética nas referências.
Os programadores de C podem não gostar das referências do C ++, pois não será mais óbvio quando a indireção acontecer ou se um argumento for passado por valor ou por ponteiro sem observar as assinaturas das funções.
Os programadores de C ++ podem não gostar de usar ponteiros, pois são considerados inseguros - embora as referências não sejam realmente mais seguras que os ponteiros constantes, exceto nos casos mais triviais - não têm a conveniência do indireto automático e carregam uma conotação semântica diferente.
Considere a seguinte declaração nas Perguntas frequentes sobre C ++ :
Mesmo que uma referência seja frequentemente implementada usando um endereço na linguagem assembly subjacente, não pense em uma referência como um ponteiro de aparência engraçada para um objeto. Uma referência é o objeto. Não é um ponteiro para o objeto, nem uma cópia do objeto. Ele é o objeto.
Mas se uma referência realmente fosse o objeto, como poderia haver referências pendentes? Em linguagens não gerenciadas, é impossível que as referências sejam mais "seguras" que os ponteiros - geralmente não existe uma maneira de aliasizar valores de maneira confiável através dos limites do escopo!
Vindo de um background C, as referências C ++ podem parecer um conceito um pouco tolo, mas ainda é possível usá-las em vez de ponteiros, sempre que possível: Indirecionamento automático é conveniente, e as referências se tornam especialmente úteis ao lidar com RAII - mas não por causa de qualquer segurança percebida vantagem, mas porque eles tornam a escrita de código idiomático menos incômoda.
RAII é um dos conceitos centrais do C ++, mas interage de maneira não trivial com a semântica de cópia. A passagem de objetos por referência evita esses problemas, pois não há cópia envolvida. Se não houvesse referências no idioma, seria necessário usar ponteiros, que são mais difíceis de usar, violando o princípio de design do idioma de que a solução de melhores práticas deve ser mais fácil do que as alternativas.
Se você quer ser realmente pedante, há uma coisa que você pode fazer com uma referência que você não pode fazer com um ponteiro: prolongar a vida útil de um objeto temporário. No C ++, se você vincular uma referência const a um objeto temporário, a vida útil desse objeto se tornará a vida útil da referência.
std::string s1 = "123";
std::string s2 = "456";
std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;
Neste exemplo, s3_copy copia o objeto temporário resultante da concatenação. Enquanto s3_reference em essência se torna o objeto temporário. É realmente uma referência a um objeto temporário que agora tem a mesma vida útil que a referência.
Se você tentar isso sem o, const
ele deverá falhar na compilação. Você não pode vincular uma referência não-const a um objeto temporário, nem pode levar seu endereço para esse assunto.
const &
ligação e somente quando a referência fica fora do escopo é chamado o destruidor do tipo real referenciado (em comparação com o tipo de referência, que pode ser uma base). Como é uma referência, nenhuma fatia ocorrerá no meio.
Animal x = fast ? getHare() : getTortoise()
então x
enfrentará o problema clássico de fatia, enquanto Animal& x = ...
funcionará corretamente.
Além do açúcar sintático, uma referência é um const
ponteiro ( não um ponteiro para a const
). Você deve estabelecer a que se refere quando declara a variável de referência e não pode alterá-la posteriormente.
Atualização: agora que penso um pouco mais, há uma diferença importante.
O alvo de um ponteiro const pode ser substituído pegando seu endereço e usando uma conversão constante.
O destino de uma referência não pode ser substituído de forma alguma abaixo do UB.
Isso deve permitir que o compilador faça mais otimização em uma referência.
T* const
açúcar sintático diferente (que acaba eliminando muito * e & do seu código).
int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
está OK.
Ao contrário da opinião popular, é possível ter uma referência que seja NULL.
int * p = NULL;
int & r = *p;
r = 1; // crash! (if you're lucky)
É verdade que é muito mais difícil fazer uma referência - mas, se você conseguir, vai arrancar o cabelo tentando encontrá-lo. As referências não são inerentemente seguras no C ++!
Tecnicamente, essa é uma referência inválida , não uma referência nula. O C ++ não suporta referências nulas como conceito, como você pode encontrar em outros idiomas. Existem outros tipos de referências inválidas também. Qualquer referência inválida aumenta o espectro de comportamento indefinido , assim como o faria com um ponteiro inválido.
O erro real está na desreferenciação do ponteiro NULL, antes da atribuição a uma referência. Mas não conheço nenhum compilador que gere erros nessa condição - o erro se propaga para um ponto mais adiante no código. É isso que torna esse problema tão insidioso. Na maioria das vezes, se você derereferenciar um ponteiro NULL, você trava naquele local e não é preciso muita depuração para descobrir isso.
Meu exemplo acima é curto e artificial. Aqui está um exemplo mais real.
class MyClass
{
...
virtual void DoSomething(int,int,int,int,int);
};
void Foo(const MyClass & bar)
{
...
bar.DoSomething(i1,i2,i3,i4,i5); // crash occurs here due to memory access violation - obvious why?
}
MyClass * GetInstance()
{
if (somecondition)
return NULL;
...
}
MyClass * p = GetInstance();
Foo(*p);
Quero reiterar que a única maneira de obter uma referência nula é através de código malformado e, quando você o obtém, está obtendo um comportamento indefinido. Ele não faz sentido para verificar se há uma referência nula; por exemplo, você pode tentar, if(&bar==NULL)...
mas o compilador pode otimizar a declaração para não existir! Uma referência válida nunca pode ser NULL; portanto, na visão do compilador, a comparação é sempre falsa e é livre para eliminar oif
cláusula como código morto - essa é a essência do comportamento indefinido.
A maneira correta de evitar problemas é evitar desreferenciar um ponteiro NULL para criar uma referência. Aqui está uma maneira automatizada de fazer isso.
template<typename T>
T& deref(T* p)
{
if (p == NULL)
throw std::invalid_argument(std::string("NULL reference"));
return *p;
}
MyClass * p = GetInstance();
Foo(deref(p));
Para uma análise mais antiga desse problema de alguém com melhores habilidades de escrita, consulte Referências nulas de Jim Hyslop e Herb Sutter.
Para outro exemplo dos perigos de desreferenciar um ponteiro nulo, consulte Expondo comportamento indefinido ao tentar portar código para outra plataforma por Raymond Chen.
Você esqueceu a parte mais importante:
acesso por membro com ponteiros usa ->
acesso por membro com referências.
foo.bar
é claramente superior ao foo->bar
da mesma maneira que vi é claramente superior ao Emacs :-)
->
fornece referências a ponteiros, assim como o próprio ponteiro.
.
e ->
tem algo a ver com o vi e emacs :)
.
é melhor do que usar ->
, mas como vi vs Emacs, é inteiramente subjetiva e você não pode provar nada
As referências são muito semelhantes aos ponteiros, mas são criadas especificamente para ajudar na otimização de compiladores.
Como um exemplo:
void maybeModify(int& x); // may modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// This function is designed to do something particularly troublesome
// for optimizers. It will constantly call maybeModify on array[0] while
// adding array[1] to array[2]..array[size-1]. There's no real reason to
// do this, other than to demonstrate the power of references.
for (int i = 2; i < (int)size; i++) {
maybeModify(array[0]);
array[i] += array[1];
}
}
Um compilador otimizador pode perceber que estamos acessando um monte de [0] e [1]. Adoraria otimizar o algoritmo para:
void hurtTheCompilersOptimizer(short size, int array[])
{
// Do the same thing as above, but instead of accessing array[1]
// all the time, access it once and store the result in a register,
// which is much faster to do arithmetic with.
register int a0 = a[0];
register int a1 = a[1]; // access a[1] once
for (int i = 2; i < (int)size; i++) {
maybeModify(a0); // Give maybeModify a reference to a register
array[i] += a1; // Use the saved register value over and over
}
a[0] = a0; // Store the modified a[0] back into the array
}
Para fazer essa otimização, é necessário provar que nada pode mudar de matriz [1] durante a chamada. Isso é bastante fácil de fazer. i nunca é menor que 2, portanto, o array [i] nunca pode se referir ao array [1]. maybeModify () recebe a0 como referência (aliasing array [0]). Como não há aritmética de "referência", o compilador apenas precisa provar que talvezModify nunca obtém o endereço de x, e provou que nada muda de matriz [1].
Ele também tem que provar que não há como uma chamada futura ler / gravar um [0] enquanto tivermos uma cópia temporária do registro em a0. Isso costuma ser trivial para provar, porque em muitos casos é óbvio que a referência nunca é armazenada em uma estrutura permanente como uma instância de classe.
Agora faça o mesmo com ponteiros
void maybeModify(int* x); // May modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// Same operation, only now with pointers, making the
// optimization trickier.
for (int i = 2; i < (int)size; i++) {
maybeModify(&(array[0]));
array[i] += array[1];
}
}
O comportamento é o mesmo; só que agora é muito mais difícil provar que o talvezModify nunca modifica o array [1], porque já o indicamos; O gato está fora da bolsa. Agora ele tem que fazer a prova muito mais difícil: uma análise estática do maybeModify para provar que nunca grava em & x + 1. Ele também tem que provar que nunca salva um ponteiro que possa se referir ao array [0], que é apenas tão complicado.
Os compiladores modernos estão cada vez melhores na análise estática, mas é sempre bom ajudá-los e usar referências.
É claro que, exceto por otimizações inteligentes, os compiladores realmente transformarão referências em indicadores quando necessário.
EDIT: Cinco anos após postar esta resposta, encontrei uma diferença técnica real em que as referências são diferentes do que apenas uma maneira diferente de ver o mesmo conceito de endereçamento. As referências podem modificar a vida útil dos objetos temporários de uma maneira que os ponteiros não podem.
F createF(int argument);
void extending()
{
const F& ref = createF(5);
std::cout << ref.getArgument() << std::endl;
};
Objetos normalmente temporários, como o criado pela chamada para, createF(5)
são destruídos no final da expressão. No entanto, vinculando esse objeto a uma referência, o ref
C ++ estenderá a vida útil desse objeto temporário até ref
ficar fora do escopo.
maybeModify
não leva o endereço de qualquer coisa relacionada x
é substancialmente mais fácil do que provar que um monte de aritmética de ponteiro não ocorre.
void maybeModify(int& x) { 1[&x]++; }
que os outros comentários acima estão discutindo
Na verdade, uma referência não é realmente como um ponteiro.
Um compilador mantém "referências" a variáveis, associando um nome a um endereço de memória; esse é o trabalho de traduzir qualquer nome de variável para um endereço de memória durante a compilação.
Ao criar uma referência, você só diz ao compilador que atribui outro nome à variável de ponteiro; é por isso que as referências não podem "apontar para nulo", porque uma variável não pode ser e não ser.
Ponteiros são variáveis; eles contêm o endereço de alguma outra variável ou podem ser nulos. O importante é que um ponteiro tenha um valor, enquanto uma referência possui apenas uma variável à qual está fazendo referência.
Agora, alguma explicação do código real:
int a = 0;
int& b = a;
Aqui você não está criando outra variável que aponta para a
; você está apenas adicionando outro nome ao conteúdo da memória que contém o valor de a
. Esta memória tem agora dois nomes, a
e b
, e que pode ser dirigida usando o nome.
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
Ao chamar uma função, o compilador geralmente gera espaços de memória para os argumentos a serem copiados. A assinatura da função define os espaços que devem ser criados e fornece o nome que deve ser usado para esses espaços. Declarar um parâmetro como referência apenas informa ao compilador para usar o espaço de memória variável de entrada em vez de alocar um novo espaço de memória durante a chamada do método. Pode parecer estranho dizer que sua função estará manipulando diretamente uma variável declarada no escopo de chamada, mas lembre-se de que, ao executar código compilado, não há mais escopo; há apenas memória simples e seu código de função pode manipular qualquer variável.
Agora, pode haver alguns casos em que seu compilador pode não ser capaz de conhecer a referência ao compilar, como ao usar uma variável externa. Portanto, uma referência pode ou não ser implementada como um ponteiro no código subjacente. Mas nos exemplos que eu lhe dei, provavelmente não será implementado com um ponteiro.
Uma referência nunca pode ser NULL
.
void Foo::bar() { virtual_baz(); }
esse segfaults. Se você não estiver ciente de que as referências podem ser nulas, não será possível rastrear o nulo até sua origem.
int &r=*p;
é um comportamento indefinido. Nesse ponto, você não tem um "apontar referência a NULL," você tem um programa que já não pode ser fundamentado sobre a todos .
Enquanto referências e ponteiros são usados para acessar indiretamente outro valor, há duas diferenças importantes entre referências e ponteiros. A primeira é que uma referência sempre se refere a um objeto: é um erro definir uma referência sem inicializá-la. O comportamento da atribuição é a segunda diferença importante: atribuir a uma referência altera o objeto ao qual a referência está vinculada; não religa a referência a outro objeto. Uma vez inicializada, uma referência sempre se refere ao mesmo objeto subjacente.
Considere esses dois fragmentos de programa. No primeiro, atribuímos um ponteiro para outro:
int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2; // pi now points to ival2
Após a atribuição, ival, o objeto endereçado por pi permanece inalterado. A atribuição altera o valor de pi, fazendo com que aponte para um objeto diferente. Agora considere um programa semelhante que atribui duas referências:
int &ri = ival, &ri2 = ival2;
ri = ri2; // assigns ival2 to ival
Essa atribuição altera ival, o valor referenciado por ri, e não a própria referência. Após a atribuição, as duas referências ainda se referem aos seus objetos originais, e o valor desses objetos agora também é o mesmo.
Há uma diferença semântica que pode parecer esotérica se você não estiver familiarizado com o estudo de linguagens de computador de maneira abstrata ou mesmo acadêmica.
No nível mais alto, a idéia das referências é que elas sejam "aliases" transparentes. Seu computador pode usar um endereço para fazê-los funcionar, mas você não deve se preocupar com isso: você deve pensar neles como "apenas outro nome" para um objeto existente e a sintaxe reflete isso. Eles são mais rígidos que os ponteiros, de modo que seu compilador poderá avisá-lo com mais segurança quando você estiver prestes a criar uma referência de oscilação, do que quando estiver prestes a criar um ponteiro de oscilação.
Além disso, existem algumas diferenças práticas entre ponteiros e referências. A sintaxe para usá-los é obviamente diferente e você não pode "recolocar" as referências, ter referências ao nada ou ter ponteiros para as referências.
Uma referência é um alias para outra variável, enquanto um ponteiro mantém o endereço de memória de uma variável. As referências são geralmente usadas como parâmetros de função, para que o objeto passado não seja a cópia, mas o próprio objeto.
void fun(int &a, int &b); // A common usage of references.
int a = 0;
int &b = a; // b is an alias for a. Not so common to use.
Não importa quanto espaço consuma, pois você não pode ver nenhum efeito colateral (sem executar o código) do espaço que ocuparia.
Por outro lado, uma grande diferença entre referências e ponteiros é que os temporários atribuídos às referências const permanecem vivos até que a referência const fique fora do escopo.
Por exemplo:
class scope_test
{
public:
~scope_test() { printf("scope_test done!\n"); }
};
...
{
const scope_test &test= scope_test();
printf("in scope\n");
}
irá imprimir:
in scope
scope_test done!
Esse é o mecanismo de linguagem que permite que o ScopeGuard funcione.
Isso é baseado no tutorial . O que está escrito torna mais claro:
>>> The address that locates a variable within memory is
what we call a reference to that variable. (5th paragraph at page 63)
>>> The variable that stores the reference to another
variable is what we call a pointer. (3rd paragraph at page 64)
Simplesmente lembrar que,
>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)
Além disso, como podemos nos referir a quase qualquer tutorial de ponteiro, um ponteiro é um objeto suportado pela aritmética de ponteiros, o que torna o ponteiro semelhante a uma matriz.
Veja a seguinte declaração,
int Tom(0);
int & alias_Tom = Tom;
alias_Tom
pode ser entendido como um alias of a variable
(diferente de typedef
, o que é alias of a type
) Tom
. Também é bom esquecer que a terminologia de tal afirmação é criar uma referência de Tom
.
nullptr
? Você já leu alguma outra parte deste tópico ou ...?
Uma referência não é outro nome dado a alguma memória. É um ponteiro imutável que é automaticamente referenciado no uso. Basicamente, tudo se resume a:
int& j = i;
Torna-se internamente
int* const j = &i;
const
ponteiro. Essa flexibilidade não prova que há uma diferença entre uma referência e um ponteiro.
O que é uma referência em C ++? Alguma instância específica do tipo que não é um tipo de objeto .
O que é um ponteiro em C ++? Alguma instância específica do tipo que é um tipo de objeto .
Na definição ISO C ++ do tipo de objeto :
Um objecto tipo é um (possivelmente cv -qualified) tipo que não é um tipo de função e não um tipo de referência, e não cv vazio.
Pode ser importante saber, o tipo de objeto é uma categoria de nível superior do universo de tipos em C ++. A referência também é uma categoria de nível superior. Mas ponteiro não é.
Ponteiros e referências são mencionados juntos no contexto do tipo composto . Isso se deve basicamente à natureza da sintaxe do declarador herdada de (e estendida) C, que não possui referências. (Além disso, há mais de um tipo de declarador de referências desde C ++ 11, enquanto os ponteiros ainda são "unificados": &
+ &&
vs *
..) Portanto, esboçar uma linguagem específica por "extensão" com estilo semelhante de C nesse contexto é algo razoável . (Eu ainda argumentam que a sintaxe de declarators resíduos a expressividade sintática muito , faz com que ambos os usuários humanos e implementações frustrante. Assim, todos eles não são qualificados para ser built-in em um novo design linguagem. Este é um tema totalmente diferente sobre o design do PL, no entanto.)
Caso contrário, é insignificante que os ponteiros possam ser qualificados como tipos específicos de tipos com referências juntos. Eles simplesmente compartilham poucas propriedades comuns além da semelhança de sintaxe, portanto, não há necessidade de reuni-las na maioria dos casos.
Observe que as instruções acima mencionam apenas "ponteiros" e "referências" como tipos. Há algumas perguntas interessadas sobre suas instâncias (como variáveis). Também existem muitos equívocos.
As diferenças das categorias de nível superior já podem revelar muitas diferenças concretas não vinculadas diretamente aos ponteiros:
cv
qualificadores de nível superior . Referências não podem.Mais algumas regras especiais sobre referências:
&&
parâmetros (como as "referências de encaminhamento") baseadas no recolhimento de referência durante a dedução de parâmetro do modelo permitem o "encaminhamento perfeito" dos parâmetros.std::initializer_list
seguem algumas regras semelhantes de extensão da vida útil de referência. É outra lata de vermes.Eu sei que as referências são açúcar sintático, então o código é mais fácil de ler e escrever.
Tecnicamente, isso está totalmente errado. As referências não são um açúcar sintático de nenhum outro recurso no C ++, porque não podem ser substituídas exatamente por outros recursos sem diferenças semânticas.
(Da mesma forma, as expressões lambda s não são um açúcar sintático de nenhum outro recurso no C ++ porque não pode ser simulado com propriedades "não especificadas", como a ordem de declaração das variáveis capturadas , o que pode ser importante porque a ordem de inicialização dessas variáveis pode ser significativo.)
O C ++ possui apenas alguns tipos de açúcares sintáticos nesse sentido estrito. Uma instância é (herdada de C) o operador interno (sem sobrecarga) []
, que é definido exatamente com as mesmas propriedades semânticas de formas específicas de combinação sobre o operador interno unário *
e binário+
.
Portanto, um ponteiro e uma referência usam a mesma quantidade de memória.
A afirmação acima está simplesmente errada. Para evitar tais conceitos errôneos, observe as regras ISO C ++:
De [intro.object] / 1 :
... Um objeto ocupa uma região de armazenamento em seu período de construção, durante sua vida útil e em seu período de destruição. ...
De [dcl.ref] / 4 :
Não é especificado se uma referência requer ou não armazenamento.
Observe que essas são propriedades semânticas .
Mesmo que os ponteiros não sejam qualificados o suficiente para serem reunidos com referências no sentido do design da linguagem, ainda existem alguns argumentos que tornam discutível a escolha entre eles em outros contextos, por exemplo, ao fazer escolhas nos tipos de parâmetros.
Mas essa não é a história toda. Quero dizer, há mais coisas do que indicadores versus referências que você deve considerar.
Se você não precisa se ater a essas escolhas superespecíficas, na maioria dos casos a resposta é curta: você não precisa usar ponteiros, portanto não precisa . Os ponteiros geralmente são ruins o suficiente porque implicam muitas coisas que você não espera e dependerão de muitas suposições implícitas que comprometem a capacidade de manutenção e (até) a portabilidade do código. Contar desnecessariamente com ponteiros é definitivamente um estilo ruim e deve ser evitado no sentido de C ++ moderno. Reconsidere seu objetivo e você finalmente descobrirá que o ponteiro é o recurso dos últimos tipos na maioria dos casos.
&
tipo de referência como o 1º tipo de parâmetro. (E geralmente deve ser const
qualificado.)&&
tipo de referência como o 1º tipo de parâmetro. (E geralmente não deve haver qualificadores.)operator=
como funções membro especiais requer tipos de referência semelhantes ao 1º parâmetro dos construtores de copiar / mover.++
requer fictício int
.unique_ptr
e shared_ptr
(ou mesmo os de homebrew, se você precisar que sejam opacos ), em vez de ponteiros brutos.std::optional
, em vez de ponteiros brutos.observer_ptr
no TS Fundamental da Biblioteca.As únicas exceções não podem ser contornadas no idioma atual:
operator new
. (No entanto, cv - void*
ainda é bem diferente e mais seguro em comparação com os ponteiros de objetos comuns, porque exclui aritmética inesperada de ponteiros, a menos que você esteja confiando em alguma extensão não conforme, void*
como a GNU.Portanto, na prática, a resposta é tão óbvia: em caso de dúvida, evite apontadores . Você precisa usar ponteiros apenas quando houver motivos muito explícitos de que nada mais é mais apropriado. Exceto alguns casos excepcionais mencionados acima, essas opções quase sempre não são puramente específicas de C ++ (mas provavelmente específicas de implementação de linguagem). Tais instâncias podem ser:
Se você vir a pergunta por meio de algum resultado de pesquisa do Google (não específico para C ++) , é muito provável que este seja o lugar errado.
Referências em C ++ é muito "estranho", pois não é essencialmente de primeira classe: eles serão tratados como os objetos ou as funções a ser referido para que eles não têm chance de apoiar algumas operações de primeira classe como estar operando esquerdo do operador de acesso de membro independentemente ao tipo do objeto referido. Outros idiomas podem ou não ter restrições semelhantes em suas referências.
As referências em C ++ provavelmente não preservarão o significado em diferentes idiomas. Por exemplo, as referências em geral não implicam propriedades não nulas em valores como eles em C ++, portanto, essas suposições podem não funcionar em outras linguagens (e você encontrará contra-exemplos com bastante facilidade, por exemplo, Java, C #, ...).
Ainda pode haver algumas propriedades comuns entre referências em diferentes linguagens de programação em geral, mas vamos deixar para outras questões no SO.
(Uma observação lateral: a questão pode ser significativa mais cedo do que qualquer linguagem "C", como ALGOL 68 vs. PL / I. )
Uma referência a um ponteiro é possível em C ++, mas o inverso não é possível significa que um ponteiro para uma referência não é possível. Uma referência a um ponteiro fornece uma sintaxe mais limpa para modificar o ponteiro. Veja este exemplo:
#include<iostream>
using namespace std;
void swap(char * &str1, char * &str2)
{
char *temp = str1;
str1 = str2;
str2 = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap(str1, str2);
cout<<"str1 is "<<str1<<endl;
cout<<"str2 is "<<str2<<endl;
return 0;
}
E considere a versão C do programa acima. Em C, você precisa usar ponteiro para ponteiro (indireto múltiplo), e isso gera confusão e o programa pode parecer complicado.
#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
char *temp = *str1_ptr;
*str1_ptr = *str2_ptr;
*str2_ptr = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap1(&str1, &str2);
printf("str1 is %s, str2 is %s", str1, str2);
return 0;
}
Visite o seguinte para obter mais informações sobre referência ao ponteiro:
Como eu disse, um ponteiro para uma referência não é possível. Tente o seguinte programa:
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int *ptr = &x;
int &*ptr1 = ptr;
}
Eu uso referências, a menos que eu precise de um destes:
Ponteiros nulos podem ser usados como um valor sentinela, geralmente uma maneira barata de evitar sobrecarga de função ou uso de um bool.
Você pode fazer aritmética em um ponteiro. Por exemplo,p += offset;
&r + offset
onde r
foi declarado como referência
Há uma diferença fundamental entre ponteiros e referências que eu não vi ninguém mencionar: as referências ativam a semântica de passagem por referência nos argumentos das funções. Os ponteiros, embora inicialmente não sejam visíveis, não fornecem: eles fornecem apenas semânticas de passagem por valor. Isso foi muito bem descrito neste artigo .
Atenciosamente, & rzej
Correndo o risco de aumentar a confusão, quero incluir alguma entrada, tenho certeza de que depende principalmente de como o compilador implementa as referências, mas no caso do gcc, a ideia de que uma referência pode apontar apenas para uma variável na pilha não está realmente correto, veja isto por exemplo:
#include <iostream>
int main(int argc, char** argv) {
// Create a string on the heap
std::string *str_ptr = new std::string("THIS IS A STRING");
// Dereference the string on the heap, and assign it to the reference
std::string &str_ref = *str_ptr;
// Not even a compiler warning! At least with gcc
// Now lets try to print it's value!
std::cout << str_ref << std::endl;
// It works! Now lets print and compare actual memory addresses
std::cout << str_ptr << " : " << &str_ref << std::endl;
// Exactly the same, now remember to free the memory on the heap
delete str_ptr;
}
O que gera isso:
THIS IS A STRING
0xbb2070 : 0xbb2070
Se você notar que mesmo os endereços de memória são exatamente os mesmos, significa que a referência está apontando com êxito para uma variável na pilha! Agora, se você realmente quer ficar esquisito, isso também funciona:
int main(int argc, char** argv) {
// In the actual new declaration let immediately de-reference and assign it to the reference
std::string &str_ref = *(new std::string("THIS IS A STRING"));
// Once again, it works! (at least in gcc)
std::cout << str_ref;
// Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
delete &str_ref;
/*And, it works, because we are taking the memory address that the reference is
storing, and deleting it, which is all a pointer is doing, just we have to specify
the address with '&' whereas a pointer does that implicitly, this is sort of like
calling delete &(*str_ptr); (which also compiles and runs fine).*/
}
O que gera isso:
THIS IS A STRING
Portanto, uma referência é um ponteiro sob o capô, ambos estão apenas armazenando um endereço de memória, onde o endereço está apontando é irrelevante, o que você acha que aconteceria se eu chamasse std :: cout << str_ref; DEPOIS de chamar delete & str_ref? Bem, obviamente ele compila bem, mas causa uma falha de segmentação no tempo de execução porque não está mais apontando para uma variável válida, temos essencialmente uma referência quebrada que ainda existe (até cair fora do escopo), mas é inútil.
Em outras palavras, uma referência não passa de um ponteiro que abstrai a mecânica do ponteiro, tornando-o mais seguro e fácil de usar (sem matemática acidental do ponteiro, sem misturar '.' E '->' etc.), supondo que você não tente nenhuma bobagem como os meus exemplos acima;)
Agora, independentemente de como um compilador lida com referências, ele sempre terá algum tipo de ponteiro sob o capô, porque uma referência deve se referir a uma variável específica em um endereço de memória específico para que funcione conforme o esperado, não há como contornar isso (portanto, o termo 'referência').
A única regra importante que é importante lembrar com referências é que elas devem ser definidas no momento da declaração (com exceção de uma referência em um cabeçalho, nesse caso, ela deve ser definida no construtor, depois que o objeto em que estiver contido for construído é tarde demais para defini-lo).
Lembre-se, meus exemplos acima são apenas isso, exemplos demonstrando o que é uma referência, você nunca iria querer usar uma referência dessa maneira! Para o uso adequado de uma referência, já existem muitas respostas aqui que atingiram a unha na cabeça
Outra diferença é que você pode ter ponteiros para um tipo de void (e isso significa ponteiro para qualquer coisa), mas as referências a void são proibidas.
int a;
void * p = &a; // ok
void & p = a; // forbidden
Não posso dizer que estou realmente feliz com essa diferença em particular. Eu preferiria que isso fosse permitido com a referência de significado para qualquer coisa com um endereço e, caso contrário, o mesmo comportamento para referências. Isso permitiria definir alguns equivalentes de funções da biblioteca C, como memcpy, usando referências.
Além disso, uma referência que é um parâmetro para uma função embutida pode ser manipulada de maneira diferente de um ponteiro.
void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
int testptr=0;
increment(&testptr);
}
void increftest()
{
int testref=0;
increment(testref);
}
Muitos compiladores ao alinhar a versão do ponteiro forçarão uma gravação na memória (estamos pegando o endereço explicitamente). No entanto, eles deixarão a referência em um registro mais ideal.
Obviamente, para funções que não estão embutidas, o ponteiro e a referência geram o mesmo código e é sempre melhor passar intrínsecos por valor do que por referência, se não forem modificados e retornados pela função.
Outro uso interessante de referências é fornecer um argumento padrão de um tipo definido pelo usuário:
class UDT
{
public:
UDT() : val_d(33) {};
UDT(int val) : val_d(val) {};
virtual ~UDT() {};
private:
int val_d;
};
class UDT_Derived : public UDT
{
public:
UDT_Derived() : UDT() {};
virtual ~UDT_Derived() {};
};
class Behavior
{
public:
Behavior(
const UDT &udt = UDT()
) {};
};
int main()
{
Behavior b; // take default
UDT u(88);
Behavior c(u);
UDT_Derived ud;
Behavior d(ud);
return 1;
}
O sabor padrão usa o 'bind const reference a um temporário' aspecto das referências.
Este programa pode ajudar a compreender a resposta da pergunta. Este é um programa simples de uma referência "j" e um ponteiro "ptr" apontando para a variável "x".
#include<iostream>
using namespace std;
int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"
cout << "x=" << x << endl;
cout << "&x=" << &x << endl;
cout << "j=" << j << endl;
cout << "&j=" << &j << endl;
cout << "*ptr=" << *ptr << endl;
cout << "ptr=" << ptr << endl;
cout << "&ptr=" << &ptr << endl;
getch();
}
Execute o programa e dê uma olhada na saída e você entenderá.
Além disso, reserve 10 minutos e assista a este vídeo: https://www.youtube.com/watch?v=rlJrrGV0iOg
Sinto que ainda há outro ponto que não foi abordado aqui.
Diferentemente dos ponteiros, as referências são sintaticamente equivalentes ao objeto a que se referem, ou seja, qualquer operação que possa ser aplicada a um objeto funciona para uma referência e com a mesma sintaxe exata (a exceção é, obviamente, a inicialização).
Embora isso possa parecer superficial, acredito que essa propriedade seja crucial para vários recursos do C ++, por exemplo:
Templates . Desde parâmetros do modelo são digitados-pato, propriedades sintáticas de um tipo é tudo o que importa, por isso, muitas vezes o mesmo modelo pode ser usado tanto com T
e T&
.
(ou std::reference_wrapper<T>
que ainda depende de uma conversão implícita para T&
)
Modelos que abrangem ambos T&
e T&&
são ainda mais comuns.
Valores . Considere a declaração str[0] = 'X';
Sem referências, isso só funcionaria para c-strings ( char* str
). Retornar o caractere por referência permite que as classes definidas pelo usuário tenham a mesma notação.
Copie construtores . Sintaticamente, faz sentido passar objetos para copiar construtores, e não ponteiros para objetos. Mas simplesmente não há como um construtor de cópia pegar um objeto por valor - isso resultaria em uma chamada recursiva para o mesmo construtor de cópia. Isso deixa as referências como a única opção aqui.
Sobrecargas do operador . Com referências, é possível introduzir indiretamente uma chamada de operador - por exemplo, operator+(const T& a, const T& b)
mantendo a mesma notação de infixo. Isso também funciona para funções sobrecarregadas regulares.
Esses pontos fortalecem uma parte considerável do C ++ e da biblioteca padrão, portanto, essa é uma propriedade bastante importante das referências.
Há uma diferença não técnica muito importante entre ponteiros e referências: um argumento passado para uma função por ponteiro é muito mais visível do que um argumento passado para uma função por referência não const. Por exemplo:
void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);
void bar() {
std::string x;
fn1(x); // Cannot modify x
fn2(x); // Cannot modify x (without const_cast)
fn3(x); // CAN modify x!
fn4(&x); // Can modify x (but is obvious about it)
}
De volta a C, uma chamada que parece fn(x)
apenas pode ser transmitida por valor; portanto, definitivamente não pode ser modificada x
; Para modificar um argumento, você precisaria passar um ponteiro fn(&x)
. Portanto, se um argumento não fosse precedido por um, &
você sabia que não seria modificado. (O inverso, &
significa que modificado, não era verdadeiro porque às vezes você precisava passar grandes estruturas somente leitura por const
ponteiro.)
Alguns argumentam que esse é um recurso tão útil na leitura de código, que os parâmetros do ponteiro sempre devem ser usados para parâmetros modificáveis, em vez de não const
referências, mesmo que a função nunca espere a nullptr
. Ou seja, essas pessoas argumentam que assinaturas de funções como fn3()
acima não devem ser permitidas. As diretrizes de estilo C ++ do Google são um exemplo disso.
Talvez algumas metáforas ajudem; No contexto do espaço na tela da sua área de trabalho -
Um ponteiro pode ser inicializado como 0 e uma referência não. De fato, uma referência também deve se referir a um objeto, mas um ponteiro pode ser o ponteiro nulo:
int* p = 0;
Mas não podemos ter int& p = 0;
e também int& p=5 ;
.
De fato, para fazê-lo corretamente, devemos ter declarado e definido um objeto no início, para que possamos fazer uma referência a esse objeto, para que a implementação correta do código anterior seja:
Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;
Outro ponto importante é que podemos fazer a declaração do ponteiro sem inicialização, no entanto, nada pode ser feito no caso de referência, que deve sempre fazer referência a variável ou objeto. No entanto, esse uso de um ponteiro é arriscado; portanto, geralmente verificamos se o ponteiro está realmente apontando para algo ou não. No caso de uma referência, essa verificação não é necessária, porque já sabemos que a referência a um objeto durante a declaração é obrigatória.
Outra diferença é que o ponteiro pode apontar para outro objeto, no entanto, a referência sempre faz referência ao mesmo objeto, vamos dar este exemplo:
Int a = 6, b = 5;
Int& rf = a;
Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.
rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased
Outro ponto: quando temos um modelo como um modelo STL, esse tipo de modelo sempre retorna uma referência, não um ponteiro, para facilitar a leitura ou a atribuição de novo valor usando o operador []:
Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="
const int& i = 0
.
A diferença é que a variável de ponteiro não constante (que não deve ser confundida com um ponteiro em constante) pode ser alterada em algum momento durante a execução do programa, requer que a semântica de ponteiros seja usada (&, *), enquanto as referências podem ser definidas na inicialização somente (é por isso que você pode defini-los apenas na lista de inicializadores de construtores, mas não de outra forma) e usar a semântica de acesso a valores comuns. Basicamente, foram introduzidas referências para permitir o suporte a sobrecarga de operadores, como eu havia lido em algum livro muito antigo. Como alguém afirmou neste tópico - o ponteiro pode ser definido como 0 ou qualquer valor que você desejar. 0 (NULL, nullptr) significa que o ponteiro é inicializado com nada. É um erro desreferenciar o ponteiro nulo. Mas, na verdade, o ponteiro pode conter um valor que não aponte para algum local correto da memória. As referências, por sua vez, tentam não permitir que um usuário inicialize uma referência a algo que não pode ser referenciado devido ao fato de você sempre fornecer um valor de tipo correto a ele. Embora existam várias maneiras de fazer com que a variável de referência seja inicializada em um local de memória errado - é melhor não aprofundar isso em detalhes. No nível da máquina, o ponteiro e a referência funcionam de maneira uniforme - por meio de ponteiros. Digamos que em referências essenciais há açúcar sintático. As referências rvalue são diferentes disso - são naturalmente objetos de pilha / pilha. Embora existam várias maneiras de fazer com que a variável de referência seja inicializada em um local de memória errado - é melhor não aprofundar isso em detalhes. No nível da máquina, o ponteiro e a referência funcionam de maneira uniforme - por meio de ponteiros. Digamos que em referências essenciais há açúcar sintático. As referências rvalue são diferentes disso - são naturalmente objetos de pilha / pilha. Embora existam várias maneiras de fazer com que a variável de referência seja inicializada em um local de memória errado - é melhor não aprofundar isso em detalhes. No nível da máquina, o ponteiro e a referência funcionam de maneira uniforme - por meio de ponteiros. Digamos que em referências essenciais há açúcar sintático. As referências rvalue são diferentes disso - são naturalmente objetos de pilha / pilha.
em palavras simples, podemos dizer que uma referência é um nome alternativo para uma variável, enquanto um ponteiro é uma variável que contém o endereço de outra variável. por exemplo
int a = 20;
int &r = a;
r = 40; /* now the value of a is changed to 40 */
int b =20;
int *ptr;
ptr = &b; /*assigns address of b to ptr not the value */