Professional Documents
Culture Documents
Introduo a POO
Captulo 1
Qualidade do software
O principal objetivo da Engenharia de Software contribuir para a produo de programas de
qualidade. Esta qualidade, porm, no uma idia simples; mas sim como um conjunto de noes e fatores.
desejvel que os programas produzidos sejam rpidos, confiveis, modulares, estruturados,
modularizados, etc. Esses qualificadores descrevem dois tipos de qualidade:
De um lado, considera-se aspectos como eficincia, facilidade de uso, extensibilidade, etc. Estes
elementos podem ser detectados pelos usurios do sistema. A esses fatores atribui-se a
qualidade externa do sofware.
Por outro lado, existe um conjunto de fatores do programa que s profissionais de computao
podem detectar. Por exemplo, legibilidade, modularidade, etc. A esses fatores atribui-se a
qualidade interna do sofware.
Na realidade, qualidade externa do programa em questo que importa quando de sua utilizao. No
entanto, os elementos de qualidade interna so a chave para a conquista da qualidade externa. Alguns
fatores de qualidade externa so apresentados na prxima seo. A seo posterior trata da qualidade
interna, analisando como a qualidade externa pode ser atingida a partir desta.
Robustez
a capacidade do programa de funcionar mesmo sob condies anormais.
A robustez do programa diz respeito ao que acontece quando do aparecimento de situaes anmalas.
Isto diferente de corretude, que define como o programa se comporta dentro de sua especificao. Na
robustez, o programa deve saber encarar situaes que no foram previstas sem efeitos colaterais
catastrficos. Neste sentido, o termo confiabilidade muito utilizado para robustez; porm denota um
conceito mais amplo que melhor interpretado como que englobando os conceitos de corretude e
robustez.
Extensibilidade
definida como a facilidade com que o programa pode ser adaptado para mudanas em sua
especificao.
Neste aspecto, dois princpios so essenciais:
Simplicidade de design: uma arquitetura simples sempre mais simples de ser adaptada ou
modificada
Descentralizao: quanto mais autnomos so os mdulos de uma arquitetura, maior ser a
probabilidade de que uma alterao implicar na manuteno de um ou poucos mdulos.
Capacidade de reuso
a capacidade do programa de ser reutilizado, totalmente ou em partes, para novas
aplicaes.
A necessidade de reutilizao vem da observao de que muitos elementos dos sistemas seguem
padres especficos. Neste sentido, possvel explorar este aspecto e evitar que a cada sistema produzido
os programadores fiquem reinventando solues de problemas j resolvidos anteriormente.
Compatibilidade
a facilidade com que o programa pode ser combinado com outros.
Esta uma qualidade importante pois os programas no so desenvolvidos stand-alone. Normalmente,
existe uma interao entre diversos programas onde o resultado de um determinado sistema utilizado
como entrada para outro.
Outros aspectos
Os aspectos resumidos acima so aqueles que podem ser beneficiados com a boa utilizao das
tcnicas de orientao por objetos. No entanto, existem outros aspectos que no podem ser esquecidos:
Eficincia: o bom aproveitamento dos recursos computacionais como processadores, memria,
dispositivos de comunicao, etc. Apesar de no explicitamente citado anteriormente, este
um requisito essencial para qualquer produto. A linguagem C++ em particular, por ser to
eficiente quanto C, permite o desenvolvimento de sistemas eficientes.
Portabilidade: a facilidade com que um programa pode ser transferido de uma plataforma
(hardware, sistema operacional, etc.). Este fator depende muito da base sobre a qual o sistema
desenvolvido. A nvel de linguagem, C++ satisfaz este fator por ser uma linguagem
padronizada com implementaes nas mais diversas plataformas.
Facildade de uso: a facilidade de aprendizagem de como o programa funciona, sua operao, etc.
Decomposio
O critrio de decomposio modular alcanado quando o modelo de design ajuda a decomposio do
problema em diversos outros subproblemas cujas solues podem ser atingidas separadamente.
O mtodo deve ajudar a reduzir a aparente complexidade de problema inicial pela sua decomposio
em um conjunto de problemas menores conectados por uma estrutura simples. De modo geral, o processo
repetitivo: os subproblemas so tambm divididos em problemas menores e assim sucessivamente.
Uma exemplificao deste tipo de design o chamado mtodo top-down. Este mtodo dirige os
desenvolvedores a comear com uma viso mais abstrata do funcionamento do sistema. Esta viso
abstrata vai sendo refinada como um conjunto de passos sucessivos menores e assim por diante at que
seus elementos estejam em um nvel que permita sua implementao. Este processo pode ser modelado
como uma rvore.
Composio
O mtodo que satisfaz o critrio de composio favorece a produo de elementos de programas que
podem ser livremente combinados com os outros de forma a produzir novos sistemas, possivelmente em
um ambiente bem diferente daquele em que cada um destes elementos foi criado.
Enquanto que a decomposio se concentra na diviso do software em elementos menores a partir da
especificao, a composio se estabelece no sentido oposto: agregando elementos de programas que
podem ser aplicados para construo de novos sistemas.
A composio diretamente relacionada com a questo da reutilizao: o objetivo achar maneiras de
desenvolver pedaos de programas que executam tarefas bem definidas e utilizveis em outros contextos.
Este contexto reflete um sonho antigo: transformar o processo de design de programas como elementos
independentes onde programas so construdos pela combinao de elementos existentes.
Um exemplo deste tipo de abordagem a construo de bibliotecas como conjuntos de elementos que
podem ser utilizados em diversos programas (pacotes grficos, bibliotecas numricas, etc.).
Entendimento
Um mtodo que satisfaz o critrio de entendimento ajuda a produo de mdulos que podem ser
separadamente compreeendidos pelos desenvolvedores; no pior caso, o leitor deve ter ateno sobre
poucos mdulos vizinhos. Este critrio especialmente relevante quando se tem em vista o aspecto da
manuteno.
Um contra-exemplo deste tipo de mtodo quando h dependncia sequencial onde um conjunto de
mdulos elaborado de modo que a execuo dos mesmos seja feita em uma ordem determinada. Desta
forma, os mdulos no so entendidos de forma individual mas em conjunto com seus vizinhos.
Continuidade
Um mtodo de design satisfaz a continuidade se uma pequena mudana na especificao do problema
resulta em alteraes em um nico ou poucos mdulos. Tal alterao no tem reflexos na arquitetura geral
do sistema; isto , no relacionamento inter-modular.
Este critrio reflete o problema de extensibilidade do sistema. A continuidade significa que eventuais
mudanas devem afetar os mdulos individualmente da estrutura do sistema e no a estrutura em si.
Um exemplo simples deste tipo de critrio a utilizao de constantes representadas por nomes
simblicos definidos em um nico local. Se o valor deve ser alterado, apenas a definio deve ser
alterada.
Proteo
Um mtodo de design satisfaz a proteo se este prov a arquitetura de isolamento quando da
ocorrncia de condies anmalas em tempo de execuo. Ao aparecimento de situaes anormais, seus
efeitos ficam restritos quele mdulo ou pelo menos se propagar a poucos mdulos vizinhos.
Os erros considerados neste critrio so somente aqueles ocorridos em tempo de execuo como falta
de espao em disco, falhas de hardware, etc. No se considera, neste caso, a correo de erros, mas um
aspecto importante para a modularidade: sua propagao.
Princpios de modularidade
Estabelecidos os critrios de modularidade, alguns princpios surgem e devem ser observados
cuidadosamente para se obter modularidade. O primeiro princpio se relaciona com a notao e os outros
se baseiam no modo de comunicao entre os mdulos.
Seguem abaixo estes princpios:
Lingustica modular
Este princpio expressa que o formalismo utilizado para expressar o design, programas, etc. deve
suportar uma viso modular; isto :
Mdulos devem correspoder s unidades sintticas da linguagem utilizada.
Onde a linguagem utilizada pode ser qualquer linguagem de programao, de design de sistemas, de
especificao, etc.
Este princpio segue de diversos critrios mencionados anteriormente:
Poucas interfaces
Este princpio restringe o nmero de canais de comunicao entre os mdulos na arquitetura do
sistema. Isto quer dizer que:
Cada mdulo deve se comunicar o mnimo possvel com outros.
A comunicao pode ocorrer das mais diversas formas. Mdulos podem chamar funes de outros,
compartilhar estruturas de dados, etc. Este princpios limita o nmero destes tipos de conexo.
Pequenas interfaces
Este princpio relaciona o tamanho das interfaces e no suas quantidades. Isto quer dizer que:
Se dois mdulos possuem canal de comunicao, estes devem trocar o mnimo de informao
possvel; isto , os canais da comunicao inter-modular devem ser limitados.
Interfaces explcitas
Este um princpio que vai mais adiante do que poucas interfaces e pequenas interfaces. Alm de
impor limitaes no nmero de mdulos que se comunicam e na quantidade de informaes trocadas, h a
imposio de que se explicite claramente esta comunicao. Isto :
Quando da comunicao de dois mdulos A e B, isto deve ser explcito no texto de A, B ou
ambos.
Este princpio segue de diversos critrios mencionados anteriormente:
Decomposio e composio: Se um elemento formado pela composio ou decomposio de
outros, as conexes devem ser bem claras entre eles.
Entendimento: Como entender o funcionamento de um mdulo A se seu comportamento
influenciado por outro mdulo B de maneira no clara?
Calculadora RPN em C
Os primeiros exemplos sero feitos a partir de um programa escrito em C. Vrias modificaes sero
feitas at que este se torne um programa C++. O programa uma calculadora RPN (notao polonesa
reversa), apresentada nesta seo.
Este tipo de calculadora utiliza uma pilha para armazenar os seus dados. Esta pilha est implementada
em um mdulo parte. O header file et listado abaixo:
#ifndef stack_h
#define stack_h
#define MAX 50
struct Stack {
int top;
int elems[MAX];
};
void push(struct Stack* s, int i);
int pop(struct Stack* s);
int empty(struct Stack* s);
struct Stack* createStack(void);
#endif
A implementao destas funes est no arquivo stack-c.c:
#include <stdlib.h>
#include "stack-c.h"
void push(struct Stack*s, int i) { s->elems[s->top++] = i; }
int pop(struct Stack*s)
{ return s->elems[--(s->top)]; }
int empty(struct Stack*s)
{ return s->top == 0; }
struct Stack* createStack(void)
{
struct Stack* s = (struct Stack*)malloc(sizeof(struct Stack));
s->top = 0;
return s;
}
A calculadora propriamente dita utiliza estes arquivos:
#include <stdlib.h>
#include <stdio.h>
#include "stack-c.h"
/* dada uma pilha, esta funo pe nos
parmetros n1 e n2 os valores do topo
da pilha. Caso a pilha tenha menos de dois
valores na pilha, um erro retornado */
int getop(struct Stack* s, int* n1, int* n2)
{
if (empty(s))
{
printf("empty stack!\n");
return 0;
}
*n2 = pop(s);
if (empty(s))
{
push(s, *n2);
printf("two operands needed!\n");
return 0;
}
*n1 = pop(s);
return 1;
}
Classes em C++
Uma classe em C++ o elemento bsico sobre o qual toda orientao por objetos est apoiada. Em
primeira instncia, uma classe uma extenso de uma estrutura, que passa a ter no apenas dados, mas
tambm funes. A idia que tipos abstratos de dados no so definidos pela sua representao interna,
e sim pelas operaes sobre o tipo. Ento no h nada mais natural do que incorporar estas operaes no
prprio tipo.Estas operaes s fazem sentido quando associadas s suas representaes.
No exemplo da calculadora, um candidato natural a se tornar uma classe a pilha. Esta uma
estrutura bem definida; no seu header file esto tanto a sua representao (struct Stack) quanto as funes
para a manipulao desta representao. Seguindo a idia de classe, estas funes no deveriam ser
globais, mas sim pertencerem estrutura Stack. A funo empty seria uma das que passariam para a
estrutura. A implementao de empty pode ficar dentro da prpria estrutura:
struct Stack {
// ...
int empty() { return top == 0; }
};
ou fora:
struct Stack {
// ...
int empty();
};
int Stack::empty(void) { return top == 0; }
No segundo caso a declarao fica no header file e a implementao no .c. A diferena entre as duas
opes ser explicada na seo sobre funes inline.
Com esta declarao, as funes so chamadas diretamente sobre a varivel que contm a
representao:
void main(void)
{
struct Stack s;
s.empty();
}
Repare que a funo deixou de ter como parmetro uma pilha. Isto era necessrio porque a funo
estava isolada da representao. Agora no, a funo faz parte da estrutura. Automaticamente todos os
campos da estrutura passam a ser visveis dentro da implementao da funo, sem necessidade se
especificar de qual pilha o campo top deve ser testado (caso da funo empty). Isto j foi dito na chamada
da funo.
Traduzindo estas definies para o C++, classes so estruturas, objetos so variveis do tipo de
alguma classe (instncia de alguma classe), mtodos so funes de classes e enviar uma mensagem para
um objeto chamar um mtodo de um objeto.
Resumindo:
Objetos so instncias de classes que respondem a mensagens de acordo com os mtodos e
atributos, descritos na classe.
Captulo 2a
Descrio de recursos de C++ no relacionados s classes
Comentrios
O primeiro recurso apresentado simplesmente uma forma alternativa de comentar o cdigo. Em
C++, os caracteres // iniciam um comentrio que termina no fim da linha na qual esto estes caracteres.
Por exemplo:
int main(void)
{
return -1; // retorna o valor -1 para o sistema operacional
}
Declarao de variveis
Em C as variveis s podem ser declaradas no incio de um bloco. Se for necessrio usar uma varivel
no meio de uma funo existem duas solues: voltar ao incio da funo para declarar a varivel ou abrir
um novo bloco, simplesmente para possibilitar uma nova declarao de varivel.
C++ no tem esta limitao, e as variveis podem ser declaradas em qualquer ponto do cdigo:
void main(void)
{
int a;
a = 1;
printf("%d\n", a);
// ...
char b[] = "teste";
printf("%s\n", b);
}
O objetivo deste recurso minimizar declaraes de variveis no inicializadas. Se a varivel pode
ser declarada em qualquer ponto, ela pode ser sempre inicializada na prpria declarao. Um exemplo
comum o de variveis usadas apenas para controle de loops:
for (int i=0; i<20; i++) // ...
A varivel i est sendo criada dentro do for, que o ponto onde se sabe o seu valor inicial. O escopo
de uma varivel desde a sua declarao at o fim do bloco corrente (fecha chaves). Na linha acima a
varivel i continua existindo depois do for, at o fim do bloco.
interessante notar que esta caracterstica diferente de se abrir um novo bloco a cada declarao.
Uma tentativa de declarar duas variveis no mesmo bloco com o mesmo nome causa um erro, o que no
acontece se um novo bloco for aberto.
Declarao de tipos
Em C++, as declaraes abaixo so equivalentes:
struct a {
// ...
};
typedef struct a {
// ...
} a;
Ou seja, no mais necessrio o uso de typedef neste caso. A simples declarao de uma estrutura j
permite que se use o nome sem a necessidade da palavra reservada struct , como mostrado abaixo:
struct a {};
void f(void)
{
struct a a1;
a
a2;
Declarao de unies
Em C++ as unies podem ser annimas. Uma unio annima uma declarao da forma:
union { lista dos campos };
Neste caso, os campos da unio so usados como se fossem declarados no escopo da prpria unio:
void f(void)
{
union { int a; char *str; };
a = 1;
// ...
str = malloc( 10 * sizeof(char) );
}
a e str so campos de uma unio annima, e so usados como se tivessem sido declarados como
variveis da funo. Mas na realidade as duas tem o mesmo endereo. Um uso mais comum para unies
annimas dentro de estruturas:
struct A {
int tipo;
union {
int
inteiro;
float real;
void *ponteiro;
};
};
Os trs campos da unio podem ser acessados diretamente, da mesma maneira que o campo tipo.
Prottipos de funes
Em C++ uma funo s pode ser usada se esta j foi declarada. Em C, o uso de uma funo no
declarada geralmente causava uma warning do compilador, mas no um erro. Em C++ isto um erro.
Para usar uma funo que no tenha sido definida antes da chamada tipicamente chamada de
funes entre mdulos , necessrio usar prottipos. Os prottipos de C++ incluem no s o tipo de
retorno da funo, mas tambm os tipos dos parmetros:
void f(int a, float b); // prottipo da funo f
void main(void)
{
f(1, 4.5); // o prottipo possibilita a utilizao de f aqui
}
void f(int a, float b)
{
printf("%.2f\n", b+a+0.5);
}
Uma tentativa de utilizar uma funo no declarada gera um erro de smbolo desconhecido.
interpretar a linha acima como o prottipo de uma funo que retorna um float e no recebe nenhum
parmetro. Ou seja, exatamente equivalente a uma funo (void):
float f(); // em C++ o mesmo que float f(void);
Funes inline
Funes inline so comuns em C++, tanto em funes globais como em mtodos. Estas funes tem
como objetivo tornar mais eficiente, em relao velocidade, o cdigo que chama estas funes. Elas so
tratadas pelo compilador quase como uma macro: a chamada da funo substituda pelo corpo da
funo. Para funes pequenas, isto extremamente eficiente, j que evita gerao de cdigo para a
chamada e o retorno da funo.
O corpo de funes inline pode ser to complexo quanto uma funo normal, no h limitao
alguma. Na realidade o fato de uma funo ser inline ou no apenas uma otimizao; a semntica de
uma funo e sua chamada a mesma seja ela inline ou no.
Este tipo de otimizao normalmente s utilizado em funes pequenas, pois funes inline grandes
podem aumentar muito o tamanho do cdigo gerado. Do ponto de vista de quem chama a funo, no faz
nenhuma diferena se a funo inline ou no.
Nem todas as funes declaradas inline so expandidas como tal. Se o compilador julgar que a funo
muito grande ou complexa, ela tratada como uma funo normal. No caso de o programador
especificar uma funo como inline e o compilador decidir que ela ser uma funo normal, ser
sinalizada uma warning. O critrio de quando expandir ou no funes inline no definido pela
linguagem C++, e pode variar de compilador para compilador.
Para a declarao de funes inline foi criada mais uma palavra reservada, inline. Esta deve preceder a
declarao de uma funo inline:
inline double quadrado(double x)
{
return x * x;
}
Neste caso a funo quadrado foi declarada como inline. A chamada desta funo feita
normalmente:
{
double c = quadrado( 7 );
double d = quadrado( c );
}
Assim como a chamada feita da mesma maneira, o significado tambm o mesmo com ou sem
inline. A diferena fica por conta do cdigo gerado. O trecho de cdigo acima ser compilado como se
fosse:
{
double c = 7 * 7;
double d = c * c;
}
Alm de ser mais eficiente, possibilita, por parte do compilador, otimizaes extras. Por exemplo,
calcular automaticamente, durante a compilao, o resultado de 7*7, gerando cdigo para uma simples
atribuio de 49 a c.
Fazendo uma comparao com as macros, que so usadas em C para produzir este efeito, nota-se que
funes inline as substituem com vantagens. A primeira que uma macro uma simples substituio de
texto, enquanto que funes inline so elementos da linguagem, e so verificadas quanto a erros. Alm
disso, macros no podem ser usadas exatamente como funes, como mostra o exemplo a seguir:
#define quadrado(x) ((x)*(x))
void main(void)
{
double a = 4;
double b = quadrado(a++);
}
Antes da compilao, a macro ser expandida para:
double b = ((a++)*(a++));
A execuo desta linha resultar na atribuio de 4*5 = 20 a b, alm de incrementar duas vezes a
varivel a. Com certeza no era este o efeito desejado.
Assim como funes, mtodos tambm podem ser declarados como inline. No caso de mtodos no
necessrio usar a palavra reservada inline. A regra a seguinte: funes com o corpo declarado dentro da
prpria classe so tratadas como inline; funes declaradas na classe mas definidas fora no so inline.
Supondo uma classe Pilha, o mtodo vazia simples o suficiente para justificar uma funo inline:
struct Pilha {
elemPilha* topo;
int vazia() { return topo==NULL; }
void push(int v);
int pop();
};
Alguns detalhes devem ser levados em considerao durante a utilizao de funes inline. O que o
compilador faz quando se depara com uma chamada de funo inline? O corpo da funo deve ser
expandido no lugar da chamada. Isto significa que o corpo da funo deve estar disponvel para o
compilador antes da chamada.
Um mdulo C++ composto por dois arquivos, o arquivo com as declaraes exportadas (.h) e outro
com as implementaes (.c, .cc ou .cpp). Tipicamente, uma funo exportada por um mdulo tem o seu
prottipo no .h e sua implementao no .c. Isso porque um mdulo no precisa conhecer a implementao
de uma funo para us-la. Mas isso s verdade para funes no inline. Por causa disso, funes inline
exportadas precisam ser implementadas no .h e no mais no .c.
Referncias
Referncia um novo modificador que permite a criao de novos tipos derivados. Assim como podese criar um tipo ponteiro para um inteiro, pode-se criar uma referncia para um inteiro. A declarao de
uma referncia anloga de um ponteiro, usando o caracter & no lugar de *.
Uma referncia para um objeto qualquer , internamente, um ponteiro para o objeto. Mas,
diferentemente de ponteiros, uma varivel que uma referncia utilizada como se fosse o prprio
objeto. Os exemplos deixaro estas idias mais claras. Vamos analisar referncias em trs utilizaes:
como variveis locais, como tipos de parmetros e como tipo de retorno de funes.
Uma varivel local que seja uma referncia deve ser sempre inicializada; a no inicializao causa um
erro de compilao. Como as referncias se referenciam a objetos, a inicializao no pode ser feita com
valores constantes:
{
int
int&
int&
int&
a;
b = a;
c;
d = 12;
//
//
//
//
}
A varivel b utilizada como se fosse realmente um inteiro, no h diferena pelo fato de ela ser uma
referncia. S que b no um novo inteiro, e sim uma referncia para o inteiro guardado em a. Qualquer
alterao em a se reflete em b e vice versa. como se b fosse um novo nome para a mesma varivel a:
{
int a = 10;
int& b = a;
printf("a=%d, b=%d\n", a, b); // produz a=10, b=10
a = 3;
printf("a=%d, b=%d\n", a, b); // produz a=3, b=3
b = 7;
printf("a=%d, b=%d\n", a, b); // produz a=7, b=7
}
No caso de a referncia ser um tipo de algum argumento de funo, o parmetro ser passado por
referncia, algo que no existia em C e era simulado passando-se ponteiros:
void f(int a1, int &a2, int *a3)
{
a1 = 1; // altera cpia local
a2 = 2; // altera a varivel passada (b2 de main)
*a3 = 3;
}
void main()
{
int b1 = 10, b2 = 20, b3 = 30;
f(b1, b2, &b3);
printf("b1=%d, b2=%d, b3=%d\n", b1, b2, b3);
// imprime b1=10, b2=2, b3=3
}
O efeito o mesmo para b2 e b3, mas repare que no caso de b3 passado o endereo explicitamente, e
a funo tem que tratar o parmetro como tal.
Falta ainda analisar um outro uso de referncias, quando esta aparece como tipo de retorno de uma
funo. Por exemplo:
int& f()
{
static int global;
return global;
// retorna uma referncia para a varivel
}
void main()
{
f() = 12;
}
importante notar que este exemplo vlido porque global uma varivel static de f, ou seja, uma
varivel global com escopo limitado a f. Se global fosse uma varivel local comum, o valor de retorno
seria invlido, pois quando a funo f terminasse a varivel global no existiria mais, e portanto a
referncia seria invlida. Como um ponteiro perdido. Outras sees apresentam exemplos prticos desta
utilizao.
Alocao de memria
A alocao dinmica de memria, que em C era tratada com as funes malloc e free, diferente em
C++. Programas C++ no precisam mais usar estas funes. Para o gerenciamento da memria, foram
criados dois novos operadores, new e delete, que so duas palavras reservadas. O operador new aloca
memria e anlogo ao malloc; delete desaloca memria e anlogo ao free. A motivao desta
modificao ficar clara no estudo de classes, mais especificamente na parte de construtores e destrutores.
Por ser um operador da linguagem, no mais necessrio, como no malloc, calcular o tamanho da
rea a ser alocada. Outra preocupao no mais necessria a converso do ponteiro resultado para o tipo
correto. O operador new faz isso automaticamente:
int * i1 = (int*)malloc(sizeof(int));
int * i2 = new int;
// C
// C++
//
//
//
//
alocado
alocado
alocado
alocado
com
com
com
com
malloc
new
malloc
new[]
(C )
(C++)
(C )
(C++)
A utilizao de delete para desalocar um vetor, assim como a utilizao de delete[] para desalocar um
nico objeto tem consequncias indefinidas. A necessidade de diferenciao se explica pela existncia de
destrutores, apresentados mais frente.
a = a + 125;
b = 3.14159 + 1.17;
O operador + est sobrecarregado. Na primeira linha, ele se refere a uma soma de inteiros; na segunda
a uma soma de reais. Estas somas exigem implementaes diferentes, e como se existissem duas
funes, a primeira sendo
int +(int, int);
e a outra como
float +(float, float);
Voltando funo display, preciso definir as vrias implementaes requeridas:
void display( char *v ) { printf("%s", v); }
void display( int
v ) { printf("%d", v); }
void display( float v ) { printf("%f", v); }
A simples declarao destas funes j tem todas as informaes suficientes para o compilador fazer a
escolha correta. Isto significa que as funes no so mais distinguidas apenas pelo seu nome, mas pelo
nome e pelo tipo dos parmetros.
A tentativa de declarar funes com mesmo nome que no possam ser diferenciadas pelo tipo dos
parmetros causa um erro:
void f(int a);
int f(int b); // erro! redeclarao de f!!!
O uso misturado de sobrecarga e valores default para parmetros tambm pode causar erros:
void f();
void f(int a = 0);
void main()
{
f( 12 ); // ok, chamando f(int)
f();
// erro!! chamada ambgua: f() ou f(int = 0)???
}
Repare que nesse caso o erro ocorre no uso da funo, e no na declarao.
Operador de escopo
C++ possui um novo operador que permite o acesso a nomes declarados em escopos que no sejam o
corrente. Por exemplo, considerando o seguinte programa C:
char *a;
void main(void)
{
int a;
a = 23;
/* como acessar a varivel global a??? */
}
A declarao da varivel local a esconde a global. Apesar de a varivel a global existir, no h como
referenciar o seu nome, pois no escopo da funo main, o nome a est associado local a.
O operador de escopo possibilita o uso de nomes que no esto no escopo corrente, o que pode ser
usado neste caso:
char *a;
void main(void)
{
int a;
a = 23;
::a = "abc";
}
A sintaxe deste operador a seguinte:
escopo::nome,
onde escopo o nome da classe onde est declarado no nome nome. Se escopo no for especificado,
como no exemplo acima, o nome procurado no espao global.
Outros usos deste operador so apresentados nas sees sobre classes aninhadas, campos de estruturas
static e mtodos static.
Palavras reservadas
Talvez a maior incompatibilidade seja causada pelo simples fato que C++ tem vrias outras palavras
reservadas. Isto significa que qualquer programa C que declare um identificador usando uma destas
palavras no um programa C++ correto. Aqui est uma lista com as novas palavras reservadas:
catch
class
delete
friend
inline
new
operator
private
protected
public
template
this
throw
try
virtual
A nica soluo para traduzir programas que fazem uso destas palavras trocar os nomes dos
identificadores, o que pode ser feito usando diretivas #define.
Exigncia de prottipos
Um dos problemas que pode aparecer durante a compilao de um programa C est relacionado ao
fato de que prottipos no so obrigatrios em C. Como C++ exige prottipos, o que era uma warning C
pode se transformar em um erro C++.
Estes erros simplesmente foram o programador a fazer uma coisa que j devia ter sido feita mesmo
com o compilador C: usar prottipos para as funes.
void main(void)
{
printf("teste"); // C:
warning! printf undeclared
// C++: error!
printf undeclared
}
// C:
//
// C++:
//
void main(void)
{
float a = square(3); // C:
ok
// C++: error! too many arguments
}
Estruturas aninhadas
Outra incompatibilidade pode aparecer em programas C que declaram estruturas aninhadas. Em C, s
h um escopo para os tipos: o global. Por esse motivo, mesmo que uma estrutura tenha sido declarada
dentro de outra, ou seja, aninhada, ela pode ser usada como se fosse global. Por exemplo:
struct S1 {
struct S2 {
int a;
} b;
};
void main(void)
{
struct S1 var1;
Esta lista no definitiva, j que constantemente novos recursos vo sendo adicionados ao padro.
struct S2 var2;
// ok em C, errado em C++
}
Este um programa correto em C. Em C++, uma estrutura s vlida no escopo em que foi declarada.
No exemplo acima, por ter sido declarada dentro de S1, a estrutura S2 s pode ser usada dentro de S1.
Nesse caso, a tentativa de declarar a varivel var2 causaria um erro, pois S2 no um nome global.
Em C++, S2 pode ser referenciada utilizando-se o operador de escopo:
struct S1::S2 var2; // soluo em C++
Dentro de um bloco extern pode aparecer qualquer tipo de declarao, no apenas funes. A soluo
ento alterar os header files das bibliotecas C, explicitando que as funes no so C++. Para isso, basta
envolver todas as declaraes em um bloco destes.
Como uma declarao extern C s faz sentido em C++, ela causa um erro de sintaxe em C. Para que
os mesmos header files sejam usados em programas C e C++, basta usar a macro pr-definida
__cplusplus, definida em todo compilador C++. Com estas alteraes, um header file de uma biblioteca C
ter a seguinte estrutura:
#ifdef __cplusplus
extern "C" {
#endif
// todas as declaraes
// ...
#ifdef __cplusplus
}
#endif
Captulo 2b
Recursos de C++ relacionados s classes
Classes aninhadas
Declaraes de classes podem ser aninhadas. Uma classe aninhada no reside no espao de nomes
global, como era em C. Para referenciar uma classe aninhada fora do seu escopo, preciso usar o
operador de escopo.
Este aninhamento usado para encapsular classes auxiliares que s fazem sentido dentro de um
escopo reduzido.
struct A {
struct B {
int b;
};
int a;
};
void main()
{
A a;
A::B b;
}
O cdigo acima simplesmente declara a estrutura B dentro de A; no existe nenhum campo de A que
seja do tipo B.
Declarao incompleta
Em C, a declarao de estruturas mutuamente dependentes feita naturalmente:
struct A { struct B *next; };
struct B { struct A *next; };
Em C++ a construo acima pode ser usada. No entanto, como a declarao de uma estrutura em C++
j define o nome como um tipo sem necessidade de typedefs, normal em C++ o uso de tipos sem o uso
da palavra reservada struct:
struct A { B *next; }; // Erro! B indefinido
struct B { A *next; };
No caso de tipos mutuamente dependentes, isto s possvel usando uma declarao incompleta:
struct B; // declarao incompleta de B
struct A { B *next; };
struct B { A *next; };
Assim como em C, antes da declarao completa da estrutura s possvel usar o nome para declarar
ponteiros e referncias deste tipo. Como as informaes esto incompletas, no possvel declarar
variveis deste tipo nem utilizar campos da estrutura.
Mtodos const
Mtodos const so mtodos que no alteram o estado interno de um objeto. Assim como era possvel
declarar variveis e parmetros const em C, possvel declarar um mtodo const em C++. A
especificao const faz parte da declarao do mtodo.
A motivao para esta nova declarao pode ser esclarecida com o exemplo abaixo:
struct A {
int value;
int get()
{ return value; }
void put (int v) { value = v; }
};
void f(const A a)
{
// parmetro a no pode ser modificado
// quais mtodos de a podem ser chamados???
int b = a.get(); // Erro! mtodo no const
}
A funo f no pode alterar o seu parmetro por causa da declarao const. Isto significa que os
campos de a podem ser lidos mas no podem ser modificados. E quanto aos mtodos? O que define quais
mtodos podem ser chamados a sua implementao. Esta verificao no pode ser feita pelo
compilador, j que na maioria das vezes o cdigo dos mtodos no est disponvel. responsabilidade do
programador declarar quais mtodos no modificam o estado interno do objeto. Estes mtodos podem ser
aplicados a objetos declarados const.
No exemplo acima, o mtodo get pode ser declarado const, o que possibilita a sua chamada na funo
f. Se o mtodo put for declarado const, o compilador acusa um erro de compilao pois o campo value
modificado em sua implementao:
struct A {
int value;
int get() const { return value; }
void put (int v) { value = v; }
};
void f(const A a)
{
// parmetro a no pode ser modificado
int b = a.get(); // ok, mtodo const
}
this
Em todo mtodo no static (ver seo sobre mtodos static), a palavra reservada this um ponteiro
para o objeto sobre o qual o mtodo est executando.
Todos os mtodos de uma classe so sempre chamados associados a um objeto. Durante a execuo
de um mtodo, os campos do objeto so manipulados normalmente, sem necessidade de referncia ao
objeto. E se um mtodo precisar acessar no os campos de um objeto, mas o prprio objeto? O exemplo
abaixo ilustra este caso:
struct A {
int i;
A& inc();
};
// Este mtodo incrementa o valor interno
// e retorna o prprio objeto
A& A::inc()
{
// estamos executando este cdigo para obj1 ou obj2?
// como retornar o prprio objeto?
i++;
return *this; // this aponta para o prprio objeto
}
void main()
{
A obj1, obj2;
obj1.i = 0;
obj2.i = 100;
for (j=0; j<10; j++)
printf("%d\n", (obj1.inc()).i);
}
O tipo de this dentro de um mtodo de uma classe X
X* const
a no ser que o mtodo seja declarado const. Nesse caso, o tipo de this :
const X* const
void main(void)
{
A a1, a2;
a1.a = 0; // modifica o campo a de a1
a1.b = 1; // modifica o campo b compartilhado por a1 e a2
a2.a = 2; // modifica o campo a de a1
a2.b = 3; // modifica o campo b compartilhado por a1 e a2
printf("%d %d %d %d", a1.a, a1.b, a2.a, a2.b);
// imprime 0 3 2 3
}
Se a definio
int A::b = 0;
for omitida, o arquivo compilado mas o linker acusa um erro de smbolo no definido.
Como uma varivel esttica nica para todos os objetos da classe, no necessrio um objeto para
referenciar este campo. Isto pode ser feito com o operador de escopo:
A::b = 4;
Mtodos static
Assim como campos static so como variveis globais com escopo reduzido classe, mtodos static
so como funes globais com escopo reduzido classe. Isto significa que mtodos static no tem o
parmetro implcito que indica o objeto sobre o qual o mtodo est sendo executado (this), e portanto
apenas os campos static podem ser acessados:
struct A {
int a;
static int b;
static void f();
};
// aplica p a a1
// aplica p a a2
}
A aplicao dos mtodos feita utilizando-se os novos operadores .* e ->*.
Captulo 3
Encapsulamento
Como foi visto anteriormente, um TAD definido pela sua interface, ou seja, como ele manipulado.
Um TAD pode ter diversas implementaes possveis, e, independentemente desta ou daquela
implementao, objetos deste tipo so usados sempre da mesma forma. Os atributos alm da interface, ou
seja, os atributos dependentes da implementao, no precisam e no devem estar disponveis para o
usurio de um objeto, pois este deve ser acessado exclusivamente atravs da interface definida. O ato de
esconder estas informaes chamado de encapsulamento.
Os mecanismos apresentados at aqui permitem a definio da interface em um tipo, permitindo
cdigos bem mais modulares e organizados. No entanto, no permitem o encapsulamento de dados e/ou
cdigo.
int valor;
void f1() { valor = 0; }
int f2() { return valor; }
};
int main()
{
SemUso p; // cria um objeto do tipo SemUso
p.f1();
// erro, f1 private!
printf("%d", p.f2() );
// erro, f2 private
return 0;
}
No exemplo anterior, todos os membros so privados, o que impossibilita o uso de um objeto da classe
SemUso. Para que as funes f1 e f2 pudessem ser usadas, elas precisariam ser pblicas. O cdigo a
seguir uma modificao da declarao da classe SemUso para tornar as funes f1 e f2 pblicas e
portanto passveis de serem chamadas em main.
struct SemUso {
private:
int valor;
public:
void f1() { valor = 0; }
int f2() { return valor; }
};
Membros protected sero explicados quando forem introduzidas as classes derivadas.
Para exemplificar melhor o uso do controle de acesso, vamos considerar uma implementao de um
conjunto de inteiros. As operaes necessrias so, por exemplo, inserir e retirar um elemento, verificar se
um elemento pertence ao conjunto e a cardinalidade do conjunto. Ento o nosso conjunto ter pelo menos
o seguinte:
struct Conjunto {
void insere(int n);
void retira(int n);
int pertence(int n);
int cardinalidade();
};
Se a implementao deste conjunto usar listas encadeadas, preciso usar uma estrutura auxiliar
elemento que seriam os ns da lista. A nova classe teria ainda uma varivel private que seria o ponteiro
para o primeiro elemento da lista. Outra varivel private seria um contador de elementos. Vamos
acrescentar ainda um mtodo para limpar o conjunto, para ser usado antes das funes do conjunto
propriamente ditas. Eis a definio da classe:
struct Conjunto {
private:
struct listElem {
listElem *prox;
int valor;
};
listElem* lista;
int nElems;
public:
void limpa() { nElems=0; lista=NULL; }
void insere(int n);
void retira(int n);
int pertence(int n);
int cardinalidade();
};
Como somente os mtodos desta classe tem acesso aos campos privados, a estrutura interna no pode
ser alterada por quem usa o conjunto, o que garante a consistncia do conjunto.
// a privado
Como as interfaces das classes devem ser as menores possveis e devem ser tambm explcitas, as
declaraes com a palavra class so mais usadas. Com class, somente os nomes explicitamente declarados
como public so exportados.
A partir de agora os exemplos vo ser feitos usando-se a palavra class.
listElem {
...
...
Tambm possvel declarar funes friend. Nesse caso, a funo ter acesso irrestrito aos
componentes da classe que a declarou como friend. Exemplo de funo friend:
class No {
friend int leValor( No* ); // d acesso privilegiado
// funo leValor
int valor;
public:
void setaValor( int v ) { valor=v; }
};
int leValor( No* n )
{
return n->valor; // acessa dado private de No
}
O recurso de classes e funes friend devem ser usados com cuidado, pois isto um furo no
encapsulamento dos dados de uma classe. Projetos bem elaborados raramente precisam lanar mo de
classes ou funes friend.
Construtores e destrutores
Como o nome j indica, um construtor uma funo usada para construir um objeto de uma dada
classe. Ele chamado automaticamente assim que um objeto criado. Analogamente, os destrutores so
chamados assim que os objetos so destrudos.
Assim como o controle de acesso desempenha um papel importante para manter a consistncia interna
dos objetos, os construtores so fundamentais para garantir que um objeto recm criado esteja tambm
consistente.
No exemplo onde foi implementada a classe Conjunto, foi necessria a introduo de uma funo,
limpa, que precisava ser chamada para inicializar o estado do objeto. Se esta funo no for chamada
antes da manipulao de cada objeto, os resultados so imprevisveis (os campos nElems e lista contm
lixo). Deixar esta chamada a cargo do programador aumentar o potencial de erro do programa. Na
realidade, a funo limpa deveria ser um construtor de Conjunto. O compilador garante que o construtor
a primeira funo a ser executada sobre um objeto. A mesma coisa acontecia em IteradorConj. A funo
inicia deveria ser um construtor, pois antes desta chamada o estado do objeto imprevisvel.
Destrutores so normalmente utilizados para liberar recursos alocados pelo objeto, como memria,
arquivos etc.
public:
Conjunto() { nElems=0; lista=NULL; }
~Conjunto(); // desaloca os ns da lista
void insere(int n);
void retira(int n);
int pertence(int n);
int cardinalidade();
};
class IteradorConj {
Conjunto::listElem *corrente;
public:
IteradorConj( Conjunto* c ) { corrente = c->lista; }
int terminou()
{ return corrent==NULL; }
int proxElem()
{ int n=corrente->valor; corrent=corrente->prox; return n; }
};
de a1
de
de
de
de
de
a2
a3
a2
a3
a1
interessante observar que os objetos globais j esto inicializados quando a funo main comea a
ser executada. Isto significa que os construtores de objetos globais so chamados antes de main:
A global;
void main()
{
printf("main\n");
}
Este cdigo produz a seguinte sada:
construtor
main
destrutor
Se for possvel criar um IteradorConj sem fornecer este conjunto, ento o construtor no est
garantindo nada. Mas no isto que acontece. A declarao de um construtor impe que os objetos s
sejam criados atravs deles. Sendo assim, no mais possvel criar um IteradorConj sem fornecer um
Conjunto. O exemplo abaixo utiliza estas classes e mostra como os construtores so chamados:
void main()
{
Conjunto conj;
conj.insere(10);
// ...
IteradorConj i; // erro! obrigatrio fornecer o parmetro
IteradorConj it( &conj ); // cria um iterador passando conj
while (!it.terminou())
// percorre os elementos
printf("%d ", it.proxElem() ); // imprimindo-os
}
Caso o iterador seja alocado dinamicamente (com new), a sintaxe a seguinte:
IteradorConj *i = new IteradorConj(&conj)
No possvel alocar um vetor de objetos passando parmetros para o construtor. Por exemplo, no
possvel criar um vetor de iteradores:
Conjunto c;
IteradorConj it(&c)[10]; // erro!!!
IteradorConj *pit;
pit = new(&c)[20];
// erro!!!
Objetos temporrios
Assim como no preciso declarar uma varivel dos tipos primitivos sempre que se quer usar um
valor temporariamente, possvel criar objetos temporrios em C++. Uma utilizao tpica a seguinte:
quando uma funo aceita como parmetro um inteiro, e voc quer cham-la passando o valor 10, no
necessrio atribuir 10 a uma varivel apenas para chamar a funo. O mesmo deve se aplicar a tipos
definidos pelo usurio (classes):
class A {
public:
A(int);
~A();
};
void f(A);
void main()
{
A a1(1);
f(a1);
f(A(10)); // cria um objeto temporrio do tipo A
// passando 10 para o construtor
// aps a chamada a f o objeto destrudo
}
//
//
//
//
a1 = A(1)
a2 = A("abc", 0)
a1 = A(2)
f(A(3))
Construtores privados
Os construtores, assim como qualquer mtodo, podem ser privados. Como o construtor chamado na
criao, os objetos s podero ser criados com este construtor dentro de mtodos da prpria classe ou em
classes e funes friend.
Destrutores privados
Destrutores tambm podem ser privados. Isto significa que objetos desta classe s podem ser
destrudos onde os destrutores podem ser chamados (mtodos da prpria classe, classes e funes friend).
Usando este recurso, possvel projetar classes cujos objetos no so nunca destrudos. Outra
possibilidade o projeto de objetos que no podem ser alocados na pilha, apenas dinamicamente.
Exemplo:
class A {
~A() {}
public:
int a;
};
void main()
{
A a1;
//
//
//
A* a2 = new A; //
}
No exemplo acima, o destrutor privado impe duas restries: objetos no podem ser alocados na
pilha e, mesmo que sejam criados dinamicamente, no podem nunca ser destrudos. Para permitir a
destruio dos objetos basta criar um mtodo que faa isso:
class A {
~A() {}
public:
int a;
void destroy() { delete this; }
};
class B {
A a;
};
Ao se criar um objeto do tipo B, que inteiro deve ser passado ao campo a? Nesse caso, o construtor de
B tem que especificar este inteiro, e o compilador no gera um construtor vazio. Ou seja, a classe B tem
que declarar um construtor para que seja possvel criar objetos deste tipo. A sintaxe a seguinte:
class B {
A a;
public:
B() : a(3) {}
};
Captulo 4
Sobrecarga de operadores
O uso de funes sobrecarregadas no s uniformiza chamadas de funes para diferentes objetos
como tambm permite que os nomes sejam mais intuitivos. Se um dos objetivos da sobrecarga permitir
que as funes sejam chamadas pelo nome mais natural possvel, no importa se o nome j foi usado,
porque no deixar o programador sobrecarregar tambm os operadores?
Na realidade, um operador executa algum cdigo com alguns parmetros, assim como qualquer
funo. A aplicao de um operador equivalente chamada de uma funo. Em C++ permitido
sobrecarregar um operador, com o objetivo de simplificar a notao e uniformizar a expresso.
Existem duas maneiras de implementar operadores para classes de objetos: como funes membro e
como funes globais. Por exemplo, dado um objeto w e um operador unrio !, a expresso
!w
equivalente s chamadas de funes
w.operator!();
operator!(w);
Vejamos agora como seria com um operador binrio, por exemplo, &. A expresso
x & y
equivalente s chamadas
x.operator&(y); // usando uma funo membro
operator&(x,y); // usando uma funo global
Um detalhe importante que uma funo y.operator&(x) nunca ser considerada pelo compilador
para resolver a expresso x&y, j que isto implicaria que o operador comutativo.
Antes do primeiro exemplo, precisamos ter em mente que C++ no permite a criao de novos
operadores; s podem ser redefinidos os operadores que j existem na linguagem. Isto implica que, por
exemplo, o operador / ser sempre binrio. Outra caracterstica que a prioridade tambm no pode ser
alterada, preservada a original do C.
Um nmero complexo pode ser modelado com uma classe que permita que as operaes matemticas
sobre ele sejam feitas da mesma maneira que os tipos primitivos, ou seja, com os operadores +, - etc. Uma
possibilidade seria:
class Complex {
public:
Complex operator+ (const Complex&);
Complex operator- (const Complex&);
};
Com estes operadores, possvel fazer:
void main()
{
Complex c1, c2, c3;
c1 = c2 + c3;
}
um float em um complexo, o que equivalente a chamar o construtor com apenas um parmetro. Esta
converso permite expresses do tipo:
c1 = c2 + 3.0; // equivalente a c1 = c2 + Complex(3.0, 0.0)
Considerando que o operador uma funo, esta expresso poderia ser vista como:
c1 = c2.operator+(Complex(3.0,0.0));
A converso foi feita porque a funo operator+ espera um Complex, e o valor era um float. Nesse
caso o compilador converte automaticamente o valor. Mas e se a expresso for a seguinte:
c1 = 3.0 + c2;
Nesse caso 3.0 no parmetro de funo nenhuma, ento a converso no feita. Para possibilitar
esta expresso, seria preciso converter o valor explicitamente:
c1 = Complex(3.0) + c2;
No entanto, se 3.0 fosse o parmetro para alguma funo, o compilador saberia fazer a converso.
Lembrando que os operadores podem ser definidos como mtodos ou como funes globais, possvel
tornar 3.0 um parmetro. o caso de definir o operador como uma funo global:
Complex operator+ (const Complex&, const Complex&);
Com esta funo definida, os dois operandos passam a ser parmetros, e ambos podem ser convertidos
automaticamente.
float Vector::operator[](int i)
{
if (i>=0 && i<size) return elems[i];
else
{
printf("ndice %d invlido\n", i);
return -1.0;
}
}
No entanto isto no suficiente. preciso lembrar que este operador pode ser usado de duas
maneiras:
a = vetor[10];
vetor[20] = b;
Na primeira linha no h problema. O operador uma funo que retorna um valor, que ser atribudo
varivel a. J na segunda linha a atribuio no permitida, pois a funo retorna o valor da posio 20
do vetor; para a atribuio necessrio saber o endereo da posio 20 do vetor. A soluo retornar uma
referncia para um float. Assim o valor de retorno o endereo (necessrio na segunda linha), mas este
usado como um valor (primeira linha). Se simplesmente mudarmos o tipo de retorno da funo para
float&, um erro ser sinalizado durante a sua compilao, pois o valor -1.0, retornado caso o ndice seja
invlido, no tem endereo. Ou seja, necessrio retornar uma varivel, mesmo nesse caso. Outra
particularidade deve ser observada aqui: como que ser retornado ser o endereo da varivel retornada,
esta precisa continuar existindo mesmo depois que afuno termina a sua execuo. O que significa que
no se pode retornar uma varivel local, pois variveis locais deixam de existir assim que a funo
termina. A implementao abaixo utiliza uma varivel static dentro da funo s para este fim:
class Vector {
float *elems;
int
size;
public:
Vector(int s);
float& operator[](int i);
};
Vector::Vector(int s)
{
size=s;
elems = new float[size];
}
float& Vector::operator[](int i)
{
if (i>=0 && i<size) return elems[i];
else
{
static float lixo;
printf("ndice %d invlido\n", i);
return lixo;
}
}
Operadores de converso
possvel definir operadores especiais para converso de tipos. Alm das converses padro, o
programador pode definir como um objeto pode ser convertido para algum outro tipo. Consideremos uma
classe Arquivo que modela um arquivo. Internamente esta classe pode ter um ponteiro para um arquivo
(tipo FILE*) privado. Se fosse necessrio fazer alguma operao j definida na biblioteca que no tenha
sido mapeada na classe, seria preciso ter acesso ao ponteiro para arquivo. Ao invs de tornar pblico este
campo, seria mais elegante definir como um objeto do tipo Arquivo se converte em um FILE*, que o
objetivo. Esta definio se d da seguinte forma:
class Arquivo {
FILE *file;
public:
Arquivo( char* nome ) { file=fopen(nome, "r"); }
~Arquivo() { fclose(file); }
char read() { return file?fgetc(file):EOF; }
int aberto() { return file!=NULL; }
operator FILE*() { return file; }
}
void main()
{
int i;
Arquivo arq("teste.c");
fscanf( (FILE*)arq, "%d", &i );
}
Captulo 5
Aspectos de reutilizao
Por que software no como hardware? Por que todo desenvolvimento comea do nada?
Deviam existir catlogos de mdulos de software, assim como existem catlogos de chips: quando
ns construmos um novo sistema, ns deveramos estar usando os componentes destes catlogos e
combinando-os, em vez de sempre reinventar a roda. Ns escreveramos menos software, a talvez
faramos um desenvolvimento melhor . Ser que alguns problemas dos quais todo mundo reclama
- custos altos, prazos insuficientes, pouca confiabilidade - no desapareceriam? Por que no
assim?
Talvez voc j tenha ouvido esta argumentao antes; talvez voc prprio j tenha pensado nisso. Em
1968, no famoso workshop da OTAN sobre a crise de software, D. McIlroy j estava pregando a
produo de componentes de software em massa. A reutilizao, como um sonho, no nova.
Qualquer pessoa que lide com o desenvolvimento de software se impressiona com seu carter
repetitivo. Diversas vezes, os programadores constroem funes e programas com o mesmo padro:
ordenao, busca de um elemento, comparao, etc. Uma maneira de interessante de avaliar esta situao
responder a seguinte pergunta: Quantas vezes, nos ltimos seis meses, voc escreveu uma rotina de
busca de um elemento x em uma tabela t ?
As dificuldades tcnicas de reutilizao se tornam mais visveis quando se observa a natureza das
repeties no desenvolvimento de sistemas. Esta anlise revela que apesar dos programadores tenderem a
escrever os mesmos tipos de rotinas diversas vezes, estas no so exatamente iguais. Se fosse, a soluo
mais simples teoricamente; na prtica, porm, muitos detalhes mudam de implementao para
implementao (tipos dos elementos, estrutura de dados associada, etc.).
Apesar das dificuldades, algumas solues foram propostas com relativos sucessos:
Reutilizao de cdigo-fonte: Muito comum no meio cientfico. Muito da cultura UNIX foi difundida
pelos laboratrios e universidades do mundo graas disponibilidade de cdigo-fonte ajudando
estudadentes a estudarem, imitarem e estenderem o sistema. No entanto, esta no a forma mais
utilizada nos meios industrial e comercial alm de que esta tcnica no suporta ocultao de
informao (information hiding).
Reutilizao de pessoal: uma forma muito comum na indstria. Consiste na transferncia de
engenheiros de software de projetos a projetos fazendo a permanncia de know-how na compania e
assegurando a aplicao de experincias passadas em novos projetos. Obviamente, esta uma maneira
no-tcnica e limitada.
Reutilizao de design: A idia por trs desta tcnica que as companhias devem acumular repositrios
de idias descrevendo designs utilizados para os tipos de aplicao mais comuns.
Variao no tipo
Um mdulo que implemente uma determinada funcionalidade deve ser capaz de faz-lo sobre
qualquer tipo a ele atribudo. Por exemplo, no caso da busca de um elemento x, o mdulo deve ser
aplicvel a diferentes instncias de tipo para x. interessante a utilizao do mesmo mdulo para
procurar um inteiro numa tabela de inteiros ou o registro de um empregado na sua tabela correspondente,
etc.
Independncia de representao
Uma estrutura modular verdadeiramente flexvel habilita seus clientes uma operao sem o
conhecimento de modo pelo qual o mdulo foi implementado. Por exemplo, deve ser possvel ao cliente
escrever a seguinte chamada para a busca de x:
esta_presente = BUSCA( x, T );
sem saber qual o tipo da tabela T no momento da chamada. Se vrios algoritmos de busca foram
implementados, os mecanismos internos do mdulo so responsveis de saber qual o apropriado sem a
interveno do cliente. De maneira simplificada, isto uma extenso do princpio de ocultao de
informao pois havendo a necessidade de mudana na implementao, os clientes esto protegidos.
No entanto, a idia vai mais alm. O princpio da independncia de representao no significa
somente que mudanas na representao devem ser invisveis para os clientes durante o ciclo de
desenvolvimento do sistema: os clientes devem ser imunes tambm a mudanas durante a execuo. No
exemplo acima, desejvel que a rotina BUSCA se adapte automaticamente para a forma de T em tempo
de execuo mesmo que esta forma tenha sido alterada do instante da ltima chamada.
Este requisito importante no somente pela questo do reuso mas tambm pela extensibilidade. Se T
pode mudar de forma em tempo de execuo, ento uma deciso no sistema deve ser tomada para a
utilizao da verso de BUSCA correta. Em outras palavras, se no houver esse mecanismo automtico o
cdigo, em algum local, deve conter um controle do tipo:
se T do tipo A ento "mecanismo A"
se T do tipo B ento "mecanismo B"
se T do tipo C ento "mecanismo C"
A estrutura de deciso deve estar ou no mdulo ou no cliente. Ambos os casos no so satisfatrios.
Se a deciso estiver no mdulo, este mdulo deve saber sobre todos as possibilidades existentes. Tal
poltica pode levar a construo de mdulos difceis de gerenciar e sujeitos a constantes manutenes.
Deixar a deciso para o cliente no melhor. Desta forma, o cliente obrigado a especificar que T uma
tabela do tipo A, B, etc. mas no requerido a dizer mais nada: esta informao suficiente para
determinar que variante de BUSCA deve ser utilizada.
A soluo para o problema introduzida pelas linguagens orientadas por objetos com o mecanismo de
herana em que o desenvolvimento feito atravs da descentralizao da arquitetura modular. Esta
construda por sucessivos incrementos e modificaes conectadas por relaes bem definidas que definem
as verses corretas de BUSCA. Este mecanismo chamado de Amarrao Dinmica (Late-Binding ou
Dynamic binding ).
Vetor
Lista Encadeada
COMEA()
i := 1
MOVE_PROXIMO
()
FINAL()
i := i +
1
i >
tamanho
l :=
ponta_lista
l := l.next
Arquivo
Seq.
rewind(
)
read()
l == NULL
eof()
Neste caso, evita-se a repetio do mtodo de BUSCA nas implementaes de busca seqencial. Temse uma nica funo de pesquisa que se difere em quais funoes especficas de COMEA,
MOVE_PRXIMO e FINAL sero chamadas.
Os mecanismos decritos acima so compreendidos nas linguagens orientadas por objetos pelo
mecanismo de herana descrito a seguir.
Herana
Provavelmente herana o recurso que torna o conceito de classe mais poderoso. Em C++, o termo
herana se aplica apenas s classes. Variveis no podem herdar de outras variveis e funes no podem
herdar de outras funes.
Herana permite que se construa e estenda continuamente classes desenvolvidas por voc mesmo ou
por outras pessoas, sem nenhum limite. Comeando da classe mais simples, pode-se derivar classes cada
vez mais complexas que no so apenas mais fceis de debuggar, mas elas prprias so mais simples.
O objetivo de um projeto em C++ desenvolver classes que resolvam um determinado problema.
Estas classes so geralmente construdas incrementalmente comeando de uma classe bsica simples,
atravs de herana. Cada vez que se deriva uma nova classe comeando de uma j existente, pode-se
herdar algumas ou todas as caractersticas da classe pai, adicionando novas quando for necessrio. Um
projeto completo pode ter centenas de classes, mas normalmente estas classes so derivadas de algumas
poucas classes bsicas. C++ permite no apenas herana simples, mas tambm mltipla, permitindo que
uma classe incorpore comportamentos de todas as suas classes bases.
Reutilizao em C++ se d atravs do uso de uma classe j existente ou da construo de uma nova
classe a partir de uma j existente.
Classes derivadas
A descrio anterior pode ser interessante, mas um exemplo a melhor forma de mostrar o que
herana e como ela funciona. Aqui est um exemplo de duas classes, a segunda herdando as propriedades
da primeira:
class Caixa {
public:
int altura, largura;
void Altura(int a) { altura=a; }
void Largura(int l) { largura=l; }
};
class CaixaColorida : public Caixa {
public:
int cor;
void Cor(int c) { cor=c; }
};
Usando a terminologia de C++, a classe Caixa chamada classe base para a classe CaixaColorida,
que chamada classe derivada. Classes base so tambm chamadas de classes pai. A classe
CaixaColorida foi declarada com apenas uma funo, mas ela herda duas funes e duas variveis da
classe base. Sendo assim, o seguinte cdigo possvel:
void main()
{
CaixaColorida cc;
cc.Cor(5);
cc.Largura(3); // herdada
cc.Altura(50); // herdada
}
Note que as funes herdadas so udadas exatamente como as no herdadas. A classe Colorida no
precisou sequer mencionar o fato de que as funes Caixa::Altura() e Caixa::Largura() foram herdadas.
Esta uniformidade de expresso um grande recurso de C++. Usar um recurso de uma classe no requer
que se saiba se este recurso foi herdado ou no, j que a notao invariante. Em muitas classes pode
existir uma cadeia de classes base derivadas de outras classes base. Uma classe herdada de uma rvore de
herana como esta herdaria caractersticas de muitas classes pai diferentes. Entretanto, em C++, no
preciso se preocupar onde ou quando um recurso foi introduzido na rvore.
Derivar uma classe de outra aumenta a flexibilidade a um custo baixo. Uma vez que j existe uma
classe base slida, apenas as mudanas feitas nas classes derivadas precisam ser depuradas. Mas quando
exatamente se usa uma classe base, e que tipos de modificaes precisam ser feitas? Quando se herda
caracterstias de uma classe base, a classe derivada pode estender, restringir, modificar, eliminar ou usar
qualquer dos recursos sem qualquer modificao.
O que no herdado
Nem tudo herdado quando se declara uma classe derivada. Alguns casos so inconsistentes com
herana por definio:
Construtores
Destrutores
Operadores new
Operadores de atribuio (=)
Relacionamentos friend
Atributos privados
Classes derivadas invocam o construtor da classe base automaticamente, assim que so instanciadas.
// ERRO!! a no visvel
// vlido (b protected)
// vlido (c public)
{
A ca;
B cb;
ca.a = 1; // ERRO! a no visvel (private)
ca.b = 2; // ERRO! b no visvel de fora (protected)
ca.c = 3; // vlido (c public)
cb.a = 4; // ERRO! a no visvel nem internamente em B
cb.b = 5; // ERRO! b continua protected em B
cb.c = 6; // vlido (c continua public em B)
}
Construtores e destrutores
Quando uma classe instanciada, seu construtor chamado. Se a classe foi derivada de alguma outra,
o construtor da classe base tambm precisa ser chamado. A ordem de chamada dos construtores fixa em
C++. Primeiro a classe base construda, para depois a derivada ser construda. Se a classe base tambm
deriva de alguma outra, o processo se repete recursivamente at que uma classe no derivada alcanada.
Desta forma, quando um construtor para uma classe derivada chamado, todos os procedimentos
efetuados pelo construtor da classe base j foram realizados. Considere a seguinte rvore de herana:
class Primeira {};
class Segunda: public Primeira {};
class Terceira: public Segunda {};
Quando a classe Terceira instanciada, os construtores so chamados da seguinte maneira:
Primeira::Primeira();
Segunda::Segunda();
Terceira::Terceira();
Esta ordem faz sentido, j que uma classe derivada uma especializao de uma classe mais genrica.
Isto significa que o construtor de uma classe derivada pode usar atributos herdados.
Os destrutores so chamados na ordem inversa dos construtores. Primeiro, os atributos mais
especializados so destrudos, depois os mais gerais. Ento a ordem de chamada dos destrutores quando
Terceira sai do escopo :
Terceira::~Terceira();
Segunda::~Segunda();
Primeira::~Primeira();
Como os construtores das classes base so chamados automaticamente, deve existir alguma maneira
de passar os argumentos corretos para estes construtores, no caso de eles necessitarem de parmetros.
Existe uma notao especial para este caso, ilustrada abaixo, para funes inline e no inline:
class Primeira {
int a, b, c;
public:
Primeira(int x, int y, int z) { a=x; b=y; c=z; }
};
class Segunda : public Primeira {
int valor;
public:
Segunda(int d) : Primeira(d, d+1, d+2) { valor = d; }
Segunda(int d, int e);
};
Segunda::Segunda(int d, int e) : Primeira(d, e, 13)
{
valor = d + e;
}
A partir do exemplo acima, no difcil perceber que, se uma classe base no possui um construtor
sem parmetros, a classe derivada tem que, obrigatoriamente, declarar um construtor, mesmo que este
construtor seja vazio:
class Base {
protected:
int valor;
public:
Base(int a) { valor = a; }
// esta classe no possui um construtor
// sem parmetros
};
class DerivadaErrada : public Base{
public:
int pegaValor() { return valor; }
// ERRO! classe no declarou construtor, compilador no
// sabe que parmetro passar para Base
};
class DerivadaCerta: public Base {
public:
int pegaValor() { return valor; }
DerivadaCerta() : Base(0) {}
// CERTO: mesmo que no haja nada a fazer
// para inicializar a classe,
// necessrio declarar um construtor
// para dizer com que parmetro
// construir a classe Base
};
Captulo 6
Polimorfismo
A origem da palavra polimorfismo vem do grego: poli (muitos) e morphos (forma) - mltiplas formas.
Polimorfismo descreve a capacidade de um cdigo C++ se comportar de diferentes formas dependendo do
contexto em tempo de execuo.
Este um dos recursos mais poderosos de linguagens orientadas a objetos (se no o mais), que permite
trabalhar em um nvel de abstrao bem alto ao mesmo tempo que facilita a incorporao de novos pedaos
em um sistema j existente. Em C++ o polimorfismo se d atravs da converso de ponteiros (ou referncias)
para objetos.
Converso de ponteiros
Normalmente se usam no objetos de classes isoladas, mas sim objetos em uma hierarquia de classes.
Considere as seguintes classes:
class A {
public: void f();
};
class B: public A {
public: void g();
};
Como B derivado de A, todos os membros disponveis em A (funo f) tambm estaro disponveis em
B. Ento B um superconjunto de A, e todas as operaes que podem ser feitas com objetos da classe A
tambm podem ser feitas com objetos do tipo B. A classe B uma especializao da classe A, e no s um
objeto do tipo B, mas tambm um objeto do tipo A. Nada impede que objetos da classe B sejam vistos como
sendo da classe A, pois todas as operaes vlidas para A so tambm vlidas para B. Ver um objeto do tipo B
como sendo do tipo A significa convert-lo para o tipo A. Esta converso pode ser feita, sempre no sentido da
classe mais especializada para a mais bsica. A converso inversa no permitida, pois operaes especficas
de B no so vlidas sobre objetos da classe A. Converso aqui no deve ser entendida como cpia. A simples
atribuio de um objeto do tipo B para um objeto do tipo A copia a parte A do objeto do tipo B para o objeto
do tipo A. O polimorfismo feito atravs da converso de ponteiros. O exemplo abaixo mostra as vrias
alternativas:
void main()
{
A a, *pa; // pa pode apontar para objetos do tipo A e derivados
B b, *pb; // pb pode apontar para objetos do tipo B e derivados
a = b;
b = a;
pa = &a;
pa = &b;
pb = pa;
pb = &b;
pb = &a;
//
//
//
//
//
//
//
}
Para tirar qualquer dvida sobre quais converses podem ser feitas, o exemplo abaixo mostra o que pode
ser feito com este recurso a partir das classes A e B:
void chamaf(A* a) // pode ser chamada para A e derivados
{
a->f();
}
funo f
tem a funo g
ser convertido para o tipo B)
funo f
funo g
Repare que as funes chamaf e chamag foram escritas para os tipos A e B, mas podem ser usadas com
qualquer objeto que seja derivado destes. Se um novo objeto derivado de B for criado no futuro, a mesma
funo poder ser usada sem necessidade de recompilao.
Estas converses s podem ser feitas quando a herana pblica. Se a herana for privada a converso
no permitida.
//
//
//
//
possvel tambm declarar um mtodo com mesmos nome e assinatura (tipo de retorno e tipo dos
parmetros) que um da classe base. O novo mtodo esconde o da classe base, que precisa do operador de
escopo para ser acessado. No entanto, esta redefinio merece ateno especial. Considerando o exemplo:
class A {
public: void f();
};
class B : public A {
public: void f();
};
void main()
{
List l1;
ListN l2;
manipula_lista(&l1);
manipula_lista(&l2);
printf("a lista l2 contm %d elementos\n", l2.nelems());
}
A funo manipula_lista utiliza apenas os mtodos add e remove, portanto pode operar tanto sobre objetos
do tipo List quanto do tipo ListN. Supondo que esta funo retorna deixando cinco elementos na lista, qual
ser o resultado do printf? Assim como na seo anterior, as funes chamadas em manipula_lista sero
List::add e List::remove. Como estas funes no alteram a varivel n, o resultado do printf ser:
a lista l2 contm 0 elementos
Ou seja, a manipulao deixou o objeto l2 inconsistente internamente. Para manter a sua consistncia,
seria preciso que os mtodos ListN::add e ListN::remove fossem chamados, o que no est acontecendo.
Mtodos virtuais
Em C++, late-binding especificado declarando-se um mtodo como virtual. Late-binding s faz sentido
para objetos que fazem parte de uma hierarquia de classes. Se um mtodo f declarado virtual em uma classe
Base e redefinido na classe Derivada, qualquer chamada a f sobre um objeto do tipo Derivada, mesmo que
via um ponteiro para Base, executar Derivada::f. A redefinio de um mtodo virtual tambm virtual. A
especificao virtual nesse caso redundante.
Este mecanismo pode ser usado com as listas encadeadas para manter a consistncia dos objetos do tipo
ListN. Basta declarar, na classe List, os mtodos add e remove como virtuais:
class List {
public:
List();
Destrutores virtuais
No faz sentido construtores poderem ser virtuais, j que eles no so chamados a partir de um objeto, mas
sim para criar objetos. Destrutores, apesar de serem mtodos especiais, podem ser virtuais porque so
chamados a partir de um objeto. A chamada se d em duas situaes: quando o objeto sai do escopo e quando
ele destrudo com delete. Na primeira situao, o compilador sabe o tipo exato do objeto e portanto chama o
destrutor correto. J na segunda situao isso pode no ocorrer, como mostra o exemplo abaixo:
class A {
// ...
};
class B : public A {
int* p;
public:
B(int size) { p = new int[size]; }
~B() { delete [] p; }
// ...
};
void destroy(A* a)
{
delete a; // chama destrutor
}
void main()
{
B* b = new B(20);
destroy(b);
}
O que est acontecendo exatamante o que acontecia na primeira verso das listas encadeadas. A funo
destroy chama (com early-binding) diretamente o destrutor A::~A, quando o certo seria chamar B::~B antes,
para depois chamar A::~A. Para forar este funcionamento correto, preciso que o destrutor seja virtual
(virtual ~A()). Caso contrrio, objetos podem ser destrudos e deixando alguma coisa para trs.
Tabelas virtuais
Esta seo tem por objetivo esclarecer um pouco o que acontece por trs dos mtodos virtuais, ou seja,
como late-binding implementado. Este conhecimento ajuda o entendimento dos mtodos virtuais, suas
limitaes, poderios e eficincia.
Imaginando a seguinte hierarquia de classes:
class A {
int a;
public:
virtual void f();
virtual void g();
};
class B : public A {
int b;
public:
a1
vtable
a = 666
b1
vtable
a = 10
b = 20
b2
vtable
a = 65
b=7
Tabela de A
A::f
A::g
Tabela de B
B::f
A::g
B::h
Considerando que a tabela virtual um campo de todos objetos (com nome vtable por exemplo), a funo
chamaf pode ser implementada assim:
void chamaf(A *a)
{
a->vtable[0](); // posio 0 corresponde ao mtodo f
}
Esta implementao funcionar com qualquer objeto derivado de A, at mesmo de classes futuras, pois
no exige que o compilador saiba quais so as classes existentes. A eficincia sempre a mesma
independente de quantas classes existam ou qual a sua altura na rvore de hierarquia. Esta eficincia no a
mesma de uma chamada de funo normal (com early-binding) pois envolve uma indireo.
int
empty();
};
Repare que, para a criao da fila, a herana usada foi a privada. Isto evita que os mtodos da lista
encadeada possam ser usados diretamente sobre a fila, o que seria um furo na consistncia do objeto.
Estas duas situaes tem mais diferenas do que uma simples herana pblica ou privada.
Conceitualmente estas duas heranas esto sendo feitas com objetivos totalmente diferentes. No primeiro
caso, o objetivo da herana simplesmente permitir a utilizao da classe StackVector como uma Stack. Ou
seja, aplicaes que manipulem Stacks tambm podem ser usadas para manipular StackVectors. Nesse caso a
herana s feita para que a classe derivada tenha o tipo da classe base. a herana de tipo.
No segundo caso, o objetivo no usar objetos do tipo Queue em cdigos que manipulem LinkedList. A
herana est sendo usada para reutilizar o cdigo escrito para implementar listas encadeadas, evitando a reimplementao do que j est pronto. a herana de cdigo.
Normalmente a herana privada se aplica a uma herana de cdigo, enquanto que a pblica se aplica
herana de tipo. Esta diferenciao importante na hora de projetar sistemas.
class BinTreeObj {
public:
virtual int operator==(BinTreeObj&) = 0;
virtual int operator> (BinTreeObj&) = 0;
};
A declarao destes mtodos virtuais puros implica que qualquer tipo derivado deste precisa fornecer uma
implementao para estes mtodos para deixar de ser abstrato. A verso modificada da rvore fica assim:
class BinTree {
struct elem {
elem* right;
elem* left;
BinTreeObj& val;
elem(BinTreeObj& f) : val(f) { right=left=0; }
} *root;
int look(elem* no, BinTreeObj& f)
{
if (!no) return 0;
if (no->val == f) return 1;
if (no->val > f) return look(no->left, f);
return look(no->right, f);
}
void put(elem*& no, BinTreeObj& f)
{
if (!no) no = new elem(f);
else if (no->val > f) put(no->left, f);
else put(no->right, f);
}
public:
BinTree() { root = 0; }
int find(BinTreeObj& v) { return look(root, v); }
void insert(BinTreeObj& v) { put(root, v); }
};
Como o polimorfismo se d pela converso de ponteiros, a rvore deve armazenar apenas ponteiros ou
referncias. Para utilizar esta rvore, basta fazer com que o tipo dos objetos a serem armazenados herdem da
classe BinTreeObj. Para armazenar algum tipo primitivo necessrio criar uma nova classe derivada de
BinTreeObj que armazene este tipo.
void main()
{
BinTree bt;
bt.insert( * new String("abc") ); // ERRO! String abstrata
}
O problema que os mtodos declarados em String no esto redefinindo os da classe abstrata, mas
escondendo-os. Analisando a assinatura dos dois mtodos, percebe-se a diferena:
int BinTreeObj::operator==(BinTreeObj&);
int String
::operator==(String& o)
Se fosse o mtodo de String estivesse redefinindo o de BinTreeObj, seria possvel converter objetos de
uma classe mais bsica para uma mais especfica. Como foi visto com polimorfismo, esta no uma
converso segura. Na implementao da rvore binria, este mtodo chamado e o parmetro sempre uma
referncia para um BinTreeObj, que pode no ser uma String. Para redefinir um mtodo, os parmetros devem
ser ou do mesmo tipo do declarado na classe base ou de um tipo mais bsico. Nunca de um tipo derivado. O
contrrio acontece com o tipo de retorno de uma funo.
Chegando concluso de que os parmetros dos mtodos de String devem ser do tipo BinTreeObj, surge
outro problema. Como acessar o campo str e fazer a comparao das Strings? Deve haver alguma maneira de
converter um BinTreeObj em uma String, mesmo que esta converso no seja segura. Na realidade esta
converso pode ser forda atravs de type casts explcitos, assim como em C possvel converter um ponteiro
para outro de um tipo completamente diferente. Se o objeto for realmente do tipo desejado, a converso
feita. Mas se o tipo no for o esperado, os resultados so imprevisveis. Mas nesse caso, a nica opo:
class String : public BinTreeObj {
char *str;
public:
String(char* s) { str = s; }
int operator==(BinTreeObj& o)
{ return !strcmp(str, ((String&)o).str); }
int operator> (BinTreeObj& o)
{ return strcmp(str, ((String&)o).str) > 0; }
};
Esta com certeza no a melhor maneira de se implementar uma classe que manipula um tipo qualquer.
Existem duas outras maneiras seguras, vistas mais frente: templates e type casts dinmicos.
Herana mltipla
Em C++, a herana no se limita a uma nica classe base. Uma classe pode ter vrios pais, herdando
caractersticas de todos eles. Este tipo de herana introduz grande dose de complexidade na linguagem e no
compilador, mas os benefcios so substanciais. Considere a criao de uma classe MesaRedonda, tendo no
s propriedades de mesas, mas tambm as caractersticas geomtricas de ser redonda. O cdigo abaixo uma
possvel implementao:
class Circulo {
float raio;
public:
Circulo(float r) { raio = r; }
float area()
{ return raio*raio*3.14159; }
};
class Mesa {
float ipeso;
float ialtura;
public:
Mesa(float p, float a) { ipeso = p; ialtura=a; }
float peso()
{ return ipeso; }
float altura() { return ialtura; }
};
class MesaRedonda: public Circulo, public Mesa {
int icor;
public:
MesaRedonda(int c, float a, float p, float r);
int cor() { return icor; }
};
MesaRedonda::MesaRedonda(int c, float a, float p, float r)
: Mesa(p, a), Circulo(r)
{
icor = c;
}
void main()
{
MesaRedonda mesa(5, 1,
printf("Peso:
%f\n",
printf("Altura: %f\n",
printf("Area:
%f\n",
printf("Cor:
%d\n",
};
20, 3.5 );
mesa.peso());
mesa.altura());
mesa.area());
mesa.cor());
Um exemplo natural poderia sair da seo que discute herana de tipo ou cdigo. Para implementar um
pilha com listas encadeadas que possa ser usada como Stack e aproveitando uma classe j implementada de
listas encadeadas, a declarao seria assim:
class StackList : public Stack, private LinkedList {
// ...
};
D(): A(3) {}
};
Captulo 7
Programao orientada a eventos
A programao tradicional
No passado, um sistema computacional era composto por uma CPU responsvel por todo o
processamento (mestre) e uma srie de perifricos (escravos), responsveis pela entrada e sada de dados.
Neste paradigma, o usurio assumia uma posio de perifrico, j que ele era somente um mecanismo
de entrada de dados.
O custo/benefcio de uma CPU era extremamente superior ao de qualquer equipamento ou (custo de
trabalho) usurio a ela conectados. Consequentemente, todos os sistemas davam prioridade mxima ao
processamento, em relao entrada e sada de dados.
Com isso, era comum encontrarmos sistemas em que a fatia de tempo dedicada aos perifricos era
sensivelmente menor do que a dedicada CPU.
Esta diferena de custos implicava uma menor dedicao, por parte dos programadores, aos
perifricos e usurios em geral.
Ao colocar o usurio como escravo, este paradigma prejudicava a interao direta com o sistema.
O usurio no tinha como realizar de forma gil suas operaes, j que o sistema no priorizava a
entrada e a sada de informaes.
O dilogo dos usurios com o sistema foi evoluindo, ao longo dos tempos, porm continuava se
apresentando como uma relao mestre-escravo, em que o primeiro ditava a seqncia da interao.
Mesmo os sistemas ditos avanados apresentavam uma hierarquia rgida de menus e/ou formulrios,
que eram preenchidos pelo usurio medida em que este navegava por essa estrutura.
Uma aplicao tpica que utiliza o DOSGRAPH (e qualquer outro pacote orientado a eventos) tem
como ncleo um loop que captura os eventos e faz o processamento adequado. Todo o processamento
feito em resposta aos eventos.
Como aplicaes que utilizam o mouse so tipicamente grficas, o DOSGRAPH fornece vrias
funes de desenho que podem ser utilizadas, por exemplo dgLine, que desenha uma linha com a cor
corrente. As funes do DOSGRAPH podem ser classificadas em:
funes de desenho: dgLine, dgRectangle, dgFill, dgSetColor e dgSetMode;
funes de consulta: dgWidth e dgHeight;
funes de inicializao: dgOpen e dgClose;
funes de controle: dgGetEvent.
Os tipos e funes definidos pelo DOSGRAPH esto definidos no apndice A desta apostila.
Captulo 8
Projeto de uma aplicao orientada a objetos
Exerccio 13 - Objetos grficos que se movem na tela.
Implementar uma aplicao grfica que permita drag de objetos. A organizao do programa deve ser
feita independentemente do tipo de objeto que ser arrastado. Podem coexistir objetos diferentes na tela.
Basicamente o funcionamento deve ser:
Assim que o programa comea, a tela est vazia;
Um click com o boto esquerdo no fundo da tela cria objetos novos e
Um click sobre um objeto comea o arraste.
Ateno para a ordem dos objetos na tela: se dois esto sobrepostos, um click na rea comum deve
pegar o objeto visualmente acima. Para diferenciar a ordem dos objetos na tela, por exemplo use cores
diferentes a cada criao.
Aplicao
rea
Desenho1
Lista
Desenho2
Desenho N
A
estrutura
apresentada esta no nvel
de
projeto.
Para
a
aplicao, necessrio
definir a criao dos
objetos e os prprios
objetos em si, o que pode
ser feito especializando-se
algumas classes. Como a
funcionalidade do drag
independe do desenho, ela
deve estar em alguma das
classes acima. Coloque em
Desenho todos os mtodos
necessrios
para
esta
funcionalidade, para que a
Area possa manipul-los.
Captulo 9
Templates
Uma classe C++ normalmente projetada para armazenar algum tipo de dado. Muitas vezes a
funcionalidade de uma classe tambm faz sentido com outros tipos de dados. Este o caso de muitos
exemplos apresentados (por exemplo, Pilha).
Se uma classe vista simplesmente como um manipulador de dados, pode fazer sentido separar a
definio desta classe da definio dos tipos manipulados; isto , pode-se fazer uma descrio de uma classe
sem definir o tipo dos dados que ela manipula. Nesse caso, definio da classe parametrizada por um tipo
genrico T, sendo chamada C<T>. Esta construo no uma classe realmente, mas uma descrio do
conjunto de classes com o mesmo comportamento e que operam sobre um tipo T qualquer. Esta construo
denominada class template.
Com class templates, permitido ao programador criar um srie de classes distintas com a mesma
descrio, mas sobre tipos distintos. Por exemplo, pode-se construir uma pilha de inteiros (Pilha<int>) ou
pilha de strings (Pilha<char*>) apartir da mesma descrio. Esta descrio abstrata da template utilizada
pelo compilador para criar uma classe real em tempo de compilao, usando o tipo dos dados especificados
quando de seu uso.
Em alguns textos acadmicos, esta funcionalidade chamada de polimorfismo paramtrico.
Declarao de templates
Consideremos novamente a classe Pilha, que j apareceu com vrias implementaes. Em todos os
exemplos, a classe s armazenava inteiros, apesar de a funcionalidade nada ter a ver com o tipo de dado
envolvido. Neste caso, ao invs de reescrevermos uma nova classe pilha para cada novo tipo demandado,
pode-se definir uma template para a classe Pilha, onde o tipo armazenado um T qualquer. A forma deste
declarao mostrada abaixo:
template<class T> class Pilha {
struct elemPilha {
elemPilha* prox;
T val;
elemPilha(elemPilha*p, T v) { prox=p; val=v; }
};
elemPilha* topo;
public:
int vazia()
{ return topo == NULL }
void push(T v) { topo = new elemPilha(topo, v); }
T pop()
{ if (topo)
{
elemPilha *ep = topo;
T v = ep->val;
topo = ep->prox;
delete ep;
return v;
}
return -1;
}
};
Tendo em vista que uma template simplesmente uma descrio de uma classe, necessrio que toda esta
descrio tenha sido lida antes de alguma declarao que envolva esta template. Isto significa, em se tratando
de templates, que necessrio colocar no arquivo .h no apenas a declarao de classe, mas tambm a
implementao de seus mtodos.
Outra consequncia de templates serem apenas descries que erros semnticos s aparecem na hora de
usar a template. Durante a declarao da template apenas erros de sintaxe so checados. Mesmo que a
template em si tenha sido compilada sem erros, podem aparecer erros quando de sua utilizao.
Esta checagem semntica realizada todas as vezes que a template instanciada para algum tipo novo.
Isto porque na definio do cdigo da template no h nenhuma restrio quanto s operaes que podem ser
aplicadas ao tipo T. A checagem ento tem que ser feita para cada tipo.
Usando templates
Definida a implementao de nossa template de pilhas, pode-se utiliz-la para qualquer tipo T criando
pilhas de inteiros, strings,etc. Na criao de objetos destas classes pilhas, necessrio especificar o tipo de
pilha na declarao do objeto:
void main()
{
Pilha<int> intPilha;
// pilha de inteiros
Pilha<char*> stringPilha;
// pilha de strings
Pilha<Pilha<int>> intPilhaPilha; // pilha de pilha de inteiros
intPilha.push(10);
stringPilha.push( "teste" );
intPilhaPilha.push( intPilha );
}
Templates de funes
Templates tambm podem ser usadas para definir funes. A mesma motivao para classes vale neste
caso. Algumas vezes funes realizam operaes sobre dados sem utilizar o contedo deles, ou seja,
indepentemente de que tipo de dado seja.
Suponha que precisamos testar a magnitude de dois elementos quaisquer. A seguinte macro resolve este
problema:
#define max(a,b) ((x>y) ? x : y)
Por muito tempo macros como esta foram usadas em programas C, mas isto tem os seus problemas. A
macro funciona, mas impede que o compilador teste os tipos dos elementos envolvidos. A macro poderia ser
utilizada para comparar um inteiro com um ponteiro, sem que sejam gerados erros.
Poderamos usar uma funo como esta:
int max(int a, int b)
{
return a > b ? a : b;
}
Mas esta funo s funciona para inteiros. Se o nosso programa s trabalha com escalares, poderamos
escrever uma funo que trabalhe com double:
double max(double a, double b)
{
return a > b ? a : b;
}
Nesse caso, o compilador se encarrega de converter os tipos char, int etc. para double, e a funo
funcionaria para todos estes casos.
Existem pelo menos duas limitaes nesta verso. Uma delas diz respeito ao tipo dos parmetros: apenas
tipos que podem ser convertidos para double podem usar esta funo. Isto significa que objetos e ponteiros
no podem ser utilizados. A outra limitao se refere ao tipo de retorno: este sempre double, independente
do tipo passado. Suponha que a funo display seja sobrecarregada para imprimir vrios tipos de dados.
Agora considere o cdigo:
display(max('1', '9'));
Apesar de estarmos trabalhando com caracteres, a funo display a ser chamada ser a verso que trabalha
com double, e o resultado ser 57.00, que o cdigo ASCII do caractere 9.
A opo de sobrecarregar max para vrios tipos tambm no boa, pois teramos que reescrever o cdigo,
que seria idntico, para todos os tipos que quisssemos usar. Ainda assim o problema no estaria resolvido,
pois novos tipos no poderiam ser usados sem que se criasse outra verso sobrecarregada de max.
Na verdade, qualquer tipo de dado que possua operaes de comparao pode ser usado com max, e
sempre da mesma maneira. No possvel escrever uma nica funo que trate todos os tipos, mas o
mecanismo de templates possibilita descrever, para o compilador, como estas funes podem ser
implementadas:
template<class T> T max( T a, T b )
{
return a > b ? a : b;
}
Esta funo faz sentido para qualquer tipo T. Na realidade existir uma implementao para cada tipo que
for usado com esta funo dentro do programa, mas estas funes sero geradas transparentemente pelo
compilador. O uso de templates de funes no exige que se especifique explicitamente os tipos genricos na
chamada, como em templates de classes. Basta usar como se existisse uma funo especfica para o tipo
envolvido:
void main()
{
printf("%c", max('1', '9'));
}
Repare que, apesar de a funo ser usada para comparar caracteres, em nenhum momento aparece a
palavra char. O compilador sabe qual max precisa ser chamada pelo tipo dos parmetros usados. Por causa
disto, os tipos genricos de templates de funes devem sempre aparecer nos parmetros da funo. Caso isto
no acontea, ser sinalizado um erro de sintaxe na linha da declarao da template.
Assim como em templates de classes, templates de funes podem ter vrios parmetros genricos.
Tratamento de Excees
Quando do desenvolvimento de bibliotecas, possvel escrever cdigo capaz de detectar erros de
execuo mas, em geral, no possvel fazer seu tratamento. Por outro lado, o usurio de uma biblioteca
capaz de fazer o correto tratamento de uma exceo mas no capaz de detect-la.
O conceito de exceo introduzido para ajudar neste tipo de problema. A idia fundamental que uma
funo que detecte um problema e no seja capaz de resolv-lo acuse a exceo esperando que quem a
chamou seja capaz de realizar o tratamento adequado.
Funcionamento bsico
O funcionamento dos tratadores de exceo composto de diversas etapas:
Definio das excees que uma classe pode levantar;
Definio de quando uma classe acusa uma exceo;
Definio do(s) tratador(es) das excees.
A definio da execees que podem ser levantadas feita na construo da classe. Neste momento
define-se as condies de erro e os representa sob a forma de uma classe. Desta forma, cada condio de
exceo descrita por uma classe de exceo. Por exemplo, considere como representar e tratar o erro de
indexao fora dos limites de um array dada pela classe Vector:
class Vector {
int* p;
int sz;
public:
class Range{ }; // classe de tratamento de exceo que
// representa acesso com ndice invlido
int& operator[]( int i );
};
A classe Range, a princpio sem dados internos, representa a condio de erro para acesso com ndice no
vlido (menor que zero, maior que o espao alocado, etc.)
A definio dos momentos da exceo so tambm definidos na construo da classe. A acusao das
excees feita normalmente nos mtodos da classe que encontrem alguma situao de erro. O levantamento
de uma exceo feita pelo comando throw que, conforme no nosso exemplo, acionado quando o ndice do
array invlido. Nestas situaes, os objetos da classe Range so utilizados como excees e so acusados da
seguinte forma:
int& Vector::operator[]( int i )
{
if ( i<0 || i>size-1 ) throw Range();
// Range() cria um objeto da classe Range
//
//
//
//
}
A definio dos tratadores aparecem, normalmente, nas funes que utilizam servios das classes que
levantam excees. No caso de C++, estas funes podem selecionar quais as excees que sero tratadas e
trechos onde estas podem surgir.
No nosso exemplo, a funo que precisa detectar a utilizao de ndices fora do limite indica seu interesse
pelo tratamento colocando cdigo correspondente na seguinte forma:
void f( Vector& v )
{
//.. cdigo qualquer sem tratamento
try{
//.. cdigo qualquer com tratamento
operao_qualquer( v );
}
catch( Vector::Range ) {
// Aqui se encerra o cdigo de tratamento da exceo
// Range.
// A funo operao_qualquer apresentou a exceo
// que est sendo tratada.
}
catch( Vector::Size ) {
// cdigo de tratamento para criao de vetor muito grande
}
// esse cdigo executado se no tiver ocorrido nenhuma
// exceo.
}
Nomeao de excees
Uma exceo tomada pelo manipulador no pelo seu tipo mas sim por um objeto. Havendo necessidade
de transmitir alguma informao do levantamento da exceo para o manipulador, necessrio haver algum
mecanismo de colocar tal informao neste objeto. Isto feito colocando-se campos dentro da classe que
representa a exceo e tomando seus valores nos manipuladores. Para tomarmos estes valores nos handles,
necessria a definio de um nome para o objeto criado na acusao.
No exemplo criado, importante saber qual o valor que foi usado como ndice na exceo Vector::Range
(indexao fora dos limites):
class Vector{
// ...
public:
class Range {
public:
int index;
// criao do campo que diz o valor invlido
Range( int i ) { index = i; }
// a criao do objeto indica o valor invlido
};
int& operator[]( int i )
// ...
};
int& Vector::operator[]( int i )
{
if ( i<0 || i>size-1 ) throw Range(i);
// acusa-se a exceo indicando
// o valor ndice invalido ao construir Range
return p[i];
}
Para examinar o ndice incorreto, o manipulador deve dar um nome ao objeto da exceo:
void f( Vector& v )
{ /...
try {
qualquer_operao(v);
}
catch( Vector::Range r ) {
// 'r' o nome do objeto Range acusado no operador []
printf( "ndice errado: %d \n", r.index );
exit(0);
}
// ...
};
interessante notar que no caso de templates, tem-se a opo de nomear a exceo de modo que cada
classe instanciada pela template tenha sua prpria classe de exceo:
Agrupamento de excees
Normalmente as excees podem ser categorizadas em famlias. Por exemplo, pode-se imaginar um erro
matemtico que inclua as excees de overflow, underflow, diviso por zero, etc. A exceo de erro
matemtico (MATHERR) pode ser determinada pelo conjunto de erros que podem ser produzidos em uma
biblioteca de funes numricas.
Uma maneira de fazer MATHERR determin-la como um tipo de todos os possveis erros numricos:
enum MATHERR { Overflow, Underflow, ZeroDivide };
e na funo que trata os erros:
void f( .... )
{
try {
// ...
}
catch( MATHERR m ) {
switch( m ) {
case
//
case
//
case
//
}
Overflow:
...
Underflow:
...
ZeroDivide:
...
}
}
De outra maneira, C++ usa a capacidade de herana e de funes virtuais para evitar este tipo de switch.
possvel a utilizao de herana para descrever colees de excees. Por exemplo:
class MATHERR {};
class Overflow: public MATHERR {} ;
class Underflow: public MATHERR {} ;
class ZeroDivide: public MATHERR {} ;
// ....
Para este caso, existem muitas ocasies em que deseja-se fazer o tratamento de MATHERR sem saber
precisamente de que tipo o erro. Com a utilizao de herana, possvel dizer:
try {
// ...
}
catch( Overflow )
// tratamento de
}
catch ( MATHERR )
// tratamento de
}
{
overflow ou tudo derivado de overflow
{
qualquer outro erro numrico
Excees derivadas
A utilizao de hierarquias de excees naturalmente direciona os manipuladores que esto interessados
somente em um subconjunto da informao carregada pelas excees. Em outras palavras, uma exceo
normalmente tomada por um manipulador da classe bsica ao invs de um da classe exata. Por exemplo:
class MATHERR {
// ...
virtual void debug_print() {};
};
class int_overflow : public MATHERR {
public:
char op;
int opr1, opr2;
int_overflow( const char p, int a, int b )
{ op=p; opr1=a; opr2=b; }
virtual void debug_print() // redefinio de debug_print
{ printf( "operador:%c:( %d, %d )", op, opr1, opr2 ); }
};
void f()
{
try{
g();
}
catch( MATHERR m ) { /* ... */ }
}
Quando um manipulador MATHERR encontrado, m um objeto MATHERR mesmo que a chamada de g
tenha acusado um int_overflow. Isto implica que a informao extra encontrada em int_overflow est
inacessvel; isto , se dentro do tratador chamarmos a funo debug_print, no conseguiremos ver nada sobre
o erro de overflow de inteiros. Isto devido ao fato do compilador no fazer late-binding com o objeto.
No entanto, ponteiros e referncias podem ser utilizados para evitar esta perda de informao. Para tal,
pode-se escrever:
int add( int x, int y )
{
if ( x>0 && y>0 && x>MAXINT - y
|| x<0 && y<0 && x<MININT + y )
throw int_overflow( '+', x, y );
return x + y;
}
void f()
{
try {
add( 1, 2 ); // ok
add( MAXINT, 3 ); // causa exceo
}
catch( MATHERR& m ) { // recebe no manipulador uma referncia
// ...
m.debug_print();
// chama o mtodo de int_overflow!!!
}
}
Re-throw
Dada uma funo que capture uma exceo, no incomum para um manipulador chegar a concluso que
nada pode ser feito a respeito do erro. Neste caso, a coisa tpica a ser feita o acusao da exceo novamente
(re-throw), esperando que outro manipulador possa faz-lo melhor. Por exemplo:
void h()
{
try {
// ...
}
catch( MATHERR ) {
if ( posso_tratar() ) tratamento();
else throw; // re-throw
}
}
Um re-throw indicado pelo comando throw sem argumentos. A exceo de relevantamento a exceo
original tomada e no somente a parte que era acessvel como MATHERR. Em outras palavras, se um
int_overflow foi acusado, a funo que chamou h pode ainda tomar um int_overflow que h tomou como
MATHERR e decidiu reacusar.
void k()
{
try {
h();
// ...
}
catch( int_overflow ) {
// ...
}
}
A verso abaixo deste tipo de comportamento pode ser til. Assim como em funes, pode-se utilizar ...
(indicando qualquer argumento) de modo que catch(...) signifique qualquer exceo. Por exemplo:
void m()
{
try {
// ...
}
catch(...) {
limpeza();
throw;
}
}
Isto , se qualquer exceo ocorrer, resultado da execuo de parte de m(), a funo limpeza() ser
chamada no manipulador e a exceo que causou a chamada da funo limpeza() ser reacusada.
Devido ao fato de que excees derivadas podem ser tratadas por manipuladores para mais de um tipo de
exceo, a ordem em que os estes aparecem aps o bloco de try relevante. Os tratadores so escolhidos em
ordem. Por exemplo:
try {
// ...
}
catch( ibuf ) {
// tratador de input overflow
}
catch( io ) {
// tratador de qualquer erro de I/O
}
catch( stdlib ) {
// tratador de qualquer erro em bibliotecas
}
catch( ... ) {
// tratador de qualquer outra exceo
}
Especificao de excees
A acusao e o tratamento de excees afetam o relacionamento entre as funes. Neste sentido,
interessante haver um mecanismo de especificar quais excees que podem ser levantadas como parte da
declarao de uma funo. Por exemplo:
void f( int a ) throw (x2, x3, x4);
especifica que a funo f s pode acusar as excees x2, x3, x4 e suas derivadas, nada mais. Deste modo,
est garantindo para quem a chama que durante sua execuo nenhuma outra exceo ser levantada. Se, por
acaso, algo acontecer que invalide esta garantia, a tentativa de acusao de uma exceo indevida ser
transformada em uma chamada para a funo unexpected. O significado default para unexpected a chamada
a terminate, que normalmente representa um abort.
Desta forma, escrever:
void f( int a ) throw (x2, x3, x4)
{ /* implementacao qualquer */ }
significa a mesma coisa que:
void f( int a )
{
try{
/* implementao qualquer */
}
catch(x2) { throw; } // re-throw
catch(x3) { throw; } // re-throw
catch(x4) { throw; } // re-throw
catch(...){ unexpected(); }
}
Mais que economia de digitao, o uso de especificao de excees explicita as excees que podem
surgir na definio da funo (.h) o que nem sempre aconteceria se esta definio ficasse em sua
implementao.
Uma funo sem especificao pode levantar qualquer exceo.
int f ();
enquanto que uma funo sem a possibilidade de acusar qualquer exceo declarada com uma lista
explicitamente vazia:
int g() throw();
Excees indesejadas
O mal uso de especificaes de excees pode levar a chamadas a funo unexpected, que indesejvel a
no ser no caso de testes. Pode-se evitar isto por uma boa estruturao e organizao das excees ou pela
interceptao das chamadas a unexpected.
A funo set_unexpected serve para interceptarmos estes casos. Esta funo redefine o comportamento do
sistema quando de uma exceo indesejada retornando o tratador antigo.
Abaixo temos um exemplo deste mecanismo. Neste caso, cria-se uma classe que representa um trecho
onde excees no previstas devem ser tratadas.
typedef void(*functype)();
functype set_unexpected( functype );
class MyPart {
functype old;
public:
MyPart( functype f ) { old = set_unexpected( f ); }
~MyPart() { set_unexpected( old ); }
}
void new_trat() { printf("novo tratamento.\n"); }
void f()
{
MyPart( &new_trat ); // construtor implica em redefinio
g();
} // destrutor reseta unexpected() anterior
Neste caso, a execuo de f protegida contra erros de excees no desejadas.
Excees no tratadas
Uma exceo acusada e no tratada implica na chamada da funo terminate. Esta tambm chamada se o
mecanismo de excees de C++ encontrar a pilha corrompida. terminate executa a ltima funo recebida
como argumento da funo set_terminate.
Este mecanismo serve como mais um nvel para erros de exceo. normalmente utilizado para medidas
mais drsticas no sistema como: aborto da execuo do processo, reinicializao do sistema, etc.
A redefinio deste comportamento feito de modo anlogo ao unexpected.
typedef void (*PFV) ();
PFV set_terminate (PFV);
public:
Stack(int s = 50) { size = s; top = 0;
elems = new int[size]; }
~Stack()
{ delete [] elems; }
void push(int i)
{ if (top>=size) throw StackFull(size);
elems[top++]=i; }
int pop()
{ if (top==0) throw StackEmpty();
return elems[--top]; }
int empty()
{ return top == 0; }
};
A prpria calculadora tem seus erros:
class RpnError{};
class RpnNoOperands : public RpnError, public StackEmpty {};
class RPN : public Stack {
void getop(int* n1, int*
public:
void sum() { int n1, n2;
void sub() { int n1, n2;
void mul() { int n1, n2;
void div() { int n1, n2;
};
n2)
throw(RpnNoOperands);
getop(&n1,
getop(&n1,
getop(&n1,
getop(&n1,
&n2);
&n2);
&n2);
&n2);
push(n1+n2);
push(n1-n2);
push(n1*n2);
push(n1/n2);
}
}
}
}