paulo1205
(usa Ubuntu)
Enviado em 22/05/2015 - 18:41h
Alguns dos comentários já foram feitos pelo Thihup. De qualquer forma, vou tentar comentar sobre todas as mexidas que eu fiz no seu programa. Vou também apontar alguns erros que permaneceram e pontos que podem ser melhorados.
1) Declaração de main () (abordado pelo Thiago, mas trago algumas observações a mais)
Em C++ para sistemas operacionais de uso geral, como Windows ou Linux,
main () tem sempre de devolver um valor inteiro e, portanto, tem de ser declarada com o tipo de retorno
int . Esse valor inteiro é usado para informar ao sistema, e eventualmente também a outros programas, quão bem-sucedida foi a execução do programa.
Essa é uma comunicação do seu programa para quem o chamou. Existe também comunicação no sentido do chamador para o seu programa. Quem chama o programa pode modificar o comportamento desejado durante a execução através de argumentos passados na forma de texto no momento em que a execução inicia.
Seu programa pode ou não ter interesse em receber e tratar argumentos, e você indica isso na hora em que declara a função
main (), escolhendo uma entre duas opções de parâmetros da função permitidas pelo padrão do C++:
* Se não quiser receber argumentos, deve declarar
main () sem parâmetros, deixando os parênteses vazios (ou, por compatibilidade com C, contendo apenas a palavra-chave
void ).
* Se quiser receber argumentos, deve especificar dois parâmetros na declaração de
main (). O primeiro deve ter o tipo
int e funciona como um contador da quantidade de argumentos (esse argumento geralmente é chamado de
argc , que é uma abreviação de
argument counter , mas a rigor pode ter qualquer nome, desde que o tipo de dados esteja correto). O segundo parâmetro é um array de ponteiros para
char (esse argumento tipicamente é chamado de
argv , que é uma abreviação de
agument vector , mas também pode ter qualquer nome, desde que você respeite o tipo de dados), sendo que cada elemento desse array é uma string contendo a representação textual do argumento correspondente.
Por ter tipo de retorno
int , o programa deve devolver um valor inteiro antes de chegar ao fim da execução de
main (). Por uma convenção compartilhada com a linguagem C, normalmente se assume que a execução bem-sucedida do programa produz um valor de retorno igual a zero. C++ levou essa convenção ao ponto extremo de não exigir que se coloque explicitamente o comando “
return 0; ” na função
main (), assumindo-o implicitamente caso o programador omita um comando
return antes do final da definição da função. (Mas atenção que isso só vale para
main () -- outras funções que você vier a declarar têm sempre de ser explícitas quanto ao valor de retorno.)
2) É bom evitar o uso de system() (especialmente para coisas triviais, como limpar tela ou fazer pausa, e o Thihup também mencionou isso)
A principal razão é que
system () custa caro. Cada chamada a
system ()
1) cria um processo novo;
2) bloqueia a execução do programa original, esperando o processo novo acabar;
3) no processo novo, executa o interpretador de comandos (comumente chamado de
shell ) padrão do sistema (
/bin/sh no mundo UNIX,
CMD.EXE no Windows,
COMMAND.COM no DOS, etc.), passando como argumento desse shell a string especificada como argumento de
system (), para que ele interprete o comando da forma apropriada;
4) o shell, então, interpreta o comando informado;
5) se o comando puder ser totalmente resolvido pelo próprio shell (melhor hipótese), ele o executa e termina; no entanto, num caso geral, esse comando faz uso de utilitários externos ao shell, o que pode demandar a criação de novos subprocessos e a execução de outros programas externos (cada um deles com um fluxo parecido com este que estou descrevendo);
6) quando o shell termina, termina também o processo que havia sido criado no passo 2;
7) o sistema operacional e/ou ambiente de execução informa sobre o final do processo ao programa original, que retoma execução;
8) ainda dentro de
system (), o programa tenta recuperar o código informado como resultado da execução do comando (tal código é justamente aquele que corresponde ao valor de retorno de
main (), discutido acima);
9) dependendo do tipo do sistema operacional, a função
system () interpreta e possivelmente manipula o valor recebido, e retorna para quem a chamou um valor inteiro dependente dessa interpretação/manipulação;
10) você, que chamou
system (), deveria também tratar o valor que ela lhe devolveu.
Além do custo computacional, esse fluxo todo tem várias possibilidades de erro e até mesmo a chance de causar surpresas, executando alguma coisa completamente inesperada. Para ter uma ideia de sobre que surpresas estou falando, imagine que seu programa chame um comando externo, como o
NET do Windows (um utilitário que reside no programa executável
C:\Windows\System32\NET.EXE ) e que serve para um monte de coisas diferentes, como sincronizar relógio, mapear unidades de rede, iniciar e parar serviços do sistema etc.). Se um dia alguém criar um script chamado
NET.CMD , que sirva para fazer outra coisa, e o colocar no mesmo diretório que a sua aplicação (ou num diretório que venha antes de
C:\Windows\System32 na lista especificada pela variável de ambiente
PATH ), o seu programa vai parar de chamar o
NET.EXE e passar a usar o
NET.CMD , sabe-se lá com quais efeitos.
Por fim, além do custo e da maior propensão a erros, ainda existe o fator compatibilidade. Quando você usa
system (), não está só se expondo a ter um programa que funciona no sistema operacional
X e n]ao funciona no
Y . Na verdade, você só sabe que ele vai realizar a função desejada no
seu sistema e
sob condições as que você usou durante a fase de desenvolvimento e testes . Dependendo da função solicitada, pode não funcionar numa máquina praticamente idêntica à sua, ou até mesmo na sua própria máquina, se você alterar algumas condições (por exemplo: usuário diferente, novos valores em variáveis de ambiente, aplicação de atualizações, mudança de permissões de arquivos etc.).
No programa alterado, eu criei as funções
my_pause () e
my_cls (), para substituir, respectivamente,
system("pause") e
system("cls") . No caso de
my_pause (), eu construí uma versão que só usa funções padronizadas e que, portanto, vai funcionar em qualquer máquina, com qualquer sistema operacional (mas que depende de não haver marcas de fim de linha sobrando no buffer de
std::cin , razão pela qual eu espalhei chamadas à função
ignore_till_eol () após vários pontos em que você usava leitura formata; além disso minha função espera que você tecle ENTER, e não qualquer tecla, como o comando PAUSE do MS-DOS ou do Windows).
Já em
my_cls (), eu fiz com que ela não dependa de comandos externos quando compilada num ambiente UNIX ou Unix-like. Não fiz o mesmo para o Windows, mantendo dentro dela uma invocação de
system("cls") , porque não conheço Windows, e preferi gastar tempo com outras coisas em lugar de procurar a função certa na MSDN, Além do mais,
system("cls") serve também para o MS-DOS, cuja solução nativa é diferente da do Windows). Mas você (ou o Thiago, se ele quiser) pode fazê-lo, transformando a solução em algo mais ou menos como segue.
#ifdef __unix__
putp(clear_screen);
#elif defined(_WIN16) || defined(_WIN32) || defined(_WIN64) // Não sei se são esses símbolos!
/* solução nativa do Windows */
#elif defined(__DOS__)
/* solução nativa do MS-DOS, ou a própria system("cls") */
#else
/* para outros sistemas, talvez seja melhor só pular uma meia dúzia de linhas em cout. */
#endif
3) Arrays e acesso a seus elementos
Como lembrou o Thiago, quando você declara arrays (ou vetores) em C e C++, você informa uma quantidade inteira de elementos. Se essa quantidade for
N , os índices inteiros que você pode usar para acesso aos elementos são aqueles que vão de
0 (zero) a
N-1 .
No entanto, o Thiago errou no diagnóstico de que o seu programa original extrapolava o maior valor válido para o índice. Não extrapolava, pois você declarava arrays com três elementos (
N=3 ) e fazia um
for variando de 0 a 2 (
N-1==2 , logo OK).
O problema é que a forma padrão entre os programadores C e C++ de construir um laço de repetição que varia a variável de controle inteira ‘
i ’ entre
0 e
N-1 é usando a condição de controle “
i<N ”, e não explicitamente “
i<=N-1 ”,
especialmente quando tal variável é usada como índice de um array de tamanho N .
Essa familiaridade é desejável quando você leva em consideração que, no mundo real e ao longo de sua carreira como programadora ou analista, outros programadores vão interagir com programas que você fizer e vice-versa.
O próprio fato de o Thiago ter sido induzido ao erro de interpretação só pela forma como você escreveu já é um sinal de como é importante falar a mesma língua que a maioria dos seus colegas programadores.
Além da questão da familiaridade, existe um motivo excelente para preferir “
i<N ” em vez de “
i<=N-1 ”: da primeira forma, você prontamente pega o valor de
N e o usa na comparação; da segunda, você tem de realizar uma subtração antes de comparar. Pior ainda: essa subtração é
repetida para cada iteração do laço de repetição. [Eu sei que você não fez essas subtrações no seu programa, porque usou direto uma constante numérica. Mas antes a tivesse feito! (Veja o porquê na próxima observação).]
No programa alterado, eu mudei os laços de repetição de modo a usar a forma mais comum de construir laços de repetição.
4) Uso de contantes numéricas
Lá em
main (), você declarou três arrays distintos (porém relacionados -- falo mais sobre isso depois), dizendo que eles tinham três elementos cada. Em outras funções, você usou laços de repetição com a variável de controle variando de 0 a 2, e neles percorria os elementos desses desses arrays, passados como argumentos para essas funções através de parâmetros ponteiros.
Quem olha para o código, vê o
3 dentro dos colchetes nas declarações dos arrays e consegue saber o número de elementos em cada array. Só que não sabe por que 3, e não 1, 2, 10 ou 1000000.
Aí a mesma pessoa olha para as outras funções e encontra um
2 numa condição de parada de um laço de repetição. A pessoa precisa ir a fundo no código (tudo bem que, no programa simples em questão, nem é tão fundo assim, mas procure entender a questão de princípio) para compreender que esse
2 é índice de arrays declarados noutra parte do programa, e ainda mais fundo para entender que ele é 2, e não 1, 3, 20 ou 400, porque está ligado àquele
3 que foi usado como tamanho nessas declarações feitas alhures (se você tivesse usado a forma comum de expressar laços de repetição, indicada na observação anterior, haveria pelo menos a coincidência do 3 da declaração com o 3 da condição de controle).
Pior: imagine que você testou e gostou do programa, e agora quer trabalhar com 3000 alunos, em lugar de três. Responda rápido: quantos pontos do seu programa original teriam de ser alterados? E com quais valores?
No programa alterado, eu criei um símbolo constante
MAX_ELEMENTS e o empreguei tanto na declaração quanto nos laços de repetição. A escolha do nome tem o componente “max”, abreviação de
maximum , indicando que se trata de um limite superior, e também “elements”, que indica que a constante está relacionada a objetos que contêm mais de um elemento. Supostamente, essa combinação é suficientemente clara para que alguém, ao ler tão-somente a primeira linha de um laço de repetição na forma
for(i=0; i<MAX_ELEMENTS; i++)
não precise de muito esforço (ou seja, ler todo o interior do bloco controlado, e potencialmente declarações feitas em outras partes do programa) para entender que o intuito desse laço de repetição é varrer sucessivos elementos de um array. Além disso, se eu quiser mudar a quantidade de alunos que o programa é capaz de tratar, eu só preciso mexer num ponto do programa, e todo o restante, que depende do valor alterado, vai permanecer totalmente consistente.
5) Acoplamento (ou falta dele) de informações
Não fiz nenhuma alteração dessa categoria no programa porque você provavelmente ainda não aprendeu alguns dos recursos que seriam envolvidos. Mesmo assim, eu exponho aqui um problema, e muito rapidamente comento sobre caminhos evolutivos para uma solução.
Os arrays
matricula ,
nome e
nota , declarados dentro de
main (), guardam informações relacionadas, mas o fato de eles serem declarados de forma independente a obriga a manualmente garantir a coesão entre eles em em várias partes diferentes do programa.
Se você usasse uma estrutura (
struct ), poderia agregar todos os dados de um aluno num objeto só. Veja o exemplo abaixo.
struct Aluno_t {
int matricula;
string nome;
float nota;
};
Você poderia ter apenas um array com elementos do tipo
Aluno_t (“_t” é um sufixo comumente usado em nomes de tipos definidos pelo usuário). As funções
cadastro () e
consulta () receberiam apenas um ponteiro, em vez de três. O mesmo elemento de um único array teria todas as informações de um dado aluno, e o acesso a cada peça de informação desse aluno pode ser feito por meio da seleção do campo da estrutura de interesse naquele momento.
Mas ainda há mais informação possível de agregar. Seu programa trata o tamanho do(s) array(s) como uma informação global, não como uma propriedade do array. Você poderia criar uma estrutura agregando o array e todas as suas propriedades. Veja um exemplo.
struct DadosEscolares_t {
Aluno_t alunos[MAX_ELEMENTS];
int qt_alunos_cadastrados; // Inicialmente deve valer 0.
};
Você poderia evoluir bastante seu tipo
DadosEscolares_t , especialmente usando ideias e recursos de programação orientada a objetos e programação genérica. Espero que você venha a aprender sobre isso em breve. Na minha opinião, é isso que faz o C++ grandemente superior ao C.
No entanto, boa parte da biblioteca padrão do C++ é dedicada justamente a implementar tipos de dados para trabalhar com coleções de dados. Possivelmente tudo o que você gostaria de fazer com
DadosEscolares_t já está pronto, e você provavelmente teria muito pouco a fazer além de aplicar uma versão melhorada de
Aluno_t a um dos tipos de coleções já existentes na biblioteca padrão.
6) Formatação de strings
Seu programa original estava desenhando as coisas na tela de uma forma meio bagunçada, e usando mais operações de escrita do que necessário.
No programa alterado, eu uso uma só operação de saída para desenhar uma versão rearrumada do menu de opções.
Nunca vi professor que mostrasse isso, então eu mesmo mostro a você que em C e C++ você pode concatenar constantes string (texto entre aspas), fazendo com que elas formem uma só string. No código abaixo, as duas strings contêm o mesmo texto.
char str1[]="P" "a"
"u""lo";
char str2[]="Paulo";
Usei isso também na nora de imprimir os resultados das consultas (uma linha acaba com
"\n" e a linha seguinte continua essa mesma string). Não é erro nem acidente, mas uma forma de representar visualmente no código do programa uma coisa parecida com o que vai sair na tela, mas economizando em número de operações (
cout<<"Pa" "u" "lo" só tem uma string e uma operação de saída;
cout<<"Pa"<<"u"<<"lo" produz a mesma saída, mas com três operações de saída com três strings diferentes).
7) Operações de leitura
A leitura de dados formatada, através do operador
>> aplicado a um stream de entrada de dados (no caso, sempre
std::cin ), muitas vezes é conveniente, mas tem algumas características que você precisa conhecer.
Uma delas é que, quando usado para ler strings, ele não permite o uso de espaços. Você não conseguiria, por exemplo, cadastrar um aluno com nome "José da Silva": a operação de leitura leria somente "José", e passaria adiante, deixando " da Silva" (com um espaço antes da palavra “
da ”) no buffer de leitura. A operação seguinte tentaria ler um valor de ponto flutuante (a nota), pulando o espaço em branco, mas encontrando o caráter
'd' , o que causaria um erro na operação de leitura, e deixaria
std::cin num estado de falha, provocando falha de todas as operações de leitura posteriores (mesmo as de strings e as chamadas de
ignore que eu coloquei no programa).
Eu não tratei todos os possíveis erros de leitura. Fazê-lo adequadamente ou deixaria o código muito poluído ou dependeria de usar exceções, que você provavelmente ainda não aprendeu. Se você deliberadamente digitar uma letra num ponto do programa que espere ler um número, pode acabar com um
loop infinito ou outro efeito deletério qualquer.
No entanto, eu troquei a leitura de nomes para usar
std::getline (), que lê uma linha inteira, permitindo digitar nomes contendo espaços. Junto com isso, eu criei uma função (
ignore_till_eol () (que na verdade é só um apelido para uma chamada a
std::istream::ignore () com argumentos adequados) para remover do buffer de entrada a marca de fim de linha (e todos os caracteres que porventura venham antes dela) que fica retida nesse buffer após a execução da operação de leitura formatada com
std::istream::operator>> (), e espalhei várias dessa funções pelo programa.
“Para não dizer que eu não falei de flores”, eu coloquei um teste explícito do estado da entrada como condição do final do laço de repetição em
main (). Não é grande coisa como garantia de consistência de I/O, pois ainda permite a geração de saída meio espúria como resultado de entrada inadequada, mas é um começo. Coisa parecida poderia ser usada em outros pontos do programa.
8) Uso de switch
Você deve ter visto que eu mudei um bocado os testes feitos dentro de
main (). A principal razão foi colocar uma garantia de que você só poderia realizar operações que dependessem dos valores dos arrays depois de ter lido tais valores.
Em
consulta (), eu não sei se você errou o local onde queria ter usado
break , que você usou para interromper o
for , ou se acertou na interrupção do
for (porque faz mesmo sentido interrompê-lo naquele ponto) mas simplesmente esqueceu o
break do
switch . Mas o fato de não ter interrompido o caso do valor 1 fazia com que o fluxo continuasse por dentro do bloco do caso do valor 2 (ou seja: se você pedisse para procurar por matrícula, ele procurava por matrícula e depois procurava também por nome).
Os casos de um
switch são análogos a
labels usados com o comando
goto . Por isso mesmo, vários livros de C falam em “
case labels ” e “
default label ”. Se você deixar de usar
break , é esperado que o fluxo continue pelos comandos do próximo
label . Essa continuação é chamada de
fall-through , e algumas vezes é desejável.
Por causa desse modo de funcionamento, o último
label dispensa o uso de
break .
default não precisa necessariamente ser o último
label . Se for útil, especialmente se você desejar
fall-through do caso
default , ele pode até ser o primeiro da lista.
Não é necessário colocar o código correspondente a cada caso dentro de um bloco delimitado por chaves. Isso só seria útil se você quisesse declarar alguma variável local, sem que ela aparecesse para outros casos dentro do mesmo
switch .
9) Seleção dos dois maiores valores
Eu dei a solução pronta. Seu código para a segunda maior nota estava certo, mas você errou no para a maior.
Eu acho que o ponto mais importante é a inicialização. Zero não é o menor valor possível de um dado do tipo
float . Dado que existem contextos em que existem notas negativas (por exemplo: concursos em que respostas erradas tiram pontos, para desestimular o cara a chutar a resposta -- se ele errar tentando honestamente acertar, pode ficar com nota negativa), começar com zero pode fazer com que se produza um resultado errado. Por isso mesmo, o melhor é você já começar com um valor que faça parte da sua coleção.