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

Tudo sobre C++ Parte 1


.Fernando~
 Compartilhar

Posts Recomendados

 

  • Introdução
  • Extensões ao C
  • Vocabulário C++
  • Classes

1-Introdução

Para muitos a transição de C para C++ não é fácil. De fato, essa transição é freqüentemente acompanhada de muita ansiedade porque C++ é popularmente envolto em uma aura de inacessibilidade. Por exemplo, você pode pegar um livro sobre C++, abri-lo em uma página qualquer e deparar-se com um parágrafo mais ou menos assim:Do ponto de vista de projeto, derivação privada é equivalente a compartimentação, exceto pela, ocasionalmente importante, questão de sobreposição. Um uso importante disso é a técnica de derivar uma classe publicamente a partir de uma classe base abstrata, definindo uma interface, e privativamente de uma classe concreta provendo uma implementação. Porque a herança implícita na derivação privada é um detalhe de implementação que não é refletido no tipo da classe derivada, ela é algumas vezes denominada "herança de implementação" e contrasta com a declaração pública, onde a interface da classe base é herdada e a conversão implícita para o tipo da classe é permitida. O restante é algumas vezes referido como uma a sub-tipificação ou "herança de interface". (Trecho extraído de "The C++ Programming Language, second edition, by Bjarne Stroustrup, page 413)

É realmente difícil iniciar-se em C++ com uma literatura assim tão rebuscada, tão hermética.

Essa série de tutoriais respondem a três questões bastante comuns:

 

  • Porque C++ existe e quais são suas vantagens sobre o C?
  • Que recursos estão disponíveis no C++ para materializar idéias orientadas a objeto?
  • Como você projeta e implementa código usando os princípios da orientação a objeto?

Uma vez que você tenha compreendido os recursos básicos disponíveis no C++, e saiba como e porque usá-los, você se tornará um programador em C++. Essas série de tutoriais vão iniciá-lo nessa direção, e tornar outros textos sobre C++, inclusive os de Stroustrup, mais fáceis de entender.

Esses tutoriais presumem que você conhece a linguagem C. Se esse não é o seu caso, gaste uma semana ou duas aprendendo C e então retorne a esses tutoriais. C++ é um superset do C, portanto quase tudo que você souber sobre C vai encontrar conexão nessa nova linguagem.

Porque C++ existe?

As pessoas que são novatas em C++, ou aqueles que lêem livros sobre C++, geralmente tem duas perguntas:

 

  • "Tudo o que leio tem um vocabulário maluco: encapsulamento, herança, funções virtuais, classes, sobrecarga, amigos... De onde vem tudo isso?
  • Essa linguagem - e programação orientada a objeto de um modo geral - obviamente implicam em uma mudança de mentalidade, então como eu faço para aprender a pensar em modo C++?

Ambas essas questões podem ser respondidas, e o projeto do C++ como um todo é facilmente inteligível, se você souber o que os projetistas do C++ pretendiam atingir quando criaram essa linguagem. Se você entender porque os projetistas fizeram as opções que fizeram, e porque introduziram certos recursos específicos na linguagem, então será muito mais fácil compreender a linguagem integralmente.

O projeto de linguagens de programação é um processo evolucionário. Uma nova linguagem é criada a partir de lições aprendidas com linguagens antigas, ou na tentativa de introduzir novos recursos e facilidades a uma linguagem existente. Por exemplo, a linguagem Ada foi projetada originalmente para resolver um problema aflitivo enfrentado pelo Pentágono. Os programadores, escrevendo código para diferentes sistemas de defesa militar, tinham usado centenas de linguagens de programação diferentes, o que tornaria, de fato, impossível manter ou aprimorar esses sistemas no futuro. Ada tenta resolver alguns desses problemas combinando os bons recursos de várias linguagens em uma única linguagem de programação.

Um outro bom exemplo é o processo evolucionário que ocorreu com as linguagens de programação a partir do desenvolvimento das linguagens estruturadas. Essas linguagens vieram em resposta a um grande problema não previsto pelos projetistas das linguagens de programação mais antigas: o uso abusivo do comando goto em programas muito grandes. Em um programa pequeno o comando goto não causa maiores problemas. Mas em um programa muito grande, especialmente quando desenvolvido por alguém viciado no comando goto, os problemas tornam-se terríveis. O código torna-se absolutamente incompreensível por um outro programador que tente lê-lo pela primeira vez. As linguagens de programação evoluíram para resolver esse problema, eliminando o comando goto inteiramente, e tornando simples subdividir um programa muito grande em módulos ou funções pequenas, compreensíveis e manejáveis.

C++ é uma linguagem orientada a objeto. Programação orientada a objeto é uma reação a problemas que foram percebidos pela primeira vez em programas muito grandes desenvolvidos na década de 70. Todas as linguagens orientadas a objeto tentam atingir três objetivos, como uma forma de impedir que ocorram os problemas inerentes a projetos muito grandes:

Todas as linguagens de programação orientadas a objeto implementam data abstraction de uma forma clara usando um conceito denominado classes. Vamos examinar data abstraction em maiores detalhes mais adiante, até porque esse é um conceito central, muito importante, em C++. Em poucas palavras, data abstraction é um modo de combinar dados e as funções usadas para manipulá-los, de tal forma que os detalhes da implementação fiquem ocultos para outros programadores. Data abstraction possibilita o desenvolvimento de programas mais fáceis de manter e de aprimorar. Todas as linguagens orientadas a objeto tentam tornar facilmente reutilizáveis e extensíveis cada uma das partes que compõem os programas. Aqui é que o termo objeto começa a fazer sentido. Os programas são quebrados, subdivididos, em objetos reutilizáveis. Esse objetos podem ser agrupados de diferentes maneiras para formar novos programas. Objetos existentes podem ainda ser estendidos. Dando aos programadores um modo muito simples de reutilizar código, e virtualmente forçando os programadores a escrever códigos para serem reutilizados, torna-se muito mais fácil desenvolver novos programas remontando peças já existentes.

Linguagens orientadas a objeto visam tornar um código existente facilmente modificável sem, na realidade, alterar fisicamente o código. Esse é um conceito único e muito poderoso, porque a primeira vista parece não ser possível modificar alguma coisa sem de fato alterá-la. Entretanto, é plenamente possível fazer isso usando dois novos conceitos: herança e polimorfismo. O objeto existente permanece o mesmo, e as alterações são como que assentadas sobre ele. A habilidade dos programadores para manter e aprimorar código de um modo livre de erros é drasticamente melhorada usando essa abordagem.

Como C++ é uma linguagem orientada a objeto, ela contém os três benefícios da orientação a objeto recém apresentados. C++ acrescenta ainda a tudo isso duas outras importantes melhorias, para eliminar problemas existentes na linguagem C original, e para tornar a programação em C++ mais fácil que em C.

C++ acrescenta um conceito denominado sobrecarga de operador. Esse recurso permite que você especifique em seus programas novos modos de uso para operadores padrões, tais como + e >>. Por exemplo, se você quiser adicionar um novo tipo de dado, como um tipo número complexo em um programa em C, tal implementação não será simples. Para somar dois números complexos, você terá que criar uma função denominada, por exemplo, soma, e então escrever c3=soma(c1,c2);, onde c1, c2 e c3 são valores do novo tipo número complexo. Em C++ você pode, ao invés de criar uma nova função, sobrecarregar os operadores + e =, de tal forma que você pode escrever c3=c1+c2. Dessa maneira, novos tipos de dados podem ser adicionados à linguagem de uma maneira clara, simples, sem ajeitamentos. O conceito de sobrecarga aplica-se a todas as funções criadas em C++.

C++ ainda simplifica a implementação de várias partes da linguagem C, principalmente no que se refere a operações de I/O e alocação de memória. Essas novas implementações foram criadas contemplando-se a sobrecarga de operadores, de tal modo que tornou-se fácil adicionar novos tipos de dados e providenciar operações de I/O e alocação de memória para os novos tipos, sem truques ou artifícios.

Vamos examinar alguns problemas que você provavelmente enfrentou usando a linguagem C, e então ver como eles são resolvidos pelo C++

O primeiro problema pode ser visto em toda biblioteca de programas construída em C. O problema é demonstrado no código a seguir, o qual atribui um valor a um string e então concatena esse valor a um outro string:

char s[100];

strcpy(s, "hello ");

strcat(s, "world");

Essa codificação não é muito bonita, mas o seu formato ilustra muito tipicamente o que se encontra em bibliotecas criadas em C. O tipo string é construído a partir do tipo matriz-de-caracteres, que é nativo em C. Devido ao fato de que o novo tipo, string, não é parte integrante da linguagem original, o programador é forçado a usar chamadas de funções para fazer qualquer operação com o novo tipo de dado. O desejável é que, ao invés disso, se possa criar novos tipos de dados e lidar com eles naturalmente, com os próprios recursos da linguagem. Alguma coisa como

string s;

 

s = "hello ";

s += "world";

Se algo assim é possível, a linguagem pode ser estendida ilimitadamente. C++ suporta esse tipo de extensão através da sobrecarga de operadores e de classes. Repare ainda que usando um novo tipo string, a implementação tornou-se completamente oculta. Ou seja, você não precisa saber como, ou se ,o tipo string foi criado usando uma matriz de caracteres, uma lista ligada, etc. ou ainda se string tem um tamanho máximo. Melhor ainda, é fácil alterar-se, no futuro, a implementação do tipo string, sem afetar negativamente os códigos que estiverem utilizando-o

Outro exemplo usando uma biblioteca pode ser visto na implementação de uma biblioteca para tratamento de pilhas.

Os protótipos das funções para uma típica biblioteca de tratamento de pilhas - normalmente encontrado no header file - é mostrado a seguir:

void stack_init(stack s, int max_size);

int stack_push(stack s, int value);

int stack_pop(stack s, int *value);

void stack_clear(stack s);

void stack_destroy(stack s);

O programador usuário dessa biblioteca pode usar funções para push, pop e clear a pilha, mas antes, e para que qualquer uma dessas operações seja válida, de inicializar a pilha com a função stack_init. Ao concluir com a utilização da pilha, deve destruí-la com a função stack_destroy. E o que acontece se você se esquece da inicialização ou da destruição? Em um caso real, o código não vai funcionar e pode ser bem difícil rastrear o problema, a menos que todas as rotinas dessa biblioteca detectem a falta da inicialização e indiquem isso especificamente. A omissão da etapa de destruição da pilha pode causar um problema denominado memory leak, que é também difícil de rastrear. C++ resolve esse problema usando construtores e destrutores, que automaticamente manejam a inicialização e a destruição de objetos, inclusive de pilhas.

Continuando ainda com o exemplo da pilha, note que a pilha, uma vez definida, pode push e pop números inteiros. O que acontece se você quer criar uma outra pilha para lidar com números reais, ou uma outra ainda para caracteres? Você terá que criar três bibliotecas separadas, ou, alternativamente, usar uma unione deixar a union manejar todos os tipos de dados possíveis. Em C++, um conceito denominado modelo permite que você crie uma única biblioteca para tratar a pilha e redefina os tipos de dados a serem armazenados na pilha quando esta for declarada.

Um outro problema que você já deve ter tido programando em C envolve a alteração de bibliotecas. Digamos que você esteja usando a função printfdefinida na biblioteca stdio, mas você quer modificá-la para manejar um novo tipo de dado que você criou em seu programa. Exemplificando, você deseja modificar printf para que ela imprima números complexos. Você não tem como fazer isso, a menos que você tenha o código fonte da implementação de printf. Mas ainda que você tenha o código fonte de printf, essa pode ser uma péssima estratégia porque você pode gerar um código não portável. Não há realmente um modo de extender facilmente uma biblioteca C uma vez que ela tenha sido compilada. Para resolver o problema de impressão de números complexos em C, como no nosso exemplo, você teria que criar uma nova função com a mesma finalidade que printf. Se você tiver vários novos tipos de dados, você terá que criar várias e diferentes novas funções de saída, à semelhança de printf. C++ lida com todos esses problemas com uma nova técnica para saída padrão. Uma combinação de sobrecarga de operador e classes permite integrar novos tipos de dados ao esquema padrão de I/O do C++.

Ainda pensando sobre a função printf, reflita sobre seu projeto e pergunte-se a si mesmo: Esse é um bom modo de se projetar um código? Dentro do código de printf há um comando switch, ou uma cadeia de if-else-if que avalia um string de formatação da saída. Um %d é usado para números decimais, um %c é usado para caracteres, um %s é usado para strings, e assim por diante. Há, no mínimo, três problemas com essa implementação:

O programador da implementação de printftem que manter o comando switch, ou cadeia de if-else-if, e modificá-lo para cada novo tipo de formatação que se quiser implementar. Modificações sempre significam a possibilidade de se introduzir novos bugs.

Não há qualquer garantia de que o programador usuário de printf vai combinar corretamente o tipo do dado com string de formatação, o que significa que a função contém um risco de falha.

Essa implementação não é extensível, a menos que você possua o código fonte, você não pode ampliar as capacidades de printf.

C++ resolve esses problemas completamente porque força o programador a estruturar o código de uma nova maneira. O comando switch é ocultado e manejado automaticamente pelo compilador através da sobrecarga de função. Torna-se assim impossível combinar erradamente os parâmetros ao invocar uma função, primeiro porque eles não são implementados como parâmetros em C++, e segundo porque o tipo da variável controla automaticamente o mecanismo de switch que é implementado pelo compilador.

C++ resolve ainda vários outros problemas. Por exemplo, resolve o problema de código comum replicado em vários lugares, permitindo que você controle código comum em uma terceira dimensão. Resolve o problema eu quero alterar o tipo de dado passado para uma função sem alterar a função, permitindo que você sobrecarregue o mesmo nome de função com múltiplas listas de parâmetros. Resolve o problema eu quero fazer uma pequena alteração no modo como isso funciona, mas eu não tenho o código fonte, e ao mesmo tempo resolve o problema eu quero reformular essa função inteiramente, sem alterar o restante da biblioteca usando o conceito de herança.

C++ torna a criação de bibliotecas mais simples e melhora drasticamente a tarefa de manutenção do código. E muito mais.

Você precisará mudar um pouco o seu modo de pensar em programação para obter as vantagens de todos esses recursos poderosos, e isso significa que você vai ter que dedicar-se um pouco mais ao projeto de seu código. Sem isso, você perderá vários dos benefícios do C++. Como em tudo na vida, a migração para C++ significa custos e benefícios, mas nesse caso o conjunto dos benefícios supera em muito os custos.

Entendendo C++

 

 

2.0 - Introdução Tudo o que você já escreveu em C funciona em C++. No entanto, em muitos casos C++ oferece um modo melhor de se realizar o mesmo trabalho. Em outros casos C++ oferece mais de uma maneira de se fazer a mesma coisa, e isso lhe dá maior flexibilidade no desenvolvimento de programas. Nessa seção vamos examinas as extensões do C++ em relação ao C. Muitas dessas extensões não foram adicionadas gratuitamente, mas sim para viabilizar programação orientada a objeto, conforme se verá mais adiante em outros tutoriais dessa série.Esse tutorial em especial apresenta muitos detalhes do C++. Nada de pânico. Se preferir apenas percorra o texto e volte mais tarde estudando cada seção, na medida de sua necessidade. Os conceitos apresentados a seguir foram todos reunidos aqui para simplificação de referência, já que serão usados em vários outros pontos dessa série de tutoriais.

 

2.1 - Comentários C++ suporta o antigo modo multi-linha de comentário, bem como a nova forma linha-única representado pelo símbolo //. Por exemplo:

// get_it function reads in input values

void get_it()

{

// do something.

}

O C++ ignora tudo o que estiver escrito após o // e até o final da linha. Você pode usar ambas as formas de comentário em um programa C++.

 

 

 

2.2 - Conversão de tipos de dados Em C você pode determinar a conversão de tipos de dados, colocando o nome do tipo entre parêntesis imediatamente antes do nome da variável, como no exemplo seguinte:

int i;

float f;

 

f = (float) i;

C++ suporta um segundo modo de conversão de tipos, que faz com que a especificação de conversão se pareça mais com a chamada de uma função:

int i;

float f;

 

f = float(i);

Vamos ver mais adiante, quando começarmos a tratar de classes, que há uma razão para esse novo formato.

 

 

 

2.3 - Input/Output 2.3.1 - I/O em Terminal

Uma das mais óbvias diferenças entre C e C++ é a substituição da biblioteca stdio do C pela iostream do C++. A biblioteca iostream beneficia-se de um grande número de extensões do C++ voltadas a orientação a objeto, conforme veremos mais adiante. Mais ainda, torna mais fácil a adição de novos tipos de dados definidos pelo programador. A biblioteca iostream contém todas as capacidades encontradas na stdio, mas as viabiliza de uma nova forma. Portanto, é importante conhecer como usar as funções básicas da iostream se você estiver convertendo seu código de C para C++. O uso de iostream para as funções básicas de input/output é de compreensão quase imediata. Veja os seguintes exemplos:

cout << "hello\n";

ou o equivalente:

cout << "hello" << endl;

Ambos os exemplos produzem a mesma saída, e fazem com que a palavra hello seguida de um newline seja escrita na unidade padrão de saída. A palavra cout indica stdout como destinação para a operação de saída, e o operador << (que significa inserção) é usado para transferir os itens de dados. Há duas outras saídas-padrão pré-definidas: cerr para notificações imediatas de erro, e clog para acumulação de notificações de erro.Pode-se escrever em qualquer saída padrão usando-se as técnicas mostradas no exemplo anterior. Múltiplos itens de dados podem ser enfileirados em uma única linha de comando, ou empilhados em várias linhas. Por exemplo:

 

int i = 2;

float f = 3.14

char c = 'A';

char *s = "hello";

 

cout << s << c << f << i << endl;

produz a saída:helloA3.142

da mesma forma que

 

cout << s << c;

cout << f;

cout i << endl;

O mecanismo cout compreende valores de endereços de memória automaticamente, e os formata para apresentação em hexadecimal. Por exemplo, se i é um inteiro, então o comando

cout << &i << endl;

Há casos, no entanto, em que essa regra de conversão para hexadecimal não se aplica. Imprimir s, onde s é um pointer para um caracter, produz a impressão do string apontado por s em lugar do endereço contido em s. Para remediar essa situação, converta s para um pointer void, como mostrado a seguir, se o que você quer é a impressão do endereço contido de s:

cout << (void *) s;

Agora o endereço contido em s será apresentado em formato hexadecimal. Se você quiser o endereço apresentado em formato decimal, converta-o para um inteiro longo:

cout << long(& i);

Essa linha imprime o endereço de i em formato decimal.

 

Da mesma forma, uma conversão para int é usada para imprimir o valor inteiro de um caracter:

cout << int('A'); // produces 65 as output

Você pode estranhar que o operador << - conhecido em C como shift left operator - tenha sido roubado para manejar operações de saída em C++. Se você quiser usá-lo para operação de shift left dentro de uma linha de saída, use-o entre parêntesis:

cout << (2 << 4); // produces 32 as output

Você pode usar várias técnicas para formatar a saída. Informações podem ser espaçadas acrescentando-se espaços, tabs ou strings literais, como mostrado a seguir:

int i = 2;

float f = 3.14

char c = 'A';

char *s = "hello";

 

cout << s << " " << c << "\t" << f

<< "\t" << i << endl;

Há ainda vários outros manipuladores que podem ser inseridos em um fluxo de saída (em alguns sistemas você precisará incluir iomanip.h para poder usá-las):

dec Usa base decimal oct Usa base octal hex Usa base hexadecimal endl Finaliza a linha ends Finaliza o string ('\0') flush Descarrega o buffer de saída setw(w) Estabelece a largura da saída para o valor de w

(0 é o default) setfill© Estabelece o caracter de preenchimento para conteúdo de c (blank é o default) setprecision(p) Estabelece a precisão flutuante para o valor de p

 

O código

cout << "[" << setw (6) << setfill('*') << 192;

cout << "]" << endl;

cout << hex << "[" << setw (6);

cout << setfill('*') << 192 << "]" << endl;

cout << setprecision(4) << 3.14159 << endl;

produz

 

[***192]

[****c0]

3.142

A saída de números em ponto flutuante pode truncar ou não zeros a direita, independente de como você estabeleça a precisão. Essa é uma característica determinada pelo compilador que você estiver usando.Você deve ter notado nos exemplos recém apresentados que não se deve usar certos nomes para variáveis ou funções para não se perder a possibilidade de usar os manipuladores intrínsecos à biblioteca iostream.

As operações de entrada são manejadas de uma maneira semelhante às operações de saída, usando-se cin para fluxo de entrada e >> como operador de extração. Por exemplo, o comando

 

int i,j,k;

cin >> i >> j >> k;

O fluxo de entrada cin automaticamente divide o string de entrada em palavras e termina quando recebe EOF.

2.3.2 - I/O em arquivos

Input e output para arquivos texto são manejados pela inclusão do arquivo fstream.h, e pela declaração de variáveis do tipo ifstream e ofstream respectivamente. Por exemplo, o seguinte programa lê um arquivo denominado xxx e escreve em um arquivo denominado yyy:

#include <iostream.h>

#include <fstream.h>

 

void main()

{

char c;

ifstream infile("xxx");

ofstream outfile("yyy");

 

if (outfile && infile) // They will be 0 on err.

while (infile >> c)

outfile << c;

}

As variáveis infile e outfile recebem o nome do arquivo na inicialização e são usadas exatamente como cin e cout. O código desse exemplo não funciona como seria de esperar porque brancos, tabs e caracteres \0 ao final de cada linha são ignorados, do mesmo modo que espaço em branco quando se usa <<. Nesse caso, melhor usar a função get, como mostrado a seguir:

while (infile.get©)

outfile << c;

ou

while (infile.get©)

outfile.put©;

É ainda possível ler linhas inteiras usando-se a função getline, da mesma maneira que se usa a função get. Para abrir um arquivo para acrescentar dados, use o seguinte:

ofstream("xxx", iosapp);

Essa linha, bem como a notação da função .get, fará mais sentido na medida em que você conheça mais sobre o C++. O fato de ofstream algumas vezes receber um, outras vezes dois parâmetros é uma característica intrínseca ao C++. Note que não há necessidade de uma função close para os arquivos de entrada ou de saída. O arquivo é fechado automaticamente quando se encera o escopo da variável que denomina o arquivo. Se você precisar fechar explicitamente um arquivo, use

 

outfile.close();

2.3.3 - I/O em string

Entradas podem ser lidas a partir de string na memória, e saídas pode ser enviadas para strings na memória, duplicando-se assim as ações de sscanf e sprintf. Para tanto você deve incluir o arquivo strstream.h e declarar a entrada e a saída em string. Uma saída em string e mostrada a seguir:

char s[100];

ostrstream outstring(s,100);

 

outstring << 3.14 << " is pi" << ends;

cout << s;

O string s é preenchido com o texto

 

3.14 is pi.

 

Se o tamanho de s for atingido, outstring vai automaticamente interromper a colocação de valores em s.

 

Se um string s já existe e você deseja ler dados a partir dele, você pode usar um fluxo de entrada string como mostrado a seguir:

char *s = "3.14 12 cat";

istrstream instring(s, strlen(s));

float f;

int i;

char t[100];

 

instring >> f >> i >> t;

A biblioteca iostream tem muitas e muitas outras capacidades não discutidas aqui. Para maiores informações, veja a documentação fornecida junto com o seu compilador. Ela geralmente contém uma referência completa para a biblioteca de I/O

 

 

 

2.4 - Declarações de variáveis Variáveis são declaradas em C++ assim como o são em C. As variáveis podem ser declaradas em qualquer ponto do código em C++, estabelecendo um nível de flexibilidade quase como o que existe em FORTRAN. A variável torna-se existente quando é declarada, e deixa de existir quando o } do bloco corrente é encontrado. Por exemplo, na seguinte codificação:

{

int i;

... code ...

int j;

... code ...

int k=func(i,j);

... code ...

}

todas as três variáveis passam a existir no ponto em que são declaradas, e desaparecem ao }.

 

 

 

2.5 - Constantes Em C você cria uma constante usando uma macro do pré-processador, como no seguinte exemplo:

#define MAX 100

Quando o programa é compilado, o pré-processador encontra cada ocorrência da palavra MAX e a substitui pelo string 100.Em C++ usa-se a palavra const, que é aplicável normalmente às declarações de variáveis:

 

const int MAX=100;

A codificação int MAX=100; é formatada exatamente da mesma maneira que uma declaração normal. O termo const que precede a declaração simplesmente define que a variável MAX não pode ser modificada. O uso de letras maiúsculas para nomes de variáveis constantes é uma tradição em C, que você pode preservar ou abandonar. O modificador const pode também ser usado em listas de parâmetros para especificar o uso correto do parâmetro. As três funções a seguir exemplificam diferentes usos de const.

 

void func1(const int i)

{

i=5; // cannot modify a constant

}

 

void func2(char * const s)

{

s="hello"; // cannot modify the pointer

}

 

void func3(const char * s)

{

s="hello"; // this is OK

*s='A'; // cannot modify what is pointed to

}

A notação mostrada em func2 deve, sempre que possível, ser usada quando um parâmetro char* é passado para a função.

 

 

 

2.6 - Sobrecarregando funções Um dos mais poderosos novos recursos do C++ é denominado sobrecarga de função. Uma função sobrecarregada possui várias e diferentes listas de parâmetros. A linguagem distingue qual das opções de chamada da função deve ser usada, com base no padrão das listas de parâmetros. Aqui está uma demonstração extremamente simples desse processo:

#include <iostream.h>

 

void func(int i)

{

cout << "function 1 called" << endl;

cout << "parameter = " << i << endl;

}

 

void func(char c)

{

cout << "function 2 called" << endl;

cout << "parameter = " << c << endl;

}

 

void func(char *s)

{

cout << "function 3 called" << endl;

cout << "parameter = " << s << endl;

}

 

void func(char *s, int i)

{

cout << "function 4 called" << endl;

cout << "parameter = " << s;

cout << ", parameter = " << i << endl;

}

 

main()

{

func(10);

func('B');

func("hello");

func("string", 4);

return 0;

}

Quando esse código é executado, cada versão da função func é escolhida, e chamada, de acordo com as correspondências entre as listas de parâmetros. Você vai conseguir usar essa capacidade, que é uma grande característica do C++, uma vez que você encare sobrecarga de função como uma solução para muitos dos problemas de programação. Por exemplo, se você cria uma função para inicializar um módulo, você pode ter uma chamada diferente, para a mesma função, dependendo da característica do parâmetro que é passado; um string, um inteiro, um ponto flutuante, e assim por diante.

 

 

 

2.7 - Argumentos default C++ também permite que você determine valores default para os parâmetros. Se o parâmetro não for passado, o valor default é usado. Essa capacidade é demonstrada no seguinte código:

#include <iostream.h>

 

void sample(char *s, int i=5)

{

cout << "parameter 1 = " << s << endl;

cout << "parameter 2 = " << i << endl;

}

 

main()

{

sample("test1");

sample("test1",10);

return 0;

}

A primeira chamada da função vai apresentar o valor default 5 para o parâmetro i, enquanto a segunda chamada vai apresentar o valor 10.Quando criando parâmetros default, você precisa evitar ambigüidade entre as listas de parâmetros default e as demais listas de parâmetros. Examinando a função de nosso último exemplo, não é possível criar uma versão sobrecarregada que aceite um parâmetro char* isolado, porque o compilador não conseguiria escolher que versão chamar no caso de se passar um string.

 

 

 

 

2.8 - Alocação de memória C++ substitui as funções C de alocação e de desalocação de memória , malloc e free, pelas novas funções new e delete, respectivamente e torna esse processo muito mais fácil para o programador. new e delete permitem que tipos de dados definidos pelo programador sejam alocados e desalocados tão facilmente quanto os tipos já existentes em C++. O código seguinte exemplifica o modo mais simples de uso de new e delete. Um pointer para um inteiro aponta para um bloco de memória criado por new:

 

int *p;

p = new int;

*p = 12;

cout << *p;

delete p;

É ainda possível alocar blocos compostos de matrizes de tamanhos variados, usando uma técnica similar. Repare o uso de [] para excluir a matriz:

int *p;

p = new int[100];

p[10] = 12;

cout << p[10];

delete [] p;

O valor 100 poderia ser uma variável, se desejável.Quando aplicada a tipos de dados definidos pelo programador, new funciona do mesmo modo. Por exemplo:

 

typedef node

{

int data;

node *next;

} node;

 

main()

{

node *p;

p=new node;

p->date = 10;

delete p;

}

Conforme veremos mais adiante nessa série de tutoriais, o operador delete é bastante sofisticado quando opera com classes definidas pelo programador.

 

 

 

 

2.9 - Declarações de referência

Em C, pointers são freqüentemente usados para passar parâmetros para funções. Por exemplo, a seguinte função swap inverte dois valores que lhe são passados:

void swap(int *i, int *j)

{

int t = *i;

*i = *j;

*j = t;

}

 

main()

{

int a=10, b=5;

 

swap(& a, & b);

cout << a << b << endl;

}

C++ provê um novo operador de referência que simplifica muito essa sintaxe. O seguinte código funciona em C++

void swap(int& i, int& j)

{

int t = i;

i = j;

j = t;

}

 

main()

{

int a=10, b=5;

 

swap(a, b);

cout << a << b << endl;

}

Os parâmetros i e j declarados como tipo int& atuam como referências para os inteiros passados (leia int& como uma referência para um inteiro). Quando uma variável é atribuída à referência de uma outra variável, a referência toma o endereço da variável e opera a atribuição para a localização real para a variável . Por exemplo:

int a;

int & b=a;

 

a=0;

b=5;

cout << a << endl;

O código acima produz 5 como saída porque b referencia a. É o mesmo que usar pointer e operadores de endereços em C, mas a sintaxe aqui é muito mais simples. Note que b deve ser inicializado quando de sua criação, como mostrado no exemplo.

 

Vocabulário C++

 

 

 

3.0 - Introdução O último tutorial enfocou elementos da linguagem C++ que extendem as capacidades originais, ou corrigem problemas inerentes à linguagem C. Essas extensões são bastante simples de se entender.O outro conjunto de recursos do C++ são dirigidas à programação orientada a objeto, e talvez não sejam de entendimento tão imediato. Enquanto as capacidades decoutsão apenas uma outra forma de manejar operações de saída de dados - as quais você já conhecia previamente - algumas extensões orientadas a objeto talvez não lhe sejam tão familiares. O objetivo desse capítulo é lhe dar uma primeira exposição de algumas dessas extensões. Assim, vamos examinar a sintaxe C++ que suporta os conceitos de orientação a objeto e posteriormente rever os conceitos.

 

 

 

 

3.1 - Vocabulário C++ Olhe o mundo a sua volta. Você pode entender uma grande parte da estrutura, do vocabulário e da organização do C++ apenas olhando a estrutura e a organização do mundo real, e refletindo sobre o vocabulário que usamos para falar sobre o mundo real. Muitos dos elementos do C++ - em da orientação a objeto em geral - tentam emular o modo como interagimos com o mundo real. Por exemplo, sempre que você olha em torno você vê uma grande quantidade de objetos. Nós organizamos esses objetos em nossas mentes arranjando-os em categorias, ou classes. Se você tem um livro em suas mãos, um livro é uma classe genérica de objetos. Você poderia dizer "esse objeto que eu tenho nas mãos é classificado como um livro." Uma hierarquia de classes de objetos envolve a classe livro e a estende em duas direções. Livros são membros da classe mais geral publicações. Há ainda tipos específicos de livros, tais como livros de computação, livros de ficção, biografias, e assim por diante. A organização hierárquica se estende em ambos os sentidos: do mais geral para o mais específico. Em nosso exemplo, você tem nas mãos um determinado livro, um livro específico. No idioma OOP, você tem nas mãos uma instância da classe livro. Livros tem certos atributos que são comuns e portanto são compartilhados por todos os livros: uma capa, vários capítulos, não tem anúncios, etc. Livros tem também atributos comuns a publicações em geral: título, data de publicação, editora, etc. Tem ainda atributos comuns a objetos físicos: localização, tamanho, forma e peso. Essa idéia de atributos comuns é muito importante em C++. C++ modela o conceito de atributos comuns usando herança.

Há certas coisas que você faz com e para certos objetos, e essas ações são diferentes de objeto para objeto. Por exemplo, você pode ler um livro, folhear suas páginas. Você pode olhar um título, procurar um capítulo específico, pesquisar o índice, contar o número de páginas, etc. Essas ações são aplicáveis unicamente a publicações. Você não poderia folhear as páginas de um martelo, por exemplo. Entretanto, há ações que são genéricas e aplicáveis a todos os objetos físicos, como pegá-los. C++ também leva em conta esse fato e modela esses casos usando herança.

A natureza hierárquica das categorias de objetos, bem como nossa organização hierárquica de atributos de objetos e de ações, estão contidas na sintaxe e no vocabulário do C++. Por exemplo, quando projetando um programa você vai subdividi-lo em objetos, cada um dos quais tem uma classe. Você vai herdar atributos de uma classe base quando você criar uma classe derivada. Ou seja, você vai criar classes mais gerais de objetos e então fazer classes mais específicas, a partir das classes gerais, derivando o particular a partir do geral. Você vai encapsular o dados em um objeto comfunções membro funções membro funções membro funções membro funções membro funções membro, e para ampliar classes você vai sobrecarregar e sobrescrever funções da classe base. Confuso? Vamos examinar um exemplo simples para ver o que esses termos significam na realidade.

O exemplo clássico de programação orientada a objeto é um programa gráfico que lhe permite desenhar objetos - linhas, retângulos, círculos, etc. - na tela do terminal. O que todos esses objetos tem em comum? Que atributos todos esses objetos compartilham? Todos tem uma localização na tela. Podem ter uma cor. Esses atributos - localização e cor - são comuns a todas as formas exibidas na tela. Portanto, como projetista do programa você poderia criar uma classe base - ou em outras palavras, uma classe genérica de objetos - para conter os atributos comuns a todos os objetos apresentados na tela. Essa classe base poderia ser denominada Forma, para melhor identificá-la como classe genérica. Você poderia então derivar diferentes objetos - círculos, quadrados, linhas - a partir dessa classe base, adicionando os novos atributos que são próprios de cada forma em particular. Um círculo específico desenhado na tela é uma instância da classe Círculo, que herdou uma parte de seus atributos de uma classe mais genérica denominada Forma. É possível criar tal conjunto de hierarquia em C, mas nem de longe com tanta facilidade quanto em C++. C++ contém sintaxe para tratar herança. Por exemplo, em C você poderia criar uma estrutura básica para conter os atributos localização e cor dos objetos. As estruturas específicas de cada objeto poderiam incluir essa estrutura básica e ampliá-la. C++ torna esse processo mais simples. Em C++, as funções são agrupadas, reunidas dentro de uma estrutura, e essa estrutura é denominada classe. Assim, a classe base pode ter funções, denominadas em C++ como funções membro, que permitam que os objetos sejam movidos ou re-coloridos. As classes derivadas podem usar essas funções membro da classe base tal como são, criar novas funções membro, ou ainda sobrescrever funções membro da classe base.

O mais importante recurso que diferencia C++ do C é a idéia de classe, tanto em nível sintático quanto em nível conceptual. Classes permitem que você use todas as facilidades de programação orientada a objeto - encapsulamento, herança e polimorfismo - em seus programas em C++. Classes são ainda a estrutura básica sobre a qual outros recursos são implementados, como sobrecarga de operador para novos tipos de dados definidos pelo programador. Tudo isso pode lhe parecer confuso ou desarticulado nesse momento, mas na medida em você de torne familiarizado com os conceitos e com esse vocabulário vai perceber todo o poder dessas técnicas

 

 

 

 

3.2 - A evolução das classes

Entendidos os conceitos poderosos agregados ao conceito de classe, a compreensão da sintaxe torna-se quase automática. Uma classe é simplesmente uma melhoria das estruturas em C. Basicamente, uma classe possibilita que você crie uma estrutura que contenha também todas as funções para lidar com os dados da estrutura. Esse processo é denominado encapsulamento. É um conceito muito simples, mas é o ponto central da orientação a objeto: dados + funções = objetos. Classes podem também ser construídas sobre outras classes, usando herança. Com herança, uma nova classe amplia as capacidades da classe base. Finalmente, novas classes podem modificar o comportamento de suas classes base, uma capacidade denominada polimorfismo.Essa é uma nova maneira de pensar sobre o código: uma abordagem tridimensional. Você pode considerar um código linear , um que não contenha e nem invoque qualquer função, como um código unidimensional. Um código que começa no início e termina no fim (sic). Nada mais. Agora você acrescenta funções a esse código linear, para remover redundância de codificação, e dá nomes a essas porções de código, identificando assim as funções. Isso é código bidimensional. Agora vamos acrescentar uma terceira dimensão a tudo isso agrupando as funções e os dados em classes para que o código fique ainda mais organizado. A hierarquia de classes criada pela herança de classes estabelece a terceira dimensão. Da mesma forma que pilotar um avião é mais difícil que dirigir um carro, porque voar acrescenta uma terceira dimensão ao problema de guiar, programação orientada a objeto pode requerer um certo tempo para ser completamente compreendida.

Uma das melhores maneiras para se entender classes e sua importância para você como programador é aprender como e porque o conceito de classe evoluiu. As raízes do conceito de classe nos levam a um tópico denominado abstração de dados.

Imagine que você está olhando uma típica sala de estudantes de computação cheia de alunos escrevendo programas. Imagine que alguns desses estudantes são alunos do primeiro semestre do curso de Pascal. Eles já sabem como criar comandos if, loops e matrizes e portanto estão quase prontos a escrever código, mas não sabem ainda como organizar o pensamento. Se você pedir a um deles que crie um programa, ele vai criar um código que funciona de qualquer maneira. Não será uma boa solução, mas provavelmente vai funcionar. Imagine agora que você peça a esses estudantes que criem um programa para executar o jogo cannon. Um jogo em que os jogadores vêm uma bola e um alvo e obstáculos no terreno. A localização do alvo, o terreno e obstáculos mudam de jogo para jogo. O objetivo é estabelecer um ângulo de trajetória e uma força a ser aplicada a bola para que esta atinja o alvo sem tocar em qualquer dos obstáculos do terreno.

 

CPPAI03A.gif

Assuma que os dados do terreno existem em um arquivo texto contendo pares de coordenadas. As coordenadas são pontos finais dos segmentos de linhas que definem o terreno. Os estudantes imaginam que precisam ler esse arquivo para poder desenhar o terreno, e ainda manter o arquivo em memória para poder verificar as interseções da trajetória da bola com as coordenadas do terreno, e assim determinar o ponto do terreno onde a bola para. Então o que eles fazem? Declaram uma matriz global para conter as coordenadas, lêem o arquivo e armazenam os dados na matriz, e usam a matriz onde for necessário, em qualquer ponto do programa. O problema com essa abordagem é que a matriz está como que embutida em todo o código. Se uma alteração se fizer necessária, por exemplo em lugar da matriz usar-se uma lista ligada, o programa terá que ser rescrito porque contém referências explícitas para a matriz. De um ponto de vista de produção de programas profissionais essa é uma péssima abordagem, porque as estruturas de dados freqüentemente são alteradas em sistemas de informação reais.

Uma maneira melhor de projetar o programa é usar um tipo de dado abstrato. Nessa abordagem, o programador primeiramente tem que decidir como os dados serão usados. Em nosso exemplo do terreno, o programador poderia pensar "Bem, eu preciso poder carregar coordenadas do terreno, independentemente de onde venham, para desenhar o terreno na tela e para verificar as interseções da trajetória da bola com os obstáculos do terreno". Repare que esta última abordagem abstrai-se da forma como os dados estarão armazenados, não fazendo qualquer menção a matriz ou lista ligada. Então o programador cria uma função para implementar as capacidades de que precisa. As funções poderiam ser denominadas

 

carrega_terreno

desenha_terreno

verifica_intersecoes.

 

Essas funções são usadas ao longo de todo o programa. As funções atuam como uma barreira. Elas ocultam a estrutura dos dados, separando-a do programa. Se mais tarde a estrutura dos dados precisar ser alterada, por exemplo de matriz para uma lista ligada, a maior parte do programa não será afetada. Apenas as funções precisarão ser modificadas. Dessa forma o programador criou um tipo de dado abstrato.

Algumas linguagens formalizam esse conceito. Em Pascal você pode usar um unit, em C você pode usar uma biblioteca para criar um arquivo de compilação em separado que contém a estrutura de dados e as funções que os processam. Você pode determinar que a estrutura de dados seja oculta, de tal maneira que a matriz seja acessada exclusivamente pelas funções internas à unidade.

Mais ainda, a unidade pode ser compilada para ocultar o próprio código. Assim, outros programadores podem chamar as funções através de uma interface pública, mas não podem modificar o código original.

Units em pascal e bibliotecas em C representam um passo nessa cadeia evolucionária. Começam a enfrentar o problema de abstração de dados mas não vão longe o bastante. Funciona, mas com alguns problemas:

O mais importante deles é que não é fácil modificar ou estender as capacidades de uma unit após a compilação.

Esses tipos abstratos não se encaixam muito bem na linguagem original. Sintaticamente são uma confusão, e não aceitam os operadores normais da linguagem. Por exemplo, se você cria um novo tipo de dado para o qual a operação de adição seria natural, não há meios de você usar o sinal + para representar a operação, ao invés disso você tem que criar uma função de soma.

Se você ocultar uma matriz em uma unit você poderá ter apenas uma matriz. Você não pode criar múltiplas instâncias de tipos de dados.

Classes em C++ eliminam essas deficiências

 

 

 

 

3.3 - C++ e abstração de dados Em resposta a esses problemas, linguagens orientadas a objeto como C++ oferece modos fáceis e extensíveis de se implementar abstração de dados. Tudo o que você tem que fazer é mudar o seu enfoque, e passar a pensar em solução de problemas com uma abordagem abstrata. Essa mudança de atitude mental será mais fácil quando você tiver examinado alguns exemplos. Primeiramente você vai tentar pensar em termos de tipos de dados. Quando você criar um novo tipo de dado, você precisa pensar em todas as coisas que pretende fazer ele, e então agrupar todas as funções criadas para lidar especificamente com o tipo de dado. Por exemplo, digamos que você está criando um programa que requer um tipo de dado retângulo, contendo dois pares de coordenadas. Você deveria pensar "o que eu vou precisar fazer com esse tipo de dado?". Você poderia iniciar com as seguintes ações: estabelecer um valor para as coordenadas, verificar sua igualdade com outro retângulo, verificar interseção com outro retângulo e verificar se um determinado ponto está dentro do retângulo. Se você precisa de um dado terreno, você segue o mesmo processo e inicia com funções para carregar os dados do terreno, desenhar o terreno, e assim por diante. Você então agrupa essas funções junto com os dados. Fazer isso para cada tipo de dado que você precisa no programa é a essência de programação orientada a objeto.

A outra técnica usada na abordagem orientada a objeto envolve treinar sua mente para pensar em hierarquia, do mais geral para o mais específico. Por exemplo, quando pensando sobre um objeto terreno, você deve reparar as semelhanças entre essa estrutura de dados e uma lista. Afinal a descrição do terreno é uma lista de coordenadas carregada a partir de um arquivo. Uma lista é um objeto genérico que pode ser usado em vários pontos de vários programas. Assim, você poderia criar uma classe genérica Listae construir o objeto terreno a partir dela. Nós vamos examinar esse processo mais detalhadamente a medida em que vejamos outros exemplos nos próximos tutoriais dessa série.

Classes

 

Digamos que você quer criar um programa Lista de Endereços, que maneje uma lista de nomes e endereços. A primeira ação a tomar no sentido de criar o programa é descrevê-lo em português. Uma boa descrição do programa em português vai ajudá-lo a descobrir os objetos que compõem o programa, por isso é útil quando se projeta programas em C++. A descrição vai ajudá-lo a ver quais os objetos que precisarão ser criados, bem como as funções que deverão ser implementadas para cada objeto. Veja a seguinte descrição típica:Eu quero criar um programa Lista de Endereços. O programa vai manejar uma lista de nomes e de endereços. O usuário do programa poderá adicionar itens à lista, exibir a lista no terminal e ainda procurar itens na lista.

Você pode notar que essa descrição é muito geral, em alto nível. Não menciona nada sobre a interface com o usuário, leitura e armazenamento de informações em disco, verificação de erros, formato dos registros, ou estrutura dos dados. Todos esses detalhes de implementação virão mais tarde. O ponto focal aqui é explorar a descrição apresentada e ver o que ela menciona. A descrição fala de um objeto - uma lista - e de um conjunto de ações sobre esse objeto: adicionar, exibir e procurar. Vamos avançar com a descrição do programa:

A lista pode ser carregada a partir de disco e é armazenada em disco. Quando o programa começar, ele vai carregar a lista e exibir um menu que permite ao usuário selecionar uma das seguintes opções: adicionar, excluir, procurar e encerrar a execução do programa. Quando o usuário selecionar Encerrar, a lista será salva em disco e o programa terminará.

A partir dessa descrição você pode ver que há duas novas ações sobre o objeto Lista: carregar e salvar. Você pode notar também que temos dois novos objetos a desenvolver: o objeto menu e o objeto programa. A descrição menciona duas ações sobre o Menu: apresentar e selecionar. O objeto programa tem, até o momento, três ações: inicializar, apresentar menu, encerrar. O principal aspecto a explorar nessa descrição é o fato de que um programa de aplicação se subdivide em objetos quase que naturalmente. Na medida em você descreve o programa você começa a ver os objetos na descrição. Os objetos são normalmente os substantivos usados na descrição. Você também pode ver naturalmente as ações sobre os objetos, que são normalmente os verbos.

Uma boa técnica para encontrar os objetos que compõem um programa é descrever o programa e então fazer uma lista dos substantivos que aparecem na descrição. Eliminando dessa lista as coisas obviamente externas ao programa, como usuário, terminal, etc. tem-se a lista dos objetos com os quais o programa terá que lidar. Da mesma forma, fazendo uma lista dos verbos que aparecem na descrição tem-se a lista das ações sobre cada um dos objetos.

 

 

 

 

4.2 - Um programa em estilo antigo Comecemos a criação desse programa Lista de Endereços pela implementação em C. Em seguida vamos migrá-lo para C++ pela adição de classes. O código seguinte mostra uma implementação bastante simples de Lista de Endereços usando as funções normais. O programa pode adicionar elementos à lista, apresentar a lista no terminal e procurar um item na lista. A lista é contida em uma matriz global.

#include <iostream.h>

#include <string.h>

 

typedef struct

{

char name[20];

char city [20];

char state[20];

} addrStruct;

 

const int MAX=10;

addrStruct list[MAX];

int numInList;

 

void addName()

{

if (numInList < MAX)

{

cout << "Enter Name: ";

cin >> list[numInList].name;

cout << "Enter City: ";

cin >> list[numInList].city;

cout << "enter State: ";

cin >> list[numInList].state;

numInList++;

}

else

{

cout << "List full\n";

}

}

 

void printOneName(int i)

{

cout << endl;

cout << list.name << endl;

cout << list.city << endl;

cout << list.state << endl;

}

 

void printNames()

{

int i;

 

for (i=0; i < numInList; i++)

printOneName(i);

cout << endl;

}

 

void findName()

{

char s[20];

int i;

int found=0;

 

if (numInList==0)

{

cout << "List empty\n";

}

else

{

cout << "Enter name to find: ";

cin >> s;

for (i=0; i < numInList; i++)

{

if (strcmp(s,list.name)==0)

{

printOneName(i);

found=1;

}

}

if (!found)

cout << "No match\n";

}

}

 

void paintMenu()

{

cout << "Address list Main Menu\n";

cout << " 1 - add to list\n";

cout << " 2 - print list\n";

cout << " 3 - find name\n";

cout << " 4 - quit\n";

cout << "Enter choice: ";

}

 

void main()

{

char choice[10];

int done=0;

numInList=0;

while (!done)

{

paintMenu();

cin >> choice;

switch(choice[0])

{

case '1':

addName();

break;

 

case '2':

printNames();

break;

 

case '3':

findName();

break;

 

case '4':

done=1;

break;

 

default:

cout << "invalid choice.\n";

}

}

}

Esse programa tem estrutura e organização antigas e amplamente conhecidas. As funções são usadas para subdividir o código. Há uma função para cada uma das opções do menu, uma função apresenta o menu, e a função printOneNamecontém um trecho de código redundante usado em dois pontos do programa. Esse programa demonstra os dois principais usos de funções no passado: decomposição/identificação de código e remoção de redundância de codificação. Há um problema fundamental com esse programa: O código está altamente vinculado à matriz global. Como se mostra no diagrama seguinte, a matriz é global e é referenciada diretamente ao longo de todo o programa.

 

CPPAI04A.gif

 

Não há um modo simples de se modificar a solução de matriz para uma outra estrutura sem re-escrever praticamente todo o código. O código que implementa o programa - que contém portanto a solução do problema - não tem porque se preocupar com a organização física da lista em uma matriz. O código para tratar a organização da lista na matriz não deveria estar embutido no programa. Está no lugar errado.A idéia subjacente à abstração de dados é proteger variáveis globais, tais como uma matriz global, da manipulação direta pelos programas de aplicação. Isolando, através de chamadas de funções, as variáveis que implementam fisicamente a lista do restante do programa, nós podemos obter três benefícios:

 

  • É muito mais fácil modificar a implementação da lista no futuro, porque apenas o código que trata especificamente da implementação da lista precisará ser alterado.
  • O programa fica melhor organizado porque os conceitos e regras inerentes à lista ficam separados do programa de aplicação, tanto quanto for possível.
  • O código específico da implementação da lista pode ser usado em outros programas de aplicação.

Em C, você poderia fazer esse programa da seguinte forma:

 

#include <iostream.h>

#include <string.h>

 

typedef struct

{

char name[20];

char city [20];

char state[20];

} addrStruct;

 

//-------- data and functions for the list -------

const int MAX=10;

addrStruct list[MAX];

int numInList;

 

void listInit()

{

numInList=0;

}

 

void listTerminate()

{

}

 

int listFull()

{

if (numInList >=MAX) return 1; else return 0;

}

 

int listEmpty()

{

if (numInList==0) return 1; else return 0;

}

 

int listSize()

{

return numInList;

}

 

int listAdd(addrStruct addr)

{

if (!listFull())

{

list[numInList++]=addr;

return 0; // returns 0 if OK

}

return 1;

}

 

int listGet(addrStruct& addr, int i)

{

if (i < listSize())

{

addr=list;

return 0; // returns 0 if OK

}

return 1;

}

//------------------------------------------------

 

void addName()

{

addrStruct a;

 

if (!listFull())

{

cout << "Enter Name: ";

cin >> a.name;

cout << "Enter City: ";

cin >> a.city;

cout << "enter State: ";

cin >> a.state;

listAdd(a);

}

else

cout << "List full\n";

}

 

void printOneName(addrStruct a)

{

cout << endl;

cout << a.name << endl;

cout << a.city << endl;

cout << a.state << endl;

}

 

void printNames()

{

int i;

addrStruct a;

 

for (i=0; i < listSize(); i++)

{

listGet(a,i);

printOneName(a);

}

cout << endl;

}

 

void findName()

{

char s[20];

int i;

int found=0;

addrStruct a;

 

if (listSize==0)

cout << "List empty\n";

else

{

cout << "Enter name to find: ";

cin >> s;

for (i=0; i < listSize(); i++)

{

listGet(a, i);

if (strcmp(s,a.name)==0)

{

printOneName(a);

found=1;

}

}

if (!found)

cout << "No match\n";

}

}

 

void paintMenu()

{

cout << "Address list Main Menu\n";

cout << " 1 - add to list\n";

cout << " 2 - print list\n";

cout << " 3 - find name\n";

cout << " 4 - quit\n";

cout << "Enter choice: ";

}

 

void main()

{

char choice[10];

int done=0;

listInit();

while (!done)

{

paintMenu();

cin >> choice;

switch(choice[0])

{

case '1':

addName();

break;

case '2':

printNames();

break;

case '3':

findName();

break;

case '4':

done=1;

break;

default: cout << "invalid choice.\n";

}

}

listTerminate();

}

No topo do programa há sete funções, bem como as variáveis usadas para implementar fisicamente a lista. O objetivo dessas funções é proteger completamente, ou encapsular, as variáveis. Usando as funções da lista, é possível fazer tudo o que o programa precisa fazer com a lista, sem usar diretamente qualquer das variáveis reais da implementação da lista. As funções atuam como uma parede entre as variáveis e o programa de aplicação. Com essa estrutura de programa, qualquer alteração na implementação da lista - por exemplo, modificá-la de matriz para lista ligada - não tem qualquer impacto no programa de aplicação, já que apenas as sete funções da lista teriam que ser modificadas. A estrutura desse programa é mostrada a seguir.

 

CPPAI04B.gif

Algumas dessas funções podem parecer irrelevantes, ou dispensáveis. Por exemplo, a função listTerminate não contém realmente qualquer código. Está presente no código prevendo-se futuras necessidades. Se a implementação for alterada de matriz para lista ligada, vamos precisar de uma função para excluir todos os elementos da lista, para evitar retenção de memória não mais utilizada. A função listSize contém apenas uma linha de código, mas se a lista for implementada usando-se uma árvore binária, essa função terá que percorrer toda a árvore recursivamente para efetuar a contagem de seus elementos e, nesse caso, será bem maior que a única linha apresentada no programa acima.

O que estamos fazendo aqui é pensar sobre todas as funções que poderiam ser realmente necessárias para uma implementação genérica da lista, sem nos limitarmos à uma forma de implementação física da lista em particular.

Mesmo que a implementação acima realize bem a missão de isolar a implementação da lista do restante do programa de aplicação, ela tem ainda alguns problemas. Por exemplo, qualquer um pode alterar o programa, passando a utilizar diretamente as variáveis da implementação da lista, como que ignorando a parede de isolamento composta pelas funções da lista. Em outras palavras, não há qualquer obrigatoriedade de se utilizar as funções da lista. Mais ainda, não será muito fácil utilizar-se duas listas em um mesmo programa de aplicação. Todas as funções da lista são dependentes da existência de uma única matriz. Você pode pensar em solucionar esse problema passando a matriz como um parâmetro para as funções, mas essa alternativa vai se mostrar muito confusa. C++ resolve esses problemas com o recurso de classes.

 

 

 

 

4.3 - Definindo uma classe O código seguinte toma os dados e as sete funções da implementação da lista do programa anterior, e os implementa como uma classe C++ e usa essa classe no programa de aplicação.

#include <iostream.h>

#include <string.h>

 

typedef struct

{

char name[20];

char city [20];

char state[20];

} addrStruct;

 

const int MAX = 10;

 

class List

{

addrStruct list[MAX];

int numInList;

public:

List(): numInList(0) // constructor

{

}

~List() // destructor

{

}

int Full()

{

if (numInList >=MAX) return 1; else return 0;

}

int Empty()

{

if (numInList==0) return 1; else return 0;

}

int Size()

{

return numInList;

}

int Add(addrStruct addr)

{

if (!Full())

{

list[numInList++]=addr;

return 0; // returns 0 if OK

}

return 1;

}

int Get(addrStruct& addr, int i)

{

if (i < Size())

{

addr=list;

return 0; // returns 0 if OK

}

return 1;

}

};

//-----------------------------------------------

 

List list;

 

void addName()

{

addrStruct a;

 

if (!list.Full())

{

cout << "Enter Name: ";

cin >> a.name;

cout << "Enter City: ";

cin >> a.city;

cout << "enter State: ";

cin >> a.state;

list.Add(a);

}

else

cout << "List full\n";

}

 

void printOneName(addrStruct a)

{

cout << endl;

cout << a.name << endl;

cout << a.city << endl;

cout << a.state << endl;

}

 

void printNames()

{

int i;

addrStruct a;

 

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

{

list.Get(a,i);

printOneName(a);

}

cout << endl;

}

 

void findName()

{

char s[20];

int i;

int found=0;

addrStruct a;

 

if (list.Size()==0)

cout << "List empty\n";

else

{

cout << "Enter name to find: ";

cin >> s;

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

{

list.Get(a, i);

if (strcmp(s,a.name)==0)

{

printOneName(a);

found=1;

}

}

if (!found)

cout << "No match\n";

}

}

 

void paintMenu()

{

cout << "Address list Main Menu\n";

cout << " 1 - add to list\n";

cout << " 2 - print list\n";

cout << " 3 - find name\n";

cout << " 4 - quit\n";

cout << "Enter choice: ";

}

 

int main()

{

char choice[10];

int done=0;

 

while (!done)

{

paintMenu();

cin >> choice;

switch(choice[0])

{

case '1':

addName();

break;

case '2':

printNames();

break;

case '3':

findName();

break;

case '4':

done=1;

break;

default:

cout << "invalid choice.\n";

}

}

return 0;

// list destroys itself when it goes out of scope.

}

A classe lista está definida próximo ao topo do programa e começa com as palavras class List. Isso é como uma declaração de tipo: a instância real de lista aparece na linhaList list;

Essa linha declara uma variável denominada list do tipo class List.

Repare que a classe List inicia-se de modo muito semelhante a uma estrutura. Ela declara duas variáveis do mesmo modo que se faria em uma declaração de estrutura. Essas variáveis são denominadas dados membro.

Em continuação, a definição da classe contem a palavra public. Essa palavra indica que as funções seguintes poderão ser invocadas por qualquer código que use essa classe. O termo de sentido oposto é private, e é usado quando funções ou dados devem permanecer ocultos dentro da classe, invisíveis a qualquer código que use a classe.

As variáveis e funções definidas dentro de uma classe são, por default, private a menos que você explicitamente as faça public.

Após a definição dos dados membro vem a definição das funções membro. São essas as funções que podem ser aplicadas às instâncias da classe. As primeiras duas funções em nosso exemplo - Liste ~List - tem um significado único. São denominadas construtor e destrutor, respectivamente.

O construtor é chamado automaticamente sempre e quando passa a existir uma instância da classe. Nesse caso, uma instância da classe List passa a existir logo que se inicia o programa porque está declarada como uma variável global, mas nem sempre as instâncias de uma classe são declaradas como variáveis globais. Então, como regra, o construtor é chamado automaticamente quando uma instância da classe passa a existir, e os construtores de pointers são ativados quando new é chamado para o pointer. O construtor tem o mesmo nome da classe:

 

List(): numInList(0) // constructor

{

}

A inicialização do dado membro numInList é única nesse caso. Um outro modo de se fazer isso é

List() // constructor

{

numInList = 0;

}

A primeira forma é, no entanto, mais eficiente em tempo de execução, devido à maneira como o C++ internamente inicializa as classes. A sintaxe, quando usada como mostrada nesse construtor, inicializa o dado membro numInList atribuindo-lhe o valor 0 (zero) e deve ser usada sempre que se inicializa dados membro em um construtor.O destrutor - ~List em nosso exemplo - é chamado automaticamente quando se encerra o escopo dentro do qual a instância da classe foi declarada, que é onde a instância é então excluída. Destrutores são únicos, rigidamente limitados às variáveis da classe, e podem referenciar variáveis da classe em qualquer momento.

A variável list é uma instância da classe List. Se list fosse uma estrutura unidimensional seria declarada de modo semelhante ao que foi declarada em nosso exemplo, e funcionaria da mesma maneira.

A variável list é tão grande quanto o tamanho de seus dados membro. As funções, em realidade, não ocupam qualquer espaço físico nas instâncias da classe. A sintaxe da linguagem apenas permite que sejam declaradas, e usadas, com instâncias da classe, mas não as implementa fisicamente a cada instância da classe.

A instância list é usada ao longo de todo o programa. A cada vez que algo precisa ser feito com list você encontra o nome da instância seguido de um ponto e do nome da função. De novo, essa notação segue a sintaxe usada para estruturas. O ponto significa chame a função membro da classe List para a instância específica list.

Isso pode não fazer sentido imediatamente para você. Ainda assim, tudo ok. O aspecto importante a ser extraído desse exemplo é que tudo o que fizemos foi tomar alguns dados - nesse caso, uma matriz e um inteiro - e as funções necessárias para manipular essas variáveis, e colocamos tudo junto dentro dos limites de uma classe. Agora as variáveis não podem ser acessadas diretamente pelo restante do código, pelo código externo à classe. Devido ao fato de serem membros privados da classe, somente podem ser acessados por funções membro da classe, e não por qualquer outra parte do código, não pertencente a classe. O objeto list - dados e funções se fundem em objeto - podem ser acessados exclusivamente via funções membro.

 

 

 

 

 

4.4 - Um exemplo mais simples O último exemplo talvez tenha sido muito grande. Vamos examinar a classe Stack para revisar alguns conceitos em um exemplo menor.

#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;}

};

 

int main()

{

Stack stack1, stack2;

 

stack1.Push(10);

stack1.Push(20);

stack1.Push(30);

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

stack2=stack1;

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

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

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

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

return 0;

}

Esse programa consiste de duas partes: a classe Stack e a função principal main. A classe define o tipo Stack, e duas instâncias desse tipo são declaradas dentro de main. Cada uma das instâncias vai ter a sua própria cópia dos dados membro stk e top, e a operação sizeof para cada uma delas indicaria exatamente o espaço necessário (204 ou 404 bytes, dependendo do ambiente) alocado para cada uma. Uma classe usa tanto espaço em memória quanto uma estrutura usaria para os mesmos dados membro. Não há acréscimo de memória pela existência de funções membro. A classe contém um construtor, um destrutor e quatro outras funções, e cada uma delas é public. Porque as funções são públicas, elas podem ser chamadas por qualquer instância da classe. O construtor é chamado quando as variáveis de stack são instanciadas, e o destrutor é chamado quando se encerra o escopo em que essas variáveis foram criadas. Dentro da função main, diferentes chamadas são feitas para as outras quatro funções membro da classe, usando o nome da instância seguido por um ponto e pelo nome da função. Por exemplo:

stack1.Push(10);

Essa linha indica que o valor 10 deve ser colocado em stack1. A instância stack1 contém dois itens de dados (stk e top) os quais contem valores. Essa linha significa chame a função Pushpara a estrutura contida em para a estrutura contida em para a estrutura contida em para a estrutura contida em para a estrutura contida em para a estrutura contida em para a estrutura contida em stack1, execute os comandos da função Push, atribuindo o valor 10 para o elemento da matriz e para o inteiro contido dentro de stack1. Há duas Stacks completamente separadas nesse programa: stack1 e stack2. Um comando como stack2.Push(5) significa que 5 deve ser colocado na estrutura stack2.

É interessante examinar o comando de atribuição colocado na metade na função main. Esse comando faz o que faria um comando de atribuição entre duas estruturas: os valores dos dados membro da instância à direita do comando são copiados para os dados membro da instância a esquerda.

stack2 = stack1;

Após a execução do comando, as duas Stacks contem os mesmos valores. Isso normalmente funciona bem, mas se qualquer dos dados membro for um pointer você precisa ter certos cuidados. Vamos ver um bom exemplo desse problema no

Tutorial 7.

 

 

 

 

4.5 - Uma classe retângulo Como você decide o que deve, e o que não deve, ser implementado sob a forma de objeto?Essencialmente o que você faz é tomar em conjunto cada pequeno grupo de elementos de dados interrelacionados que você encontra no programa, anexar algumas funções ao conjunto e definir uma classe. No exemplo anterior - classe Stack, a matriz stk e o inteiro top são os elementos de dados necessários para implementar a pilha. As funções relacionadas a esse pequeno grupo de dados são Push, Pop, Clear e Size. Juntando-se os dados e as funções obtem-se uma classe.

Digamos que você precise armazenar as coordenadas para um retângulo em um de seus programas. Suas variáveis são denominadas x1, y1, x2 e y2. X1 e y1 representam o canto superior esquerdo, e x2 e y2 representam o canto inferior direito. Essas quatro variáveis juntas representam um retângulo. Quais são as funções úteis a implementar junto com essas variáveis? Você precisa inicializar as variáveis (um trabalho perfeito para o construtor), e talvez precise ter meios de encontrar o perímetro e a área do retângulo. A classe poderia se implementada como no exemplo seguinte:

 

class Rect

{

int x1, y1, x2, y2;

public:

Rect(int left=0,int top=0,

int right=0,int bottom=0):

x1(left), y1(top), x2(right), y2(bottom)

{

}

~Rect() {}

int Height() { return (y2-y1); }

int Width() { return (x2-x1); }

int Area() { return Width()*Height(); }

int Perimeter() { return 2*Width()+2*Height();}

};

Se você apenas examinar o programa que estiver desenvolvendo, e tentar identificar cada agrupamento natural de dados, e as funções úteis que manipulam esses grupos de dados, você já estará dando um grande passo na direção de implementar seu programa de modo orientado a objetos

 

 

 

4.6 - Características específicas das classes Vamos rever algumas das características específicas das classes aprendidas neste tutorial.Em primeiro lugar, cada classe tem um construtor e um destrutor. O construtor é chamado quando uma instância da classe passa a existir, e o destrutor é chamado quando a instância é destruída, que normalmente é o ponto do programa em que se encerra o escopo dentro do qual a instância da classe foi criada. O exemplo seguinte poderá ajudá-lo a aprender um pouco mais sobre construtores e destrutores:

 

#include <iostream.h>

 

class Sample

{

int num;

public:

Sample(int i): num(i)

{

cout << "constructor " << num

<< " called" << endl;

}

~Sample()

{

cout << "destructor " << num

<< " called" << endl;}

};

 

int main()

{

Sample *sp;

Sample s(1);

 

cout << "line 1" << endl;

{

Sample temp(2);

cout << "line 2" << endl;

}

cout << "line 3" << endl;

sp = new Sample(3);

cout << "line 4" << endl;

delete sp;

cout << "line 5" << endl;

return 0;

}

Com papel e lápis, siga em sua mesa, passo a passo, a execução desse código e tente predizer o que vai acontecer em uma execução real. Depois execute esse mesmo código com uma ferramenta de debug na modalidade single-etepping e veja o que aconteceDados membro e funções membro podem ser public ou private, dependendo de como tenham sido definidos dentro do programa. A melhor regra, para preservar os benefícios da orientação a objetos, e não usar dados membro public. Um dado membro public pode ser acessado a partir de qualquer ponto do programa, enquanto os dados membro private somente podem ser acessados pelas funções membro da classe. Vamos modificar um pouco a classe Rect para ver o que acontece.

 

class Rect

{

int x1, y1, x2, y2;

public:

Rect(int left=0,int top=0,

int right=0,int bottom=0):

x1(left), y1(top), x2(right), y2(bottom)

{

}

~Rect() {}

private:

int Height() { return (y2-y1); }

int Width() { return (x2-x1); }

public:

int Area() { return Width()*Height(); }

int Perimeter() { return 2*Width()+2*Height();}

};

Agora as funções Width e Heigth são private. Elas podem ser chamadas como mostradas aqui porque Area e Perimeter são funções membro. Mas se você tentar

Rect r;

...

cout << r.Height();

você vai incorrer em um erro de compilação porque Heigth é uma função private.Atribuição entre duas instâncias de uma mesma classe simplesmente copia os dados membro de uma instância para a outra. Por exemplo:

 

Rect r1,r2;

...

r1=r2;

é o mesmo que

r1.x1 = r2.x1;

r1.y1 = r2.y1;

r1.x2 = r2.x2;

r1.y2 = r2.y2;

Finalmente, há dois modos aceitáveis de se especificar funções membro. Os exemplos mostrados anteriormente nesse tutorial representam um dos métodos, denominado funções inline. O código a seguir mostra o segundo método, aplicado na classe Rect:

class Rect

{

int x1, y1, x2, y2;

public:

// the constructor uses default param. See tutor 2

Rect(int left=0,int top=0,

int right=0,int bottom=0);

~Rect();

int Height();

int Width();

int Area();

int Perimeter();

};

Rect::Rect(int left, int top, int right, int bottom):

x1(left), y1(top), x2(right), y2(bottom)

// default values are understood from the prototype

{

} Rect::~Rect()

{

}

 

int Rect::Height()

{

return (x2-x1);

}

 

int Rect::Width()

{

return (y2-y1);

}

 

int Rect::Area()

{

return Width()*Height();

}

 

int Rect::Perimeter()

{

return 2*Width()+2*Height();

}

Essa última forma é normalmente mais fácil de se ler quando as funções da classe são extensas. A notação Rect:: especifica a classe a qual a função pertence. O código de definição da classe contém basicamente os protótipos das funções membro da classe.Há várias outras coisas que você pode fazer quando usa classes, mas o material apresentado aqui contém as lições suficientes para que você crie abstrações de dados simples, e as correspondentes funções para assim definir classes. Agora já podemos iniciar a criação de hierarquia de classes.

 

Créditos :Fernando,google e arnaut.

Link para o comentário
Compartilhar em outros sites

Muito bom, diferente de muito material (lixo) que ronda pela internet. Fala sobre muita coisa que ninguem fala, mas que é bem interessante ( setw() e setfill() :aplauso:).

 

Eu também vi umas comparações entre C e C++. Eu ainda tenho mania de programar num estilo C, e não sou muito bom implementando facilidades do C++ no código (Como funções virtuais, amigas, herança...). Eu também li seu segundo tópico (Falando sobre construtores, virtuais, etc..), foi legal lembrar da época que eu penava para aprender pointers xD.

 

Muito bom mesmo cara, Parabéns. :aplauso::yes::nerd:

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.