Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação

Algoritmos e Estruturas de Dados I

Referência

Uma referência é um elemento da linguagem C++ que permite criar um cognome, um “apelido” (alias) para outros elementos em particular variáveis e objetos.

Uma referência pode ser pensada como sendo um ponteiro que foi “disciplinado”. As referências podem ser pensadas como sendo ponteiros que sacrificaram sua expressividade em favor de maior segurança. O relacionamento de referências com ponteiros ocorre em função de algumas interpretações sobre como implementar as referências.

A declaração de uma referência segue a forma <tipo> &<nome>; Exemplos:

int A=5;
int&  rA=A;
extern int& rB
int& f();
void g(int& rP);
class MinhaClasse{ int& x; /*…*/};

Tipos “referência para <tipo>” são também chamados “tipos referência” e os identificadores são chamados de “variáveis referência”(!) mas chama-los de variável pode levar a erros de entendimento pois uma referência não é como um ponteiro que pode apontar diferentes elementos:

Não é possível tratar a referência por sí mesma, a referência deve sempre ser inicializada!

ponteiro

referência

int *p;

/*ok!*/

int &r;

/*erro!*/

O problema acima pode ser consequência das referências serem confrontadas com ponteiros e também pelo fato da definição de parâmetros de funções do tipo referência  poder ter a forma acima. No caso de parâmetros a referência ( parâmetro formal) se liga a cada parâmetro real a cada invocação. Não podemos manipular a <referência por si mesma> depois de definida (ou ligada a um item), ela funciona sempre como um sinônimo do item.

ponteiro

referência

int elemento;
int *p;
p=&elemento;
*p /*elemento*/
p /*ponteiro*/
&p /*endereço do ponteiro*/

int elemento;
int &r=elemento;
*r /*erro*/
r /*mesmo que elemento*/
&r/*endereço de elemento*/

Em particular não podemos anular as referências! Uma vez feita a “ligação/atribuição” da referência com algum elemento uma nova  tentativa de “ligação/atribuição” (p.ex. atribuir NULL) para uma referência se for válida em termos sintáticos corresponde a atribuir ao alvoda referência (p.ex. atribuir NULL ao elemento referido). Referências não podem ser nulas! (mas podem existir referências inválidas!)

Usos das referências

Um dos prinicipais usos das referências está relacionado à passagem de parâmetros. Observe o programa abaixo:
#include <cstdlib>
#include <iostream>

using namespace std;
void swap(int &rp1, int &rp2){
  int aux;
  aux=rp1;
  rp1=rp2;
  rp2=aux;
}
int main(int argc, char *argv[]){
  int a=1, b=2;
  swap(a,b);
  cout <<a <<" "<< b <<"\n";
  system("PAUSE");
  return EXIT_SUCCESS;
}

Observe que a “atribuição/inicialização” de referências que correspondem a parâmetros formais, quando da chamada da função correspondente, segue a “idéia de atribuição” mas com aspectos próprios às “referências”.

Observe o seguinte trecho de programa:
void f_lenta(ObjectosGrandes x) { /* ... */ } 
void f_rapida(const ObjetosGrandes& x) { /* ... */ }

...

ObjetosGrandes y;
f_lenta(y); // devagar..., copia os valores de y para x
f_rapida(y); // mais rápido..., acesso “read-only” a y

No trecho de programa acima o parâmetro x da função f_rapida() além de ser uma referência está qualificado como “const” isto significa que x não será usado como alvo de atribuições e portanto não há necessidade de criar uma cópia em memória! (“ponteiros constantes” não dão esta garantia!).

O conceito que deve ser guardado com relação a referências é que elas são um novo nome para uma variável ou para uma instância de classe. Com base nisso fica fácil aceitar que:
(i) não devemos ter referência para uma referência (obtemos uma referência para o que é referido!);
(ii) não devemos ter ponteiros para referência (obtemos um ponteiro para o que é referido!)

---
Se
T é um tipo então (i)em T *v1 o tipo de v1 é ponteiro para instâncias de T; (ii) em T &v2 o tipo de v2 é referência para instâncias de T (ou referência para l-values do tipo T); em T &&v3 o tipo de v3 é referência para r-values do tipo T.

Neste material não vamos discutir as “referências para r-values” (p.ex. void f(Tipo &&p1){). Conforme pode ser encontrado na Internet:
----
As we all know, the First Amendment to the C++ Standard states:
"The committee shall make no rule that prevents C++ programmers from shooting themselves in the foot."
 Speaking less facetiously, when it comes to choosing between
giving programmers more control and
saving them from their own carelessness,
C++ tends to err on the side of giving more control.
Being true to that spirit, C++11 allows you to use move semantics not just on rvalues,
 but, at your discretion, on lvalues as well.
---

 

Sobrecarga de operadores (operator overloading)

observação: O assunto “operator overloading” não depende de forma direta do assunto “references”, mas os exemplos de “operator overloading” que não fazem uso de “references” impõe limitações de expressividade.

De maneira similar a várias linguagens de programação a linguagem C++ suporta um conjunto de operadores (são mais de quarenta, p.ex. +, -, *, /, %) com várias regras relacionadas aos tipos prédefinidos da linguagem. Os projetistas de C++ definiram regras para permitir que quase todos estes operadores pudessem ser usados juntos a um tipo definido por um programador.

A linguagem C++ permite a redefinição do uso de operadores desde que um dos operandos seja de um tipo (por exemplo classe) definido pelo programador. Não são todos operadores que podem ser redefinidos e o programador só deve usar a redefinição se ela ajudar no entendimento e não para dificultar o entendimento dos programas. Sem uma boa “metáfora” ou “analogia” justificando o uso de operadores traz problemas e deve ser evitado.

 Alguns poucos operadores não podem ser sobrecarregados, dentre eles os seguintes:
:: (resolução de escopo), . (seleção), .*(seleção via ponteiro para função), ?: (condicional)

Os demais operadores podem ser usados, incluindo new, new[], delete, delete[] etc.

O formato geral de sobrecarga de operadores utiliza nomes de funções com formato especial. Este formato envolve dois elementos, o primeiro elemento é a palavra chave operator e o segundo elemento é o operador. Os parâmetros desta função de sobrecarga também devem seguir regras estritas. Exemplos de nomes de funções:

operator+
operator-
operator++
operator--
operator<<
operator>>

Uma função como acima é referida como função de operador (operator function). A classe ostream e istream da biblioteca padrão da linguagem C++ define várias funções de operador. Quando escrevemos a sentença:
cout<<123<<endl;
O compilador trata esta sentença como se fosse:
cout.operator<<(123).operator<<(endl);
ou seja o uso do operador de deslocamento de bits junto a uma instância da classe ostream invoca uma função de operador << global (avulsa) ou de instância. Várias outras classes além da ostream e istream utilizam a sobrecarga de operadores. A classe string define por exemplo a função de operador [] e a idéia é indexar os caracteres que formam uma cadeia de caracteres. Considere uma declaração string s(“abcdef”); a sentença
cout<<s[1]<<endl;
é tratada como se fosse:
operator<<(cout, s.operator[](1)).operator<<(endl);

A classe string sobrecarrega o operador + para significar concatenação e sobrecarrega o operador += para significar anexação ou apensamento (append). Esta classe também sobrecarregou os operadores de comparação permitindo assim uma comparação razoavelmente intuitiva de strings. Vários tutoriais sobre a classe string de C++ estão disponíveis na internet (como por exemplo http://xoax.net/cpp/ref/other/incl/stl_strings/ ; http://www.youtube.com/watch?v=16tcuX8VBsA ;)

Um operador binário pode ser definido tanto por uma função de instância com um argumento (o outro argumento é a instância) quanto por uma função global ou de classe com dois argumentos. Para um operador binário @, a expressão op1@op2 pode ser interpretada

op1.operator@(op2) ou

operator@(op1, op2)

Um operador unário, prefixado ou posfixado, pode ser definido tanto por uma função de instância sem argumentos quanto por por uma função global ou de classe com um argumento. Para qualquer operador prefixado @ a expressão @op pode ser interpretada

op.operator@() ou

operator@(op)

Para qualquer operador posfixado @ a expressão op@ pode ser interpretada

op.operator@(int) ou

operator@(op,int)

Uma função de operador que queira aceitar um tipo básico (ou primitivo) como primeiro operando tem que ser uma função global (função avulsa).

A análise correta de expressões não é trivial, observe que

std::cout<<op

pode ser interpretado como sendo

(i) std::cout.operator<<(op) ou

(ii) operator<<(std::cout, op)

As classes relacionadas ao objeto cout redefinem o operador deslocamento para esquerda (<<) e as classes relacionadas ao objeto cin redefinem o operador deslocamento para direita. O programa abaixo ilustra a redefinição de operador utilizando uma função global (“função avulsa”), mas a redefinição também pode ser feita com funções de instâncias.

#include <cstdlib>
#include <iostream>

using namespace std;

class C{};// tipo definido pelo programador

//funcao “avulsa” (funcao global)
ostream &operator<<(ostream &p1, C &p2){p1<<"um objeto tipo C!!!"; return p1;}


int main(int argc, char *argv[]){
    C v1;
    cout<<v1<<"propagou!"<<endl;
    system("PAUSE");
    return EXIT_SUCCESS;
}
A função operator<< coloca no fluxo um string e retorna o fluxo. A sentença
    cout<<v1<<"propagou!"<<endl;
 deve ser entendida como
 ...operator<<(operator<<(cout,v1),"propagou!").operator<<(endl);
ou ainda
....endl(operator<<(operator<<(cout,v1),"propagou!"));

A última forma evidencia que endl é um símbolo especial. Juntamente com os fluxos cin e cout são definidos vários manipuladores e endl é um deles. A semântica de endl é inserir “nova linha” e descarregar (flush) a área de amortecimento (buffer) do fluxo. O programador em geral é aconselhado a considerar as duas formas:

forma 1:

objeto1.funcaoDeInstancia(objeto2)

forma 2

funcaoGlobal(objeto1, objeto2);

Além disso a eventual comutatividade aconselha o programador a considerar:

funçãoGlobal(objeto2, objeto1) e

Objeto2.funçãoDeInstancia(objeto1)

Mas isso deve ser visto caso a caso!! Os projetistas de C++ não permitem a sentença:

“abc”>>cout; é permitida apenas cout<<”abc”;

 Reescreva a classe acima para ter um membro de instância do tipo int, o construtor inicializa este campo; reesecreva a função de sobrecarga: ao invés de colocar no fluxo um string coloca no fluxo o valor deste campo de tipo int.
class C{public: int i; C(int v){i=v;}};

Alguns tipos de sobrecarga só podem ser feitos através de métodos de classes (não podem ser funções globais). As funções operator=, operator[], operator(), e operator->

Não pode ser mudado o carater sintático dos operadores. Por exemplo existe (i)um + unário e existe (ii)um + binário, podemos escolher um destes dois operadores para ser sobrecarregado, mas não podemos sobrecarregar o operador % para ser operador com mais ou menos do que 2 operandos . O operador % só existe como operador binário na linguagem C++ e não pode ser sobrecarregado como sendo um operador unário ou como sendo um operador ternário.

No programa abaixo um método de classe faz a sobrecarga do operador de comparação == (igual igual):

#include <cstdlib>
#include <iostream>

using namespace std;

class C{public:
        static int cntId;
        int id;
        C(){id=cntId++;}
        bool operator==(C& outro){return outro.id==id;}
};
int C::cntId=0;

int main(int argc, char *argv[]){
    C v1,v2;
    cout<<(v1==v1)<<endl;
    cout<<(v1==v2)<<endl;
    system("PAUSE");
    return EXIT_SUCCESS;
}

No programa acima cada instância de C recebe um valor do tipo int na sequência a partir de zero. O procedimento para verificar se dois objetos são iguais consiste em comparar seus “id”. Exercício: escreva a função global correspondente.

 

O programa abaixo ilustra novamente o uso de função avulsa para redefinição do operador de deslocamento de bits para esquerda junto a fluxos do tipo de “cout” (ostream). O programa faz uma definição bem precária de uma classe que lida com frações, onde é armazenado um denominador e um numerador do tipo int.

#include <cstdlib>
#include <iostream>

using namespace std;

class Fracao{private: int numer, denom;
        public:
        friend ostream &operator<<(ostream &p, const Fracao &p);
        Fracao(int n, int d){
          numer=n; denom=d;
        }
};
ostream &operator<<(ostream &s, const Fracao &p){
  s<<p.numer<<"/"<<p.denom;
  return s;
}

int main(int argc, char *argv[]){
   
    Fracao f1(3,10);
    cout<<f1<<endl;
    system("PAUSE");
    return EXIT_SUCCESS;
}

No programa acima a função “avulsa” operator<< precisa fazer acesso a campos privados de instâncias de Fracao. Para isso ser possível a classe Fracao declara a função operator<<() como uma função “amiga”. A relação de amizade é discutida em outra parte da disciplina.

 

O construtor de cópia (copy constructor)

 

Na linguagem C++ ,além dos construtores definidos no material sobre classes, podemos definir um contrutor “especial” conhecido como construtor de cópia (copy constructor). O conceito de “construtor de cópia” envolve uma sintaxe especial na definição de classe e envolve a correspondente invocação deste construtor. Com relação ao aspecto de definição um possível formato é como abaixo:

class C{ ... C(C &p){...} ...};

É possível definir este construtor com mais parâmetros, mas eles devem ter valores default associados. Este construtor será invocado de forma “implícita” nas sentenças de definição de objetos desta classe onde existe a cópia de um objeto para outro:

C obj1;  // é chamado o construtor “default”

C obj2=obj1; // a “construção” de  obj2 é feita através da chamada do construtor cópia.

Observe o resultado escrito na tela pelo programa abaixo:

#include <cstdlib>
#include <iostream>

using namespace std;

class C{public:
      C(){cout<<"default"<<endl;}
      C(C &p){cout<<"copia"<<endl;}
      };

int main(int argc, char *argv[])
{
    C obj1;
    C obj2=obj1;
    system("PAUSE");
    return EXIT_SUCCESS;
}


A construção do objeto obj1 envolve a chamada implícita do construtor C() (sem parâmetros), mas a construção do objeto obj2 envolve a chamada implícita do construtor C(C &p). O compilador gera a chamada associando a referência p ao objeto obj1.

Um construtor de cópia permite que o programador defina que tipo de semântica ele deseja para a criação de instâncias que sejam inicializadas a partir de outras. Em um cenário o programador copia exatamente os valores de uma instância para outra, e, por exemplo, compartilham os elementos que são apontados. Em outra situação o programador faz novas instanciações e evita que a instância que está sendo construída compartilhe estrutura com a instância que serviu de “right-value” na expressão de inicialização.