C++ seus endereços e ponteiros

1. C++ seus endereços e ponteiros

Bruno
uNclear

(usa Slackware)

Enviado em 29/09/2016 - 14:27h

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


  


2. Re: C++ seus endereços e ponteiros

Paulo
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.


3. Re: C++ seus endereços e ponteiros

Paulo
paulo1205

(usa Ubuntu)

Enviado em 30/09/2016 - 10:50h

Modifiquei a postagem anterior, pois ela estava incompleta (parei de responder por causa do sono, já na madrugada de ontem). Acho que agora ela trata de todos os pontos levantados na pergunta original.

Como modifiquei também parte do texto que já tinha escrito na mensagem original, talvez queiram lê-la toda de novo.


4. C++ seus endereços e ponteiros

Bruno
uNclear

(usa Slackware)

Enviado em 30/09/2016 - 18:39h

paulo1205 escreveu:

Modifiquei a postagem anterior, pois ela estava incompleta (parei de responder por causa do sono, já na madrugada de ontem). Acho que agora ela trata de todos os pontos levantados na pergunta original.

Como modifiquei também parte do texto que já tinha escrito na mensagem original, talvez queiram lê-la toda de novo.


Meu amigo muito obrigado por essa atenção sua resposta está muita clara e com muitas informações em poucas linhas. Eu não entendi muito o erro da minha função ainda estou aprendendo sobre operator. Tenho mais uma pergunta, trabalhar com referencias em c++ impedindo a copia de variáveis é um tipo de design pattern?
Desculpe minha ignorância, não conheço a linguagem o suficiente como queria.



5. Re: C++ seus endereços e ponteiros

Paulo
paulo1205

(usa Ubuntu)

Enviado em 01/10/2016 - 09:45h

uNclear escreveu:

Eu não entendi muito o erro da minha função ainda estou aprendendo sobre operator.


Como eu disse, o erro é semântico, não sintático. Logo não é o tipo de erro que vai interromper a compilação e fazer o compilador alarmar. É um erro que você tem de entender.

O problema é que os operadores aritméticos, quando aplicados a tipos nativos, devolvem um valor, não uma referência: a*b, por exemplo, é o resultado da produto do valor de a pelo valor de b. Esse valor é o mero resultado do cálculo, e não possui um endereço próprio na memória que possa ser transformado em ponteiro ou referência.

Por outro lado, operadores de atribuição, incluindo os compostos (como +=, -=, *= etc.), devolvem referências em C++. Isso permite construir até algumas coisas aparentemente estranhas, como, por exemplo, o seguinte.

int a=0;
((((a-=0)/=1)+=2)*=3)=4; // No fim das contas, a=4, mas antes ele foi triplicado,
// e antes disso acrescido de 2, e antes dividido por 1 (neutro) e diminuído de zero (neutro).


Se você tentar fazer algo parecido com um operador aritmético simples, vai tomar erro.

int a;
a*2=4; // Isto não é uma equação que faz a=2, mas um erro de programação
// em C++, pois “a*2” não é uma expressão que pode receber um valor.


Então esse é o motivo pelo qual eu disse que a declaração de função que você mostrou está errada.

Bom, “errada” talvez seja uma palavra muito forte. Ao devolver uma referência em vez de um valor, sua função de multiplicação passa a ter um comportamento diferente das multiplicações nativas da linguagem. Esse comportamento diferente não é uma coisa estritamente proibida, mas é tão distinto que possivelmente deveria ser evitado para não produzir confusão.

Tenho mais uma pergunta, trabalhar com referencias em c++ impedindo a copia de variáveis é um tipo de design pattern?


Não diria que é um padrão de projeto. Referências são um recurso da linguagem.

Pode ser que haja padrões de projeto que usem pesadamente referências, mas como não sou programador profissional, não conheço muitos padrões de projeto.






Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts