Com um tipo para representar caracteres (char) um para representar números inteiros (int), outros para representar números reais (float e double) e outro ainda para representar sequências de caracteres, poderíamos ser levados a pensar que já não precisaríamos de mais tipos. Isso é em parte verdade pois estes tipos permitem-nos representar toda a informação que queiramos; afinal tudo são números ou caracteres!
Mas se pensarmos melhor rapidamente concluímos que se assim fosse nem sequer precisaríamos destes tipos pois, afinal, eles mais não são do que bits na memória do computador! Ora já vimos que isso não nos interessa pois os bits são demasiado "primitivos", ou seja, são de difícil leitura, compreensão e manipulação. O que os tipos existentes em linguagens como a linguagem C++ nos oferecem é exactamente uma mais fácil leitura, compreensão e manipulação. E isto porquê? Porque se encontram mais próximos daquilo a que estamos habituados a encontrar: números, letras, frases; e não apenas sequências de zeros e uns.
Algumas linguagens contentam-se com estes tipos "fundamentais" (ou fundamentalistas(!)) obrigando o programador a reduzir e a pensar tudo em termos de números, caracteres e sequências de caracteres. Ora a verdade é que a maior parte dos objectos que os programas manipulam não são números e caracteres mas sim coisas muito menos abstractas como pessoas, carros, discos, livros, etc.. Claro que nenhuma linguagem de programação pode prever quais os objectos que cada programador vai necessitar, mas pode, pelo menos, permitir-lhe definir novos tipos que corresponderão muito mais de perto aos objectos que cada programa irá manipular.
|
|
|
|
Os fornecidos pela linguagem | Os definidos pelo utilizador e os já definidos em bibliotecas. |
|
char, int, double,
float, long, char [] |
Ao critério dos utilizadores e dos criadores das
bibliotecas.
Exemplos: istream, ofstream, Empregado, Livro, etc. |
Quadro 1 - Tipos pré-definidos e definidos pelo utilizador. A estes últimos é usual chamar classes e às respectivas variáveis objectos.
Importa notar que é possível definir conjuntos de classes prontas a serem utilizadas. Estes conjuntos de classes denominam-se bibliotecas de classes. É usual encontrar num mesmo "pacote" o compilador de C++ e uma (ou mais) biblioteca(s) de classes. Ainda não existe o standard da linguagem C++ pelo que é frequente encontrar diferentes compiladores acompanhados de diferentes bibliotecas. Por enquanto (Junho de 1997) apenas a biblioteca de input/output se encontra razoavelmente estável o que significa que podemos com bastante segurança acreditar que o código que utiliza streams para input/output irá funcionar na maioria dos compiladores e respectivas bibliotecas.
Muito bonitos estes nomes todos! Mas como é que afinal definimos
os nossos tipos?! Já sabemos que os tipos que a linguagem nos oferece
são abstracções do que realmente se passa na memória
do computador. Sabemos que nesse terrível lugar só existem
bits e que para que nós, pobres programadores consigamos, facilmente
perceber alguma coisa do que lá se passa, foram inventados os tipos.
Dessa forma conseguimos afastarmo-nos dos bits. Ora os tipos que nós
podemos definir mais não fazem do que nos afastar ainda mais dos
tenebrosos bits. E como é que podemos fazer tal proeza? Utilizando
os nossos já conhecidos tipos pré-definidos Assim temos uma
hierarquia de utilização:
|
|
|
Quadro 2 - Hierarquia de tipos.
Importa notar que os tipos pré-definidos podem também utilizar outros tipos pré-definidos. Por exemplo, uma classe Linha e uma classe Triangulo poderiam ambas utilizar uma classe Ponto; cada objecto da classe Linha conteria dois objectos da classe Ponto, enquanto um objecto da classe Triangulo conteria três objectos da classe Ponto. Por outro lado, a classe Ponto conteria duas variáveis de um tipo pré-definido, o tipo double, que corresponderiam às coordenadas do ponto no plano.
struct Aluno { String nome; // considera-se que existe disponível uma classe String int numero; String curso; };Ora agora que já temos um tipo novinho em folha é de esperar que possamos fazer com ele pelo menos algumas das coisas que fazemos com os tipos pré-definidos. Por exemplo, se Aluno é um tipo então podemos definir os respectivos objectos assim como fazemos com os tipos pré-definidos:
Aluno umAluno; int umInteiro;Neste caso definimos um objecto do tipo Aluno e um objecto do tipo int. Mas será que podemos escrevê-lo e lê-lo?
cout << "Indique os dados do aluno: "; cin >> umAluno;
cout << umAluno;Ora de facto, podemos definir objectos da classe Aluno mas infelizmente, por enquanto, não podemos lê-los nem escrevê-los, pelo que as duas últimas linhas de código iriam originar erros de compilação! A razão é muito simples: a definição define a parte estática dos objectos do tipo aluno, ou sejam, os dados destes. Para definir um objecto não é necessária mais informação. Mas para fazer algo com o (ou ao) objecto é necessário haver funções que operem sobre objectos da classe Aluno. Ora, nós ainda não definimos nenhuma dessas funções. Para o nosso exemplo é necessário definir os operadores >> e << para que possamos ler e escrever objectos da classe Aluno. Estes operadores são, na realidade, funções que são chamadas utilizando uma notação infixa e que dão pelo nome de operator<< e operator>>, respectivamente.
Seguidamente apresentam-se as definições destes operadores para a classe Aluno:
ostream& operator<<(ostream& output, const Aluno& umAluno) { output << umAluno.nome << " " << umAluno.numero << " " << umAluno.curso; return output; } istream& operator>>(istream& input, Aluno& umAluno) { input >> umAluno.nome >> umAluno.numero >> umAluno.curso; return input; }Relativamente a estas duas definições deve-se constatar o seguinte:
Estes operadores devolvem o seu primeiro argumento, o qual é uma stream (ostream ou istream). Note que o objecto da classe Aluno é passado por referência e, no primeiro caso, a declaração do parâmetro é acompanhada do modificador const. Por último, o & no tipo a devolver (ostream& ou istream&) resulta da necessidade do operador devolver uma stream.
Os operadores << e >> já se encontram definidos para os tipos pré-definidos. Na verdade, todos os tipos pré-definidos têm associados conjuntos de funções e operadores que operam sobre eles. Basta pensarmos que, por exemplo, para o tipo int alêm de podermos definir variáveis podemos depois operar sobre elas somando, subtraindo, afectando, etc. Ou seja, cada tipo pré-definido é na realidade constituído por duas partes: a parte estática (variáveis e constantes) e a parte dinâmica (funções que operam sobre ele).
O mesmo sucede com os tipos definidos pelo utilizador. Além da parte estática que define a estrutura, existe um conjunto de funções que operam sobre ela. Essas funções são, como seria de esperar, também elas definidas pelo utilizador.
Como a definição das classes utiliza os tipos pré-definidos e/ou nas classes já existentes para sua parte estática (por exemplo a classe Aluno utiliza a classe String e o tipo pré-definido int) tal vai implicar que as funções que operam sobre a classe vão utilizar funções que operam sobre esses mesmos tipos pré-definidos e/ou classes. É o que sucede na classe Aluno. O operador << utiliza na sua definição ao operador << que opera sobre Strings e ao operador << que opera sobre ints, para escrever o valor de um objecto da classe Aluno. Nem outra coisa seria de esperar, se um objecto Aluno é constituído por objectos da classe String e uma variável do tipo int então escrever o valor de um Aluno corresponde a escrever o valor de duas Strings e de um int.
A possibilidade de definir os operadores << e >> para qualquer tipo definido pelo utilizador permite apresentar de forma muito genérica as operações sobre ficheiros.
bool operator==(const Aluno& alunoEsq, const Aluno& alunoDir) { return (alunoEsq.nome == alunoDir.nome) && (alunoEsq.numero == alunoDir.numero) && (alunoEsq.curso == alunoDir.curso); }Mas se pensarmos mais um pouco, facilmente nos apercebemos que esta função está a fazer coisas de mais! Dois alunos com o mesmo número são de certeza o mesmo aluno! Além disso há muitos alunos no mesmo curso e até talvez alguns com o mesmo nome! Ou seja, nem todos os campos (outro nome para servem para distinguir os alunos uns dos outros. Aos campos que servem para distinguir um objecto de outro da mesma classe é usual chamar chave. Os restantes campos são denominados campos de dados e não servem para distinguir os objectos entre si pois diferentes objectos podem conter igual informação num ou mais destes campos. Assim sendo, podemos simplificar a definição do nosso operador:
bool operator==(const Aluno& alunoEsq, const Aluno& alunoDir) { return (alunoEsq.numero == alunoDir.numero); }Quando definimos o operador == é simpático (e coerente) definir também o operador !=:
bool operator!=(const Aluno& alunoEsq, const Aluno& alunoDir) { return (alunoEsq.numero != alunoDir.numero); }Se quisermos considerar uma ordenação de objectos da classe Aluno, necessitamos de uma função que nos diga quando é que um aluno é maior ou menor que outro. Claro que o operador < é um excelente candidato a tal proeza:
bool operator<(const Aluno& alunoEsq, const Aluno& alunoDir) { return (alunoEsq.numero < alunoDir.numero); }Neste caso a ordenação dos alunos seria feita com base no seu número. Claro que poderiamos ordená-los por nome. Nesse caso o nosso operador teria o seguinte aspecto:
bool operator<(const Aluno& alunoEsq, const Aluno& alunoDir) { return (alunoEsq.nome < alunoDir.nome); }Falta-nos ver qual a melhor forma de estrutura o programa quando este utiliza classes ainda que, como no nosso caso, representadas por structs.
Aluno.h |
Aluno.cpp |
#ifndef ALUNO_H #define ALUNO_H #include <iostream.h> struct Aluno { String nome; int numero; String curso; }; ostream& operator<<(ostream& output, const Aluno& umAluno); istream& operator>>(istream& input, Aluno& umAluno); #endif |
#include "Aluno.h" ostream& operator<<(ostream& output, const Aluno& umAluno) { output << umAluno.nome << " "; output << umAluno.numero << " "; output << umAluno.curso; return output; } istream& operator>>(istream& input, Aluno& umAluno) { input >> umAluno.nome; input >> umAluno.numero; input >> umAluno.curso; return input; } |
Portanto, todo o código executável, ou seja, aquele que contem instruções, é colocado num ficheiro com a extensão .cpp. Todo o código não executável, ou seja, aquele que contem protótipos de funções e definições de classes, é colocado em ficheiros com a extensão .h.
Importa notar a presença de três estranhas linhas no ficheiro Aluno.h. Todas elas se iniciam por #. Estas três linhas devem sempre ser incluídas num ficheiro com a extensão .h. Elas correspondem a instruções do pré-processador e nada têm a ver com a linguagem C++. Apenas servem para evitar que um ficheiro .h seja incluído (através da directiva #include) mais do que uma vez num ficheiro com a extensão .cpp, o que originaria erros devido à duplicação de definições. De facto a classe Aluno só pode ser definida uma vez!
A função main é colocada sozinha num ficheiro
com o nome do programa e a extensão .cpp visto tratar-se
de um ficheiro com código executavel:
prog.cpp |
#include <iostream.h> #include "Aluno.h" int main() { Aluno umAluno; cout << "Indique os dados do aluno: "; cin >> umAluno; cout << umAluno; return 0; } |
Por último resta assinalar que existem alternativas à extensão .cpp. Por exemplo: .cc, .cxx, .C. Por vezes é através desta extensão que o compilador determina que o ficheiro contem código C++, razão pela qual esta extensão pode ser muito importante. Por outro lado, é também comum que o compilador inclua uma opção que permite indicar que o ficheiro contem código C++. Nesse caso a extensão não é utilizada pelo compilador, mas ainda assim é boa ideia especificá-la dado que facilita a identificação do tipo de conteúdo do ficheiro.
É também possível encontrar alternativas à extensão .h, nomeadamente: .hpp, .hxx, .H. Esta extensão não é tão relevante dado que os ficheiros .h não são passados para o compilador, mas apenas incluidos pela directiva #include. As razões para a sua utilização prende-se unicamente com uma correcta especificação do tipo de conteúdo do ficheiro.
A definição de tipos em C++ é realizada utilizando classes. Quando a classe C++ apenas contem objectos é usual dar-lhe o nome de struct. Vimos apenas classes que podem ser representadas utilizando structs e que portanto não contêm funções membro.
Podemos redefinir alguns dos operadores já existentes para os tipo pré-definidos de forma a que possam operar sobre os tipos definidos pelo utilizador.
Para cada struct denominada X devem existir dois ficheiro: X.h e X.cpp.
Apenas os ficheiros com a extensão .cpp contêm código executável e como tal são os únicos que devem ser compilados.