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

Aulas teóricas

Artur Miguel Dias



Teórica 05 (22/out/2018)

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.

Vantagens dum programa legível:

Eis alguns requisitos de legibilidade que devem ser seguidos e se aplicam à escrita de funções:

Requisitos 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, exceto os exemplos negativos abaixo, escritos a vermelho.

Eis alguns exemplos de código ilegível ou confuso:

[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, desastroso, maldito, socobro, trágico, assustador, dantesco, horrífero, temerso, feiarrão, horrorífico, horrípilo.]

Tudo sobre ciclos na linguagem C

Já conhecemos a instrução for, que corresponde a uma dar formas que existe para representar ciclos. Um ciclo ciclo serve para executar repetidamente um pedaço de código.

Nos ciclos mais simples, o número de repetições a efetuar é conhecido à partida. Vejamos dois exemplos:

A seguinte função void escreve uma linha com n asteriscos. n é uma valor que será conhecido quando a função for ativada.

A próxima função não tem argumentos e calcula o valor do seguinte somatório : Considera-se que um ciclo é mais complicado quando se desconhece à partida o número de repetições.

Exemplo: 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 típico é constituído por quatro elementos: A ordem de execução é a seguinte:
  1. Primeiro executa-se o código de inicialização. Este é executado apenas uma vez, no início.
  2. Depois testa-se a condição de continuação. Se for verdadeira o ciclo continua.
  3. Depois executam-se as ações.
  4. Depois executa-se o avanço.
  5. Regressa-se ao ponto 2.

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

Chama-se iteração a cada execução das ações dum ciclo. Portanto, durante a execução dum ciclo, as iterações sucedem-se.

Normalmente, a escrita de ciclos é complicada e uma habitual fonte de erros. 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 a condição de continuaçã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 while

Enunciado

Problema da 'Mercearia da Esquina' - Para determinar o valor monetário recebido ao longo do dia, 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

Analise bem a função sales, e em particular o ciclo no seu interior. O ciclo está programado de maneira razoavelmente elegante, mas repare que a leitura dos valores teve de ser feita em dois locais distintos. Desta forma, algum código ficou 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 sales 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 caso a condição seja falsa à entrada do ciclo, não se chega a executar qualquer iteração.

A instrução for tem a particularidade simpática 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, da seguinte forma:

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 chegar ao fim da execução):

Eis a função sales 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 consistiria em ler o valor seguinte. Mas não é possível colocar o printf e o scanf 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 sales 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 do programador.


Exercícios

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

Vetores unidimensionais (arrays unidimensionais)

Muitas vezes precisamos de usar tabelas para guardar numerosos valores, 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 das notas de 200 alunos define-se assim:

O vetor que representa a tabela das temperaturas define-se assim: O primeiro vetor guarda valores de tipo inteiro. O segundo vetor guarda valores de tipo real.

Intuitivamente, um vetor de elementos do tipo T consiste num agregado de variáveis do tipo T, as quais podem ser acedidas individualmente usando índices inteiros.

A representação dum vetor na memória do computador requer o uso dum bloco contíguo, que pode ser 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 num índice, que indica uma posição no vetor. Por este motivo, diz-se que um vetor é uma estrutura de dados indexada.

No caso de um vetor com as notas finais dos 200 alunos, os índices variam de 0 a 199. O elemento que se encontra na posição 0 é representado por final_grades[0]; o elemento que se encontra na posição 1 é representado por final_grades[1]; ...; o elemento que se encontra na posição 199 é representado por final_grades[199].

Portanto, 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 uma atribuição assim:

A modificação direta de elementos individuais dum vetor faz-se 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 7*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 dois índices. Por exemplo, a nota final do primeiro aluno da primeira turma escreve-se final_grades[0][0]. No caso do vetor final_grades, o primeiro índice vai de 0 até 6; o segundo índice vai de 0 até 29.

O acesso aos elementos dum vetor bidimensional faz-se 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, final_grades[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: final_grades[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

Nos exemplos de manipulação de vetores, não escrevemos ainda funções completas porque há regras especiais que é preciso aprender primeiro.

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 regras.

  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 segundo argumento, um argumento inteiro, ao lado. [Por vezes, diz-se que os vetores passados por parâmetro são acompanhados, sendo a companhia o parâmetro inteiro que indica o tamanho.

    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. É uma vantagem importante podermos escrever funções que funcionam com vetores de diferentes tamanhos.

  2. Foi mostrado na aula 04 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 regras são úteis e temos de as aproveitar bem nos nossos programas. Para perceber o funcionamento dos programas que se seguem, temos de levar em conta as duas regras.

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

Repare que todos os vetores passados por parâmetro têm companhia. Repare que o vetor lido pela função de interface read_grades fica disponível no exterior.

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

Repare que todos os vetores passados por parâmetro têm companhia. Repare que o vetor lido pela função de interface read_grades fica disponível no exterior.

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: Eis um exemplo de 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 nas duas funções read_grades que aparecem atrás.

Cópia

A seguinte função exemplifica a cópia dum vetor unidimensional de notas para outro vetor do mesmo tipo. Neste exemplo considera-se que o argumento n_grades é a companhia de ambos os vetores.

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 nas duas funções average que aparecem atrás.

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.

Como os índices começam em 0, é habitual convencionar-se que o valor especial -1 representa falhanço.

Outras operações

Mais adiante, noutra aula, serão discutidas mais operações importantes sobre vetores, tais como busca dicotómica e ordenação.



#