• Nenhum resultado encontrado

Do C ao C++: O C++ é um C melhor, bem vindo ao C++.

N/A
N/A
Protected

Academic year: 2021

Share "Do C ao C++: O C++ é um C melhor, bem vindo ao C++."

Copied!
54
0
0

Texto

(1)

NOTA: Estes apontamentos deverão servir de apoio à aprendizagem do C++, mas não dispensam a leitura dos apontamentos das aulas teóricas da disciplina de Programação Orientada por Objectos e de outros livros recomendados. Espero que sejam de alguma utilidade.

Ao longo do texto, as palavras reservadas do C++ e os exemplos de código estão no tipo de letra arial, termos em inglês que não foram traduzidos estão em itálico.

Ana Paula Costa

Do C ao C++: O C++ é um C melhor, bem vindo ao C++.

v 1.0

(2)

Elaborado por Ana Paula Costa

Capítulo 1 – C versus C++

O C++ é mais restrito na verificação do tipo dos dados que o C. O C++ continua a permitir que se misture tipos de dados (colocar um caracter numa variável do tipo float, por exemplo), mas é preciso indicar essa intenção fazendo typecast antes do valor.

Ao contrário do C, o C++ trata os caracteres de forma diferente dos inteiros. Em C, os caracteres e os inteiros podem ser usados indiferentemente, uma vez que os caracteres são convertidos automaticamente para inteiros. Em C++, um caracter ocupa apenas um byte, em C ocupa 2 bytes (como o inteiro).

O C++, tal como o C, é uma linguagem de programação pequena e estruturada em blocos. Tem apenas cerca de 45 palavras reservadas.

Como o C++ é um super conjunto do C, a maioria dos compiladores de C++ não tem qualquer problema em compilar código C normal. Até se pode misturar C com C++ no mesmo código.

O C++ não é difícil! Pode-se aprender C++ tão bem como se aprende C. A Programação Orientada a Objectos (POO) é apenas uma forma diferente de abordar os problemas de programação.

Em C++ os comentários são indicados com // no início de cada linha de comentário. O C++ continua a suportar a nomenclatura do C - /* comentário */ - se bem que não seja muito usada pelos programadores de C++, por ser fácil esquecer o */ no final do comentário.

Os programas em C++ ficam gravados em ficheiros com extensão .cpp. Ficheiros com extensão .h ou .hpp são ficheiros com declarações (header files).

Em C, existem apenas dois locais onde se podem declarar variáveis: globalmente antes do nome de uma função, e localmente logo após a abertura de uma função. Em C++ podemos declarar variáveis locais em praticamente qualquer parte de um programa desde que se declare a variável antes de a usar. Existe muito debate sobre qual a utilidade disto. É difícil encontrar variáveis que estão declaradas no meio dos programas. A utilização mais comum deste tipo de declarações é na estrutura de controlo for. Como as variáveis contadoras dos ciclos for raramente são variáveis importantes, fazendo a sua declaração no ciclo limpa a zona de declarações do início do bloco, onde se encontram as declarações das variáveis mais importantes.

Em C, se tivermos:

char nome[6] = “POOC++”; /* não tem espaço para null */

O C aceita, se bem que possa vir a dar problemas se nome for usada como uma string. Em C++ a instrução em cima nem compila. Tem que se fazer da seguinte forma para atribuir a uma string um array de caracteres:

char nome[ ]=”POOC++”; /* C e C++ criam espaço para null */ Em C, factor = (float)I * 2.5; /* faz o typecast de I para floating point */ Em C++, factor = float(I) * 2.5; // faz o typecast de I para floating point */ float não é uma chamada a uma função, é um operador.

Em C++ existe o operador ::, que é chamado operador de resolução do escopo e que resolve conflitos entre variáveis/funções com o mesmo nome. O operador de resolução do escopo fornece uma forma de aceder a variáveis globais que tenham o mesmo nome do que variáveis locais. Precedendo a variável com :: estamos a instruir o C++ que queremos aceder à variável global. Aceder a variáveis globais não é o principal propósito do operador ::, veremos depois uma outra utilidade para este operador.

Em C++, deve-se usar const e funções inline em vez de #define.

Em C, a directiva #define é muito comum e é usada para dar nomes a constantes. const é melhor do que #define para programar constantes em C++. const reserva espaço de armazenamento para os dados e torna-os apenas de leitura. Para declarar uma constante com const, é só colocar const antes da definição da variável:

const int agelimit = 18;

É preciso sempre especificar um valor inicial quando se declaram constantes. O seguinte não é possível: const float sales; // inválido

(3)

Usar valores constantes como índices dos arrays quando se declaram os arrays, não é permitido em C, mas é aconselhado em C++:

main() {

const int limit = 1000;

float sales [limit]; // não permitido em C int quantity [limit]; //não permitido em C …

}

Funções inline em C++, é o mesmo que macros em C, só que melhor. Exemplo de uma macro em C:

#define cube(x) x*x*x

c = cube(num); // substitui cube pela macro: c= num*num*num, // no corpo do programa

O compilador de C++ tenta substituir a chamada a uma função inline pelo código dessa função, antes da compilação, pelo que não há chamada à função.

Para transformar uma função numa função inline, é só colocar a palavra reservada inline antes do nome da função. O compilador de C++ pode optar por ignorar o pedido inline. Deve-se reservar o uso de inline para pequenas funções (com um máximo de 5 linhas de código).

Quanto mais funções inline temos num programa, maior será o programa compilado. Se se tem demasiadas funções inline, o espaço de disco ocupado pelo programa ou o tempo que leva a carregar o programa para memória pode ser tão elevado que deixa de justificar as vantagens das funções inline. O C++ ignora a designação inline para funções recursivas. As funções recursivas têm que ser funções normais.

Constantes globais têm escopo no ficheiro. Ao contrário do que acontece no C, os valores constantes globais (aquelas constantes declaradas for a das funções) têm escopo no ficheiro (lincagem interna) e não podem ser referenciadas por outros programas. Se se compilar separadamente vários programas e depois se lincar todos juntos num módulo, temos que utilizar extern antes da declaração da constante se se desejar que outros ficheiros tenham acesso às constantes globais desse ficheiro.

extern const int max = 1500; // variável global com lincagem externa

Outro ficheiro lincado a este poderá usar o valor constante max se contiver a seguinte linha de código: extern const int max; // valor definido noutro ficheiro

Em C++ temos que declarar (prototype) todas as funções antes que elas apareçam no programa. A única excepção é a função main ( ), que por convenção deve ser a primeira a aparecer no programa. Assim, antes da função main ( ) temos as declarações de todas as outras funções do programa, mas a sua definição pode ser feita depois.

Tipo de retorno das funções. Em C:

int funcao (int n) {

/* pode não retornar nada que não dá erro */ }

Em C++:

void funcao (int n) {

// se não retornar nada tem que ter void, senão dá erro }

Lista de argumentos das funções. Em C:

float funcao ( ); /* lista de argumentos não especificado na declaração */ Em C++:

(4)

Elaborado por Ana Paula Costa float funcao (void); // equivalente ao de cima

Em C++, se se quiser definir uma lista de argumentos não especificada, temos que usar (…) na declaração:

float funcao (…); // função com lista de argumentos não especificada na declaração

Capítulo 2 – Input e Output em C++

O C++ utiliza métodos de input e output diferentes do C. Não é que haja algo de errado com o printf ( ) e o scanf ( ) e todas as outras funções de stdio.h. Aliás, o C++ até suporta o uso dessas funções. O que se passa é que o C++ tem algo mais completo e mais prático de utilizar do que essas funções.

O C++ utiliza operadores (<< e >>) para realizar o I/O standard. Para usar os operadores de I/O do C++ temos que incluir o ficheiro cabeçalho iostream.h no nosso programa. O ficheiro iostream.h inclui definições para input, output e erro. cin, cout, cerr e clog são objectos. cin representa o standard input (teclado), cout o standard output (écran), cerr o standard erro e o clog representa um buffer para erros (não mostra as mensagens de imediato).

Os operadores << e >> combinados com cin e cout fazem com que ocorra input e output, e são simples de usar, para além de serem flexíveis: << e >> trabalham com todos os tipos de dados standard (char, int, float, …). Uma das grandes vantagens de cout e cin é que trabalham com qualquer tipo de dados, incluindo os criados por nós, ao contrário de printf ( ) e scanf ( ) que trabalham apenas com os tipos de dados pré definidos.

Para além da utilidade de I/O, os operadores << e >> continuam a ter a utilidade que tinham em C, bitwise shift. Como têm mais que uma utilidade são chamados operadores sobrecarregados (overloaded).

O operador insersor (<<), para fazer output para o écran

Como o C++ usa operadores para realizar I/O, e não funções, os operadores já sabem qual o tipo do dado que vai mostrar. Assim, em C++ não é preciso informar o compilador do tipo de dados de cada dado que se pretende mostrar.

cout << “strings” << variáveis << endl;

// endl significa fim de linha, ou seja, muda de linha

O operador insersor (<<) significa inserir algo no objecto cout (o écran), ou seja, significa enviar algo para o écran.

O C++ reconhece todos os códigos de controlo usados em C: \n, \t, \r, … (têm que estar incluídos numa string ou então isolados, como caracter).

O operador extractor (>>), para fazer input do teclado

Consiste em extrair valores do dispositivo de entrada (o teclado tipicamente). Como acontece com o scanf ( ), cin >> só apanha strings que não contenham espaços, novas linhas ou tabs.

cin >> variável;

cin >> var1 >> var2 >> …;

Mostrar mensagens de erro no écran

Deve-se utilizar o objecto cerr para mensagens de erro. clog praticamente não é utilizado. cerr é mais rápido e mais confiável.

Para enviar uma mensagem de erro para cerr (o écran), é só substituir cout por cerr: cerr << “Perigo! Ficheiro sem backup \n”;

(5)

Existem manipuladores da base numérica, manipuladores de controlo de caracteres, manipuladores de controlo do formato. Os manipuladores permanecem em efeito até que se especifique um outro manipulador.

Manipuladores da base numérica: dec – output decimal hex – output hexadecimal oct – output octal

Manipuladores de controlo de caracteres: endl – nova linha e esvazia o buffer ends – insere ‘\0’ na stream flush – esvazia o buffer

Manipuladores de controlo do formato (necessitam de #include <iomanip.h>): resetiosflags (long) – faz o reset do efeito de setiosflags (long) setprecision (int) – especifica os dígitos de precisão de floating point

setiosflags (format flag) – activa o formato de output indicado na flag (ver em baixo)

setw (int) – activa a largura do campo para um tamanho específico. É o único manipulador que faz o reset (perde o seu valor) após cada I/O. Se o valor é maior do que o tamanho especificado, o C++ ignora setw ( ).

Flags de formatação para iostream.h:

ios::left – justifica à esquerda no tamanho especificado por setw ( ). Por defeito, os números são justificados à direita e os caracteres são justificados à esquerda.

ios::right – justifica à direita no tamanho especificado por setw ( ). ios::scientific – formata números na notação científica.

ios::fixed – formata números no formato decimal normal. ios::dec – formata números na base 10.

ios::hex – formata números na base 16. ios::oct – formata números na base 8.

ios::uppercase – formata todos os caracteres para maiúsculas.

ios::showbase – inclui como prefixo a base do número (0x – hexadecimal, 0 – octal). ios::showpos – inclui + quando mostra valores positivos.

Exemplos:

cout << hex << 192 << ´\n´; // mostra o valor em hexadecimal

cin >> oct >> var; // espera um valor octal. Se o utilizador insere um valor inválido, var // não se altera. Se o utilizador insere um valor válido seguido e algo // inválido, em var fica guardada apenas a parte válida da inserção. cout << setw (10) << setiosflags (ios::left) << I << endl;

Combinação de flags de formatação: Com setiosflags ( ) ou resetiosflags ( ), em vez de ter uma formatação por linha, também se pode fazer:

cout << setiosflags (ios::uppercase | ios::hex | ios::left);

Capítulo 3 – Ponteiros, referências e alocação dinâmica de memória

O C++ trata os ponteiros e a alocação dinâmica de memória de forma diferente do C. Acrescenta poder e simplicidade aos ponteiros e à alocação dinâmica de memória.

Ponteiros void

Um ponteiro void é uma variável ponteiro que pode apontar para qualquer tipo de dados. Para declarar um ponteiro void:

(6)

Elaborado por Ana Paula Costa Não se pode declarar um ponteiro regular e depois fazê-lo apontar para uma variável de um tipo diferente, a não ser que esse ponteiro seja um ponteiro void. Também se pode assignar ponteiros de tipos de dados específicos a ponteiros void.

Referências

Referências são aliases para outras variáveis. Para criar uma variável de referência, colocar na declaração & antes do seu nome. Temos que inicializar todas as variáveis de referência quando são declaradas. Referências são nomes alternativos para valores e variáveis. Exemplo: memória

int ivar = 58;

int & ref1 = ivar; // ref1 é um alias de ivar

ivar/ref1 58

O endereço de ambos é igual. Assim, alterar o valor de uma das variáveis, altera também o valor da outra. ref1 não é um ponteiro constante porque podemos alterar o seu valor. Contudo, não se pode fazer ref1 apontar para um outro valor diferente de ivar. Nas referências, ao contrário dos ponteiros, não se utiliza * nome_var. Não é preciso, basta usar o nome da variável, usar * dá erro.

Variáveis de referência não são exactamente a mesma coisa que ponteiros. Com os ponteiros, temos uma variável distinta que guarda o endereço de outra variável. Podemos alterar o endereço na variável ponteiro (fazer o ponteiro apontar para outro endereço) ou alterar o valor que se encontra no endereço apontado

pelo ponteiro. Exemplo: memória

int val = 65; // variável normal ref/val 65 int * ptr = &val; // ponteiro para val

int &ref = val; // referência para val

ptr Também se pode referenciar valores numéricos:

int &ref = 10; ref++;

cout << ref << endl; // resultado será 11

Ponteiros e constantes

Podemos aplicar a palavra reservada const a ponteiros, fazendo com que esses ponteiros se tornem ponteiros constantes. No entanto, isto trás algumas subtilezas que é preciso ter em atenção:

Assim que se declara um valor como constante, já não se pode alterar o seu valor. Até aqui nada de novo. Ao fazer: const float a = 9.0;

Não se pode atribuir um valor diferente a a. Se se fizer: a = 10.0; // é inválido

Não assim tão óbvia é a situação de não se poder alterar o valor da constante usando um ponteiro indirecto. O compilador de C++ não permite que um ponteiro aponte para o endereço de uma constante, porque isso criaria uma forma de se alterar o valor da constante, o que não se pode.

const float a = 9.0; float * p;

p = &a; // é inválido

Esclarecimentos sobre ponteiros, referências e constantes

Existem quatro tipos de dados distintos que podem resultar da combinação de constantes, referências e ponteiros:

Ponteiros para constantes. Exemplos:

const float * val; // podemos alterar o endereço armazenado em val, porque val // é um ponteiro variável

val = &a_float; // pode-se fazer, não está errado.

*val = 125.0; // inválido porque o C++ sabe que val aponta apenas para // valores constantes

(7)

Ponteiros constantes. Exemplos: float a = 8.2;

float * const val = &a; // não se pode alterar val, mas podemos alterar o valor // para o qual val aponta

*val = 100.0; // podemos fazer, estamos a alterar o valor para o qual val // aponta, a zona de memória apontada por val

val = &a_float; // inválido, estamos a querer alterar o endereço para o qual val // aponta, e isso não se pode fazer por val ser constante Ponteiros constantes para valores constantes. Exemplos:

const float a = 5.5;

const float const * val = &a; // val é um ponteiro constante para um valor // constante chamado a, não se pode alterar nem o // valor do endereço val, nem o valor da constante a, // apontada por val.

a = 10.0; // inválido *val = 10.0; // inválido Referências para constantes. Exemplos:

float value = 8.5; // value é uma variável float normal const float &ref = value; // referência para uma constante value = 12.3 * 0.4; // ok

ref = 12.3 * 0.4; // inválido, não se pode alterar ref por ser constante, // mesmo que esteja a apontar para a mesma // localização de memória que value. ref é uma // referência read-only.

Deve-se evitar usar as referências como aliases de variáveis nos nossos programas. Os aliases adicionam complexidade e pioram a legibilidade dos programas. As referências são muito úteis quando usadas para listas de argumentos de funções.

Estruturas e tipos de dados enumerados

Em C podemos ter:

struct name_adr { ou typedef struct {

char name[30]; char name[30];

int age; int age;

float salary; float salary;

}; } name_adr;

struct name_adr pessoa; name_adr pessoa;

struct name_adr pessoal[100]; name_adr pessoal[100];

/* struct adiciona redundância

O C não tem built-in para novos tipos de dados. Em C, typedef resolve a questão da redundância de se estar sempre a escrever struct em cada declaração de variável.

Em C++: struct name_adr { char name[30]; int age; float salary; }; name_adr pessoa; name_adr pessoal[100];

Em C++, assim que se declara um tipo de dados novo (estrutura, enum ou union), o C++ não se esquece desse tipo de dados, pelo que não é preciso estar sempre a repetir struct.

(8)

Elaborado por Ana Paula Costa Em C:

enum color { preto, azul, verde };

enum color c1, c2, c3; // variáveis que ficam com os valores do enumerado union bitflags { int i1; int i2; float f1; };

union bitflags memória1, memória2; // define duas variáveis union

Em C++, é semelhante ao caso das estruturas, já não precisa de repetir a palavra reservada na declaração das variáveis desse tipo, porque o C++ não se esqueceu do tipo. O exemplo de cima em C++, ficaria:

enum color { preto, azul, verde };

color c1, c2, c3; // variáveis que ficam com os valores do enumerado union bitflags { int i1; int i2; float f1; };

bitflags memória1, memória2; // define duas variáveis union

Alocação de memória em C e C++

Em C, para alocar e libertar memória da heap, usa-se as funções malloc ( ) e free ( ). A vantagem de se alocar memória dinamicamente (na altura da execução do programa, em oposto a declarar todos os dados na altura da compilação usando declarações de variáveis) é que se vai utilizar apenas a memória que é necessária nesse momento. Exemplo, só para recordar como se faz:

struct inventory { char partno[8]; char partname[30]; … }; inventory *parts[1000];

parts[26] = (inventory *) malloc (sizeof (inventory));

Em C++, as funções malloc ( ) e free ( ) são suportadas e requerem as bibliotecas alloc.h ou stdlib.h. Mas o C++ oferece uma forma muito melhor de alocar e libertar memória, com os dois operadores new e delete. new é equivalente ao malloc ( ) e delete é equivalente a free ( ). Como new e delete são operadores e não funções, não requerem o typecasting ou o sizeof () para saber o tamanho correcto de memória a alocar.

Em C++, o exemplo de cima ficaria: struct inventory {

char partno[8]; char partname[30]; …

};

parts[26] = new inventory; // aloca um novo item ao inventário

new retorna um ponteiro para a memória que acabou de ser alocada. Se não existir memória livre suficiente para satisfazer o pedido de alocação de memória, new retorna null. Alguns exemplos:

char * char = new char; // aloca um novo caracter char * ivar = new int; // aloca um novo inteiro

float * farray = new float[50]; // aloca um array de floats. farray é um ponteiro que // aponta para um array de 50 floats.

float * farray = new float[50] (0.0); // aloca e inicializa a zeros todo o array Para libertar a memória usada pelo array, usar delete:

delete [ ] farray; // liberta os 50 floats, não é necessário colocar 50 dentro dos [ ], // embora se possa fazer

Se a memória para um array é alocada com new é bom garantir que vai ser libertada por delete [ ], para que o C++ saiba que é para libertar todo o array e não apenas o ponteiro para o array.

(9)

Nunca misturar new e delete com malloc ( ), free ( ) e outras funções relacionadas. new e delete não funcionam bem com essas funções.

Para tratar de erros de alocação pode-se usar a função set_new_handler ( ) da biblioteca new.h, para verificar automaticamente se a memória foi alocada com sucesso ou não.

Capítulo 4 – O C++ e as vantagens das funções

Uma das mais importantes vantagens não OO do C++ em relação ao C, é a manutenção inteligente de funções. Através da sobrecarga de funções (overload) e de listas de argumento por defeito, podemos escrever código mais legível que é executado com maior exactidão e é mais fácil de manter.

O C++ também “limpa” a passagem de valores por endereço do C. O C++ permite passar referências para funções, eliminando a necessidade de preceder parâmetros com &, como acontecia no C.

Não é preciso enviar valores a todas as funções que recebem argumentos. O C++ permite a especificação de listas de argumentos por defeito. As funções assumem esses valores se não lhes for passado nenhum argumento.

O C++ também permite escrever mais do que uma função com o mesmo nome, é a chamada sobrecarga de funções. Desde que a lista de argumentos seja diferente entre as funções, o C++ consegue determinar qual das funções deverá ser chamada (qual o código a utilizar).

Passar variáveis referência por endereço

Ao contrário do C, o C++ permite a passagem por referência, para além da passagem por valor e por endereço (que o C também permite).

• Passagem por valor:

Por defeito, o C e o C++ passam argumentos por valor (por vezes também chamado por cópia). Isto é, uma cópia do valor na função de chamada é enviado para a função receptora. Quando a função receptora modifica o valor que lhe foi passado, modifica apenas uma cópia, de tal forma que o valor na função de chamada continua inalterado. Genericamente:

Na declaração: tipo_retorno nome_funcao (tipo argumento1, tipo argumento2, …) Na chamada: nome_funcao (val1, val2, …); // o tipo e numero de parâmetros tem que

// corresponder à lista de argumentos da função Só com os arrays é que não é assim: O nome do array já representa o endereço onde o array começa, só por si, sem * ou &. Quando um array é passado, é passada uma cópia do endereço do argumento. Assim, quando o array é alterado na função receptora, o array na função de chamada é também alterado.

Passar dados por valor protege o conteúdo original dos dados mas a passagem por valor ocupa memória extra e leva mais tempo.

• Passagem por endereço:

Na passagem por endereço, é passado o endereço do argumento em vez da cópia do valor do argumento. Assim sendo, como se está a passar a localização de memória onde está o argumento, alterações feitas ao argumento na função receptora, vão-se reflectir também no argumento na função de chamada, ambos são alterados. Genericamente:

Na declaração: tipo_retorno nome_funcao (tipo *argumento1) Na chamada: nome_funcao (&val1);

• Passagem por referência:

Na passagem por referência, só válida para C++, não é preciso usar & e *, o que se torna uma vantagem. Quando se passa uma variável referência está-se a passar um alias da variável, ou seja, está-se a passar não apenas o endereço, mas o próprio valor, que na função receptora poderá ter um nome diferente (alias), mas que é exactamente o mesmo valor. Por isso não é preciso usar & e * como acontece na

(10)

Elaborado por Ana Paula Costa passagem por endereço. Claro que se o valor for alterado na função receptora, ele também é alterado na função de chamada. Genericamente:

Na declaração: tipo_retorno nome_funcao (tipo &argumento1) Na chamada: nome_funcao (val1);

Deve-se usar referências com cuidado. Os programadores de C++ reservam a passagem por referência para as seguintes situações:

Quando querem poupar tempo e memória, e por isso não querem utilizar a passagem por valor. Quando o valor passado não vai ser alterado na função receptora. Nesta situação usa-se a palavra reservada const para garantir que o valor é passado e não é alterado na função receptora.

Listas de argumentos por defeito

Os argumentos por defeito devem ser listados na declaração da função. Assim, o C++ assume que se pretende passar para a função os argumentos especificados na lista por defeito. A não ser que na chamada da função se passem valores diferentes. Claro que a lista pode conter vários argumentos por defeito, e não apenas um. Genericamente:

Na declaração: tipo_retorno nome_funcao (tipo arg1=valor1, tipo arg2=valor2, …)

Na chamada: nome_funcao ( ); // se for sem nenhum argumento, ficam todos os valores por // defeito

Um exemplo:

#include <iostream.h> #include <iomanip.h>

void fun (int i=5, long j=40034, float x=10.25, char c=’Z’, double d=4.3234); //declaração void main ( )

{

fun ( ); // usa todos os valores por defeito

fun (2); // primeiro valor por defeito fica sem efeito

fun (2, 34565, 23.88); // primeiro, segundo e terceiro valores por defeito não usados fun (2, 34565, 23.88, ‘G’, .0023); // sem valores por defeito

}

void fun (int i, long j, float x, char ch, double d) {

cout << setprecision(4) << “i: “ << i << “ “ << “j: “ << j ; cout << “x: “ << x << “ “ << “ch: “ << ch;

cout << “ d:” << d << “\n”; return;

}

Sobrecarga de funções (overloading)

Ao contrário do C, em C++ podemos ter mais do que uma função com o mesmo nome (funções sobrecarregadas). A única condição é que a lista de argumentos tem que ser diferentes entre elas. A sobrecarga de funções permite ter funções similares que trabalham sobre diferentes tipos de dados. Um exemplo:

// sem sobrecarga // com sobrecarga

#include … #include …

int iabs (int j) int abs (int j)

{ { if (j < 0) if (j < 0) { return ( j * -1); } { return ( j * -1); } else else { return (j); } { return (j); } } }

(11)

float fabs (float x) float abs (float x) { { if (x < 0.0) if (x < 0.0) { return (x * -1.0); } { return (x * -1.0); } else else { return (x); } { return (x); } } }

void main ( ) { void main ( ) {

int ipos, val1=3; int ipos, val1=3;

float fpos, val2=-2.8; float fpos, val2=-2.8;

… …

ipos = iabs(val1); ipos = abs(val1);

fpos = fabs(val2); fpos = abs(val2);

} }

Fazer a sobrecarga de funções simplifica bastante a programação. Em vez de ser preciso saber vários nomes diferentes de funções que fazem essencialmente o mesmo, só é preciso saber o nome de uma função. O C++ encarrega-se de passar os argumentos para a função apropriada.

Atenção: Se duas ou mais funções diferem apenas no tipo de retorno, o C++ não pode fazer a sua sobrecarga, porque deixa de ter uma forma de as distinguir. Se a diferença for apenas no tipo de retorno, essas funções têm que ter nomes diferentes e deixa de haver sobrecarga.

Capítulo 5 – Classes em C++

Um dos conceitos mais importantes em POO é manter os dados afastados das funções que não necessitam desses dados. Esconder os dados pode ser tão simples como utilizar variáveis locais, mas o C++ tem uma forma melhor de o conseguir, através das classes.

A maior vantagem do C++ é a possibilidade de se esconder dados. Esconder dados não é mais do que proteger os dados, apenas aquelas funções que deveriam alterar os dados podem efectivamente alterar os dados. Se se compreende o conceito de variável local compreende-se o conceito de esconder dados. Contudo, em vez de as funções conterem variáveis, no C++, as variáveis é que têm funções! São as próprias variáveis que controlam quais as funções que lhes podem aceder.

O C++ fornece uma nova forma de armazenar dados: a classe. A classe alarga a capacidade das estruturas do C++ e forma a fundação de POO.

Classes e estruturas (struct) são muito similares em C++. Declaram-se classes tal como se declaram estruturas, é só substituir struct por class.

Classe versus objectos: Objectos são instâncias da classe.

Classe Objecto

Descrição de como o objecto deve ser. Simplesmente uma variável que é uma

instância da classe.

Exemplo: Exemplo:

class pagamento { pagamento empregado;

char nome[30];

float salario; // empregado é um objecto da classe

}; // pagamento

(12)

Elaborado por Ana Paula Costa

Membros públicos e privados

Claro que existe mais uma diferença entre uma classe e uma estrutura em C++, para além das palavras class e struct (caso contrário não se justificava a utilização de classes). A diferença determina que código tem acesso aos dados membro da classe. Todos os membros de uma estrutura são públicos, o que significa que qualquer função pode ler, alterar ou apagar esses dados membro. Todos os membros de uma classe, por defeito, são privados, o que significa que nenhuma função do programa pode ler, alterar ou apagar esses membros.

Existem três palavras reservadas em C++ que são designadas especificadores de acesso: private, public e protected.

Tornando public os membros de uma classe, dá acesso aos dados dessa classe para o resto do programa. Se todos os membros da classe forem públicos, deixa de haver diferença entre a classe e a estrutura: class Data { tem o mesmo efeito de: struct data {

public: int dia;

int dia; int mes;

int mes; int ano;

int ano; };

};

Capítulo 6 – Funções membro

Como se consegue acesso aos dados privados de uma classe? Este capítulo ensina um novo conceito, que não existe em C: O conceito de função membro.

Uma função membro é apenas isso: Uma função que é membro de uma classe. No C++, as estruturas e as classes podem conter, para além dos dados membro, também funções membro.

Exemplo: class Data {

int dia; // dados membro privados int mes;

int ano; public:

int bonus_flag; // dado membro publico int get_mes (void); // funções membro públicas int get_dia (void);

int get_ano (void);

void set (const int d, const int m, const int a); };

O exemplo representa uma declaração de uma classe chamada Data, com dados e funções membro. Existem dados privados, que não podem ser acedidos nem por main ( ) nem por nenhuma outra função. Existe um dado membro público, bonus_flag, a que main ( ) e outras funções podem aceder. E contem ainda quatro declarações de funções membro. As funções membro ainda não estão completas, aqui está feita apenas a sua declaração, o código da função pode ser definido dentro da classe, logo a seguir à declaração, ou então o código das suas funções membro pode ser definido depois.

Resumindo, a classe contem dados normais e também comandos (funções membro) que actuam sobre esses dados.

O C++ modela o mundo real com maior exactidão do que o C. As funções membro fornecem às classes (e estruturas) capacidades activas sobre os dados. Com ambos, dados e funções membro, consegue-se modelar objectos do mundo real. Na terminologia do C++, os objectos das classes têm propriedades (os dados membro) e têm comportamento (as funções membro). Em C e outras linguagens não orientadas a objectos, podemos modelar apenas os dados (as propriedades).

(13)

Porque são as funções membro públicas, no exemplo? Têm que ser públicas, porque se não forem, nenhuma outra função as pode chamar. As funções membro públicas são o meio de aceder aos membros de dados privados. As funções membro têm acesso aos dados privados, e apenas elas têm acesso. Existem excepções como veremos mais à frente, mas para já aceitem este facto: Os dados membro provados só podem ser acedidos por funções membro da mesma classe.

NOTA: Na terminologia do C++, as funções membro são por vezes chamadas métodos da classe.

A necessidade de funções membro

O código que se segue é a definição completa da classe. Está “completa” porque temos os dados membro e também o código completo para cada função membro. Claro que se poderia adicionar muito mais a esta classe, mas como está é uma boa introdução para se compreender funções membro.

class Data {

int dia; // dados membro privados int mes;

int ano; public:

int bonus_flag; // dado membro publico int get_mes (void) // funções membro públicas

{ return (mes); } // função inline, não necessita da palavra reservada inline int get_dia (void)

{ return (dia); } int get_ano (void) { return (ano); }

void set (const int d, const int m, const int a) { dia = d; mes = m; ano = a; return; }

}; // fim da declaração da classe, termina com ;

Data hoje, amanha; // define dois objectos, duas instâncias, da classe

Encapsulamento é a combinação de dados membro e funções membro, num objecto (numa cápsula). O código e os dados não estavam ligados em C, e demasiadas funções tinham demasiado acesso aos dados. O corpo de um programa em C++ fica bem mais pequeno, porque o código é retirado do “bolo” geral do programa, e é colocado junto com os dados que vai trabalhar. Há uma compartimentalização do código em segmentos fáceis de manter (os objectos), o que diminui o tempo e os erros de programação.

Escopo da classe: Aceder aos membros privados

Os objectos hoje e amanha são variáveis globais e são visíveis (podem ser usadas) pela função main ( ). Mesmo que main ( ) possa aceder aos objectos hoje e amanha, main ( ) não tem acesso a todas as partes dessas variáveis. O C++ tem um escopo adicional que não existe em C: É o chamado escopo da classe. Os membros privados da classe têm escopo de classe, são visíveis apenas dentro da classe, e não têm escopo global, nem mesmo escopo local.

Acessores

O único propósito das três funções membro get_dia ( ), get_mes ( ) e get_ano ( ), é informar quais os valores dos membros de dados privados. A maioria das classes tem funções que retornam os valores dos seus membros. A este tipo de funções é chamado funções de acesso – acessores. Elas acedem aos dados privados para o resto do programa. O resto do programa pode usar as funções membro públicas, assim quando o programa necessita de saber o valor de um membro de dados privado, o programa pode chamar uma das funções de acesso para saber um valor particular dessa classe. Pode-se dar um nome qualquer

(14)

Elaborado por Ana Paula Costa aos acessores, só não podem ter o mesmo nome dos dados. Nem todas as classes precisam de acessores, embora a maioria os tenha.

Genericamente, para chamar uma função membro: nome_objecto.nome_funcao ( ); Exemplo: cout << amanha.get_dia( );

Também se pode chamar uma função membro usando o operador ->, o operador -> requer um ponteiro do lado esquerdo e um membro do lado direito (tal como as estruturas normais em C). Exemplo:

Data * ptr = hoje; // define um ponteiro para o objecto hoje ptr -> get_dia ( ); // chama a função get_dia ( ) de hoje

Claro que antes de se aceder ou imprimir os membros de dados da classe Data, é preciso inicializá-los (dar-lhes um valor com significado). É isso que faz a função membro set ( ); atribui valores aos dados membro da classe.

Para chamar: amanha.set (21,8,1999);

Em terminologia do C++, não se está a chamar uma função, está-se a enviar uma mensagem a um objecto. O objecto sabe como fazer várias coisas a partir das funções membro. O programador só tem que dizer ao objecto o que quer que ele faça.

Exemplo de um programa completo usando a classe Data: #include <iostream.h>

class Data {

int dia; // dados membro privados int mes;

int ano; public:

int bonus_flag; // dado membro publico int get_mes (void) // funções membro públicas

{ return (mes); } // função inline, não necessita da palavra reservada inline int get_dia (void)

{ return (dia); } int get_ano (void) { return (ano); }

void set (const int d, const int m, const int a) { dia = d;

mes = m; ano = a; return; }

}; // fim da declaração da classe, termina com ; void main ( )

{

Data data1, data2; // define duas variáveis da classe, dois objectos data1.set (4,7,2000); // inicializa os dados membro da primeira variável data2.set (31,12,2003); // inicializa os dados membro da segunda variável // mostra as datas usando os acessores

cout << data1.get_dia ( ) << “/” << data1.get_mes ( ) << “/” << data1.get_ano ( ) << “\n”; cout << data2.get_dia ( ) << “/” << data2.get_mes ( ) << “/” << data2.get_ano ( ) << “\n”; }

Ao desenvolver uma classe, deve-se incluir todas as funções que o resto do programa necessita para aceder aos dados privados. Se o resto do programa não é suposto alterar os dados, não se deve escrever funções membro públicas que permitam ao programa fazê-lo.

(15)

Quanto mais trabalho o objecto fizer (verificação de erros, etc.), menos o programador que utiliza esse objecto terá que fazer.

Sempre que exista uma função da classe que é chamada apenas por outra função membro, deve-se declarar essa função como privada, uma vez que o resto do programa não tem que ter acesso a ela.

A classe Data ainda mais completa: #include <iostream.h>

class Data {

int dia; // dados membro privados int mes;

int ano;

int testa_dia (const int d) // funções membro privadas { return (d>31) ? 0 : 1; }

int testa_mes (const int m) { return (m>12) ? 0 : 1; } int testa_ano (const int a) { return (a<1990) ? 0 : 1; } public:

int bonus_flag; // dado membro publico int get_mes (void) // funções membro públicas

{ return (mes); } // função inline, não necessita da palavra reservada inline int get_dia (void)

{ return (dia); } // mais uma função inline int get_ano (void)

{ return (ano); }

int set (const int d, const int m, const int a) {

if (testa_dia (d) && testa_mes (m) && testa_ano (a)) { dia = d;

mes = m; // definição do código da função ano = a;

return (1); }

else return (0); // valores errados }

}; // fim da declaração da classe, termina com ; void main ( )

{

Data hoje; // define um objecto da classe int d, m, a;

cout << “Que dia, mês e ano é hoje? “;

cin >> d >> m >> a; // recebe valores do utilizador if (hoje.set (d,m,a)) // retorna 1 se a data estiver ok {

cout << “A data de hoje é: “ << hoje.get_dia ( ) << “/” << hoje.get_mes ( ); cout << hoje.get_ano ( ) << “\n”;

} else

cout << “A data introduzida foi incorrecta.”; }

(16)

Elaborado por Ana Paula Costa Reparem no programa anterior, que as funções que não alteram os seus parâmetros incluem a palavra reservada const antes dos parâmetros. const, embora não seja requerido, assegura que a função não irá alterar o conteúdo dos parâmetros.

Uma outra situação em que const é utilizado, é para garantir que os valores dos dados membro da classe não serão alterados. Se fizermos, por exemplo:

int get_ano const (void) // const nesta localização vai indicar ao compilador de C++ que { return (ano); } // a função membro pode aceder aos valores dos dados, mas

// não os pode alterar

Capítulo 7 – O escopo das funções

Como escrever o código da função membro sem ser na altura da sua declaração?

Funções membro não inline

Objectos são como cápsulas que contêm informação activa. Podemos aceder aos membros privados do objecto (sejam dados ou funções), apenas através das funções membro públicas declaradas na classe. Mas não é necessário colocar todo o código do objecto dentro da declaração da classe. Dentro da declaração da classe tem que ficar apenas a definição dos dados e a declaração das funções. Mas a descrição/definição das funções (o código que vai ser executado) pode ser escrito fora da classe.

Para fazer a descrição fora da classe, genericamente:

tipo_retorno nome_classe::nome_funcao (lista de parâmetros ) //conforme a declaração {

// código // … }

Assim, as classes ficam mais legíveis, mais limpas. A classe Data, por exemplo ficaria: class Data {

int dia; // dados membro privados int mes;

int ano;

int testa_dia (const int d); // funções membro privadas int testa_mes (const int m);

int testa_ano (const int a); public:

int bonus_flag; // dado membro publico int get_mes (void); // funções membro públicas int get_dia (void);

int get_ano (void);

int set (const int d, const int m, const int a); }; // fim da declaração da classe, termina com ;

int Data::testa_dia (const int d) // funções membro privadas { return (d>31) ? 0 : 1; }

int Data::testa_mes (const int m) { return (m>12) ? 0 : 1; } int Data::testa_ano (const int a) { return (a<1990) ? 0 : 1; }

int Data::get_mes (void) // funções membro públicas { return (mes); }

(17)

int Data::get_dia (void) { return (dia); } int Data::get_ano (void) { return (ano); }

int Data::set (const int d, const int m, const int a) {

if (testa_dia (d) && testa_mes (m) && testa_ano (a)) { dia = d;

mes = m; // definição do código da função ano = a;

return (1); }

else return (0); // valores errados }

Reparem no operador ::, quando se escreve código de funções membro sem ser inline, deve-se sempre preceder o nome da função com o nome da classe e ::. A razão é que duas ou mais classes podem ter funções com o mesmo nome, e então o operador :: (chamado resolução de escopo, permite fazer a distinção). Também ajuda o compilador e o programador a diferenciar as funções membro da classe das funções regulares.

Mas também não é preciso definir todas as funções fora da classe. As funções com uma ou duas linhas de código podem ser definidas inline (dentro da classe), as funções maiores (com três ou mais linhas de código) devem ser definidas fora da classe.

De novo a classe Data: class Data {

int dia; // dados membro privados int mes;

int ano;

int testa_dia (const int d); // funções membro privadas não inline int testa_mes (const int m);

int testa_ano (const int a); public:

int bonus_flag; // dado membro publico

int get_mes (void) // funções membro públicas inline { return (mes); }

int get_dia (void) { return (dia); } int get_ano (void) { return (ano); }

int set (const int d, const int m, const int a); // função membro pública não inline };

int Data::testa_dia (const int d) // funções membro privadas não inline { return (d>31) ? 0 : 1; }

int Data::testa_mes (const int m) { return (m>12) ? 0 : 1; } int Data::testa_ano (const int a) { return (a<1990) ? 0 : 1; }

int Data::set (const int d, const int m, const int a) // função membro pública não inline {

(18)

Elaborado por Ana Paula Costa { dia = d;

mes = m; // definição do código da função ano = a;

return (1); }

else return (0); // valores errados }

Se a função for suficientemente pequena (uma a três linhas de código), podemos pedir para que a função membro definida for a da classe seja considerada inline. Para isso é só colocar a palavra reservada inline no início da definição da função:

inline int Data::testa_dia (int d) // função inline { … }

O C++ pode não respeitar o pedido de inline.

Uma função inline é uma função cuja chamada é substituída pelo código da função imediatamente, e não na altura da compilação.

Separar o código da classe do código do programa

Devemos, por questões de organização e legibilidade, fazer da seguinte forma: • Declaração da classe – ficheiro com extensão .h (ficheiro cabeçalho, header file).

• Definição de funções da classe – ficheiro com extensão .cpp (com #include “ficheiro.h” e sem função main ( ).

• Programa que usa a classe – ficheiro com extensão .cpp (com #include “ficheiro.h” e com função main ( ), onde se usam objectos da classe, para vários fins.

A mesma função, objectos diferentes: Polimorfismo

Polimorfismo: Quando a mesma função trabalha/funciona em objectos de tipos diferentes. Quando a função assume várias formas diferentes. Atenção, neste caso, os nomes das funções é o mesmo, mas o código para cada uma delas é diferente.

O ponteiro this

O C++ envia para todas as funções membro, um argumento escondido que não consta da lista de argumentos (mesmo que seja void, o C++ passa um ponteiro escondido). O nome desse ponteiro secreto passado pelo C++ é this.

Quando se chama uma função membro, ao preceder o nome da função com o nome do objecto, o C++ pega no endereço desse objecto e passa-o para a função membro que foi chamada. Claro que na classe tem que existir um endereço para receber o objecto, mas esse parâmetro extra também não aparece na declaração da função membro. É algo que o C++ faz por si, mas escondido.

O que nos aparece é: Data hoje;

hoje.set (13,1,2004); // chamada da função

void set (const int d, const int m, const int a); // declaração da função, na classe Mas o que realmente lá está é:

Data hoje;

set (&hoje,13,1,2004); // chamada da função

void set (Data * this, const int d, const int m, const int a); // declaração da função, na classe O C++ chama o ponteiro para o objecto escondido this e precede todos os dados membros com uma referência para this:

(19)

void Data::set (const int d, const int m, const int a) { dia = d; mes = m; ano = a; return; }

Internamente, o C++ altera para:

void Data::set (Data * this, const int d, const int m, const int a) { this->dia = d;

this->mes = m; this->ano = a; return; }

Não é preciso haver preocupações com o argumento extra. Não temos que nos preocupar com isso. Podemos aceder ao objecto, dentro da função membro, usando *this. Nem sempre é preciso utilizar *this, mas por vezes dá geito. Uma das ocasiões em que se utiliza *this é para retornar o objecto da função membro, com:

return (*this);

NOTA: *this é uma constante, não se deve tentar mudar.

Capítulo 8 – Construtores e destruidores

Construtores e destruidores são funções membro.

Os construtores são uma forma de assegurar que os objectos são definidos com valores iniciais. Servem para inicializar os objectos da classe. Os destruidores são importantes quando é necessário libertar a memória correspondente a um objecto que foi alocado com o construtor.

Uma classe pode ter um ou mais construtores e apenas um destruidor, no máximo. Ao contrário das restantes funções membro, os construtores e destruidores têm regras de nomeação sem estritas, são as seguintes:

• Os construtores têm que ter o mesmo nome que a própria classe. • destruidor tem o mesmo nome da classe precedido com ~.

Os construtores retornam sempre um objecto da classe inicializado, pelo que o tipo de retorno não é necessário nem permitido. Os construtores podem ter argumentos. Os argumentos permitem fazer sobrecarga de construtores (é a forma que o compilador tem de diferenciar entre vários construtores para uma mesma classe). O destruidor não tem nem tipo de retorno nem argumentos.

Ao contrário das outras funções membro, nunca se deve chamar os construtores e destruidores explicitamente. O compilador chama-os automaticamente quando se define o objecto (chama o construtor) e quando ele sai do escopo (chama o destruidor).

Construtores

Vamos supor que se definem as seguintes variáveis: int limite 100;

float peso = 80.0;

O C e o C++ fazem o seguinte: Chamam um construtor interno para reservar memória para as duas variáveis, atribuem-lhes os nomes e colocam os seus respectivos valores lá dentro.

A memória não estava alocada antes da definição, e depois do espaço estar reservado, fica reservado até que as variáveis fiquem for a de escopo. Nessa altura, os dois compiladores libertam a memória usada pelas variáveis, chamando um destruidor interno. As variáveis e os seus valores desaparecem completamente.

(20)

Elaborado por Ana Paula Costa O C e o C++ não têm qualquer problema ao chamar os construtores e destruidores internos para todos os tipos de dados built-in, tipos de dados que já vêm definidos com as linguagens. Mesmo que não se inicialize os valores:

int limite; float peso;

O processo é idêntico, com a única diferença que no espaço reservado para a variável se encontram valores desconhecidos. Nem o C, nem o C++ colocam a memória a zeros automaticamente (a única excepção são as variáveis static), pelo que é da responsabilidade do programador a inicialização das variáveis.

O C++ introduz um novo problema que são os tipos de dados struct e class, que são definidos pelo programador. O C++ não consegue ler a mente do programador para saber o que ele deseja quando define a classe. O C++ faz o melhor que pode com a informação que tem sobre a nova classe, mas as coisas funcionam muito melhor quando o programador diz ao C++ como ele deve definir os dados da classe, e isso é feito com o construtor. Com o construtor, o compilador declara, define e inicializa adequadamente os objectos, exactamente da forma que se pretende.

#include <iostream.h> #include <iomanip.h> class Amostra { char c; int j; float x; public:

Amostra ( ); // função construtor void print_it (void);

};

Amostra::Amostra // código do construtor {

c = ‘A’; j = 1; x = 2.0; }

inline void Amostra::print_it (void) {

cout << setprecision (1);

cout << “c, j e x são: “ << c << “, ” << j << “e ” << x << endl; }

void main ( ) {

Amostra avar; // define o espaço de memória para avar e chama o construtor avar.print_it ( );

}

A única coisa que main ( ) faz é definir o objecto da classe, chamado avar. A classe Amostra depois trata do resto da inicialização. Quando em main ( ) é feita a definição de avar, o C++ chama o construtor da classe Amostra, que vai inicializar os membros de avar.

Resumindo, os passos seguintes acontecem quando se define um objecto (uma variável de uma classe) e quando existe construtor para a classe:

1. C++ reserva memória para o objecto. 2. C++ chama o construtor.

(21)

Argumentos nos construtores

Como um construtor actua como qualquer outra função membro (excepto na forma como é chamada), pode-se fornecer uma lista de argumentos, passando valores para o construtor e utilizando listas de argumentos por defeito. Os programadores frequentemente utilizam os construtores para inicializar os valores dos dados a zero. Estes construtores são bons candidatos para listas de argumentos por defeito. Eis um exemplo com a classe Amostra:

#include <iostream.h> #include <iomanip.h> class Amostra { char c; int j; float x; public:

Amostra (char, int float ); // função construtor void print_it (void);

};

Amostra::Amostra (char p1=’A’, int p2=0, float p3=0.0 ) // código do construtor {

c = p1; j = p2; x = p3; }

inline void Amostra::print_it (void) {

cout << setprecision (1);

cout << “c, j e x são: “ << c << “, ” << j << “e ” << x << endl; }

void main ( ) {

Amostra avar; // define o espaço de memória para avar e chama o construtor avar.print_it ( );

}

Como com qualquer lista de argumentos por defeito, pode-se especificar outros valores diferentes quando se define o objecto da classe, substituindo os valores por defeito.

No caso do construtor ter argumentos, como fazer se não existe chamada da função construtor? É só colocar os argumentos entre parêntesis depois da definição do objecto. Por exemplo:

Amostra avar (‘Q’, 10); // substitui os dois primeiros argumentos, o terceiro argumento fica com // o seu valor por defeito

Se o objecto deve receber a lista de argumentos por defeito, nunca colocar ( ) à frente do seu nome: Amostra avar ( ); // não está correcto

Deve-se retirar os parêntesis se se pretender usar todos os valores por defeito da lista de argumentos.

Construtores por defeito

Construtores por defeito são construtores que não têm parâmetros, ou que não necessitam de nenhum parâmetro porque foram especificados parâmetros por defeito:

(22)

Elaborado por Ana Paula Costa e

Amostra:: Amostra ( )

São dois exemplos de construtores por defeito.

Construtores e arrays

Os construtores são vitais quando se pretende declarar um array de objectos e se tem um construtor chamado para cada elemento do array.

#include <iostream.h> #include <iomanip.h> class Amostra { char c; int j; float x; public:

Amostra (char, int float ); // função construtor void print_it (void);

};

Amostra::Amostra (char p1=’A’, int p2=0, float p3=0.0 ) // código do construtor {

c = p1; j = p2; x = p3; }

inline void Amostra::print_it (void) {

cout << setprecision (1);

cout << “c, j e x são: “ << c << “, ” << j << “e ” << x << endl; }

void main ( ) {

const count=5; // por defeito o tipo da variável será int

Amostra a[count]; // reserva espaço de memória e inicializa todos os elementos do array for (int j=0; j<count; j++)

{

a[j].print_it ( ); // mostra o conteúdo do array }

}

O C++ chama sempre um construtor quando se define um objecto. Uma destas quatro situações é possível:

1. Não se fornece um construtor, o C++ chama o seu próprio, geralmente reservando espaço para o objecto, mas não o inicializando.

2. Se se fornecer um construtor, o C++ utiliza-o.

3. Se se fornecer vários construtores, fazendo sobrecarga, o C++ executa aquele cujos parâmetros coincidem com os argumentos na chamada. Como qualquer outra sobrecarga de funções, é preciso garantir que não existem dois construtores que tenham listas de argumentos iguais.

4. Se se fornecer mais do que um construtor por defeito, o C++ vai ficar confundido, porque assim vai haver duas sobrecargas iguais.

(23)

Deve-se sempre fornecer construtores para os objectos, mesmo que o construtor tenha uma lista de parâmetros vazia e um corpo vazio. Assim, a classe fica mais rica e mais fácil de alterar no futuro. Já que se está a fornecer um construtor por defeito, podemos inicializar os dados a um valor inicial, normalmente zero.

Se se fornecer um construtor, mas não um construtor por defeito, o C++ vai mostrar um erro de compilação se se tentar definir um objecto sem usar os argumentos de acordo com o construtor fornecido. O C++ não utiliza o seu construtor interno se se fornecer algum construtor.

O exemplo que se segue é idêntico ao anterior, com a diferença de que o array é alocado dinamicamente, com o operador new:

#include <iostream.h> #include <iomanip.h> class Amostra { char c; int j; float x; public:

Amostra (char, int float ); // função construtor void print_it (void);

};

Amostra::Amostra (char p1=’A’, int p2=0, float p3=0.0 ) // código do construtor {

c = p1; j = p2; x = p3; }

inline void Amostra::print_it (void) {

cout << setprecision (1);

cout << “c, j e x são: “ << c << “, ” << j << “e ” << x << endl; }

void main ( ) {

const count=5; // por defeito o tipo da variável será int

Amostra *a = new Amostra[count]; // reserva espaço de memória e inicializa todos os // elementos do array

for (int j=0; j<count; j++) {

a[j].print_it ( ); // mostra o conteúdo do array }

delete [ ] a; // os [ ] garantem que delete vai apagar os count elementos do objecto }

Sem o delete, o C++ iria libertar a memória de a (o ponteiro) quando ficasse fora do escopo, mas não o array apontado por a. Os [ ] são necessários para informar o C++ que um array, e não apenas um valor único, tem que ser apagado.

Destruidores

Uma classe pode ter no máximo um destruidor. O C++ chama sempre o destruidor quando o objecto sai do escopo.

(24)

Elaborado por Ana Paula Costa Uma necessidade importante e óbvia para limpeza através do destruidor é quando a classe contem um ponteiro para memória alocada dinamicamente. Quando o ponteiro sai do escopo, o compilador liberta a memória do ponteiro, mas não liberta a memória que está a ser apontada pelo ponteiro. Em vez de ser o programa a ter que se preocupar com isso, deve ser o próprio objecto a fazê-lo, através do destruidor. Mais uma vez, quanto mais o objecto fizer, menos o programa que utiliza essa classe terá que fazer, menos tempo leva em debugging e mais rapidamente se produz código.

Propriedade do destruidor:

1. nome do destruidor é o nome da classe precedido de ~. 2. Não tem tipo de retorno.

3. Não tem argumentos.

4. Cada classe tem no máximo um destruidor.

5. compilador chama automaticamente o destruidor de um objecto quando o objecto sai do escopo. Aqui está o programa da classe Amostra, agora com uma função destruidor, que neste caso apenas envia uma mensagem para o écran:

#include <iostream.h> #include <iomanip.h> class Amostra { char c; int j; float x; public:

Amostra (char, int float ); // função construtor ~Amostra ( ); // função destruidor

void print_it (void); };

Amostra::Amostra (char p1=’A’, int p2=0, float p3=0.0 ) // código do construtor { c = p1; j = p2; x = p3; } Amostra::~Amostra ( ) {

cout << “chamada ao destruidor…\n”; }

inline void Amostra::print_it (void) {

cout << setprecision (1);

cout << “c, j e x são: “ << c << “, ” << j << “e ” << x << endl; }

void main ( ) {

const count=5; // por defeito o tipo da variável será int

Amostra *a = new Amostra[count]; // reserva espaço de memória e inicializa todos os // elementos do array

for (int j=0; j<count; j++) {

a[j].print_it ( ); // mostra o conteúdo do array }

(25)

delete [ ] a; // os [ ] garantem que delete vai apagar os count elementos do objecto }

Reparem bem no resultado final do programa: c, j e x são A, 0 e 0.0 c, j e x são A, 0 e 0.0 c, j e x são A, 0 e 0.0 c, j e x são A, 0 e 0.0 c, j e x são A, 0 e 0.0 chamada ao destruidor… chamada ao destruidor… chamada ao destruidor… chamada ao destruidor… chamada ao destruidor…

A instrução delete [ ] a; chama o destruidor para cada um dos objectos no array. Mas o objecto tem que ter lá o destruidor para que a memória por ele ocupada seja libertada. Caso contrário, apaga apenas o ponteiro, mas não os objectos em si.

Capítulo 9 – Classes e funções friend

Vamos agora aprender como chegar até aos dados escondidos. O C++ oferece uma forma das funções acederem aos membros privados de uma classe mesmo que essas funções não sejam membros da classe. Algumas classes e funções necessitam de ter acesso aos membros privados de outra classe, quando trabalham muito próximas umas das outras.

Funções friend podem aceder a membros privados de outras classes. Classes friend podem aceder a membros privados de outras classes. As funções friend são usadas normalmente para fazer sobrecarga de operadores (ver capítulo seguinte). Deve-se usar friend com moderação, não abusar.

Uma função friend é como outra função qualquer, com a particularidade de poder aceder às partes privadas de uma classe, como se fosse membro da classe. As funções friend podem ser, ou não, funções membro de outra classe. Eis um exemplo de uma função friend:

class Inventory { char partno[5]; int quantity; public: Inventory ( ); // construtor ~Inventory ( ); // destruidor

friend void reset (Inventory i, int const val); };

void reset ( Inventory i, int const val) { i.quantity = val;

}

A função reset ( ) altera o valor de um dos membros privados de Inventory, não sendo contudo uma função membro da classe. reset ( ) é simplesmente uma função que tem acesso aos membros privados de Inventory. De notar que é preciso passar o objecto de Inventory para reset ( ), como é uma função não membro, a função não tem outra forma de aceder ao objecto.

Para definir uma função como friend é só colocar, na classe que vai permitir o acesso aos seus dados privados, a palavra reservada friend no início da declaração da função.

(26)

Elaborado por Ana Paula Costa Pode-se também fazer uma classe ser friend de outra. Normalmente é utilizado quando todas as funções da classe friend necessitam de ter acesso aos membros de outra classe.

class Parttime { int horas; float taxa; public: void print ( ); }; class Empregado { char nome[30]; int enum;

friend class Parttime; // classe friend };

NOTA: Existem outras formas mais elegantes de conseguir isto, como se verá num dos capítulos mais adiante.

Quando se diz que A é friend de B, isso não faz com que B se torne automaticamente friend de A. É possível, mas tem que se especificar explicitamente.

Se se designar a classe A friend de B, e B friend de C, não fica automaticamente definido que C é friend de A, a não ser que se especifique explicitamente.

A designação de uma classe friend é uma designação simples. Não existe relação de herança, transição ou reversão.

Capítulo 10 – Sobrecarga de operadores

Sobrecarga de operadores é o processo pelo qual se diz ao C++ como deverá aplicar os operadores (+,/, <<, etc.) aos nossos dados abstractos (classes por nós criadas).

Pode-se fazer a sobrecarga de qualquer operador excepto os seguintes: sizeof, ? : , ::, . e *

O procedimento básico para fazer a sobrecarga é o mesmo para a maioria dos operadores em C++, embora possa haver pequenas nuances.

Não se pode redefinir a forma como os operadores trabalham com tipos de dados pré-definidos pela linguagem. Só se pode especificar a forma como os operadores vão trabalhar com os tipos de dados criados por nós. Também não se pode alterar a precedência dos operadores.

Sobrecarga de operadores matemáticos

Se o C++ “vê” um dos seus tipos de dados pré-definidos de ambos os lados de um sinal +, executa a rotina correspondente. Se o C++ “vê” um dos nossos tipos de dados em qualquer dos lados do sinal +, vai procurar se se escreveu uma função de sobrecarga para esse operador, que lhe indique o que deverá fazer. Se encontrar a função de sobrecarga, executa-a. Caso contrário, emite uma mensagem de erro informando que não sabe como somar aquele tipo de dado.

Uma função de sobrecarga de um operador tem a mesma aparência de qualquer outra função em C++, com a excepção que o nome da função tem que ser a palavra reservada operator, seguido do operador que se pretende sobrecarregar. Exemplos:

operator + ( ); operator – ( ); operator * ( ); operator / ( );

(27)

class Empregado { char nome[30]; int enum; float salario; public:

Empregado (char [ ], int, float); // construtor ~Empregado ( ); // destruidor

double operator + (Empregado & e); // sobrecarga de + };

double Empregado::operator + (Empregado & e) // código para a sobrecarga de + {

double soma;

soma = salario + e.salario; return soma;

}

void main (void) { // …

total_sal = pessoal[0] + pessoal[1]; // …

}

A função operator + ( ) retorna um double e requer um objecto da classe Empregado como argumento. Mas o operador + não é um operador binário, ou seja, não necessita de dois operandos para realizar a operação? Sim.

Então, como é que a função operator + ( ) só necessita de um argumento de entrada? Como a função operator + ( ) é uma função membro da classe, o primeiro argumento é passado secretamente para a função, usando o operador *this. Então, o C++ na verdade “vê” a função da seguinte forma, quando a compila:

double Empregado::operator + (*this, Empregado & e) {

double soma;

soma = this->salario + e.salario; return soma;

}

A passagem de parâmetros para dentro das funções de sobrecarga de operadores é feita usando referências (esta é uma convenção praticamente universal entre os programadores de C++).

Não é preciso lembrar nomes de funções quando se quer realizar uma operação simples com um dos nossos tipos de dados, a única coisa que é preciso é escrever a função sobrecarga desse operador, que irá descrever o que fazer com os dados.

Sobrecarga de I/O

O operador << já está sobrecarregado pelo próprio C++. << é o operador bitwise de deslocamento para a esquerda e é também o operador de output quando aparece cout do lado esquerdo. Para este último funcionar não nos podemos esquecer de fazer o include de iostream.h, que é onde se encontra a definição da sobrecarga da função e a descrição do objecto cout. cout é um objecto da classe ostream (stream de saída). As definições para a sobrecarga de >> também se encontram em iostream.h.

A sobrecarga de uma função de saída (operador <<) tem o seguinte aspecto:

ostream & operator << (ostream & c, const int & n); // o segundo argumento pode ser // substituído por qualquer outro tipo de dados

(28)

Elaborado por Ana Paula Costa Esta função necessita de dois e não apenas de um argumento de entrada, porque a função operator << ( ) não é uma função membro da classe ostream, mas é friend da classe. Requer os dois argumentos porque o ponteiro *this não é conhecido, não se tem acesso a ele.

Como é fácil de imaginar, podemos definir o operador << para mostrar os nossos tipos de dados da forma que se desejar. Por exemplo, para a classe Empregado:

class Empregado { char nome[30]; int enum; float salario; public:

Empregado (char [ ], int, float); // construtor ~Empregado ( ); // destruidor

friend ostream & operator << (ostream & out, Empregado & e); // sobrecarga de << };

ostream & operator << (ostream & out, Empregado & e) // código para a sobrecarga de << {

out << “Empregado: “ << e.nome << endl;

out << “Número: “ << e.enum << “Salario: “ << e.salario << endl; return out;

}

void main (void) { Empregado e1(“João”, 12, 150.0); Empregado e2(“Maria”, 16, 203.0); cout << e1; cout << e2; }

A função de sobrecarga do operador << é friend para poder ter acesso aos membros privados da classe, caso contrário não teria acesso a eles.

Agora um exemplo com sobrecarga do operador >>: class Data {

int dia; int mes; int ano;

friend istream & operator >> (istream & in, Data & d); };

istream & operator >> (istream & in, Data & d) {

cout << “Data a introduzir\n”; cout << “Indique o dia: “; cin >> d.dia;

cout << “Indique o mês: “; cin >> d.mes;

cout << “Indique o ano: “; cin >> d.ano;

return in; }

void main (void) {

Referências

Documentos relacionados

Na oportunidade, o presidente Marcelo Queiroz ressaltou a importante parceria do Sistema Fecomércio, através do Sesc e do Senac, com o Sindivarejo local, para a

Para discutir a temática, iniciaremos nossa incursão teórica a partir da concepção de religião implícita em DURKHEIM (1996), mais especificamente sobre a religião como

1) As Operadoras obrigam-se a cumprir o que foi declarado na CPI da Telefonia Móvel em Santa Catarina quanto à execução de investimentos previstos nos termos do Plano de Melhoria

Ganhos significativos em algumas competências socioemocionais a curto prazo (relacionamento com os pares e competência social)?. Os alunos do GI com níveis médios no

Chora Peito Chora Joao Bosco e Vinicius 000 / 001.. Chão De Giz Camila e

observados os demais termos desta Cláusula, das Condições Gerais e as demais Disposições Contratuais. 1.1.1 O pagamento da Indenização relativa a esta Cláusula

A MP altera integralmente tais dispositivos, dando-lhes nova redação. No novo § 1º, substitui-se o texto em que são estabelecidos os parâmetros sobre efetivo aproveitamento por outro

3 — Os membros do Conselho de Escola a que se refere a alínea c) do n.º 1 do artigo 17.º dos Estatutos são eleitos pelo conjunto do pessoal não docente e não investigador..