| 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 com funçõ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 |
| 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. |
| 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 |
| 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 Lista
e 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. |