Entendendo C++
Herança

5.0 - Introdução
5.1 - Um exemplo
5.2 - Um exemplo mais avançado
5.3 - Conclusão

5.0 - Introdução

Digamos que você já implementou uma lista de classes, e que agora deseja modificá-las. No velho mundo da programação você tomaria o código fonte de cada classe e começaria a alterá-los. No mundo da programação orientada a objeto você faz as coisas de modo bem diferente. Você deixa as classes existentes inalteradas, deixa o código fonte já implementado tal como está, e aplica as suas alterações sobre a implementação atual, usando um processo denominado herança.

A aplicação de alterações através de herança nos leva a um dos pontos centrais da programação orientada a objeto. Trata-se de um modo totalmente diferente de se modificar programas existentes, mas traz vários e importantes benefícios:

  • Suponha que você está usando uma classe desenvolvida por terceiros, e que você não tem o código fonte. Com o mecanismo de herança você deixa a classe existente intocada e como que assenta suas alterações sobre ela, sem necessidade de conhecer o código fonte original.
  • A implementação original da classe está - é de se esperar - completamente testada e isenta de bugs. Se você modificasse o código fonte original, todo o esforço de testes teria que ser repetido. Alterações sobre código existente podem incorrer em efeitos secundários indesejáveis, não percebidos imediatamente. Acomodando suas alterações sobre a classe existente, você preserva o código original livre de erros, e apenas o código da alteração precisa ser testado.
  • O processo de assentar alterações sobre código existente nos força a pensar no sentido do mais genérico para o mais específico. Você implementa uma classe genérica e posteriormente assenta sobre ela alterações para tratar situações específicas. Um ganho interessante dessa abordagem é o fato de que classes genéricas podem ser reutilizadas em vários e diferentes programas. Cada novo programa assenta alterações sobre a classe original, mas esta permanece a mesma em todos os programas onde for utilizada.
  • Se a classe base for otimizada, todas as classes construídas sobre ela recebem os benefícios dessa otimização, sem qualquer modificação nos programas. Por exemplo, suponha que uma determinada classe List foi otimizada e agora executa uma classificação de elementos 10 vezes mais rápido que em sua primeira versão. Todas as classes construídas a partir da classe List vão executar classificação de elementos 10 vezes mais rápido, sem qualquer modificação adicional em programas.

Esses são os benefícios que normalmente entusiasmam as pessoas com programação orientada a objeto. 

Topo
5.1 - Um exemplo

Vamos examinar um exemplo específico para percebermos como a herança funciona. Suponha que você comprou um gerenciador de listas, que tem as habilidades de inserir elementos em uma localização determinada, resgatar itens da lista e informar o tamanho da lista. O código da classe List é mostrado a seguir, juntamente com um pequeno trecho de código para teste.
#include <iostream.h> 
 
class List
{
int array[100];
int count;
public:
List(): count(0) {}
~List() {}
void Insert( int n, int location )
{
int i;
for (i=count; i >= location; i--)
array[i+1] = array[i];
array[location]=n;
count++;
}
int  Get( int location ) {return array[location];}
int Size() { return count; }
};
 
void main()
{
List list;
int i, value;
 
for (i=0; i < 10; i++)
list.Insert(i,i);
list.Insert(100,5);
list.Insert(200,7);
list.Insert(300,0);
for (i=0; i < list.Size(); i++)
cout << list.Get(i) << endl;
}
A classe contém uma pequena rotina de verificação de erros, obviamente essa rotina teria que ser ampliada se este fosse um produto com fins comerciais.

Suponha agora que você quer fazer duas modificações nessa classe, para adicionar dois novos recursos.

Primeiramente, você quer uma função de inserção classificada, de tal modo que após a inserção a classe mantenha a lista classificada corretamente.

Em segundo lugar, você quer manter atualizada a soma total dos valores dos itens que compões a lista. Ao invés de percorrer toda a lista efetuando a totalização a cada vez que a função soma for chamada, você quer que a soma total seja atualizada a cada inserção de novo item.

Obviamente você poderia simplesmente modificar o código da classe List mostrado anteriormente. Em C++ você usa herança ao invés de modificar o código existente. Vamos criar uma classe SortedList herdando a classe List e assentando sobre ela as nossas modificações. Comecemos por adicionar a capacidade de inserção classificada.

class SortedList: public List
{
public:
SortedList():List() {}
 
SortedInsert(int n)
{
int i,j;
 
i=0;
do
{
j = Get(i);
if (j < n ) i++;
} while (j < n && i < Size());
Insert(n, i);
}
};
A classe List original permanece totalmente inalterada. Nós simplesmente criamos a classe SortedList sobre a classe List. A classe SortedList herda o comportamento da classe List, ou seja, a classe SortedList é uma classe derivada da classe List. A classe List é a classe base para SortedList.

A classe List é herdada na primeira linha de código de SortedList:

class SortedList: public List

Os dois pontos ( : ) após SortedList indicam que queremos usar o mecanismo de herança. O termo public indica que queremos que as funções e variáveis public na classe List permaneçam public na classe SortedList. Em lugar do termo public, poderíamos optar por private ou protected. Em qualquer desses casos todas as variáveis e funções públicas da classe base seriam convertidas para a classe derivada. O uso de public nesses casos é o padrão. O diagrama seguinte ilustra o que acontece.

A classe SortedList simplesmente estende, amplia as capacidades da classe List. Qualquer um que use a classe SortedList tem acesso tanto às funções de List quanto às novas funções de SortedList.

O construtor de SortedList também tem um formato novo. Usamos o sinal de dois pontos ( : )  para chamar o construtor da classe base.

SortedList():List() {}

Essa linha significa que o construtor denominado List da classe base deve ser chamado, e que construtor de SortedList não tem nada a fazer.

No restante do código da classe SortedList nós simplesmente adicionamos a nova função SortedInsert à classe. Essa nova função faz uso das funções originais Insert, Get e Size pertencentes à classe List, mas não acessa diretamente nenhum dado membro da classe List, até porque não poderia. Repare que os dados membro da classe List são private e portanto podem ser acessados exclusivamente por funções membro da classe List. São invisíveis à classe derivada.

Suponha que você queira ter uma variável ou uma função que pareça private para usuários externos, mas se comporte como public para a classe derivada. Por exemplo, digamos que a classe SortedList precise acessar diretamente a matriz contida em List para melhor performance, mas ainda desejamos impedir que os programas de aplicação, usuários de List ou de SortedList, acessem diretamente a matriz. Podemos fazer isso usando protected:, onde usamos public: ou private: Declarando que a matriz é um membro protected na classe List, nós a tornamos acessível pelas classes derivadas de List, mas não pelas instâncias normais de List ou de SortedList.

Agora vamos adicionar a capacidade de totalização à classe SortedList. Para isso vamos precisar de uma nova variável, e vamos ainda precisar modificar o comportamento da função de inserção, para que esta passe a atualizar a soma total. O código necessário para essa implementação é mostrado a seguir.

class SortedList: public List
{
private:
int total;
public:
SortedList():List(), total(0) {}
void Insert( int n, int location )
{
total = total + n;
List::Insert(n, location);
}
int GetTotal() { return total; }
SortedInsert(int n)
{
int i,j;
i=0;
do
{
j = Get(i);
if (j < n ) i++;
} while (j < n && i < Size());
Insert(n, i);
}
};
Nessa nova versão da classe SortedList, nós acrescentamos um novo dado membro denominado total, uma nova função membro de nome GetTotal para resgatar o total atual, e ainda uma nova função Insert que se sobrepõe à função Insert original. Modificamos o construtor de SortedList que passa a inicializar a variável total.

Agora, sempre que a classe SortedList for utilizada e a função Insert for invocada, a nova versão da função Insert será ativada, ao invés da versão original que permanece inalterada dentro da classe List. Isso vale inclusive para a função SortedInsert da classe SortedList. Quando a função SortedInsert chama a função Insert, a nova função Insert é ativada.

O código da nova função Insert é simples e de compreensão quase automática:

void Insert( int n, int location )
{
total = total + n;
List::Insert(n, location);
}
Essa função primeiramente adiciona o novo valor ao conteúdo atual da variável total. Depois chama a versão original da função Insert, herdada da classe base, que processa a inserção no novo valor na lista. A notação List:: determina a classe, dentro da hierarquia de classes, a que pertence a função Insert que deve ser invocada. Em nosso exemplo há uma hierarquia simples, de apenas dois níveis - a classe base e uma classe derivada - o que torna simples a decisão sobre que classe usar. Mas em uma hierarquia com vários níveis, várias camadas de herança, essa técnica deve ser usada para se explicitar o classe que contém a versão da função que se quer invocar.

É essa acomodação de alterações em camadas através do mecanismo de herança, e a habilidade de se pensar e trabalhar com múltiplos níveis de herança, como mostrado aqui, que dá ao C++ um sentido tridimensional.

Topo
5.2 - Um exemplo mais avançado

Vamos usar o que aprendemos sobre herança até agora para criar um exemplo mais realista.

O que queremos fazer é criar uma classe para um novo tipo numérico denominado inteiro de múltipla precisão ou, abreviadamente, mint. Esse novo tipo inteiro vai operar de modo semelhante ao inteiro normal, mas poderá conter até 100 dígitos (por enquanto - mais adiante vamos ver como estender um número para ter tantos dígitos quantos possam ser armazenados em memória, usando listas ligadas). Um número do tipo mint permite que se faça operações tais como calcular o valor de 60! (fatorial de 60) ou encontrar o 300º valor em uma seqüência Fibonacci.

Qual a boa norma de se criar novas classes em um ambiente de programação orientada a objeto?

Uma boa norma é pensar no sentido do mais genérico para o mais específico. Por exemplo, o que é um inteiro de múltipla precisão? É apenas uma lista de dígitos. Portanto, você pode criar uma classe genérica para manejar uma lista, com todas as capacidades de inserção de elementos na lista necessárias para controlar uma seqüência de dígitos, e então acrescentar a essa classe os recursos necessários para implementar um número mint.

Como vamos escolher os recursos necessários à nossa lista?

Uma boa forma é pensar sobre o que vamos ter que fazer com os dígitos em operações mint típicas. Alternativamente você poderia pegar uma classe List já existente e construir a solução sobre ela. Vamos usar a primeira abordagem - criar uma classe List - já que não dispomos de uma boa classe List para utilizar.

Como você inicializa mint? O mint se inicia sem conter qualquer dígito significativo. Vamos inserir um dígito por vez para criar um número mint. Para o valor 4.269 o mint vai se parecer com o seguinte: 

Cada quadrado nesse diagrama representa um elemento na lista, e cada elemento da lista contém um valor inteiro entre 0 e 9. Precisamos da habilidade de inserir dígitos ao início ou ao fim da lista, dependendo de onde venha o valor inicial.

Vamos examinar uma adição, como mostrado na figura seguinte: 

Para implementar adição nós vamos precisar começar o processamento resgatando o último dígito de cada um dos dois números mint a serem somados, somar esses dígitos e então inserir o dígito resultante no novo número mint que conterá o resultado da soma. Em seguida vamos tomar os dois dígitos à esquerda dos últimos dígitos somados e repetir a mesma operação, e assim por diante.

Está claro que vamos precisar de um modo eficiente de nos deslocarmos do final para o início da lista (por exemplo, funções GetLast e GetPrevious), e vamos também precisar de um recurso que nos avise que atingimos o início da lista (talvez um valor de retorno de GetPrevious possa indicar que a ação não é mais possível, ou uma função Size possa indicar até onde podemos nos deslocar).

Tendo em mente todas as nossas discussões e exemplos anteriores com listas, podemos, resumidamente, concluir que a nossa nova classe List precisará ter as seguintes capacidades:

  • Construtor e destrutor
  • AddToFront
  • AddToEnd
  • GetFirst
  • GetLast
  • GetPrevious
  • GetNext
  • Size
  • Clear

O código mostrado a seguir implementa a classe List:

class List
{
int array[100];
int count;
int pointer;
public:
List(): count(0), pointer(0) {}
~List() {}
void AddToFront(int n)
{
int i;
for(i=count; i >= 1; i--)
array[i]=array[i-1];
array[0]=n;
count++;
}
void AddToEnd(int n)
{
array[count++]=n;
}
// &n is a reference - see tutor 2
int GetFirst(int & n)  
{
if (count==0)
return 1;
else
{
n=array[0];
pointer=0;
return 0;
}
}
int GetLast(int & n)
{
if (count==0)
return 1;
else
{
n=array[count-1];
pointer=count-1;
return 0;
}
}
int GetPrevious(int & n)
{
if (pointer-1 < 0)
return 1;
else
{
pointer--;
n=array[pointer];
return 0;
}
}
int GetNext(int & n)
{
if (pointer+1 > count-1)
return 1;
else
{
pointer++;
n=array[pointer];
return 0;
}
}
int Size() { return count; }
void Clear() { count = 0; }
};
A essa altura, esse código já deve ser facilmente compreensível por você.

List é simplesmente uma lista genérica de números inteiros. Um dado membro denominado pointer aponta para um dos elementos da lista e é atualizado pelas quatro funções Get.... Cada uma dessas funções retorna 0 para indicar que a operação foi bem sucedida, ou 1 para indicar insucesso na operação. Por exemplo, se pointer não aponta para o elemento 0 da lista (o elemento mais a esquerda), então ainda há elementos a esquerda do elemento apontado, e a função GetPrevious vai retornar 0). As duas funções Add.... realizam soma no início e no fim da lista. Na versão atual, essas funções não possuem código para verificação de erros.

A função AddToFront contém uma ineficiência intrínseca, porque a cada inserção ela desloca todo o conteúdo da matriz uma posição para baixo.

A classe Mint herda a classe List e a utiliza para construir o tipo numérico mint. A classe Mint implementa dois construtores: um construtor default que não recebe qualquer parâmetro, e um segundo construtor que recebe um string e o utiliza para preencher a lista. Implementa ainda as funções para somar e para imprimir dois números mint. O código é mostrado a seguir:

class Mint: public List
{
public:
Mint():List() {}
Mint(char *s):List()
{
char *p;
for (p=s; *p; p++)
AddToEnd(*p-'0');
}
void Add(Mint & a, Mint & b)
{
int carry, temp;
int erra, errb, na, nb;
 
carry=0;
Clear();
erra=a.GetLast(na);
errb=b.GetLast(nb);
while (!erra || !errb)
{
if (erra)
temp=nb+carry;
else if (errb)
temp=na+carry;
else
temp=na+nb+carry;
AddToFront(temp%10);
carry=temp/10;
erra=a.GetPrevious(na);
errb=b.GetPrevious(nb);
}
if (carry > 0)
AddToFront(carry);
}
void Print()
{
int n, err;
 
err=GetFirst(n);
while( !err )
{
cout << n;
err=GetNext(n);

        cout << endl;
}
}; 
A seguinte função main testa a classe Mint somando dois números e imprimindo o resultado da soma:
void main()
{
Mint a("1234567");
Mint b("1234");
Mint c;
 
c.Add(a,b);
c.Print();
}
Os construtores e a função Print são simples e facilmente compreensíveis.

A função Add talvez remeta você aos dias de banco escolar porque faz adição à moda antiga. Começa com os dois últimos dígitos de cada um dos números a serem somados; soma esses dois dígitos; salva o resultado e anota o valor da casa da dezena decorrente da soma ("vai um"). Move-se então para o elemento anterior da lista e repete as mesmas operações. Provavelmente os dois números mint não tenham a mesma quantidade de dígitos, portanto o código deve certificar-se de que não está operando além do dígito mais a esquerda de um dos números mint. Isso é feito usando as variáveis erra e errb. Quando os dois números mint forem inteiramente processados, o código verifica a existência de vai um e salva o último dígito, se necessário.

Executando o código de teste você verá que a classe Mint funciona como descrito aqui e pode somar dois números de até 100 dígitos cada um.

Após usar a classe Mint algumas vezes, você começará a perceber um problema com a função Add - não há como escrever algo parecido com m = m + 1 já que o formato obrigatório da chamada de Add é m.Add(m,one), onde a variável one foi inicializada com conteúdo 1. A causa dessa limitação é que Add deve limpar a área destinada a conter a soma antes de salvar aí qualquer resultado. Isso leva a perda de dados quando a função Add é usada para m.Add(m,one).

A solução para esse problema nos leva a criação de uma área temporária para conter o resultado durante a execução da soma. Ao término da função, o resultado final deve então ser copiado para a instância atual. O pointer this é usado para solução desse problema, como mostrado a seguir:

void Add(Mint & a, Mint & b)
{
int carry, temp;
int erra, errb, na, nb;
Mint x;
 
carry=0;
erra=a.GetLast(na);
errb=b.GetLast(nb);
while (!erra || !errb)
{
if (erra)
temp=nb+carry;
else if (errb)
temp=na+carry;
else
temp=na+nb+carry;
x.AddToFront(temp%10);
carry=temp/10;
erra=a.GetPrevious(na);
errb=b.GetPrevious(nb);
}
if (carry > 0)
x.AddToFront(carry);
*this = x;
}
Nessa última versão da função Add, foi criado uma variável temporária denominada x. O resultado da soma é colocado em x, dígito a dígito. A última linha do código da função copia o conteúdo de x para a instância atual. O pointer this aponta para a instância corrente da classe e pode ser aplicado a qualquer instância de classes em C++. Em outras palavras, this é um pointer que aponta para o conjunto dos dados membro (a estrutura de dados) que formam a instância corrente da classe. Nesse caso nós usamos this para economia de código. Uma alternativa seria substituir a última linha da função Add por
array = x.array;
count = x.count;
pointer = x.pointer;
O valor de *this é a estrutura apontada por this. É uma forma mais expressa de se copiar toda a estrutura de dados de uma só vez.

Como exemplo final da classe Mint, vamos usá-la para implementar um localizador de número Fibonacci. A seqüência Fibonacci tem a seguinte forma:

1, 1, 2, 3, 5, 8, 13, 21, 34, etc.

Cada número na seqüência é a soma dos dois números anteriores.

Para implementar a função que queremos vamos precisar de um modo de verificar igualdade em números mint de modo a poder controlar um loop. A função membro seguinte poderia ser acrescentada a classe Mint para verificar igualdade entre dois números mint:

int Equal(Mint & a)
{
if (a.Size()!=Size())
return 0;
else
{
int i, na, nb;
a.GetFirst(na);
GetFirst(nb);
for (i=0; i < a.Size(); i++)
if (na!=nb)
return 0;
else
{
a.GetNext(na);
GetNext(nb);
}
return 1;
}
}
Implementada essa nova função, o seguinte código vai encontrar o centésimo número em uma seqüência Fibonacci:
void main()
{
Mint max("100");
Mint counter("1"), one("1");
Mint t1("0"), t2("1");
Mint d;
 
do
{
d.Add(t1,t2);
t1=t2;
t2=d;
counter.Add(counter,one);
} while (!counter.Equal(max));
d.Print();
}
O código usa duas variáveis t1 e t2 para conter os valores anteriores. Esses valores são somados e t1 e t2 são atualizados para os próximos dois valores. O contador é incrementado e o loop continua até que o contador atinja o valor pré-determinado. Usando esse código, o centésimo número foi encontrado:

354.224.848.179.261.915.075

Topo
5.3 - Conclusão

Neste tutorial você viu como o mecanismo de herança é usado para criar uma hierarquia de classes, e como a existência de herança favorece o desenvolvimento de código com uma abordagem no sentido do mais genérico para o mais específico. A classe Mint é um exemplo perfeito dessa abordagem: uma classe genérica foi usada para construir a classe Mint porque um mint nada mais é do que um lista de dígitos.

Embora tenhamos atingido nosso objetivo, a classe Mint ainda não está bem integrada aos recursos da linguagem. Nós ainda vamos ver na próxima seção o uso do operador + para a função de soma e do operador == para a de igualdade.

Topo Índice
© 1998 Interface Technologies, Inc by Marshall Brain
Tradução de Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |