A linguagem C++ Conceitos gerais Novas facilidades da linguagem C++ A linguagem de programação C++ foi concebida para: Ser um C melhorado Suportar Abstracção de Dados Suportar Programação Orientada por Objectos O suporte mínimo para a programação procedimental consiste em funções, operadores aritméticos, instruções de selecção, ciclos e entrada/saída de dados. O C++ herda todos estes mecanismos básicos e inclui alguns novos 1. Entrada/saída de dados Tal como em C, o C++ também define streams para os dispositivos de entrada e saída que são abertos quando um programa é executado Os streams são: 1. cout, equivalente ao stdout; 2. cin, equivalente ao stdin; 3. cerr, equivalente ao stderr; De facto cout é um objecto do tipo ostream. A classe ostream foi definida com o operador operator<< (“put to”) para fornecer as saídas dos tipos predefinidos nas linguagens C/C++. Vamos abordar um exemplo: #include <iostream.h> int main() { cout << "This is the second argument\n"; } Isto é o primeiro argumento do operador (da função) operator<< Isto é o segundo argumento do operador (da função) operator<< O operador << (“put to”) escreve o valor do segundo argumento no primeiro argumento. Isto significa que, no exemplo, o texto This is the second argument\n será escrito no dispositivo de saída standard cout. cerr é um outro objecto do tipo ostream. Por analogia cin é um objecto do tipo istream. A classe istream foi definida com o operador operator>> (“get from”) para fornecer as entradas dos tipos predefinidos nas linguagens C/C++. Vamos considerar o exemplo: int a; float b; char c; cin >> a >> b >> c; Os streams são declarados no ficheiro iostream.h Um programa poderá usar para entradas e saídas tanto a biblioteca standard de C como a de C++ (ou as duas) 2. Overloading (Sobrecarga) do nome das funções Funções diferentes têm geralmente nomes diferentes. Contudo, para funções que realizam tarefas semelhantes em tipos de objectos diferentes, por vezes é preferível que essas funções mantenham o mesmo nome. Quando o tipo dos seus argumentos é diferente, o compilador consegue diferenciá-las e escolher a função correcta a invocar. Por exemplo, podemos ter uma função potência para inteiros e outra para variáveis de vírgula flutuante. Vamos abordar o exemplo: int pow(int,int); double pow(double,double); // ... x = pow (3,5); // chamada pow(int,int) y = pow (2.0, 4.0); // chamada pow(double,double) Este tipo de uso múltiplo de um nome de uma função é designado por function-name overloading ou simplesmente overloading. Os argumentos de uma função podem ser passados quer “por valor”, quer “por ponteiro”, quer “por referência”. void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } void swap(int& a, int& b) { int temp = a; a = b; b = temp; } void f(int i, int j) { swap(&i,&j); } void f(int i, int j) { swap(i,j); } Usando a passagem por referência podemos trabalhar com ponteiros implícitos 3. Referências Uma referência é um nome alternativo para um objecto Tal como um apontador, uma referência corresponde a um local onde se situa uma variável Com o tipo de dados referência, criamos um nome alternativo para uma variável previamente declarada Numa declaração, a notação T& significa referência para um valor do tipo T Seja, por exemplo: int i=3; int &j=i; j=2; // ..... A diferença entre os ponteiros e as referências é a seguinte: o utilizador pode usar uma referência como um objecto ordinário ainda que o acesso seja fornecido através do endereço void main(void) { int i=3; int &j=i; j=2; // ..... void main(void) { int i=3; int *j=&i; * j=2; // ..... Uma referência é um ponteiro implícito i=3 ss bp-2 bp // mov word ptr [bp-2],3 Isto // mov [bp-4],ss // mov [bp-6],ax ; lea ax,[bp-2] j=2 and i=2 mov es:word ptr [bx],2 es bx ss é um ponteiro explícito mov word ptr[bp-2],3 lea ax,[bp-2] mov [bp-4],ss mov [bp-6],ax les bx,[bp-6] mov es:word ptr [bx],2 Como uma referência é um ponteiro podemos obter um resultado inesperado, por exemplo int F(int& ri) { ++ri; return ri; } int main(...) { int m1=1; cout << F(m1) << endl; cout << m1 << endl; // o resultado é 2 // o resultado é 2 } A referência para o objecto m1 é passada à função F. Esta função alterou o valor da variável m1 indirectamente através da referência De facto, uma referência é implementada pelo compilador como um apontador constante que é desreferenciado automaticamente de cada vez que foi usado Na maioria dos casos as referências e os apontadores têm uma relação de equivalência, por exemplo: O seguinte código usa uma referência: O seguinte código usa um ponteiro: int ii=5; int *rii=&ii; cout << ii << '\t' << *rii << endl; *rii = 10; cout << ii << '\t' << *rii << endl; int i=5; int &ri=i; cout << i << '\t' << ri << endl; ri = 10; cout << i << '\t' << ri << endl; Os resultados são iguais: 5 10 5 10 int &a,&b,&c; As referências devem ser inicializadas, por exemplo: int *aa,*bb,*cc; int k; int &a=k,&b=k,&c=k; // erro // Ok // Ok // Ok Vamos considerar o seguinte código: X = Y; Esta atribuição está correcta se a expressão esquerda for um endereço (lvalue - a left value) e se a expressão direita produzir um valor (rvalue - a right value), que é um valor permissível. Como resultado o valor do lado direito (Y) vai ser copiado para a célula da memória que tem o endereço X. Uma referência é um endereço e por isso pode ser considerada como lvalue. Então, a referência pode ser escrita no lado esquerdo da expressão. Vamos considerar uma função. Podemos passar o argumento à função como valor. Neste caso este valor (de facto rvalue) vai ser copiado na pilha da função. A função não tem acesso ao valor da variável original que foi passada. Podemos passar o argumento à função como referência. Neste caso o endereço (de facto lvalue) vai ser copiado na pilha da função. Usando este endereço (esta referência) podemos alterar o valor da variável original que foi passada. Por outras palavras a referência pode ser usada como um apontador implícito. A referência pode ser devolvida da função. Neste caso a função pode aparecer no lado esquerdo da expressão. Vamos abordar das seguintes funções: 1. int F(int& i) { return i; } 2. int& RF(int& j) { return j; } F(x) = 6; // erro RF(x) = 6; // Ok podemos, por exemplo obter um resultado inesperado int x=3; cout << " x = " << x << endl; RF(x) = 6; cout << " x = " << x << endl; // x=3 // x=6