Introdução à Programação B (2017/2018)

Aulas teóricas

Artur Miguel Dias



Teórica 05 (23/out/2017)

Novamente a legibilidade dos programas
Tudo sobre ciclos na linguagem C.
Vetores (Arrays) unidimensionais e multidimensionais. Operações sobre vetores.

Novamente a legibilidade dos programas

Novamente, o tema da legibilidade dos programas, agora com mais informação.

Quem escreve um programa tem a responsabilidade de escrever código código legível, ou seja código muito claro, fácil de entender por outras pessoas.

Um programa legível:

Critérios de legibilidade relativos a funções:

Critérios de legibilidade relativos a outros aspetos:

Todo o código que aparece na documentação desta disciplina está escrito com a maior legibilidade possível, com as exceções abaixo, escritas a vermelho.

Eis alguns exemplos de código ilegível, que só cria confusão:

[Adjetivos de reserva: abismal, tétrico, atroz, doloroso, monstruoso, horrendo, medonho, pavoroso, tremendo, hediondo, hórrido, horrífico, horroroso, execrável, tetérrimo, infando, demoníaco, diabólico, mefistofélico, satânico, tenebroso, atro, funesto, horripilante, lôbrego, lúgubre, metuendo, sinistro, torvo, aberrante, repelente, teratológico, caliginoso, terrificante, terrífico, catastrófico, desastre, maldito, socobro, trágico, assustador, dantesco, horrífero, temerso, feiarrão, horrorífico, horrípilo.]

Tudo sobre ciclos na linguagem C

Até ao momento, já usámos a instrução for para escrever principalmente ciclos para executar repetidamente um pedaço de código, um número de vezes conhecido à partida.

Vejamos dois exemplos:

A seguinte função void escreve uma linha com n asteriscos, para um valor n que já é conhecido na altura em que se executa o ciclo:

A seguinte função sem argumentos calcula o valor do somatório : Mas agora, mais complicado, um ciclo que executa repetidamente instruções, um número de vezes que é desconhecido à partida.

A seguinte função determina qual o primeiro primo superior a n (assume-se que a função eh_primo já foi programada):


Partes dum ciclo

Um ciclo tem, tipicamente, quatro elementos:

Exercício: Identifique estes quatro elementos nos três ciclos anteriores.

Chama-se iteração a cada execução das ações dum ciclo.

A escrita de ciclos é por vezes complicada. Por isso é uma fonte de erros muito habitual. A questão da terminação tem de ser bem analisada, para se evitar a escrita de ciclos que terminam no momento errado, ou que nunca terminam (dizem-se ciclos infinitos).


A instrução while

O uso prático duma instrução while obedece à seguinte estrutura típica:

A instrução while permite executar repetidamente o seu corpo, zero ou mais vezes, enquanto uma dada condição se mantiver verdadeira.

Como pode ver, o corpo deste while é uma instrução composta que agrupa instruções que definem ações e ainda um avanço.

A condição de continuação é testada no início de cada iteração: se for falsa à entrada do ciclo, então não se chega a executar qualquer iteração.

Programa que usa um ciclo condicional - escrito usando uma instrução while

Enunciado

Problema da 'Mercearia da Esquina' - Para obter o total recebido, pretende-se um programa que leia uma sequência de valores em cêntimos, um valor por linha, e calcule a soma desses valores. O final da sequência de valores de entrada é assinalado por um valor especial: -1. Repare que não se conhece à partida o número de iterações a executar.

Solução

O ciclo está programado de maneira razoavelmente elegante, mas repare que a leitura dos valores teve de ser feita em dois locais distintos. Assim algum código fica repetido, o que não é muito bom, embora não seja grave.


A instrução do-while

O uso prático duma instrução do-while obedece à seguinte estrutura típica: A instrução do-while permite executar repetidamente o seu corpo, uma ou mais vezes, enquanto uma dada condição se mantiver verdadeira.

Como pode ver, o corpo do do-while é uma sequência de instruções que incluem um avanço no final.

A condição de continuação é testada no final de cada iteração, o que significa que um do-while executa sempre uma iteração, pelo menos.

Eis a função vendas reescrita usando um ciclo do-while:

Agora foi possível escrever a leitura for valores num único ponto do programa, mas há a desvantagem de ter de se testar a condição de continuação em dois sítios diferentes, o que também não é muito bom, embora não seja grave.


A instrução for

Tal como a instrução while, a instrução for permite executar repetidamente uma instrução, zero ou mais vezes, enquanto uma dada condição se mantiver verdadeira

O uso prático duma instrução for obedece à seguinte estrutura típica:

O corpo dum for costuma ser uma instrução composta que agrupa instruções que definem ações. Mas também pode ser uma única instrução (nesse caso as chavetas não são precisas, mas também não faz mal deixá-las ficar).

Tal como num while, a condição dum for é testada no início de cada iteração, o que significa que se a condição for logo falsa à entrada do ciclo, então não se chega a executar qualquer iteração.

A instrução for tem a novidade de integrar na sua sintaxe os quatro elementos que caraterizam um ciclo.

Interessa saber que qualquer instrução for, pode ser traduzida para uma instrução while complicada, assim:

Numa instrução for é permitido omitir quaisquer elementos dos quatro elementos constituintes, e mesmo os quatro ao mesmo tempo. No caso da condição estar omissa, isso significa implicitamente que a condição é true. O seguinte ciclo é infinito, ou seja nunca termina, e não produz qualquer efeito útil (a não ser gerar calor, pois o CPU fica a funcionar a grande velocidade, tentando terminar o ciclo):

Eis a função vendas reescrita, agora usando um ciclo for:

O ciclo está programado de maneira razoavelmente elegante, mas repare que a leitura for valores continua a ser feita em dois locais do programa, tal como no exemplo do while.

Neste ciclo, o avanço consiste em ler o valor seguinte. Mas não é possível colocar todo esse código na terceira componente do for, pelo que o colocámos na zona das ações. A zona do avanço ficou vazia.


Instrução break, para quebrar ciclos

A instrução break pode ser usada dentro de qualquer ciclo, para terminar esse ciclo imediatamente. Há problemas cuja solução fica um pouco mais simples e legível se for programada usando um ciclo com um break dentro. Usando um break podemos escrever ciclos com saída pelo meio.

Eis a função vendas reescrita usando um ciclo for com um break lá dentro:

Pode argumentar-se que esta é a versão mais elegante de todas:


Conclusões

Em C existem diversas instruções que permitem escrever ciclos. Quando se escreve um ciclo, convém decidir sobre qual é a forma que permite obter código mais simples e mais fácil de ler. No exemplo da "Mercearia da Esquina" vimos que a versão mais legível usava um for com um break. Mas a melhor forma de escrever um ciclo varia muito de problema para problema.

Quando se escreve um ciclo, há três opções diferentes possíveis, relativamente ao ponto onde se testa a condição:

  1. Testar a condição no início de cada iteração - para isso usa-se um while ou um for;
  2. Testar a condição no final de cada iteração - para isso usa-se um do-while;
  3. Testar a condição no meio de cada iteração - para isso usa-se um if e um break dentro do ciclo.

Por vezes hesita-se entre escrever um ciclo usando um while ou um for. Se no ciclo estiverem bem identificados uma inicialização e um avanço, normalmente a opção pelo for resulta mais legível. Mas esta não é uma regra rígida e a escolha também pode depender da preferência pessoal.


Exercício

Diga o que escreve cada um dos três ciclos abaixo:

Vetores unidimensionais (arrays unidimensionais)

Muitas vezes precisamos de construir tabelas de elementos, todos eles do mesmo tipo. Por exemplo: A linguagem C oferece um mecanismo específico que responde a este tipo de necessidade: o mecanismo dos vetores.

Definição dum vetor unidimensional

Vamos aprender a definir variáveis de tipo vetor. O vetor que representa a tabela de notas define-se assim: O vetor que representa a tabela de temperaturas define-se assim: Intuitivamente, um vetor de elementos do tipo T consiste num agregado de variáveis do tipo T, que podem ser acedidas individualmente usando um índice inteiro.

A representação dum vetor na memória do computador requer o uso dum bloco contíguo, que em alguns casos é bastante grande. No caso do vetor das notas, com capacidade para 200 elementos, a organização em memória é a seguinte:

Acesso aos elementos dum vetor unidimensional

Cada elemento dum vetor é acedido com base na posição que ocupa nesse vetor, ou seja com base num índice. Por este motivo, diz-se que um vetor é uma estrutura indexada.

No caso de um vetor com as notas finais dos 200 alunos, cada elemento do vetor é uma nota final, sendo 200 a capacidade do vetor. Este vetor é indexado pelos números de 0 a 199. Para referir ao elemento que se encontra na posição 0 escreve-se notas_finais[0], ao elemento que se encontra na posição 1 escreve-se notas_finais[1] e assim sucessivamente, até ao elemento que se encontra na posição 199, que se escreve notas_finais[199].

O acesso a elementos individuais dum vetor faz-se sempre usando uma expressão da forma

O seguinte pedaço de código, calcula a média das notas de todos os alunos, assumindo que o vetor das notas já foi preenchido:

Não esqueçamos o vetor das temperaturas. O seguinte pedaço de código, calcula a média das temperaturas de um ano, assumindo que o vetor das temperaturas já foi preenchido:

Modificação dos elementos dum vetor unidimensional

Para modificar, digamos, a nota do primeiro aluno do curso usa-se normalmente atribuição assim:

A modificação de elementos individuais dum vetor faz-se tipicamente usando uma expressão da forma

Para exemplificar, o seguinte pedaço de código coloca a nota de todos os alunos a vinte!

Também se pode modificar um elemento dum vetor usando uma instrução de leitura, por exemplo:



Vetores multidimensionais (arrays multidimensionais)

E se o nosso programa precisar de lidar: No primeiro caso, podemos pensar numa tabela com 5 posições, onde cada posição tem uma tabela com 30 alunos. No segundo caso, pensamos logo numa tabela de 8 por 8 casas, em que cada casa, ou está vazia (contém o caráter espaço ' '), ou tem uma peça de xadrez (representada por uma letra). Repare que, em ambas as situações, precisamos de usar tabelas com duas dimensões.

A linguagem C permite lidar com vetores unidimensionais, bidimensionais, tridimensionais, etc. Não há limite para o número de dimensões, embora muito raramente seja preciso ultrapassar 3 dimensões.

Definição dum vetor multidimensional

O vetor das notas define-se assim: O tabuleiro de xadrez define-se assim: No caso do vetor das notas, como sabemos com capacidade para 8*30 elementos, a organização em memória é a seguinte:

Acesso aos elementos dum vetor multidimensional

Para aceder a um elemento dum vetor bidimensional é necessário indicar duas posições (índices). Por exemplo, a nota final do primeiro aluno da primeira turma escreve-se notas_finais[0][0]. No caso do vetor notas_finais, o primeiro índice vai de 0 até 6; o segundo índice vai de 0 até 29.

O acesso aos elementos dum vetor faz-se geralmente usando uma expressão da forma

O seguinte pedaço de código, calcula a média das notas de todos os alunos, assumindo que o vetor das notas já foi preenchido. Repare que se usa um ciclo for para percorrer as turmas, e no corpo deste se usa outro ciclo for para percorrer os alunos da turma corrente.

Tecnicamente, um vetor multidimensional não é mais do que um vetor unidimensional cujos elementos são vetores da dimensão inferior. Assim, num vetor multidimensional, também podemos aceder aos vetores de dimensão inferior que constituem o vetor completo. Por exemplo, notas_finais[0] é um vetor que contém as notas finais dos alunos da turma 0.

Modificação dos elementos dum vetor multidimensional

Para modificar, digamos, a nota do primeiro aluno do curso usa-se geralmente uma atribuição assim: notas_finais[0][0] = 20.

A modificação dos elementos dum vetor faz-se tipicamente usando uma expressão da forma

Para exemplificar, o seguinte pedaço de código "limpa" todas as casas dum tabuleiro de xadrez:

Também se pode modificar um elemento dum vetor multidimensional usando uma instrução de leitura, por exemplo:



Passagem de vetores como parâmetro para funções

Há duas particularidades muito importantes respeitantes à passagem de vetores como parâmetro para funções. Para se trabalhar em C com vetores, é preciso realmente assimilar estas duas ideias.
  1. Quando se define um parâmetro de tipo vetor numa função, nunca se deve indicar o tamanho da primeira dimensão, pois as funções do C aceitam vetores em que essa primeira dimensão pode ter um tamanho qualquer. O tamanho da primeira dimensão é normalmente passado num argumento inteiro, ao lado do vetor.

    Esta opção da linguagem C tem a seguinte justificação: assim a função fica mais geral e torna-se útil em mais situações.

  2. Foi dito na aula anterior que todos os parâmetros de tipos primitivos são de entrada. Pelo contrário, os parâmetros de tipo vetor são parâmetros de entrada e saída. Dentro duma função, um parâmetro de tipo vetor representa sempre o vetor original que foi passado (e não uma variável local inicializada). Por isso, quando se faz uma atribuição a uma componente do parâmetro está-se realmente a alterar uma componente do vetor original que foi passado.
Estas duas ideias podem parecer estranhas, mas temos de nos habituar a elas pois, por exemplo, só assim conseguimos compreender o funcionamento dos programas que se seguem:

Programa para ler as notas dos alunos para um vetor unidimensional e calcular a média

Programa para ler as notas dos alunos para um vetor bidimensional e calcular a média



Operações sobre vetores

Inicialização usando literais

Um vetor pode ser inicializado no ponto da definição usando um literal especial chamado lista de inicialização. Consiste numa lista de valores separados por virgulas e enquadrados por chavetas.

O seguinte exemplo mostra a inicialização dum vetor com o número de dias de cada mês:

Quando se define um vetor com uma lista de inicialização, não é obrigatório indicar o tamanho do vetor. Esse tamanho é obtido a partir do tamanho da lista de inicialização:

Se uma lista de inicialização tiver elementos a menos, considera-se que os elementos em falta são implicitamente zero. Na seguinte definição, as três últimas componentes do vetor recebem o valor zero:

No limite, se a lista de inicialização for vazia, como se mostra em baixo, todo o vetor é inicializado a zeros.

Os vetores de carateres podem também ser inicializados de acordo com estas regras, como no seguinte exemplo: Para acabar, eis a inicialização dum vetor bidimensional de carateres:

Inicialização interativa

Neste caso o vetor é inicializado usando valores que são pedidos ao utilizador. Já vimos exemplos de inicializações interativa na secção da passagem de vetores como parâmetro.

Cópia

A seguinte função exemplifica a cópia dum vetor unidimensional de notas para outro vetor do mesmo tipo.

Acumulação

A necessidade de programar operações de acumulação sobre vetores surge com muita frequência. Por exemplo, achar a soma das componentes dum vetor é uma operação de acumulação. Outras operações de acumulação são: achar a média das componentes dum vetor; achar o mínimo das componentes dum vetor. Cada operação de acumulação programa-se separadamente numa função distinta. Já vimos exemplos de operações de acumulação interativa na secção da passagem de vetores como parâmetro.

Busca sequencial

Por vezes precisamos de saber se um dado valor ocorre, e onde ocorre, num vetor. A forma mais simples de detetar o valor em que estamos interessados consiste em percorrer o vetor sequencialmente, a partir do início, em busca do elemento pretendido.

A seguinte função de busca sequencial procura uma dada nota num vetor de notas: retorna o índice da primeira ocorrência dessa nota, mas se tal nota não ocorrer no vetor, então devolve o valor -1 (combinamos que -1 representa falhanço).

Outras operações

Mais adiante, na cadeira, serão discutidas outras operações importantes sobre vetores, tais como busca dicotómica e ordenação.



#70