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

Aulas teóricas

Artur Miguel Dias



Teórica 04 (15/out/2018)

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 básicos.
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 que geralmente acontece é exatamente o contrário. A primeira versão do programa geralmente tem falhas e exibe comportamentos errados para determinados valores de entrada válidos.

Devemos ter um olhar muito crítico sobre da primeira versão do nosso programa. Para sabermos se o programa está correto, precisamos de ser desconfiados e de testar diversos valores de entrada.

Os valores usados no teste não são 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 superfície retangular que tem um dado comprimento e uma dada largura. Os azulejos são quadrados com 20 cm de lado. O programa escreve o número de azulejos completos que cabem no retângulo e qual a área 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, cada 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 básico int:

A linguagem C tem a capacidade de manipular uma grande variedade de tipos de dados, tanto tipos básicos - inteiros, reais, carateres, etc. -, como tipos estruturados. Os valores dos tipos básicos são atómicos; os valores dos tipos estruturados têm estrutura.

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

Tipos de dados básicos

A linguagem C tem as seguintes quatro grandes famílias de tipos dados básicos:

Tipos inteiros

Poderá parecer estranho, mas em C existem muitas variedades de tipos inteiros. Os tipos inteiros mais usados são os seguintes três (e não usaremos outros na nossa disciplina): 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 (de -2147483648 até 2147483647), mas uma variável de tipo char só tem capacidade para guardar inteiros que cabem num único byte (de -128 até 127).

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

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

O tipo bool foi introduzido na linguagem C tardiamente, apenas na norma de 1999. Atualmente, para usar booleanos num programa, é preciso escrever a diretiva

nas primeiras linhas do programa. Antigamente, 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 simplesmente duas constantes que valem, respetivamente, 0 e 1. O include atrás referido define essas duas constantes.

Eis um exemplo dum programa onde aparece uma função booleana, a nossa primeira! Veja com atenção a definição da função e a chamada da função:

Literais inteiros

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

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

Só há 2 literais booleanos: false, true. O primeiro é representado internamente pelo valor 0 e o segundo pelo valor 1

Exemplos de literais carateres: '\n', '!', 'A', 'Z', 'a', 'z', '_'. Na maioria dos casos, trata-se dum símbolo envolvido por duas plicas. Cada literal caráter é internamente representado por um número inteiro específico. Por exemplo, o literal 'a' é representado pelo valor 97, numa máquina que use a representação de carateres ASCII.

Tabela ASCII - uma norma antiga de codificação de dados:

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

Agora um exemplo de leitura e escrita de carateres. Repare no designador de formato %c usado no scanf e printf. Não nos interessa memorizar a tabela ASCII. Em todo o caso, tem interesse reter três factos: (1) os algarismos aparecem todos de seguida; (2) as letras maiúsculas também aparecem todas de seguida; (3) as letras minúsculas também aparecem todas de seguida.

Operações inteiras

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.

Atenção que a operação resto da divisão é muito importante no mundo da programação! Usando essa operação, torna-se possível resolver muitos problemas sobre inteiros usando apenas inteiros, sem ter de passar pelos reais (que, em informática, têm algumas complicações).

Transbordo (Overflow)

Qualquer variável tem uma capacidade limitada. O que acontecerá 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 alguns programas não funcionam bem com valores demasiado grandes.

Exercício: O que acontece se a função factorial, definida mais atrás, for chamada com um argumento demasiado 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 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, com os limites mínimo e máximo dum compilador de C moderno.


Tipos reais

Em C, o tipo real mais usado é o tipo double.

Literais reais

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

Operações reais

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:

Adicionalmente, existem as funções do módulo math da biblioteca. Ganhamos acesso a elas escrevendo no programa

São muitas as funções disponíveis neste módulo. 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 serão absolutamente exatos. Significa que os dígitos decimais menos significativos terão normalmente um pouquinho de "ruído". Cuidado, porque 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 interna do valor 1.0/3.0 = 0.3333333.... O programa decompõe o valor 0.3333333 em 100 partes iguais e depois adiciona as 100 partes. 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 menos 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, evitamos 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, e numa dada altura atinge inf e não sai mais de lá:

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, afastando-se do zero. Exemplos:

Solução


Tipos complexos

Existem três tipos para representar números complexos com diferentes magnitudes:

O tipo complex é uma 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.

Para começar, 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 com resultado de tipo void.

Até agora, temos vindo a definir sempre funções que calculam e retornam um resultado. Então para que serve uma função void, que não calcula qualquer resultado?

Bem, uma função void serve para executar ações! Usando uma função void podemos dar um nome a uma sequência de ações, por exemplo uma sequência de printf.

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: a primeira para escrever uma linha; a segunda para controlar a escrita das várias linhas.

Passagem de parâmetros de tipos básicos

Nas funções, os parâmetros de tipos básicos 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 básicos 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: Curiosamente, a seguinte expressão não necessita de nenhuns parêntesis. Por favor, adicione todos os parêntesis que estão implicitos. 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 ',' é habitualmente 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, 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.

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

Os dois argumentos destes operadores binários são sempre avaliados da esquerda para a direita.

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 tipo double.
  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. Pior 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 oficialmente 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 o plural "carateres" não tem, nem nunca teve, acento.



#