paulo1205
(usa Ubuntu)
Enviado em 30/09/2016 - 03:34h
uNclear escreveu:
Galera alguém poderia me explicar como funciona as funçoes as quais trabalham com endereços const como por exemplo:
vectorx& vectorx::operator*(const int& n);
queria entender como isso funcionaria por baixo dos panos tipo o retorno do endereço de algum objeto vectorx, tanto quanto a entrada do endereço n na função, e o porque de coloca-ló como const.
Espero ter sido claro. Obrigado
Antes de começar a discussão, uma nota histórica introdutória. C++ se baseou originalmente na linguagem C. O C, até hoje, passa todos os argumentos de funções para dentro delas por valor (isto é, os valores dos argumentos são
copiados para dentro da função). Semelhantemente, o valor retornado por uma função também é uma cópia do valor da expressão passada ao comando
return . Se você quiser acesso aos objetos originais, tem manualmente de obter seus endereços e passar cópias desses endereços, e então usar o operador de indireção (
* ) para aceder aos objetos a partir dos endereços recebidos.
Como herdeiro do C, o C++ herdou esse comportamento de cópia de valores quando se usa sintaxe semelhante àquela do C. No entanto, o C++ introduziu também passagem por referência (em vez de cópias dos valores, passam-se os próprios objetos), através de uma sintaxe nova, não existente no C.
Você falou em “por baixo dos panos”. Eu não gosto muito desse termo, porque tem conotação de safadeza ou de corrupção. Prefiro falar em “por trás das cortinas”, que tem o sentido daquilo que acontece num teatro e que é fundamental para o bom andamento do espetáculo, mas que nem sempre aparece aos olhos do público.
Você está correto na ideia geral: quando se usam referências, o que é passado internamente de um lado para outro, quer como argumento, quer como entidade retornada, são os endereços dos objetos referidos por nome. Esse trânsito de endereços acontece apenas nos bastidores do código compilado. Para você, programador em C++, a forma de operar com objetos referenciados é como se fosse uma variável comum de um tipo que geralmente não é ponteiro.
Considere o seguinte caso.
int a=0;
int &ra=a;
int *pa=&a;
++a;
++ra;
++*pa;
As três operações de incremento operam sobre o mesmo dado, contido numa única posição de memória. O dado é originalmente designado pela variável
a , de modo que a primeira operação de incremento atua diretamente sobre a variável. Para associar o ponteiro ao dado e para operar com o valor apontado, você tem de ser explícito tanto na hora de obter o endereço do dado original quanto na de dizer que quer voltar ao dado a partir do endereço. Já com a referência, o compilador faz a implicitamente substituição da obtenção do endereço (sobre
a , na hora da declaração de
ra ) e do regresso ao dado original (penúltima linha).
Na prática, é como se a referência fosse um sinônimo exato da variável original que ela referencia.
O caso mais comum de uso de referência, no entanto, é com funções. Veja duas formas de fazer uma função para intercâmbio de valores de duas variáveis.
// As duas versões podem ter o mesmo nome porque os argumentos são diferentes!
void swap(int *v1, int *v2){
int temp=*v1;
*v1=*v2;
*v2=temp;
}
void swap(int &v1, int &v2){
int temp=v1;
v1=v2;
v2=temp;
}
int main(){
int a=1, b=2;
swap(&a, &b); // Explicitamente passo os endereços como argumentos.
// Aqui, a==2 e b==1.
swap(a, b); // O compilador se encarrega de obter endereços e passá-los.
// Agora, a==1 e b==2 novamente.
}
É bem possível que, se você examinar o código compilado das duas funções, eles sejam absolutamente idênticos, lidando internamente com endereços. Também as formas de chamar as duas funções podem produzir exatamente o mesmo Assembly, só que numa delas você tem de lidar com os endereços explicitamente, enquanto na outra o compilador faz isso por você.
Uma função que receba referências como argumentos se livra de um inconveniente que pode existir quando se trabalha com ponteiros, que é a possibilidade de receber um argumento contendo um ponteiro nulo ou inválido. Uma referência sempre estará associada a um objeto real.
void triplica(int *pi){
(*pi)*=3; // Inseguro: função pode dar pau se pi for nulo ou inválido.
}
void triplica(int &ri){
ri*=3; // Seguro (no que diz respeito à função).
}
int main(){
int a=1;
triplica(&a); // OK: eu passo um endereço de um objeto válido.
tiplica(a); // OK: referência sempre é válida.
int *pa=nullptr;
triplica(pa); // Chamada é sintaticamente válida, mas vai dar pau dentro da função!
triplica(*pa); // Essa feitiçaria também é sintaticamente válida, mas dá pau ANTES de chamar a função.
}
Por outro lado, a possibilidade de alterar o valor de um dado numa chamada de função sem indicar explicitamente no código uma passagem por referência pode causar um pouco de confusão. Funções que possam modificar argumentos deveriam indicar isso muito bem através dos seus nomes, pelo menos. (Em tempo, essa consideração se aplica também a argumentos ponteiros, mas o simples fato de ter de usar explicitamente o operador
& para indicar a passagem por referência já serve de alerta para muita gente, mesmo que o argumento acabe não sendo modificado.)
int a;
func1(a);
/*
Será que func1() modifica a? Só dá para saber conhecendo
suficientemente bem a função, o que nem sempre é possível
imediatamente para alguém que esteja lendo o código pela
primeira vez.
*/
func2(&a);
/*
Também não dá para ter certeza de se func2() modifica a ou
não, mas os programadores C e C++ ficariam menos surpresos
se esta aqui o modificasse do que se a outra o fizesse.
*/
inverte_bits(a);
/*
O nome da função sugere alteração, logo a chance de surpresa
é menor.
*/
Quanto a referências constantes, você deve usá-las quando quiser obter benefícios de referências, mesmo que não vá (ou não possa) modificar os valores originais.
void f_ri(int &);
void f_cri(const int &);
void f(){
int i=0;
const int ci=1;
int &ri=i; // OK.
int &ri_ci=ci; // Erro de tipo incompatível (int!=const int).
int &ri_lit=5; // Erro: referência não-const para constante literal (rvalue).
const int &cri=ci; // OK: referência constante para lvalue constante.
const int &cri_i=i; // OK: referência constante para lvalue não-const.
const int &cri_lit=5; // OK: referência constante para rvalue.
cri++; // Erro: tentando modificar ref. constante.
cri_i++; // Erro: tentando modificar ref. constante (mesmo com original não-const).
cri_lit++; // Erro: tentando modificar ref. constante.
f_ri(i); // OK.
f_ri(ci); // Erro de tipo incompatível.
f_ri(5); // Erro: referência não-const para constante literal (rvalue).
f_cri(i); // OK: referência constante para lvalue não-const.
f_cri(ci); // OK: referência constante para lvalue constante.
f_cri(5); // OK: referência constante para rvalue.
}
(Por falar em
rvalue , o padrão C++ de 2011 (C++11) introduziu referências para
rvalues , que são usadas sobretudo para mover dados de objetos temporários para outros objetos. Não vou entrar em detalhes a respeito aqui, mas você pode ler um artigo muito didático em
http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html (em Inglês).)
Já o retorno de referências por uma função tem propósito semelhante: entregar um meio de fazer acesso a um objeto já existente, em lugar de fazer cópia dele.
A mesma coisa já se podia fazer antes, retornando um ponteiro para o objeto, em vez do seu valor, mas isso exigia algumas operações explícitas de indireção e alguns cuidados a mais, como o de verificar se o ponteiro recebido é válido antes de usá-lo para ter acesso ao dado.
Algo interessante no exemplo que você mostrou foi que você usou justamente uma função que faz sobrecarga de um operador para um tipo definido pelo usuário (embora com um ligeiro erro semântico, sobre o qual falarei mais a diante). Eu nunca li isso em lugar nenhum, mas eu tenho a interpretação de que um grande motivador para uso de referências, assim como para a sobrecarga de operadores, é a abordagem do C++ de permitir que tipos definidos pelo usuário possam se parecer com tipos nativos.
Considere o seguinte código.
int a;
a=5;
a++; // pós-incremento
++a; // pré-incremento
a+=5;
std::cout << a;
(a+=4)=2; // Estanho, mas válido: acrescenta 4 a ‘a’, e depois o sobrescreve com 2.
int b;
b=a*10;
Se, em vez de
int , você usar qualquer outro tipo nativo (exceto
void , e ajustando também os tipos das constantes, para fazer sentido), o trecho acima vai continuar válido. Mas o C++ permite a você usar qualquer outro tipo mesmo, não apenas entre os tipos nativos, mas também tipos definidos por você, desde que você forneça os operadores corretos e com a semântica correta para cada tipo.
Um exemplo de classe que poderia substituir o
int do exemplo acima seria o seguinte.
#include <ostream>
#include <iostream>
class my_int {
private:
int value;
public:
// Construtor de construção de tipo com argumento default.
my_int(int val=int()): value(val) { }
// Construtor de cópia (mostrado apenas para ser completo no exemplo,
// esta versão faz o mesmo que faria um construtor de cópia default,
// provido automaticamente pelo compilador, caso omitido; se, no entanto,
// o tipo fosse mais complexo e envolvesse ponteiros, provavelmente o
// construtor default não seria adequado, e possivelmente deveria haver
// também um construtor de movimentação de rvalues).
my_int(const my_int &other): value(other.value) { }
// Operador de atribuição com conversão de tipo (retorna referência, para
// ser semanticamente compatível com funcionamento de tipos nativos, pois
// a atribuição devolve um lvalue).
my_int &operator=(int val){
value=val;
return *this; // Referência retornada á ao próprio objeto que está sendo alterado.
// Note que eu retorno “*this”: “this” é um ponteiro, “*this” é o objeto apontado, e a
// referência é sobre o objeto.
}
// Se eu tivesse um tipo contendo ponteiros, provavelmente eu teria de definir
// duas versões do operador de atribuição: uma para copiar objetos do tipo my_int
// que fossem lvalues, e outra para movimentar rvalues (como objetos temporários).
// Operador de pré-incremento devolve lvalue, logo tem de retornar referência.
my_int &operator++(){
++value;
return *this;
}
// Operador de pós-incremento devolve um rvalue, logo devolve uma cópia.
my_int operator++(int){
return value++; // Chama implicitamente construtor de conversão de tipo com
// valor de ‘value’ antes do incremento, e retorna o objeto
// temporário construído.
}
// O operador para escrever um my_int num stream de saída tem como
// primeiro operando um objeto de outro tipo, logo a função operadora
// tem de ser defina fora da classe. Fazê-la friend da classe, no entanto,
// permite que essa função eventualmente tenha acesso a membros
// internos do objeto my_int.
friend std::ostream &operator<<(std::ostream &, const my_int &); // Declara forma da função friend.
// Operador de atribuição com soma também devolve lvalue, logo tem de
// retornar referência. Note que eu forneço apenas uma versão que recebe
// outro operando do tipo my_int (por referência constante). Como existe
// conversão implícita de int para my_int (via construtor de conversão de
// tipo), isso é suficiente para funcionar o equivalente ao exemplo de “a+=4”
// mostrado acima.
my_int &operator+=(const my_int &other){
value+=other.value;
return *this;
}
// Operador de multiplicação simples retorna rvalue, logo retorna apenas
// uma cópia de valor.
my_int operator*(const my_int &other){
return value*other.value; // Usa implicitamente construtor de conversão de tipo
// e devolve objeto temporário construído.
// Operador de conversão de tipo inverso: produz dado de outro tipo a partir
// de objeto. Note que o tipo retornado não vem antes da palavra “operator”,
// mas é caracterizado pela própria operação. E tem de retornar rvalue.
operator int(){ return value; }
};
// Função que faz a saída do dado my_int. Como ela tem a mesma assinatura
// da que foi declarada como friend de my_int, então pode fazer acesso a dados
// privados da classe. Como o objeto os é modificado pela operação de escrita,
// a referência a ele não pode ser constante. Já o objeto que vai ser escrito não
// será modificado, logo pode -- e deve! -- ser uma referência constante.
std::ostream &operator(std::ostream &os, const my_int &mi){
return os << my.value;
}
int main(){
my_int a;
a=5;
a++; // pós-incremento
++a; // pré-incremento
a+=5;
std::cout << a;
(a+=4)=2; // Estanho, mas válido: acrescenta 4 a ‘a’, e depois o sobrescreve com 2.
int b;
b=a*10;
}
Por fim, o erro da função mostrada por você foi justamente um erro semântico no tipo retornado pelo operador, que dá ao objeto um sentido diferente do que teria a mesma operação sobre um tipo nativo.
Tipicamente o operador
* binário devolve um
rvalue , com uma cópia do resultado da operação. A função que você mostrou parece devolver um
lvalue , já que devolve uma referência.
Esse erro é muito comum, por sinal: a pessoa implementa
X::operator*(const Y &) como se fosse
X::operator*=(const Y &) . É bom saber distinguir as duas coisas.