Exercícios

  1. Olá mundo!:

    1. Escolha um directório de trabalho e entre dentro desse directório. Por exemplo, pode criar na sua área de trabalho um novo directório IAED_lab1, escrevendo no terminal (a seguir à prompt $) o comando
      $ mkdir ~/IAED_lab1 
      e entrar no directório criado com o comando:
      $ cd ~/IAED_lab1
    2. Utilizando um editor de texto à sua escolha (ex: sublime,  emacs,  vi,  kate, gedit, etc.), crie um ficheiro ola.c  escrevendo por exemplo
      $ subl ola.c &
      e copie o seguinte código, e guarde-o no directório criado:
      #include <stdio.h>

      int main()

         printf("Olá mundo!\n");
         return 0;
      }

    3. Experimente compilar e correr, escrevendo no terminal (no mesmo ponto da árvore de directórios em que criou o ficheiro hello.c):
      $ gcc -ansi -pedantic -Wall ola.c
      (A utilização das opções "-ansi -pedantic -Wall" é sempre recomendada no âmbito desta cadeira.)
      Foi criado um ficheiro executável a.out com o programa que escreveu.
    4. Encontre o ficheiro criado listando o directório corrente com o comando:  
      $ ls -l
    5. Pode agora executar o programa criado:
      $ ./a.out
    6. O executável pode ser nomeado durante a compilação com a opção -o:
      $ gcc -ansi -pedantic -Wall -o ola ola.c
      $ ./ola

    7. Faça o download do ficheiro obfuscated.c e compile com a opção -w (suprime quaisquer avisos ou warnings) o ficheiro obfuscated.c:
      $ gcc -w obfuscated.c
      e execute o resultado:
      $ ./a.out
      Embora o programa esteja correto e produza o mesmo resultado do ola.c é um exemplo exagerado daquilo que deve ser evitado, embora possível, quando se programa na linguagem C.

  2. Compilação e ligação de múltiplos ficheiros:

    1. Faça o download do ficheiro fact.tgz para o seu directório de trabalho.
    2. Descomprima o ficheiro com o comando: 
      $ tar xvfz fact.tgz
      Foi criado um directório denominado fact com o conteúdo do arquivo.   Entre dentro desse directório.
    3. Visualize os ficheiros main.c, fact.hiter.c recurs.c com um editor de texto, e observe:
      • declaração da função main: argumentos e tipo de retorno
      • valores de retorno
      • função do programa
      • includes e variáveis globais
    4. Compile os módulos fonte do programa iterativo (main.c e iter.c) e faça a ligação do código objecto.
      $ gcc -ansi -pedantic -Wall -o iter main.c iter.c
    5. Execute o programa iter e  verifique que este imprime o factorial de 5.
    6. Repita os passos 4 e 5 utilizando a versão recursiva.

  3. Passos intermédios do processo de compilação:

    1. Pré-processamento: fase que antecede a compilação e que executa as directivas iniciadas por #. Por exemplo, no ficheiro main.c serão processadas as directivas include e   define
      $ gcc -E main.c
      O resultado do pré-processamento é enviado para o terminal. (O pré processador pode ser invocado separadamente com o comando cpp)
    2. Compilação: fase que gera código final em assembly. O assembly ainda tem um formato textual, pode ser lido e modificado por um vulgar editor de texto, mas o código gerado já depende do processador, arquitectura e sistema operativo. 
      $ gcc -S iter.c
      Verifique o código assembly gerado no ficheiro iter.s. Compare as variantes do ficheiro iter.s quando utiliza o optimizador (adicionar a opção -O) e a informação para o debugger (adicionar a opção -g).
    3. Montagem ou assemblagem: fase que produz os códigos binários, ficheiros objecto (".o", nada tem a ver com linguagens orientada para objectos), que serão processados pelo CPU. Esta fase é independente da linguagem de alto nível utilizada: C, Pascal, Fortran, etc. 
      $ gcc -c main.c
      Verifique o tipo de ficheiro gerado, com o comando: 
      $ file main.o
      e as dimensões das secções, com o comando: 
      $ size main.o
      e a tabela de símbolos, com o comando: 
      $ nm main.o
    4. Ligação ou Linkagem: fase que produz o ficheiro executável final através da interligação dos vários ficheiros objectos ou de bibliotecas (conjuntos de ficheiros objecto). 
      $ gcc -o factorial main.o iter.s
      Verifique o tipo de ficheiro gerado e as dimensões das secções. Use o comando ldd  para verificar as dependências das bibliotecas dinâmicas. Tente gerar um ficheiro executável com as variantes iterativa e recursiva, simultaneamente, 
      $ gcc main.c iter.c recurs.c
      e verifique que existem duas realizações de factorial com o mesmo nome. Por outro lado, se tentar criar um ficheiro executável apenas com o ficheiro  main.c
      $ gcc main.c
      falta um ficheiro ou biblioteca que forneça uma realização de factorial.
    5. O processo completo (executável estático vs dinâmico): o comando gcc permite, como já pode observar, controlar todo o processo de compilação para a linguagem C. Contudo, pode verificar a execução das diversas fases através da opção  -v
      $ gcc -v -static main.c iter.c
      Neste exemplo, geramos um executável estático, isto é, não depende na execução da existência das bibliotecas dinâmicas. Como contrapartida, o executável final fica muito maior, pois inclui no próprio ficheiro uma cópia de todas as funções utilizadas, como por exemplo o printf. Verifique o tipo, dimensões do ficheiro ( ls -l), dimensões das secções e dependências do ficheiro a.outgerado, face ao executável dinâmico gerado na alínea anterior. Retire a informação simbólica, com o comando strip, e verifique que o executável ficou mais pequeno e que já não é possível saber a posição dos símbolos (comando nm).
    6. Faça o download do ficheiro fact-makefile.zip.  Inspeccione o exemplo de ficheiro "Makefile" com um editor de texto e crie os dois executáveis através do comando  make.

  4. A importância das declarações

    1. Faça o download do ficheiro decl.tgz para o seu directório de trabalho.
    2. Descomprima o ficheiro com o comando: 
      $ tar xvfz decl.tgz
      Foram criados dois directórios denominados sigseg e pot com o conteúdo do arquivo.
    3. Mude para o directório sigseg e compile os dois ficheiros com o comando: 
      $ gcc -Wall -Wextra -ansi -pedantic *.c
      A opção -Wextra obriga o compilador a efectuar ainda mais verificações que numa compilação normal. No entanto, como o erro se deve a uma inconsistência na declaração da variável a em ambos os ficheiros, a variável esta declarada como inteira (x.c) num e cadeia de caracteres (z.c) noutro, o compilador não consegue relacioná-las pois compila cada ficheiro separadamente.
    4. Agora mude para o directório pot e comece por compilar o ficheiro power.c com o comando: 
      $ gcc power.c
      obtém um erro pois não existe a função pot, que está no ficheiro pot.c, logo basta juntá-lo ao comando de compilação:
      $ gcc power.c pot.c
      obtém um erro pois não existe a função pow, que calcula a potência de dois números reais. Esta função encontra-se declarada no ficheiro math.h e o seu código está compilado na biblioteca libm.a que pode ser utilizada com a opção -lm. Na realidade a opção é -l e o m vem do nome da biblioteca. O comando de compilação fica pois:
      $ gcc -w power.c pot.c -lm
      No entanto, ao executar o programa para calcular 2^5=32 obtemos um resultado sem sentido.
      Mais uma vez o erro está na declaração, pelo que necessito de adicionar uma linha com a declaração double pot(double, double); antes da rotina main do ficheiro power.c e voltar a compilar e executar, agora obtendo um valor correcto.

      Como ter de saber de antemão as declarações de todas as rotinas seria fastidioso, as declarações são agrupadas em ficheiros de declaração .h por quem desenvolve as rotinas e incluídas #include por quem as usa. Assim, vamos substituir a declaração anterior double pot(double, double); pela inclusão do ficheiro pot.h no seu lugar: #include "pot.h"
      Podemos compilar e executar o resultado.

  5. Comparação de linguagens e algoritmos

    1. Faça o download do ficheiro langs.tgz para o seu directório de trabalho.
    2. Descomprima o ficheiro com o comando: 
      $ tar xvfz langs.tgz
      Foram criados dois directórios denominados fib e ack com o conteúdo do arquivo.
    3. Mude para o directório fib e compile o ficheiro fibrec.c com o comando: 
      $ gcc -o fibrec -O3 fibrec.c
      A opção -O3 indica que o compilador de C deve gerar código de boa qualidade, mesmo gastando um pouco mais de tempo, embora o resultado não possa ser depurado debug. (Nota: os programas em java .java já estão compilados nos respectivos .class e são independentes da arquitectura. No entanto, caso tenha instalado o ambiente de desenvolvimento java pode compilar com javac fibrec.java. Para executar os programas em java é necessário ter instalado o ambiente de execução jre, já disponível na maioria das instalações.) Para comparar o desempenho das diversas linguagens vamos utilizar o comando time existente nos sistemas unix para determinar o tempo gasto pela aplicação a executar (user time). Primeiro, utilizando a versão recursiva para calcular a série de fibonacci vamos calcular o elemento 35 da série em python, java e C:
      $ time python fibrec.py 35
      $ time java fibrec 35
      $ time ./fibrec 35
      Note que os tempos de execução vão diminuindo consoante a linguagem vai sendo menos interpretada e mais compilada. Caso os tempos sejam muito próximos de zero, pode utilizar termos maiores da série, desde que inferiores a 46.

      Para tornar a execução mais rápida é necessário dispor de um algoritmo mais eficiente. Neste caso, existe uma versão recursiva, significativamente mais rápida. Repetindo o processo para os ficheiros fibiter nas três linguagens, pode-se verificar que os tempos são significativamente melhores. Na realidade as outras linguagens ficam quase ao nível da versão recursiva em C, embora mais lentos que a mesma versão em C. Com uma versão tão eficiente é possível calcular termos da série superiores a 46 (até 92) desde que os inteiros de 32-bits sejam substituídos por inteiros de 64-bits (long) com mais capacidade, tal como presente no ficheiro fiblong.c.

    4. Mude para o directório ack e compile o ficheiro ack.c com o comando: 
      $ gcc -o ack -O3 ack.c
      Tal como no caso anterior pode comprar as três linguagens executando, no caso do C:
      $ time ./ack 3 10
      nas restantes linguagens, o valor 10 pode ter de ser reduzido para que os tempos de execução sejam aceitáveis. O objectivo deste exemplo é exemplificar o crescimento de funções. A função de ackermann, codificada nestes ficheiros, tem um crescimento exponencial, pelo que um pequeno aumento nos argumentos aumenta significativamente o tempo de execução. Sugere-se determinar os tempos de execução quando o segundo parâmetro varia entre 8 e 15, na linguagem C. Caso pretenda testar as outras linguagens, deverá ser mais modesto nos valores utilizados. Valores superiores são ainda demasiado grandes para os computadores actuais.

  6. Criação de projecto usando o Eclipse (Eclipse IDE for C/C++ Developers — procurar por "eclipse-cdt" no software center do ubuntu) (facultativo)

    1. Crie um projecto de nome factorial  (File->New->Project->C Project) e dê um nome ao projecto.  Crie uma directoria “src” se esta não existir e um novo source file (.c) nessa directoria. 
    2. Implemente um dos programas discutidos nas aulas teóricas 1 e 2 (ver slides). 
    3. Altere as opções de compilação no Eclipse por forma a incluir as opções  -ansi -pedantic -Wall    (ver “Properties” do projecto).  Compile e verifique as mensagens de compilação.  Corra o programa e verifique o output na "Console" do eclipse. 
    4. Crie um novo projecto e implemente um programa que calcule o valor do factorial de 5 usando uma implementação iterativa com 3 ficheiros, tal como no exemplo anterior.
    5. Compile e execute o programa. Verifique as mensagens de compilação.
    6. Inspeccione a estrutura do projecto “factorial” através da linha de comandos. Recompile a versão “Debug” na linha de comandos usando o Makefile  criado automaticamente pelo Eclipse (se necessário, faça "make clean", seguido de "make") e corra o programa na linha de comandos.
    7. Altere o programa de forma a que o valor a calcular seja inserido pelo utilizador.

  7. Depuração de um programa usando o Eclipse (facultativo) 
    1. Defina um ponto de quebra (breakpoint) no seu programa no início da função que calcula o valor do factorial (double click na barra lateral ou Ctrl+Shift+b na linha desejada).
    2. Recorrendo à perspectiva “Debug”, execute o programa passo a passo a partir do breakpoint. Verifique o valor das variáveis a cada passo da execução do programa (F5=next step).