Linguagens e Ambientes de Programação (2017/2018)



Teórica 16 (03/mai/2018)

Bibliotecas. Biblioteca padrão do C.
Tratamento de erros.
Funções com número variável de argumentos.
Input/Output.
Strings.
Output formatado.
Input formatado.

Funções em C.
O qualificador de tipo const.
Portabilidade - Independência da máquina.
Falhas de proteção da linguagem C.



Bibliotecas

Em diferentes linguagens de programação, existem diferentes filosofias relativamente à inclusão de funcionalidades específicas na linguagem.

Há linguagens de programação onde as funcionalidades específicas para manipular strings, ficheiros, etc. estão disponíveis ao nível da própria linguagem e não em bibliotecas externas. São os casos das linguagens: Fortran, Cobol e Pascal, por exemplo.

Pelo contrário, noutra linguagens essas funcionalidades estão disponíveis em bibliotecas externas e não ao nível da linguagem. São o caso das linguagens: Java, OCaml, C, C++, etc.

Esta aula vai ser integralmente dedicada a estudar algumas das funcionalidades da linguagem C que foram remetidas para a sua biblioteca.

Biblioteca padrão

No caso do C, existe uma pequena biblioteca padronizada, conhecida por libc.

A funcionalidade da biblioteca padrão está disponível através de um pouco mais de duas dezenas ficheiros de interface, dos quais destacamos os seguintes: <ctype.h>, <limits.h>, <locale.h>, <math.h>, <setjmp.h>. <signal.h>, <stdarg.h>, <stdbool.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>, <thread.h>.

A aula de hoje envolve temas que permitem a exploração de parte das funcionalidades que estão disponíveis na biblioteca padrão do C.

Outras bibliotecas

A biblioteca padrão do C é muito útil, mas é relativamente limitada (mais limitada do que a do Java, por exemplo). Por isso alguns programas em C costumam recorrer a outras bibliotecas (tipicamente bibliotecas de código livre), tais como: Num sistema Linux observe o conteúdo das diretorias /lib e especialmente /usr/lib para ver o enorme número de bibliotecas disponível. Os ficheiros de interface correspondentes a todas essas bibliotecas são geralmente guardados na diretoria /usr/include.

Usar bibliotecas

Para usar uma biblioteca é preciso primeiro incluir um ou mais ficheiros de interface no código fonte. Por exemplo:

Depois de escrito o programa, na altura de compilar é preciso indicar quais as bibliotecas a ligar. Usa-se para isso a opção -l do compilador. Exemplo:

Algumas funções de biblioteca

Apresentamos algumas das funções da biblioteca padrão do C. No Linux, o comando man permite obter a documentação sobre qualquer uma destas funções (desde que o pacote "manpages-dev" esteja instalado).

Input/Output

These operations work with text files and binary files, except the last four operations that work only with text files.

Matemática

Strings

Memória


Funções com número variável de argumentos

Vamos adicionar ao módulo LinkedList uma função com número variável de argumentos para criar listas novas. A função tem de ter pelo menos um argumento com nome (neste caso n) com informação sobre os argumentos que se seguem (neste exemplo, n indica o número de argumentos); os argumentos anónimos são representados por .... Pode ser testada usando: Os argumentos anónimos não podem ser validados no ponto da chamada. Se, na chamada, forem passados argumentos a mais, os argumentos em excesso são ignorados pela função. Se forem passados argumentos a menos, ou argumentos de tipo errado alguns argumentos da função conterão valores indefinidos.

Para aprender mais sobre funções com número variável de argumentos, usar o comando man stdarg.


Tratamento de erros

Tratamento de erros gerados ao nível do sistema operativo ou das bibliotecas

Na linguagem C, muitas das chamadas a funções do sistema operativo ou a chamadas de funções de biblioteca retornam um valor especial a informar que um erro ocorreu. Esse valor deve ser testado e medidas apropriadas devem ser tomadas.

Alguns exemplos:

Depois de detetado o erro é possível obter mais informação sobre este usando o módulo da biblioteca padrão errno. Ao incluirmos o ficheiro de interface <errno.h> ganhamos acesso a uma variável inteira chamada errno. Após um erro de sistema, essa variável contém sempre um código que indica qual a razão exata do erro. O valor 0 significa que não há erro. Exemplo: Este esquema de tratamento de erros é bastante mau pois obriga a misturar o tratamento de erros com a lógica normal do programa.

Saltos não-locais

A linguagem C não dispõe de qualquer mecanismo de alto-nível para tratamento de exceções. Contudo a biblioteca padrão contém um módulo chamado setjmp que fornece um mecanismo de saltos não locais que permite tratar exceções a baixo nível.

O módulo setjmp disponibiliza as funções setjmp e longjmp:

Comparando com o OCaml, repare que em C a função longjmp desempenha o mesmo papel da expressão raise. A função setjmp, juntamente com o switch envolvente, desempenha o mesmo papel da expressão try-with.

Tratamento de erros gerados ao nível do hardware

Para apanhar erros do género divisão-por-zero, a aplicação tem de instalar rotinas de serviço para detetar determinadas interrupções geradas pelo hardware. Para isso é necessário usar os serviços do módulo signal da biblioteca padrão.

O seguinte exemplo ilustra a utilização da função signal e apanha todas as interrupções geradas a partir do teclado usando CTRL-C.

É possível fazer um longjmp para fora duma rotina de serviço de interrupções? O padrão não dá garantias sobre o assunto e geralmente não funciona mesmo.

Em muitas implementações de C (e.g. GCC) o módulo setjmp inclui funções extra (sigsetjmp e siglongjmp) que permitem fazer isso. Mas esta funcionalidade faz parte do padrão da biblioteca C. Faz sim parte do padrão POSIX para bibliotecas de acesso aos serviços do sistema operativo.


Input/Output

Exemplo 1 - Cópia de ficheiro (de texto ou binário) byte a byte.

Note que estamos a abrir os dois ficheiros usando um modo "b" especial que indica processamento de dados binários e não de texto. Este "b" é ignorado em diversos sistemas operativos, como no Unix, mas convém acrescentar o "b" porque há sistemas operativos que o requerem.

Exemplo 2 - Cópia de ficheiro de texto linha a linha.

Repare que aqui deixámos de usar o "b".

Exemplo 3 - Cópia de ficheiro (de texto ou binário) em blocos de 1024 bytes.

Aqui usa-se o "b".


Strings

Na linguagem C, as strings são simples vetores de carateres e cada string é terminada pelo caráter nulo, que se escreve '\0'. Na seguinte inicialização de variável, a string tem um comprimento nominal de 3, mas internamente ocupa 4 bytes, por causa do terminador. O facto das strings terem todas uma marca de fim, faz com elas sejam muito flexíveis e práticas de usar. A sua flexibilidade é equivalente à dos vetores acompanhados, pois o programa pode controlar o comprimento duma string.

Funções sobre strings

Para exemplificar a manipulação de strings, eis uma função que conta o número de ocorrências dum caráter numa string. Examine com atenção o ciclo for, porque este é típico das funções que manipulam strings. A chamada count("hello",'l') produz o resultado 2.

A seguinte função acrescente um caráter no final duma string, assumindo que há espaço na string:

Eis outra forma de programar as mesmas duas funções, desta vez usando apontadores em vez de indexação:

A biblioteca padrão 'string'

A seguinte diretiva no início do nosso programa permite ganhar acesso a numerosas funções predefinidas de manipulação de strings. Eis as funções de biblioteca mais usadas: O seguinte código copia uma string: Este código tem o mesmo efeito:

Output formatado

O output formatado em C é gerado usando funções da família printf. A função original escreve o output no canal de saída padrão, mas há uma variante que escreve num ficheiro de saída qualquer e outra variante que escreve numa string. Estas funções aceitam um número variável de argumentos. A string de formatação é quase toda copiada literalmente para o output. A exceção são os especificadores de formato, começados pelo caráter especial '%', que provocam a escrita dos parâmetros que estiverem colocados após a strings de formatação. Eis um exemplo, seguido do respetivo output: A função printf retorna o número de carateres escritos. Em caso de erro retorna um valor negativo.

printf em C++

Em C++ existem outras primitivas de output baseadas no operador "<<", mas a operação printf também está disponível.

printf em Java

A operação printf é tão popular, que foi incorporada na biblioteca do Java. Em Java, os especificadores de formato são ainda em maior número do no C.

printf em OCaml

A operação printf também está disponível na biblioteca do OCaml:

Input formatado

O input formatado em C é lido usando funções da família scanf. A função original lê o input no canal de entrada padrão, mas há uma variante que lê num ficheiro de entrada qualquer e outra variante que lê duma string. Estas funções aceitam um número variável de argumentos. A string de formatação é usada de forma literal (forma exata) para fazer emparelhamento com o input, mas há duas exceções: (1) os carateres brancos (' ', '\t', '\n') emparelham com qualquer sequência de brancos; (2) os especificadores de formato, começados pelo caráter especial '%', causam a leitura de valores para os argumentos que estiverem colocados após a string de formatação.

Note que todos os argumentos que se seguem à string de formatação são argumentos de saída, sendo portanto implementados usando apontadores. Eis um exemplo:

A função scanf retorna o número de parâmetros lidos com sucesso; portanto, em caso de falhanço de emparelhamento, esse valor pode ser inferior ao esperado. Antes do primeiro argumento ter sido lido, caso o input termine subitamente durante emparelhamento que esteja a ser bem sucedido, então é retornado o valor EOF.

scanf em C++

Em C++ existem outras primitivas de input baseadas no operador ">>", mas a operação scanf também está disponível.

scanf não existe em Java

A operação scanf não existe na biblioteca do Java, mas as classes Scanner e Pattern oferecem uma solução ainda mais complete e sofisticada.

scanf em OCaml

A operação scanf também está disponível na biblioteca do OCaml:

Funções em C

Quando se define uma função é preciso indicar o nome da função, o tipo e nome dos argumentos e o tipo do resultado. Um exemplo: As funções podem produzir efeitos laterais para além de calcular um resultado.

No caso extremo duma função que retorne void, ela não produz qualquer resultado e apenas efeitos laterais. Exemplo:

Parâmetros

Nas funções, a linguagem C suporta apenas passagem de parâmetros por valor. Se quisermos definir funções com parâmetros de saída é necessário passar (por valor, claro) apontadores como argumentos (isto já foi estudado na secção sobre apontadores, numa aula passada).

Eis mais um exemplo duma função com parâmetro de saída, em que esse parâmetro é usado de forma sofisticada em diversos pontos do corpo da função - tente perceber bem o exemplo, pois ele está cheio de detalhes subtis e instrutivos:

Já sabemos duma aula anterior que o nome dum vetor é sempre um apontador constante para a sua primeira posição. Este facto determina o que acontece quando se passa um vetor como argumento para uma função. O que realmente é passado é o tal apontador. Logo, um argumento-vetor é um parâmetro de saída (ou se entrada-saída, conforme a forma como for usado).

Outra consequência dos vetores serem simplesmente passados como um apontadores é o facto da função não saber o tamanho do vetor recebido. Por causa disso, o programador geralmente passa o tamanho do vetor num argumento suplementar. Repare que isto até é uma vantagem, pois assim a mesma função consegue processar vetores de diferentes tamanhos.

Antes da norma C89, valores de tipos registo ou tipos união não podiam ser passados diretamente como argumentos de funções; só se permitia passar apontadores para eles.

Validação dos parâmetros das funções

Antes da norma C89, não era feita verificação do número e tipo dos argumentos nas chamadas das funções. O programa compilava e tudo parecia estar bem até o programa começar a correr!?

No C89, a verificação do número e tipo dos argumentos passou a ser possível, embora não obrigatória. Para validar todas as chamadas duma função, o programador precisa de garantir que o cabeçalho dessa função é conhecido nos ponto da chamada. No caso de chamada para a frente ou chamadas a partir de outros módulos, é preciso que apareça o respetivo protótipo (cabeçalho de função, sem corpo) antes da chamada.

No C99, a verificação do número e tipo dos argumentos passou a ser obrigatória, ou seja passou a ser obrigatorio escrever protótipos nas chamadas para a frente ou chamadas a partir de outros módulos.

No seguinte exemplo, se retirarmos o protótipo, o programa deixa de poder ser compilado num compilador que seja estritamente C99:

Funções sem argumentos

As funções com zero argumentos devem ser definidas com a palavra void na posição dos argumentos, tal como na função f anterior. Curiosamente, quando a lista de argumentos é vazia, isso significa que a função pode ser chamada com qualquer número de argumentos, não sendo validada a chamada da função.

Função main

A função main, onde o programa começa a correr, pode ser escrita usando qualquer dos três cabeçalhos seguintes:

Parâmetros funcionais

Usando apontadores para funções, em C é possível escrever funções com parâmetros funcionais. Para adicionar ao módulo LinkedList uma função ListSearch para procurar o primeiro valor da lista com uma propriedade dada, faz-se assim. A propriedade é especificada usando uma função booleana. Pode ser testada assim: Mas note que a possibilidade de passar função por parâmetro não é tão poderosa quanto se poderia esperar, porque em C não existe aninhamento de função, mas só funções globais.

O qualificador de tipo const

O qualificador const pode ser usado nos tipos da linguagem C para indicar que determinadas variáveis ou determinadas zonas de memória não podem ser alterados depois de inicializado.

Há duas vantagens em usar este qualificador:

O qualificador const é de utilização muito flexível. Veja os seguintes exemplos: A leitura destes tipos nem sempre é simples. Veja as regras:

Portabilidade - Independência da máquina

Apesar da possibilidade de usar diretamente os recursos duma máquina, a linguagem C não está comprometida com nenhuma arquitetura particular e até encoraja a escrita de programas portáveis, ou seja programas que funcionam em qualquer máquina.

Mas a portabilidade não se obtém automaticamente e o programador tem de se preocupar com essa questão.

Problema 1: tamanho dos valores

O tamanho dos valores de diversos tipos variam de implementação para implementação e a norma da linguagem especifica apenas valores mínimos. Por exemplo, uma variável de tipo int precisa de ter um mínimo de 16 bits (mas tem muitas vezes 32 bits) e uma variável de tipo long precisa de ter um mínimo de 32 bits (mas tem muitas vezes 64 bits).

Nunca se deve assumir que um inteiro ou um long tem um tamanho particular. Se for preciso conhecer o tamanho, então ele deve ser obtido através da função sizeof.

Para não se ter de pensar no tamanho dos valores, por vezes usam-se os seguintes tipos de biblioteca:

Em todo o caso, sempre que o mínimo de 16 bits for suficiente, deve usar-se o tipo int, pois é normalmente o tipo inteiros mais eficiente da máquina.

O C99 introduziu um módulo com header stdint.h para ajudar nas situações em que precisarmos de inteiros com um número de bits conhecido à partida. Nesse ficheiro estão definidos os tipos int8_t, int16_t, int32_t, int64_t. Se precisarmos dum inteiro com tamanho suficiente para guardar um apontador, podemos usar o tipo intptr_t.

Problema 2: Arrumação dos campos nos registos

A arrumação dos campos num registo é dependente do compilador. Quando se enviam registos através da rede, dum programa para outro, convém implementar um mecanismo de serialização.

Problema 3: ordem dos bytes (Big endian/Little endian)

Os diversos bytes que constituem um inteiro podem ser guardados por duas ordens diferentes, consoante arquitetura do computador. Quando se enviam valores através da rede, convém usar um mecanismo de serialização que leve isso em conta.



Falhas de proteção da linguagem C

A linguagem C tem diversas falhas de proteção relativamente a erros de tipo: A linguagem C também tem diversas falhas de proteção relativamente a erros de lógica básicos:

Ferramentas que ajudam a aumentar a proteção em C

Para detetar problemas, chamar o compilador de C com todos os warnings ligados (cc -Wall) já ajuda unm pouco, mas em geral recomenda-se a utilização duma ferramenta especializada para detetar código duvidoso.

Em 1979 foi criado um verificador chamado lint que passou a ser distribuído com todas as versões do Unix.

Uma versão melhorada, atualmente em uso, chama-se splint:

Uma situação dramática em C e C++ é quando, numa fase adiantada de desenvolvimento, um programa grande começa a rebentar com problemas de acesso à memória. Quando não há a mínima ideia sobre qual possa ser a origem do problema, a ferramenta valgrind pode ser a salvação. Permite executar o programa executável dentro duma máquina virtual que valida todos os acessos à memória, conseguindo detetar: Para usar o Valgring, faça assim: Compile o programa da seguinte forma: e depois corra o programa assim: Exercício: Considere o seguinte programa em C. Este programa não tem erros sintáticos e portanto compila. No entanto tem diversos erros de lógica que os programadores fazem frequentemente.
  • 1 - Depois descubra e explique todos os erros deste programa.

  • 2 - Use a ferramenta Valgrind para descobrir automaticamente os erros anteriores.

    #100