4.1 - Um programa Lista de Endereços
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.
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 printOneName conté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.
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:
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.
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.
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 linha
List 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 - List e ~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:
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.
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:
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 Push 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 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.
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:
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:
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 acontece
Dados 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.
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
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:
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:
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.
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.
Código:
#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[i].name << endl;
cout << list[i].city << endl;
cout << list[i].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[i].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 printOneName conté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.
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:
Código:
#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[i];
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.
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.
Código:
#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[i];
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 linha
List 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 - List e ~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:
Código:
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.
Código:
#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:
Código:
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 Push 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 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.
Código:
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:
Código:
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:
Código:
#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 acontece
Dados 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.
Código:
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
Código:
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:
Código:
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:
Código:
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.