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

Aulas teóricas

Artur Miguel Dias



Teórica 04 (16/out/2017)

A importância dos testes na programação.

Tipos. Tipos inteiros. Tipos reais.
Literais. Operações. Transbordo.
O tipo void e funções void.
Passagem de parâmetros de tipos primitivos.
Expressões. Avaliação de expressões.

A importância dos testes na programação

Quando escrevemos um programa, por vezes ficamos convencidos que o programa está perfeito e que se comporta da maneira pretendida em todas as circunstâncias. Mas um programador experiente sabe que é o contrário que geralmente acontece. A primeira versão do programa geralmente tem falhas e exibe comportamentos errados para determinados valores de entrada válidos.

Devemos ter um olhar crítico sobre da primeira versão do nosso programa. Para sabermos se o programa está correto, na prática precisamos de o testar para diversos valores de entrada.

Os valores usados no teste não devem ser escolhidos ao acaso. Os testes a um programa devem sempre incluir:

Os testes ajudam a descobrir as falhas do programa. Se o programa passar todos os testes, então ganhamos confiança no programa.

Programa que usa valores inteiros - O problema dos azulejos

Enunciado

Desenvolva um programa que determine o número de azulejos necessários para cobrir uma certa superfície retangular com um dado comprimento e uma dada largura. Os azulejos são quadrados com 20 cm de lado. O programa calcula e apresenta o número de azulejos completos que nela cabem e qual a área do retângulo que fica por cobrir.

Solução

Testando o programa

A seguinte tabela ilustra os testes a que o programa anterior deve ser submetido para ganharmos confiança nele. A tabela inclui um caso normal e três casos limite. Exercício: Conceba testes para os programas-exemplo que aparecem nesta mesma página, mais abaixo: ano bissexto, arredondamento, e quadrado.



Tipos

O C é uma linguagem tipificada. Isto significa que cada expressão, variável, cada parâmetro e cada resultado têm um tipo associado. Por exemplo, qual é o tipo da expressão 1+1?

A noção de tipo faz lembrar um pouco a noção de conjunto da matemática, mas é um pouco mais rica. Um tipo é um conjunto de valores equipado com um conjunto de literais (notação para representar valores fixos) e um conjunto de operações.

Por exemplo, consideremos o tipo primitivo int:

A linguagem C tem a capacidade de manipular uma grande variedade de tipos de dados, tanto tipos primitivos - inteiros, reais, carateres, etc. -, como tipos definidos pelo próprio programador.

Hoje, a nossa atenção incidirá nos tipos primitivos.

Tipos de dados primitivos

A linguagem C tem as seguintes quatro grandes famílias de tipos dados primitivos:

Tipos inteiros

Poderá parecer estranho, mas em C existem diversas variedades de tipos inteiros. Os tipos inteiros mais usados na prática são os seguintes três: Estes três tipos diferem essencialmente na magnitude dos valores que se podem representar. Por exemplo, uma variável de tipo int tem capacidade para guardar inteiros que cabem em 4 bytes, mas uma variável de tipo char só tem apenas capacidade para guardar inteiros que caibam num único byte (convém saber que os carateres da norma ASCII, têm códigos que vão de 0 a 127).

Na maioria das linguagens de programação, usam-se tipos distintos, incompatíveis entre si, para representar inteiros, carateres e valores lógicos. Mas na linguagem C optou-se por usar valores inteiros para representar todas estas entidades. Por isso até é possível misturar expressões de diferentes tipos inteiros, sem ter de recorrer a funções de conversão de tipo.

Já trabalhámos bastante com o tipo int. Os tipos char e bool são novidades da aula de hoje.

O tipo bool só foi introduzido na linguagem C um pouco tardiamente, na norma de 1999. Atualmente, para usar booleanos num programa, é preciso escrever a diretiva #include <stdbool.h> nas primeiras linhas do programa. Antes de existir o tipo bool usavam-se os valores 0 e 1 para representar valores lógicos. Em todo o caso, note-se que false e true não são mais do que duas constantes que valem, respetivamente, 0 e 1.

Eis um exemplo dum programa que usa uma função booleana:

Literais

Um literal é uma notação especial para representar um valor concreto numa linguagem de programação.

Exemplos de literais inteiros: 0, 1, 21056, -123.

Exemplos de literais carateres: '\n', '!', 'A', 'Z', 'a', 'z', '_'.

Só há 2 literais booleanos: false, true.

Já sabemos que cada literal caráter é representado por um número inteiro específico: exatamente o inteiro que corresponde ao código interno desse caráter. Por exemplo, o literal 'a' é representado pelo valor 97, numa máquina que use a representação de carateres ASCII.

Esta representação de carateres até é útil quando é preciso fazer contas com carateres. Veja as funções seguintes, todas relacionadas com carateres:

Não interessa de todo memorizar os códigos dos vários carateres. Em todo o caso, tem algum interesse observar a tabela dos códigos ASCII. Repare que os algarismos aparecem todos de seguida, as letras maiúsculas também aparecem todas de seguida, as letras minúsculas também aparecem todas de seguida.

Tabela ASCII

Operações

Neste ponto destacamos apenas as operações aritméticas que estão disponíveis para os tipos inteiros.

Em C, há 5 operações aritméticas sobre inteiros:

A barra / denota a operação de divisão inteira e a percentagem % denota a operação resto da divisão. Exemplo: A divisão inteira arredonda no sentido do zero: se o numerador for positivo arredonda para baixo; se for negativo arredonda para cima.

Transbordo (Overflow)

Qualquer variável tem uma capacidade limitada. O que é que acontece se atribuirmos a uma variável inteira o maior valor possível e, depois, lhe somarmos uma unidade? Podemos experimentar: A saída deste programa é: Explicação: O valor da variável "deu a volta" tal como acontece ao contador dum automóvel que atinge o limite: a variável passou diretamente do maior valor inteiro para o menor valor inteiro.

Quando se escrevem programas é preciso levar em conta a possibilidade de existirem transbordos. Por vezes, alterar a ordem pelas quais se fazem as contas ajuda a controlar o problema. Também podemos tentar usar inteiros com maior capacidade, nomeadamente do tipo long long. Mas nem sempre há solução para este problema e temos de aceitar que os programas não funcionem bem se introduzirmos valores demasiadamente grandes.

Exercício: O que acontece se a função fatorial, definida mais atrás, for chamada com um argumento demasiadamente grande, digamos 30?

Todos os tipos inteiros da linguagem C

Além dos três tipos inteiros já discutidos (int, chat e bool), a linguagem C suporta muitos outros tipos inteiros que não vamos ter necessidade de usar na nossa disciplina.

De forma geral, os vários tipos inteiros diferem entre si nos seguintes aspetos:

A seguinte tabela apresenta todos os tipos inteiros da linguagem C. Os valores indicados nas 3 últimas colunas são os valores típicos usados nas implementações atuais da linguagem C.


Tipos reais

Em C, o tipo real mais usado é o, já nosso conhecido, tipo double.

Literais

Exemplos de literais reais: 3.14159, 12.3e56, -1e-2, 12.0.

Operações

Neste ponto começamos por destacar as operações aritméticas básicas que estão disponíveis para os tipos reais.

Em C, há 4 operações aritméticas básicas sobre reais:

Para além destas 4 operações, existem as funções da biblioteca math, às quais ganhamos acesso escrevendo no programa #include <math.h>.

São muitas as funções disponíveis. A seguinte tabela apresenta cerca de um terço, as mais usadas:

Inexatidão dos reais

Em matemática, qualquer intervalo [a, b] de números reais, com a < b, contém um número infinito de valores. Mas uma variável de tipo double tem um número limitado de bits. Isso obriga a perder precisão. Assim, muitos valores de tipo double são simples aproximações.

Quando se trabalha com números reais, temos de aceitar que as contas e os resultados não são absolutamente exatos. Os dígitos decimais menos significativos têm normalmente um pouquinho de "ruído". Quando se fazem muitas contas com reais, os erros de aproximação vão-se acumulando e o "ruído" aumenta.

No seguinte exemplo "amplifica-se" de propósito o erro minúsculo associado à representação do valor 1.0/3.0 = 0.3333333.... Matematicamente, o programa deveria escrever "iguais", mas na realidade escreve "diferentes":

Quando trabalhamos com números reais temos de tomar em consideração o problema da inexatidão dos reais e em algumas situações evitar usar a operação de igualdade. É melhor testar se o resultado está a uma distância muito pequena, digamos a memos de 0.00000000001, do valor a comparar.

Outra questão que vale a pena referir: as quatro operações básicas dos reais (+ - * /) são mais exatas do que as operações da biblioteca math.h. Por exemplo, para se obter o quadrado dum número real X, a forma certa é X*X e jamais pow(X,2). Se o problema se resolver de forma simples usando as operações aritméticas básicas, preferimos evitar usar as funções de biblioteca.

Transbordo (Overflow)

Uma variável real tem uma capacidade limitada relativamente à magnitude dos valores representáveis. Quando se tenta fazer uma variável real exceder os limites, nessa variável fica um de três valores especiais: O primeiro representa infinito, o segundo representa menos infinito, e o terceiro significa not a number. O valor nan é gerado quando se tenta efetuar uma operação com argumentos inválidos, por exemplo, uma divisão de zero por zero.

Esta forma de tratar o transbordo é melhor do que a usada nos inteiros. O facto dos inteiros "darem a volta" pode causar equívocos, pois o utilizador pode pensar que o resultado está correto. Com os reais esse problema não ocorre pois a resposta "infinito" deixa perfeitamente claro o que aconteceu.

O seguinte programa gera valores crescentes até atingir o valor inf:

Todos os tipos reais da linguagem C

Além do tipo double, já discutido, a linguagem C suporta mais dois tipos de reais. Os vários tipos reais diferem apenas na magnitude dos valores que permitem representar.

A seguinte tabela apresenta todos os tipos reais da linguagem C. Os valores indicados nas última coluna são valores típicos usados em muitas implementação atuais da linguagem C.

Programa que usa valores reais - arredondamento

Enunciado

Desenvolva um programa para arredondar valores reais, de acordo com os seguintes exemplos:

Solução


Tipos complexos

Existem três tipos para representar números complexos: float complex, double complex, e long double complex.

O tipo complex é uma simples abreviatura de double complex.

Sobre os tipos complexos, limitamo-nos a dar um exemplo de utilização. Não vamos usar números complexos na nossa disciplina.


Tipo void

Já dissemos que um tipo representa um conjunto de valores. O tipo void é particularmente curioso pois representa um conjunto vazio de valores.

Não é possível definir variáveis de tipo void.

Mas o tipo void pode aparecer no cabeçalho da definição de funções:

Chama-se função void a qualquer função cujo resultado seja de tipo void.

Uma função que tenha por objetivo escrever apenas informação na consola deve ser uma função void pois, realmente, ela não tem qualquer valor para retornar.

As funções void podem ser consideradas uma grande novidade na nossa cadeira, pois até agora só tínhamos escrito funções para calcular resultados a partir de argumentos. Afinal, também se podem escrever funções para executar ações.

Programa que usa duas funções void - Quadrado

Enunciado

Desenvolva um programa para escrever um quadrado de asteriscos, semelhante ao seguinte:

Solução

Esta solução usa duas funções void: uma para escrever uma linha; a outra para controlar a escrita das várias linhas.

Passagem de parâmetros de tipos primitivos

Nas funções, os parâmetros de tipos primitivos são sempre parâmetros de entrada. Os dados circulam apenas num sentido, concretamente de fora para dentro.

Na linguagem C, os parâmetros de tipos primitivos são tratados como simples variáveis locais que têm apenas a particularidade de serem inicializadas no momento da chamada da função. Se, porventura, dentro duma função se fizer uma atribuição a um desses parâmetro, está-se apenas a alterar a correspondente variável local; nada está a ser alterado no exterior da função.

Se experimentar correr o seguinte programa, verá que ele escreve "6 5", o que prova que o parâmetro v é puramente de entrada.



Expressões

Eis a lista completa dos operadores que podem ser usados em expressões em C:

Operadores

Os operadores lógicos aplicam-se a booleanos e produzem resultados booleanos. Os operadores relacionais aplicam-se a diversos tipos (geralmente tipos numéricos) e produzem resultados booleanos.

Note que em C a atribuição produz um resultado (vimos isso na aula anterior) e por isso é considerada uma expressão. Há linguagens de programação, e.g. Pascal e Fortran, em que a atribuição é uma instrução.

Já trabalhámos com cerca de metade destes operadores. A seu tempo, iremos conhecendo mais alguns...

Precedências e associatividades

Precedências e associatividades dos operadores por ordem decrescente de prioridade: Exercício: Colocar na seguinte expressão todos parêntesis, por forma a explicitar a estrutura (implícita) da expressão: Solução:

Operador ,

O operador de sequenciação ',' é um operador binário, que se usa assim: Esta expressão composta é avaliada assim:

Mas se o valor da subexpressão da esquerda é deitado fora, qual o interesse em usar o operador de sequenciação? Resposta: interessa se a expressão da esquerda executar alguma ação, por exemplo uma atribuição.

O operador ',' é usado na instrução for sempre que a inicialização seja múltipla ou o avanço seja múltiplo. Exemplo:


Avaliação de expressões

Ordem de avaliação

O compilador de C tem a liberdade de rearranjar as expressões por forma a otimizar a eficiência da sua avaliação. Os parêntesis podem mesmo não ser respeitados se a sua mudança de posição não violar nenhuma das regras da álgebra.

O compilador também tem a liberdade de avaliar os argumentos nas chamadas das funções por qualquer ordem, por exemplo da esquerda para a direita ou da direita para a esquerda.

No seguinte exemplo, qualquer das função, f ou g, pode ser executada em primeiro lugar; depende do compilador de C que se estiver a usar.

Como a ordem de avaliação não está geralmente definida e devemos evitar escrever expressões cujas ações e/ou resultados dependam da ordem de avaliação.

A seguinte expressão é perigosa, porque os seus efeitos não estão definidos de forma clara: atribui a i o valor de i antes de ser incrementado, mas qual o valor que no final fica em i? O programador de C evita escrever expressões destas.

Pontos de sequenciação

Mas nem tudo está indefinido na ordem de avaliação de expressões. Temos os pontos de sequenciação para nos ajudar. São pontos dentro das expressões que garantem que as ações das expressões anteriores já foram completamente concretizadas e as ações das expressões que se seguem ainda não foram começaram a ser concretizadas.

Os pontos de sequenciação do C estão associados aos seguintes operadores binários:

Conversões automáticas entre tipos numéricos durante a avaliação de expressões

Os tipos numéricos podem ser livremente misturados em expressões. Quando isso acontece, são efetuadas promoções automáticas de tipo e, mais raramente, despromoções automáticas de tipo.

Vejamos um exemplo simples, para começar:

Nesta expressão estão a ser somados dois valores de tipos diferentes. O valor inteiro 23 é promovido ao valor real 23.0 e só depois a soma é efetuada entre dois valores de tipo double. O resultado final é um valor de tipo double, concretamente 27.56.

As regras gerais de conversão automática de tipo são relativamente simples:

  1. Em primeiro lugar, todos os valores de tipo char e short são convertidos em valores de tipo int; e todos os valores de tipo float são convertidos em valores de tipodouble.
  2. Para cada operador binário com operandos de tipo diferente, o operando de tipo menos importante é convertido para o tipo mais importante, antes de se aplicar a operação binária.
  3. Nas atribuições v = exp, o tipo do valor da expressão da direita é convertido num valor do tipo da esquerda antes de se fazer a atribuição. Muitas vezes isso implica fazer uma despromoção de tipo.
Para efeitos de promoções e despromoções, a hierarquia dos tipos numéricos é a seguinte: Exemplos:

casts - Operadores de conversão explicita de tipo

Em algumas situações raras pode ser necessário usar um cast, ou seja, um operador conversão explicita.

Vejamos um exemplo. No seguinte código, é efetuada uma divisão inteira e o valor de f fica igual a 3.

Para forçar a divisão real, faz-se assim, usando o operador de cast (double): O inteiro i foi convertido num real e o valor final de f fica 3.5.

Outra hipótese é multiplicar pelo número real 1.0 assim:

Outros operadores de cast disponíveis: (int), (unsigned), (long long), etc.

caráter/carateres - léxico de Portugal

Nas palavras caráter/carateres, repare que se usa um acento no singular e que não há acento no plural. Esta é a forma mais simples, sendo válida tanto em Portugal como no Brasil.

Atualmente, também se pode continuar a usar carácter/caracteres (com a letra "c" adicionada) como antes do acordo ortográfico de 1990. Mas esta forma só se aplica a Portugal.

Existe ainda outra forma, só usada no Brasil.

Em todo o caso, repare que a palavras "carateres" não tem, nem nunca teve, acento.

Norma oficial de Portugal (procure a palavra "caráter").



#90