Ir para conteúdo
Faça parte da equipe! (2024) ×

Tudo sobre C++ Parte 2


.Fernando~
 Compartilhar

Posts Recomendados

  • Indice
  • Herança
  • Sobrecarga de Operador
  • Trabalhando com Pointers
  • Funções Virtuais

 

Herança

 

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.

 

 

 

 

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;

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.

 

CPPAI05A.gif

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.

 

 

 

 

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:

 

CPPAI05B.gif

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:

 

CPPAI05C.gif

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=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

 

 

 

 

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.

 

Sobrecarga de Operador

 

 

No tutorial anterior nós implementamos uma versão da classe Mint, e finalizamos com o código que calcula elementos da seqüência Fibonacci. O código usado para executar o cálculo era

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 desejável é que pudéssemos escrever esse mesmo código de modo semelhante a um código normal. Como o seguinte:

void main()

{

Mint max("100");

Mint counter("1");

Mint t1("0"), t2("1");

Mint d;

 

do

{

d = t1 + t2;

t1=t2;

t2=d;

counter = counter + "1";

} while (! (counter==max));

cout << d << endl;

}

C++ permite esse tipo de operação com novos tipos de dados usando um processo denominado sobrecarga de operador. Os operadores normais, como +, == e << são sobrecarregados e passam a manejar também os novos tipos de dados. Alguns casos de sobrecarga de operador envolvem o uso de funções friend. Uma função friend é como uma função C normal, mas tem a permissão de acessar membros private da classe dentro da qual é declarada. O fato de ser como uma função C significa que ela não tem acesso ao pointer this, e também que pode ser chamada sem que se identifique a classe para a qual ela opera. Por exemplo, uma função membro normal, tal como a função Insert da classe List, requer um instanciação de List para que possa ser chamada.

 

List lst;

...

lst.Insert(5);

Uma função friend não requer necessariamente a instanciação de uma classe porque ela não tem acesso ao pointer this.Quase todos os operadores em C++ podem ser sobrecarregados:

 

+ - * / % ^ & |

~ ! , = < > <= >=

++ -- << >> == != && ||

+= -= /= %= ^= & = |= *=

<<= >>= [ ] ( ) -> ->* new delete

Alguns desses operadores são usados raramente, e mais raramente ainda são sobrecarregados. Mas sobrecarregando os operadores mais comuns, como + e == você pode dar à suas classes interfaces mais simples e auto-explicativas.O código seguinte mostra a classe Mint reformulada para usar os operadores +, == e << sobrecarregados, e ainda um trecho de código para testar o uso desses três operadores.

 

class Mint: public List

{

public:

Mint():List() {}

Mint(char *s):List()

{

char *p;

for (p=s; *p; p++)

AddToEnd(*p-'0');

}

 

friend Mint operator+ (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);

return x;

}

 

int operator==(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;

}

}

 

friend ostream& operator << (ostream& s, Mint & m)

{

int n, err;

 

err=m.GetFirst(n);

while( !err )

{

s << n;

err=m.GetNext(n);

}

return s;

}

};

 

void main()

{

// add two numbers

Mint a("1234567");

Mint b("1234");

Mint c;

 

c = a + b;

cout << "it's fine " << c << "...really" << endl;

cout << a + "3333" << endl;

 

// find the 100th Fibonacci number

Mint counter;

Mint t1, t2;

Mint d;

 

t1 = "0";

t2 = "1";

counter = "1";

do

{

d = t1 + t2;

t1 = t2;

t2 = d;

counter = counter + "1";

} while (! (counter == "100") );

cout << d << endl;

}

Vamos começar examinando a função ==:int operator== (Mint & a)

Porque esta é uma função membro da classe Mint, esse cabeçalho da função indica que o operador deve retornar um inteiro, que usa o que está a esquerda de == como this, e que usa o que está a direita de == como um a. Dentro do código para a função do operador ==, quando nós usamos diretamente uma outra função, como GetFirst, nós estamos nos referindo ao valor a esquerda de ==. Ao contrário, uma chamada de função no formato a.GetFirst refere-se ao que está à direita de ==:

 

Mint b, m;

...

if (b == m)

O restante do código é idêntico ao da função Equal que vimos no Tutorial 5. O valor inteiro de retorno é usado como resultado da comparação. Uma vez implementada essa função, nosso operador == é chamado sempre que o compilador encontra um operador == entre dois valores do tipo Mint.O operador + sobrecarregado é uma função friend:

 

friend Mint operator+ (Mint & a, Mint & b)

 

Essa função é declarada como uma friend porque nós não queremos que ela use automaticamente o lado esquerdo do sinal de soma, porque essa função limpa a variável destinada ao resultado da soma, como já vimos no Tutorial 5. Uma vez definida como uma função friend, ela se comporta como uma função C normal, sem acesso ao pointer this. Ela apenas soma os dois números mint que lhe forem passados, e retorna o resultado no formato de um número mint.Na função main há vários comandos com a seguinte forma:

c = "3333"

 

ou

c = c + "1"

Como o compilador sabe o que fazer? Como ele sabe converter um "1" para um mint? Uma vez que tenhamos um construtor que aceita um tipo char*, o construtor é automaticamente invocado na tentativa de fazer a combinação de tipos para o operador +. Se nós criarmos outro construtor que aceite um parâmetro do tipo inteiro longo, então poderemos escrever comandos como

c = c + 1;

A conversão do valor inteiro 1 será automática. O seguinte comando não vai funcionar:

c = "2222" + "3333";

porque o compilador não tem nada que lhe diga que + significa a adição de mints, portanto ele não pode fazer a conversão de tipos - um dos lados de + deve ser um mint para alertar o compilador.

O operador << também está sobrecarregado. A função para sobrecarregá-lo deve ser uma friend porque o parâmetro a esquerda não é do mesmo tipo da classe. A função deve aceitar uma referência para um parâmetro ostream e um parâmetro do tipo da classe. Deve ainda retornar uma referência para ostream.

Com essa função implementada, qualquer operação de saída em C++ vai funcionar normalmente com números mint.

O operador >> é sobrecarregado de modo similar:

 

friend istream& operator >> (istream& s, Mint& m)

{

buf[100];

 

s >> buf;

m = buf; // calls the constructor

return s;

}

Outros operadores, tais como ++, +=, !=, etc podem ser facilmente sobrecarregados usando os exemplos acima.

Trabalhando com Pointers

 

Quando uma classe contém dados membro que são pointers, há uma série de cuidados adicionais para assegurar que essa classe realmente funcione como esperado. Por exemplo, quando uma instância de uma classe é destruída, o construtor deve certificar-se que todos os blocos de memória dentro da classe sejam liberados. Um outro cuidado envolve o operador de atribuição: o comportamento padrão para o operador = - cópia de todos os dados membro - como vimos até agora, não funciona para dados membro pointers.Para tornar mais perceptível essa diferença, vamos construir uma classe Stack com matriz e com pointers. Aqui está a versão com matriz e uma função main contendo o código para teste. (Esse código é idêntico ao visto no Tutorial 4).

 

#include <iostream.h>

 

class Stack

{

int stk[100];

int top;

public:

Stack(): top(0) {}

~Stack() {}

void Clear() {top=0;}

void Push(int i) {if (top < 100) stk[top++]=i;}

int Pop()

{

if (top > 0) return stk[--top];

else return 0;

}

int Size() {return top;}

};

 

void main()

{

Stack stack1, stack2;

 

stack1.Push(10);

stack1.Push(20);

stack1.Push(30);

cout << stack1.Pop() << endl;

stack2=stack1;

cout << stack1.Size() << endl;

cout << stack2.Size() << endl;

cout << stack2.Pop() << endl;

cout << stack2.Pop() << endl;

}

O código seguinte implementa a mesma classe Stack usando pointers, mas contém uma série de problemas que iremos examinar em seguida.

typedef struct node

{

int data;

node *next;

} node;

 

class Stack

{

node *top;

public:

Stack(): top(0) {}

~Stack() { Clear(); }

void Clear()

{

node *p=top;

while (p)

{

top = top->next;

delete p;

p = top;

}

}

void Push(int i)

{

node *p = new node;

p->data = i;

p->next = top;

top = p;

}

int Pop()

{

if (top != 0)

{

int d = top->data;

node *p=top;

top = top->next;

delete p;

return d;

}

else return 0;

}

int Size()

{

int c=0;

node *p=top;

while (p)

{

c++;

p = p->next;

}

return c;

}

};

Essa é uma implementação completa da classe. Ela realiza os procedimentos de liberação de memória dentro de seu destrutor, e funciona da mesma maneira que versão anterior da classe Stack. Contudo, essa implementação não funciona conforme o esperado após um comando de atribuição comostack1 = stack2;

O diagrama seguinte demonstra o que acontece. A operação de atribuição, nesse caso, apenas copia os dados membro de stack2 para stack1, deixando o mesmo conjunto de dados em memória sendo apontado por dois pointers.

 

CPPAI07A.gif

Após a atribuição, os pointers stack1.pop e stack2.pop apontam ambos para a mesma cadeia de blocos de memória. Se uma das pilhas for excluída, ou se uma delas executar a função Pop, a outra pilha vai apontar para um endereço de memória que não mais será válido.Em algumas máquinas esse código será compilado sem erros e tudo parecerá estar correto por um certo tempo durante a execução do programa. Mas tão logo o sistema comece a apontar para endereços de memória que não mais sejam válidos, a execução começará a ter um comportamento errático sem razão aparente até que o programa finalmente falhe.

O que precisamos é de uma maneira de reformular a operação de atribuição para criar uma cópia dos blocos de memória apontados pelos pointers. Mas de onde vem esse operador de atribuição, e como podemos modificá-lo?

 

 

 

 

7.1 - Funções default Quando você cria uma classe, quatro funções default são criadas automaticamente e serão utilizadas, a menos que você as sobrescreva. Essas funções default são:

  • O construtor default
  • O construtor de cópia default
  • O operador de atribuição default
  • O destrutor default

O construtor default é invocado quando você declara uma instância da classe sem passar qualquer parâmetro. Por exemplo, se você criar uma classe Sample sem a definição explícita de um construtor, então o comando seguinte invoca o construtor default para s:

Sample s;

A seguinte declaração com inicialização de s2 invoca o construtor de cópia:

Sample s1;

Sample s2 = s1;

O destrutor default é chamado quando se encerra o escopo dentro do qual a variável foi criada, e o construtor de atribuição é chamado quando ocorre uma operação de atribuição normal.

Você pode sobrescrever qualquer um desses construtores, definindo as suas próprias funções. Por exemplo, se você define explicitamente um construtor para a classe, o construtor default não será criado pelo compilador.

O código seguinte vai nos ajudar a ter uma melhor compreensão do que fazem o construtor e o destrutor default:

 

#include <iostream.h>

 

class Class0

{

int data0;

public:

Class0 () { cout << "class0 constructor" << endl; }

~Class0 () { cout << "class0 destructor" << endl; }

};

 

class Class1

{

int data1;

public:

Class1 () { cout << "class1 constructor" << endl; }

~Class1 () { cout << "class1 destructor" << endl; }

};

 

class Class2: public Class1

{

int data2;

Class0 c0;

};

 

void main()

{

Class2 c;

}

A classe Clas2 não tem construtor nem destrutor definidos explicitamente, mas esse código produz a seguinte saída:class1 constructor

class0 constructor

class0 destructor

class1 destructor

O que aconteceu é que o compilador criou automaticamente tanto o construtor quanto o destrutor default para Clas2. O comportamento do default construtor é chamar o construtor da classe base, bem como o construtor default para cada um dos dados membro que são classes. O destrutor default chama o destrutor da classe base e dos dados membro que são classes.

Digamos que você crie um novo construtor para Clas2 que aceite um inteiro. O compilador ainda assim vai chamar os necessários construtores da classe base e dos dados membro que são classes.

O código seguinte demonstra esse processo:

 

class Class2: public Class1

{

int data2;

Class0 c0;

public:

Class2(int i)

{

cout << "class2 constructor" << endl;

}

};

 

void main()

{

Class2 c(1);

}

Isso também funciona e produz a seguinte saída:

 

class1 constructor

class0 constructor

class2 constructor

class0 destructor

class1 destructor

Mas agora você não pode mais declarar uma variável não inicializada do tipo Clas2 porque não há mais um construtor default. O código seguinte demonstra:

Class2 c(1); // OK

Class2 e; // not OK--no default constructor

É também possível declarar uma matriz de uma classe, a menos que não haja um construtor default definido.Contudo, você pode recriar o construtor default, criando explicitamente um construtor com uma lista de parâmetros vazia, da mesma maneira que cria outros construtores para a classe.

O operador de atribuição e o construtor de cópia também são criados automaticamente. Ambos apenas copiam os dados membro da instância à direita do sinal = para a instância à esquerda. No caso de nossa classe Stack, nós queremos eliminar essas funções default e usar funções próprias, para que a operação de atribuição funcione corretamente. A seguir estão as duas novas funções para a classe Stack, e a função Copy compartilhada por ambas:

 

void Copy(const Stack& s)

{

node *q=0;

node *p=s.top;

 

while (p)

{

if (top==0)

{

top = new node;

q=top;

}

else

{

q->next = new node;

q = q->next;

}

 

q->data = p->data;

p = p->next;

q->next=0;

}

}

Stack& operator= (const Stack& s) //assignment

{

if (this == & s)

return *this;

Clear();

Copy(s);

return *this;

}

Stack(const Stack& s): top(0) // copy constructor

{

Copy(s);

}

A função de atribuição se inicia verificando o caso de auto-atribuição, como ems = s;

Se verifica tratar-se de auto-atribuição, a função não faz nada, ou seja, não efetua a auto-atribuição. Não sendo auto-atribuição, a função limpa a instância recipiente e copia a lista ligada existente na memória, de modo que a instância à esquerda do operador de atribuição tenha sua própria cópia da pilha.

O construtor de cópia é basicamente o mesmo que qualquer outro construtor. É usado para manejar os seguintes casos:

 

Stack s1;

s1.Push(10);

s1.Push(20);

Stack s2(s1); // copy constructor invoked

Stack s3 = s1; // copy constructor invoked

Uma vez implementados o operador de atribuição e o construtor de cópia, a classe Stack está completa. Pode manejar qualquer condição e funcionar corretamente.

 

 

 

7.2 - Conclusão Tudo isso talvez lhe pareça muito trabalho a fazer, mas geralmente esses cuidados adicionais são necessários apenas quando se trabalha com pointers. O que acontece é que você tem que realmente proteger suas estruturas baseadas em pointers contra contingências que invalidem os dados. Em alguns programa em C, os programadores poderão fazer pressuposições como eu posso apontar o mesmo bloco de memória com vários pointers sem problemas, porque nessa parte do código nada modifica os blocos apontados. Contudo, se um outro programador viola essa pressuposição, ainda que acidentalmente, o programa pode falhar, e falhas decorrentes de problemas com pointers são difíceis de seguir e de localizar.

Tais problemas não vão ocorrer em uma classe C++ definida com segurança, porque todas essas contingências estão previamente consideradas e cobertas.

Você pode verificar que a implementação mostrada acima é ainda ineficiente. O que acontecerá se você quiser ter apenas uma cópia dos blocos de memória que formam a pilha? Por exemplo, o que acontecerá se os dados da pilha ocuparem alguns megabytes de memória, e você não tiver memória suficiente para fazer uma cópia?

O que você faz nesse caso é usar uma técnica como um contador de referências - cada instância da classe incrementa uma variável global estática que contém o número de instâncias usando a mesma cópia dos dados. Cada destrutor decrementa esse mesmo contador. Somente quando um construtor, após decrementar o contador, detecta que não há mais qualquer instância da classe usando os dados, é que realmente se libera a memória usada para conter os dados.

 

 

Funções Virtuais

 

Ao longo desses tutoriais nós vimos vários exemplos de herança, porque herança é muito importante para programação orientada a objeto. Vimos que herança permite que dados membro e que funções membro sejam acrescentadas às funções derivadas. Vimos também vários exemplos em que o mecanismo de herança foi usado para modificar o comportamento de uma função. Por exemplo, no Tutorial 3 nós vimos um exemplo onde a função Insert da classe base List foi sobreposta por uma outra função Insert que continha totalização. Uma hierarquia similar é mostrada a seguir, usando uma classe base denominada List e uma classe derivada denominada TotalingList:

 

#include <iostream.h>

 

class List

{

int array[100];

int count;

public:

List(): count(0) {}

void Insert(int n) { array[count++]=n; }

int Get(int i) { return array; }

int Size() { return count; }

};

 

void ManipList(List list)

{

// do things to the list

list.Insert(100);

list.Insert(200);

// do things to the list

}

 

class TotalingList: public List

{

int total;

public:

TotalingList(): List(), total(0) {}

void Insert(int n)

{

total += n;

List::Insert(n);

}

int GetTotal() { return total; }

};

 

void main()

{

TotalingList list;

int x;

 

list.Insert(10);

list.Insert(5);

cout << list.GetTotal() << endl;

ManipList(list);

cout << list.GetTotal() << endl;

for (x=0; x < list.Size(); x++)

cout << list.Get(x) << ' ';

cout << endl;

}

Nesse código, a classe List implementa a lista mais simples possível com as três funções membro Insert, Get e Size e o construtor. A função ManipList é um exemplo de uma função qualquer que usa a classe List, e que chama a função Insert duas vezes apenas como ilustração do que pretendemos demonstrar.A classe TotalingList herda a classe List e acrescenta um dado membro denominado total. Esse dado membro armazena o total corrente de todos os números contidos na lista. A função Insert é sobreposta para que total seja atualizado a cada inserção.

A função main declara uma instância da classe TotalingList. Insere os valores 10 e 5 e imprime o total. Em seguida chama ManipList. Pode ser uma surpresa para você que essa construção não resulte em erro de compilação: se você olhar o protótipo de ManipList vai ver que ela espera uma parâmetro do tipo List, e não do tipo TotalingList. Acontece que C++ entende certas peculiaridades sobre herança, uma delas é que um parâmetro do tipo classe base deve aceitar também qualquer tipo classe derivada daquela classe base. Portanto, desde que TotalingList é derivada da classe List, ManipList vai aceitar um parâmetro do tipo TotalingList. Esse é um dos recursos do C++ que torna o mecanismo de herança tão poderoso: você pode criar classes derivadas e passá-las para funções existentes que conhecem apenas a classe base.

Quando o código mostrado for executado, ele não vai produzir o resultado correto. Ele vai produzir a seguinte saída:

15

15

10 5

Essa saída indica que não apenas a totalização não funcionou, mas que os valores 100 e 200 não foram inseridos na lista quando da chamada a ManipList. Uma parte desse erro se deve a um erro no código: o parâmetro aceito por ManipList deve ser um pointer ou uma referência, do contrário nenhum valor será retornado. Modificando o protótipo de ManipList para corrigir parcialmente o problema:

void ManipList(List& list)

Agora teremos a seguinte saída:

15

15

10 5 100 200

É didático seguir a execução de ManipList passo-a-passo e observar o que acontece. Quando ocorre a chamada para a função Insert, a função invocada é List:Insert ao invés de TotalingList:Insert.

Esse problema também pode ser resolvido. É possível, em C++, criar uma função com um prefixo virtual, e isso leva o C++ a sempre chamar a função na classe derivada. Ou seja, quando uma função é declarada como virtual, o compilador pode chamar versões da função que sequer existiam quando o código da função chamada foi escrito. Para verificar isso, acrescente a palavra virtual a frente das funções Insert tanto na classe List quanto na classe TotalingList, como mostrado a seguir:

 

class List

{

int array[100];

int count;

public:

List(): count(0) {}

virtual void Insert(int n) { array[count++]=n; }

int Get(int i) { return array; }

int Size() { return count; }

};

 

void ManipList(List& list)

{

// do things to the list

list.Insert(100);

list.Insert(200);

// do things to the list

}

 

class TotalingList: public List

{

int total;

public:

TotalingList(): List(), total(0) {}

virtual void Insert(int n)

{

total += n;

List::Insert(n);

}

int GetTotal() { return total; }

};

De fato é necessário colocar a palavra virtual apenas a frente do nome da função na classe base, mas é um bom hábito repeti-la nas funções das classes derivadas, como uma indicação explícita do que está ocorrendo. Agora você pode executar o programa e obter as saídas corretas:

15

315

10 5 100 200

O que está acontecendo? A palavra virtual a frente da função determina para o C++ que você planeja criar novas versões dessa mesma função em classes derivadas. Ou seja, virtual permite que você declare suas futuras intenções em relação à classe em definição. Quando uma função virtual é chamada, o C++ examina a classe que chamou a função, e busca a versão da função para esta classe, mesmo que a classe derivada ainda não existisse quando a chamada da função foi escrita.

Isso significa que em certos casos você tem que pensar no futuro quando escreve o código. Você deve refletir eu, ou qualquer outro programador, poderia no futuro precisar modificar o comportamento dessa função? Se a resposta for sim então a função deve ser declarada como virtual.

Deve-se ter alguns cuidados para que uma função virtual funcione corretamente. Por exemplo, você deve realmente prever a necessidade de se modificar o comportamento da função e lembrar-se de declará-la como virtual na classe base. Um outro ponto importante pode ser visto no exemplo acima: experimente remover o & da lista de parâmetros em ManipList e siga o código passo-a-passo. Mesmo a função Insert estando marcada como virtual na classe base, a função List::Insert é chamada, ao invés da função TotalingList::Insert.

 

 

Créditos : Fernando, google, e arnaut

Isso acontece porque, quando o & não está presente, a lista de tipos de parâmetros em List está atuando como se fosse uma definição de conversão de tipos. Qualquer classe passada é convertida para a classe base List. Quando o & está presente na lista de parâmetros, tal conversão não acontece.

Pode-se ver funções virtuais em praticamente toda hierarquia de classes em C++. Um hierarquia de classes típica espera que, no futuro, se altere o comportamento das funções para adaptar a biblioteca de classes às necessidades específicas de uma ou de outra aplicação.

Funções virtuais são usadas com freqüência quando o projetista da classe não pode realmente saber o que será feito com a classe no futuro. Suponha que você está usando a interface de uma classe que implementa buttons na tela. Quando você cria uma instância do button, ele se desenha na tela e se comporta de modo padrão, ou seja, ilumina-se quando é clicado pelo usuário. Entretanto, o programador que projetou essa classe não tinha idéia do que os programadores usuários da classe poderiam querer fazer quando o button fosse clicado. Para esses casos, o projetista da classe poderia ter definido uma função virtual denominada, por exemplo, handleEvent que é chamada sempre que o button é clicado. Feito isso, você pode sobrescrever a função virtual com uma função própria que maneja a situação do modo mais adequado à sua aplicação.

 

Conclusão Cobrimos uma grande quantidade de temas nessa série de tutoriais, mas você talvez tenha a sensação de que ainda há muito o que se aprofundar em C++. Isso é verdade em certo sentido: C++ é uma linguagem muito profunda, com algumas sutilezas e artifícios que somente a experiência pode lhe ajudar a dominar. C também é assim, só que em escala menor. A única maneira de se compreender completamente uma linguagem de programação é escrever, e ler, muito código. Você pode aprender muito usando, e estudando, bibliotecas criadas por outros. Todos os benefícios do C++ se tornarão mais evidentes para você na medida em que você compreenda mais e mais essa linguagem. Então... vamos à codificação!

 

Link para o comentário
Compartilhar em outros sites

  • Velha Guarda

Nossa cara..

Está tudo muito bem explicado..

Está falando muito bem dos beneficios do C++

Parabéns pelo topico..

Está bem organizado.

Irá ajudar muitas pessoas..

 

Vlws por compartilhar.

META: tibia.png

Link para o comentário
Compartilhar em outros sites

MakeZero

Vlw man....

mas se quiser também dar uma olhada no tutorial Parte 1

http://www.webcheats.com.br/forum/c-c-c/495626-tudo-sobre-c-parte-1-a.html

tentei explicar o maximo para todos daqui da webcheats que querem aprender.

flws

Link para o comentário
Compartilhar em outros sites

Este tópico está impedido de receber novos posts.
 Compartilhar

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...

Informação Importante

Nós fazemos uso de cookies no seu dispositivo para ajudar a tornar este site melhor. Você pode ajustar suas configurações de cookies , caso contrário, vamos supor que você está bem para continuar.