ponteiro
|
referência
|
int *p;
/*ok!*/
|
int &r;
/*erro!*/
|
ponteiro
|
referência
|
int elemento;
|
int 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.
---
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.
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.