



















































Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Os melhores documentos à venda: Trabalhos de alunos formados
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Comunidade
Peça ajuda à comunidade e tire suas dúvidas relacionadas ao estudo
Descubra as melhores universidades em seu país de acordo com os usuários da Docsity
Guias grátis
Baixe gratuitamente nossos guias de estudo, métodos para diminuir a ansiedade, dicas de TCC preparadas pelos professores da Docsity
- - -
Tipologia: Notas de estudo
1 / 59
Esta página não é visível na pré-visualização
Não perca as partes importantes!
Os primeiros sistemas de computadores eram programados por chaves no painel. A medida em que foram crescendo os programas, surgiram os montadores assembler onde o programador podia se valer de mnemônicos para facilitar o trabalho de programação. Como os programas continuaram crescendo, surgiram as linguagens de alto nível que ofereceram melhores ferramentas para o programador enfrentar a crescente complexidade dos programas. Fortran certamente foi a primeira linguagem de alto nível de grande utilização. Apesar de ser um primeiro passo, não se pode dizer que é uma linguagem que estimula a construção de programas organizados.
Nos anos sessenta surgiu a programação estruturada, base de linguagens como "C" e Pascal. Utilizando- se a programação estruturada pôde-se, pela primeira vez, escrever com bastante flexibilidade programas de complexidade moderada. Se o sistema cresce demais, no entanto, os recursos da programação estruturada passam a ser insuficientes. Os maiores efeitos deste fato podem ser sentidos quando da manutenção dos programas. Por exemplo, imagine o conjunto de funções e procedimentos que compõem um programa de bom tamanho alterando diretamente ou através de parâmetros as diferentes estruturas de dados de um sistema. A alteração de um destes procedimentos pode ter "efeitos colaterais" imprevisíveis e de alcance difícil de ser determinado.
A programação orientada a objetos conservou todas as boas idéias da programação estruturada e acrescentou alguns aspectos novos que permitem uma nova abordagem de programação. Uma das idéias básicas em programação orientada a objetos diz que o projeto do sistema deve ser feito a partir das entidades do mundo real e não a partir dos dados ou procedimentos. A maior justificativa para tanto é que dados e procedimentos mudam com muita facilidade enquanto que as entidades do mundo real tendem a ser mais estáveis.
Na modelagem de objetos do mundo real, no entanto, percebe-se que existem conjuntos de objetos com características comuns. "Classifica-se" este grupo, então, criando-se uma classificação ou "classe" para o mesmo. Em uma linguagem orientada a objetos, uma classe corresponderá a um tipo abstrato de dados, ou seja, um conjunto de atributos que descrevem a classe e o os procedimentos que podem agir sobre esses atributos. Ao conjunto de atributos e procedimentos daremos o nome de propriedades da classe. Os diferentes objetos do mundo real são então representados por instâncias dessas classes. Em um sistema de controle de um estacionamento, por exemplo, uma classe possível seria a classe veículo. Poderíamos ter como atributos de veículo o modelo, a placa, a hora de chegada e a hora de saída. Associados a esses atributos poderíamos ter métodos de consulta que nos informassem por exemplo a placa do veículo ou a quanto tempo o mesmo está estacionado. Um determinado veículo é representado como uma instância desta classe (figura 2).
Fig. 1 - Classes x Instâncias
Uma das vantagens de se trabalhar dessa maneira é o encapsulamento dos atributos e procedimentos. Os atributos e os procedimentos que atuam sobre os mesmos se encontram agrupados permitindo que se tenha uma melhor definição do escopo dos procedimentos e uma maior segurança no acesso aos dados. É diferente de uma linguagem de programação convencional onde os dados são declarados de maneira independente das funções. Nestas, a responsabilidade de manter a conexão entre os dados e as funções cabe exclusivamente ao programador. Outra possibilidade, é a definição em separado da interface dos métodos de sua implementação.
Desta forma se alteramos a implementação de um método (por questões de otimização ou correção de erros) não haverão repercussões além do escopo da classe, ou seja: se a interface é mantida, o método continua respondendo da mesma maneira. Mesmo a alteração de um atributo pode ser feita seguindo-se este princípio. No exemplo anterior a alteração do atributo placa de maneira a permitir placas com 3 letras ao invés de duas, não terá maiores consequências. Provavelmente as alterações irão se restringir ao método "inf_placa" através do qual temos acesso a mesma.
A idéia de definição de interface permite ainda definir atributos privados, aos quais só se tem acesso através de procedimentos de consulta. A possibilidade de se definir atributos e procedimentos privados para uma classe oferece pelo menos duas vantagens: a) permite que se oculte aspectos da implementação que não interessam ao "usuário" da classe; b) facilita a manutenção da integridade dos dados uma vez que o acesso pode ser limitado a procedimentos que garantem essa integridade.
O paradigma de programação orientada a objetos diz que todo o objeto deve ter um identificador único. Este princípio é baseado na idéia de que todos os objetos do mundo real tem identidade própria. Desta forma, um objeto deverá ter uma identidade própria que o diferencie dos demais mesmo quando todos os atributos que descrevem dois objetos de uma mesma classe tiverem conteúdo idêntico. Em "C++", em geral, esse identificador único será representado por uma referência ao objeto (endereço). Esse tipo de identidade, entretanto, terá de ser substituído caso seja necessário armazenar os objetos em memória secundária.
Outras dois conceitos importantes das linguagens orientadas a objetos são subclasse e polimorfismo. O conceito de subclasse permite criar classes derivadas de classes mais genéricas com o objetivo de detalhar melhor um subconjunto dos componentes da superclasse. A principal característica é que a subclasse herda todos os atributos e procedimentos da superclasse e acrescenta os que lhe são particulares. No exemplo 3, as subclasses "func gerente" e "func superv" herdam os atributos e os procedimentos da superclasse funcionário, além de acrescentar aqueles que as particularizam. Com esta estrutura é possível criar instâncias de funcionários comuns, funcionários gerentes e funcionários supervisores. Observe que cada tipo de funcionário tem seu próprio método para cálculo de salário, embora todos compartilhem o método de cálculo do valor hora (re- utilização de código).
Fig. 2 - Herança
O conceito de polimorfismo permite que se especialize um método da superclasse em algumas ou todas as subclasses. No exemplo da figura 3, o método "calcula salário" é especializado em cada uma das subclasses pois o cálculo do salário apresenta diferenças para cada categoria. No momento da ativação do método "calcula salário", conforme a classe ou subclasse que o objeto pertença, o procedimento correto será ativado sem a necessidade de testes como em uma linguagem de programação comum.
Finalmente podemos modelar nossos objetos utilizando o conceito de agregação. Em uma agregação procura-se representar relacionamentos do tipo "é parte de" e não do tipo "subclasse de" como no exemplo anterior. Um objeto carro pode ser formado por um objeto porta e um objeto motor entre outros. Motor e porta são
Visando contornar os problemas de manutenção existentes nos grandes sistemas, em 1980 Bjarne Stroustrup adicionou o conceito de classes e de verificação de parâmetros de funções além de algumas outras facilidades à linguagem "C". A linguagem resultante foi chamada de "C com classes". Em 1983/84 o "C com classes" foi extendido e re-implementado resultando na linguagem conhecida como "C++". As maiores extensões foram as funções virtuais e a sobrecarga de operadores. Após mais alguns refinamentos a linguagem "C++" tornou-se disponível ao público em 1985 e foi documentada no livro "The C++ Programming Language" (Addison Wesley 1986).
A linguagem "C++" pode ser considerada como uma extensão da linguagem "C". Os primeiros compiladores "C++" não eram ‘nativos’, uma vez que não geravam diretamente código executável, mas convertiam código "C++" em código "C" para uma posterior compilação com um compilador "C" comum. A figura 1 mostra o relacionamento entre as duas linguagens.
Fig. 3 - Relação entre a linguagem "C" e a linguagem "C++"
A linguagem "C++" acrescenta à linguagem "C" facilidades que permitem o desenvolvimento de uma programação orientada a objetos sem, entretanto, desfazer-se das características da linguagem "C". Se por um lado ganha-se uma ferramenta extremamente poderosa deve-se tomar alguns cuidados. A linguagem "C++" permite operações que normalmente só são aceitas em linguagens de baixo nível ao mesmo tempo em que oferece facilidades para programação orientada a objetos. Isso implica em que a linguagem oferece as condições mas não impõem a disciplina, exigindo portanto programadores experientes e disciplinados. Assim como na linguagem "C", todas as liberdades oferecidas exigem responsabilidade por parte do programador.
Como já foi enfatizado anteriormente, "C++" é uma extensão da linguagem "C". Por ser uma extensão, é natural supor que um programa "C" seja compilado sem problemas com um compilador "C++", já que este compilador deve ser capaz de compilar todos os comandos da linguagem "C" mais os comandos acrescentados que compõem o "C++". Neste capítulo são apresentadas algumas diferenças de estrutura entre as linguagens "C" e "C++" que não dizem respeito aos comandos que suportam programação orientada a objetos e que permitirão verificar quando um programa "C" pode ser compilado em um programa "C++" e vice-versa.
a) Prototipação das funções: a prototipação das funções em "C++" é obrigatória. Nenhuma função pode ser utilizada dentro de um módulo de programa sem anteriormente ter sido totalmente prototipada ou declarada, isto é, o número e tipo dos parâmetros das funções devem ser conhecidos (isto revela que a ‘tipagem’ é mais forte em C++ que em C.
b) Declaração de variáveis: embora a declaração de variáveis ocorra do mesmo modo tanto em "C" como em "C++", em "C++" a ordem de declaração é mais flexível. Em "C" todas as declarações devem ocorrer no início de um bloco de função ou bloco de comandos. As variáveis devem ser declaradas não apenas antes de serem executadas, mas antes de qualquer proposição executável. Em "C++" as declarações podem ser colocadas em qualquer ponto do programa. Na verdade devem ser colocadas o mais próximo possível do ponto de utilização de forma a salientar ao programador sua relação com as proposições que a utilizam e lembrar ao programador sobre seu escopo.
c) Escopo das variáveis: as regras de escopo são semelhantes em "C" e "C++". Existem 2 tipos comuns de escopo: local e file. "C++" apresenta ainda o tipo de escopo class que será discutido mais adiante. Em "C", no caso de ocorrerem identificadores locais idênticos a identificadores globais, o identificador global fica "mascarado" pelo identificador global. Em "C++" a regra é a mesma, porém essa regra pode ser quebrada com o uso do operador de resolução de abrangência (escopo): " :: ".
Exemplo:
int cout = 5; int exemplo() { int count = 10; printf("Contagem externa: %d\n", :: count); printf("Contagem interna %d\n",count); }
d) Comentários: os comentários em "C++" podem ser de dois tipos:
e) Casts: os "casts" podem ser usados em "C++" da mesma forma que em "C", porém o "C++" oferece uma forma alternativa que enfatiza melhor o escopo de abrangência do "cast". São expressões válidas em "C++":
x = ( float )a + b / 2.0; x = float (a) + b / 2.0; // ver construtores adiante
f) Argumentos de funções: em "C++" os argumentos de funções devem ser declarados conforme o padrão ANSI. O estilo K&R não é aceito.
Exercícios:
Analise o fonte "exer1.c". Observe as declarações de variáveis e prototipação exclusivas do "C++". Compile o programa com um compilador "C" e observe o tipo de mensagem de erro. Compile, depois, com um compilador "C++" e execute o programa.
Altere o uso do "cast" no programa "exer1.c" para o estilo do "C++".
Comente a linha que contém o protótipo da função "media" e observe o tipo de mensagem de erro (use o comentário de linha do "C++").
As funções da linguagem "C++" possuem uma série de aprimoramentos concebidos para tornar mais fácil sua programação e uso.
Quando uma determinada função é muito pequena ou simples, o custo de chamada da função (empilhamento dos parâmetros e do endereço de retorno, desvio etc), pode ser proibitivo principalmente se a função for ativada muitas vezes. Uma prática bastante conhecida é o uso de "funções macro" construídas com o auxílio de pré-processador. Não sendo funções, mas sim expansões de macro, não possuem o custo de uma função e evitam a desestruturação do programa.
Exemplo:
#define maior(a,b) (a > b)? a : b void main() { int x,y,m; x=4; y= 3; m = maior(x,y); printf("%d\n",m); } O grande problema associado ao uso de macros é que a verificação de erros normalmente feita para uma função não ocorre.
Para contornar esse problema o "C++" introduziu as funções " inline ". Uma função definida como " inline " não gera código. Toda vez que a função é chamada, seu código é expandido no ponto de chamada como se fosse uma macro.
Exemplo:
inline int soma( int a, int b) { return (a+b); } Funções " inline " devem ser usadas com cuidado. Embora gerem um código mais rápido, o tamanho do mesmo pode crescer de maneira assustadora na proporção em que a função é ativada muitas vezes. A declaração " inline " foi criada com o propósito de ser utilizada com funções pequenas. Um caso típico são as funções de consulta a um objeto. Outra grande vantagem do uso de funções " inline " é que uma função " inline " definida, porém, não utilizada, não irá gerar código nenhum.
A linguagem "C++" apresenta o conceito de sobrecarga de função. Isso significa que uma função pode ser chamada de vários modos, dependendo da necessidade. Em geral usa-se sobrecarga entre os métodos de uma classe, mas pode se usar com qualquer função.
Exemplo:
int soma( int a, int b) { return (a+b); } double soma( double a, double b) { return (a+b); } int soma( int a, int b, int c) { return (a+b+c); }
void main() { int s1,s2; double s3; s1 = soma(23,89); s2 = soma(1,4,8); s3 = soma(4.57,90.87); printf("%d %d %f\n",s1,s2,s3); } As funções podem ser sobrecarregadas quanto ao tipo dos parâmetros, quanto a quantidade dos mesmos ou ambos. Para manter as normas de programação orientada a objeto, entretanto, o significado semântico da função não deve mudar de uma implementação sobrecarregada para outra. Em resumo, todas as versões da função devem fazer a mesma coisa. Importante: é responsabilidade do programador garantir esta consistência, uma vez que o compilador não faz nenhuma checagem quanto à semântica da operação.
Uma função em "C++" pode ter argumentos com valores pré-definidos que o compilador usa quando outros novos valores não são fornecidos. Os argumentos default são especificados quando a função é declarada. Uma função pode ter vários argumentos com valores default, porém, eles devem ser os últimos argumentos da função.
Exemplo:
void teste( int a, int b, int op=1) { int aux; aux = a*b; if (op) if (aux < 0) aux *= -1; printf("%d\n",aux); } void main() { teste(-10,20); teste(-10,20,0); } Argumentos padrão são úteis quando um determinado valor para um argumento se repete na maior parte das chamadas ou em funções com grande número de argumentos que não precisam ser necessariamente especificados (funções de janela por exemplo).
Exercícios:
Analise o fonte "exer2.cpp". Substitua as funções "aski" e "askf" por duas versões sobrecarregadas de uma função chamada "ASK".
Altere as funções "ASK" e "impperg" fazendo com que o parâmetro "nl" passe a ter o valor padrão "0". Altere, também, as chamadas correspondentes para que informem o valor para este parâmetro apenas quando for necessário.
Analise o fonte "exer3.cpp". Compile-o e verifique seu tamanho. Retire as declarações " inline " e observe as alterações no tamanho do código.
Experimente acrescentar na função inline do "exer3.cpp" um comando " for " ou " switch " e observe o tipo de mensagem de erro.
Porque a função "teste" do "exer3.cpp" não precisa de protótipo?
Uma das principais características da linguagem "C++" é o conceito de objetos e de sua estrutura, a classe. Uma classe define um tipo de dados abstrato que agrupa tanto campos de dados (da mesma forma que uma estrutura) como rotinas.
Uma classe é como uma declaração de tipo de dados. Assim como todas as variáveis de um determinado tipo, todos os objetos de uma dada classe possuem seus próprios campos de dados (atributos) e rotinas (procedimentos), mas compartilham os nomes desses atributos e procedimentos com os demais membros da mesma classe.
Sendo assim poderíamos usar uma declaração de classe para agrupar as rotinas e os campos de dados do exemplo anterior. Teríamos, então, a classe PILHA.
Ex:
#define TAM_MAX_PILHA 100 class PILHA { int pilha[TAM_MAX_PILHA]; int tamanho; int topo; public : void init( int tam); void ins( int num); int ret( void ); } ; Uma classe pode conter tanto elementos públicos quanto privados. Por definição, todos os ítens definidos na " class " são privados. Desta forma os atributos "pilha", "tamanho" e "topo" são privados. Desta forma não podem ser acessados por nenhuma função que não seja membro da " class ". Essa é uma maneira de se obter o encapsulamento, ou seja, o acesso a certos itens de dados pode ser controlado.
As partes públicas da classe, ou seja, aquelas que podem ser acessadas por rotinas que não são membros da classe devem ser explicitamente definidas depois da palavra reservada " public ". Pode-se ter tanto atributos como procedimentos na parte pública de uma classe embora uma boa norma de programação seja restringir os atributos de dados a parte privada.
Uma vez definida uma classe pode-se criar objetos daquele tipo.
Ex:
PILHA p1; Dentro da declaração da classe forma indicados apenas os protótipos das funções membro da classe. Quando chega o momento de codificar uma das funções membros, deve-se indicar a qual classe a mesma pertence. A declaração completa da classe fila fica então: #define TAM_MAX_PILHA 100 class PILHA { int pilha[TAM_MAX_PILHA]; int tamanho; int topo; public : void init( int tam); void ins( int num); int ret( void ); } ; void PILHA :: init( int tam) { int r;
_/_* Verifica o tamanho _/ if_* (tam >= TAM_MAX_PILHA) exit(0); else tamanho = tam; _/_* Zera o vetor que serve de base a pilha _/ for_* (r=0; r<tamanho; r++) pilha[r] = 0; _/_* Inicializa o topo _/_* topo = 0; } void PILHA :: ins( int num) _{ /_* Verifica se ha espaco na pilha _/ if_* (topo == tamanho) exit(0); _/_* Acrescenta o valor na pilha _/_* pilha[topo++] = num; } int PILHA :: ret() _{ /_* Verifica se a pilha nao esta vazia _/ if_* (topo == 0) exit(0); _/_* Retorna o elemento do topo _/ return_* (pilha[topo--]); } Observe que os atributos de dados são acessados nas rotinas como se fossem variáveis globais. Isso só é possível porque as rotinas pertencem a classe onde estes atributos estão declarados (escopo de classe).
Para acessarmos os elementos de um objeto usamos a mesma sintaxe usada para acessar os membros de uma " struct ". O programa mostrado anteriormente fica, então:
void main() { PILHA p1; int r; p1.init(50); for (r=0; r<50; r++) p1.ins(r); for (r=0; r<50; r++) printf("%d\n",p1.ret()); } Observe que o encapsulamento eliminou duas das regras de utilização das rotinas que implementavam a pilha sem o uso de classes:
Exercícios:
int obtem_rodas( void ); void fixa_passageiros( int num); int obtem_passageiros( void ); } ; As características acima são bastante genéricas e não servem para conter os dados de todos os tipos veículos. Por isto, são necessárias classes mais especializadas, aptas a particularizar os dados em função dos tipos de veículos que transitam na estrada. Por exemplo, se é necessário manter dados sobre caminhões, seria importante conter os dados genéricos de "veiculos_de_estrada", além de informações mais específicas como a sua carga transportável, por exemplo. Assim, abaixo é definida uma classe que herda todas as características de "veiculos_de_estrada" e acrescenta a carga: class caminhao: public veiculos_de_estrada { int carga; public : void fixa_carga( int tamanho); int obtem_carga( void ); void exibe( void ); } ; Da mesma forma, podemos particularizar as coisas para automóveis, criando uma classe que especifica de que tipo de automóvel estamos tratando:
enum type { carro, furgao, perua } ; class automovel: public veiculos_de_estrada { enum type tipo_de_carro; public : void fixa_tipo( enum type t); enum type obtem_tipo( void ); void exibe( void ); } ; Abaixo está um exemplo completo mostrando operações com herança de classes: #include <iostream.h> // Cria a Superclasse "veiculos_de_estrada": class veiculos_de_estrada { int rodas; int passageiros; public : void fixa_rodas( int num); int obtem_rodas( void ); void fixa_passageiros( int num); int obtem_passageiros( void ); } ; // Classe caminhao : Herda de "veiculos_de_estrada": class caminhao: public veiculos_de_estrada { int carga; public : void fixa_carga( int tamanho); int obtem_carga( void ); void exibe( void ); } ; enum type { carro, furgao, perua } ; // Define a classe automovel, que tambem herda de "veiculos_de_estrada": class automovel: public veiculos_de_estrada { enum type tipo_de_carro; public : void fixa_tipo( enum type t); enum type obtem_tipo( void );
void exibe( void ); } ; void veiculos_de_estrada :: fixa_rodas( int num) { rodas = num; } int veiculos_de_estrada :: obtem_rodas( void ) { return (rodas); } void veiculos_de_estrada :: fixa_passageiros( int num) { passageiros = num; } int veiculos_de_estrada :: obtem_passageiros( void ) { return (passageiros); } void caminhao :: fixa_carga( int tamanho) { carga = tamanho; } void caminhao :: exibe( void ) { cout << "rodas: " << obtem_rodas() << "\n"; cout << "passageiros: " << obtem_passageiros() << "\n"; cout << "capacidade de carga em m3: " << carga << "\n"; } void automovel :: fixa_tipo( enum type t) { tipo_de_carro = t; } enum type automovel :: obtem_tipo( void ) { return tipo_de_carro; } void automovel :: exibe( void ) { cout << "rodas: " << obtem_rodas() << "\n"; cout << "passageiros: " << obtem_passageiros() << "\n"; cout << "tipo "; switch (obtem_tipo()) { case furgao : cout << "furgao\n"; break; case carro : cout << "carro\n"; break; case perua: cout << "perua\n"; break; } } void main() { caminhao t1,t2; automovel c; t1.fixa_rodas(18); t1.fixa_passageiros(2); t1.fixa_carga(3200); t2.fixa_rodas(6); t2.fixa_passageiros(3);
No item 5.4 o uso de uma função construtora foi vantajoso na medida em que tornou automática a inicialização das estruturas que controlam a "PILHA". A desvantagem, entretanto, foi o fato de que o tamanho máximo da PILHA passou a ser fixo, visto que a função construtora não tinha parâmetros. As funções construtoras, embora não admitam retorno pelo nome da função, admitem lista de parâmetros como qualquer outra função. No caso a função construtora da classe PILHA ficaria:
PILHA :: PILHA( int tam) { int r; if (tam >= TAM_MAX_PILHA) exit(0); else tamanho = tam; _/_* Verifica o tamanho _/ /** Zera o vetor que serve de base a pilha **/ for_* (r=0; r<tamanho; r++) pilha[r] = 0; _/_* Inicializa o topo _/_* topo = 0; } Para passar um parâmetro a uma função construtora deve-se associar os valores que estão sendo passados com o objeto quando ele esta sendo declarado. No caso o parâmetro ou parâmetros devem seguir o nome do objeto e estar entre parênteses.
Ex:
PILHA x(40); PILHA p1(10), p2(67); int x;
scanf("%d",&x); PILHA pilha(x);
Quando se trabalha com herança, deve-se cuidar a passagem de parâmetros para as funções construtoras. A função construtora de uma classe derivada deve sempre providenciar a passagem dos parâmetros para a construtora da classe pai.
Ex:
class C { int x,y; public : C1( int px, int py) { x = px; y = py; } } ; class C2 : public C { int b,c,d; public : C2( int a); } ; C2 :: C2( int a) :C1(a,a*2) { b = a; c = a*2; d = 23; }
void main() { C2 n(12); }
Exercícios:
Analise o fonte "exer7.cpp". Compile-o e verifique (depurador - opção debug|inspec) a passagem dos parâmetros para as funções construtoras e a hierarquia de herança utilizada.
Altere a construtora da classe relógio de maneira que a mesma possua um parâmetro que especifica se a mesma deve emitir um "bip" ou não quando um objeto "RELOGIO" for criado (não use argumentos default).
Crie a classe "RELOGIO_ANALOGICO_COLORIDO" derivada de "RELOGIO_ANALOGICO". A construtora desta classe deve ter um parâmetro a mais que indica a cor dos ponteiros. Observe que esta classe necessita apenas de um atributo de dados para armazenar a cor dos ponteiros, da rotina construtora e de uma nova versão para a rotina "atualiza_mostrador". Crie uma instância desta nova classe no "main" e integre o funcionamento deste novo relógio com os demais.
Altere uma das classes do programa "exer7.cpp" de maneira que toda a vez que um relógio for destruído ele exiba a mensagem "Relogio destruido".
O que deve ser acrescentado para que a mensagem exibida na destrutora criada no exercício anterior possa indicar que relógio foi destruido e a que classe ele pertence?