Notas de aula - C


[Home] [Dissertação] [Biba] [Linux] [Conjugue] [br.ispell] [axw3] [uplink]
      Introdução à Linguagem C - notas de aula, por Ricardo Ueda
      ----------------------------------------------------------

      Das seis aulas previstas para o curso de introdução ao C e C++,
      as três primeiras serão dedicadas à linguagem C, e a elas
      referem-se estas notas, que completam a apostila da Intersoft
      "Introducción al Lenguaje C".


      Notas à Introdução
      ------------------

      [pg 1] Em C comentários são colocados entre "/*" e "*/". Na
      construção

        /*
         *
         */

      frequentemente usada na apostila, o que importa, em termos
      sintáticos, é que ela começa com "/*" e termina com "*/".


      [pg 2] Em termos estritos, os tipos da linguagem C são apenas
      char, int, float e double; "unsigned", "long" e "short" são
      modificadores que se aplicam a "int" (os três) ou a "char"
      (apenas "unsigned"). Frequentemente é preferível pensar que
      "char" armazena inteiros (geralmente de 8 bits), e não
      caracteres.

      Em máquinas de 32 bits (a maior parte das estações de trabalho),
      "long" e "int" costumam ser equivalentes (ambas com 32 bits). Em
      máquinas de 16 bits (286s), via de regra "int" é implementado em
      16 bits e "long" em 32.


      [pg 4] (exercício) escreva à direita de cada comando a saída
      produzida por ele:

      printf("%d",6)
      printf("%7.2f",44.193)
      printf("%c",65)
      printf("%d",65)
      printf("%x",65)
      printf("%o",65)


      [pg 5] Numa plataforma Unix, o comando "cc prog.c" compilará o
      programa C armazenado no arquivo prog.c criando um arquivo
      executável chamado a.out


      [pg 8] A definição explícita de EOF como sendo -1 não é uma boa
      idéia. O programador não tem a liberdade de escolher um valor
      para EOF, isso é determinado ou pelo sistema operacional ou pela
      biblioteca C em uso. Ao invés de definir-se EOF deve-se incluir o
      header (veja o ítem 1.9) que contém a definição desse símbolo
      (normalmente stdio.h), ou seja, substituir a linha

      #define EOF (-1)

      por

      #include <stdio.h>


      [pg 17] Em princípio, qualquer sistema operacional que conte com
      um compilador C deve oferecer um conjunto de "headers" (arquivos
      .h que são incluídos pelo preprocessador) específicos desse
      sistema. Na prática, esses arquivos acabam sendo específicos
      tanto do sistema operacional quanto do compilador utilizado,
      principalmente quando se usa MS-DOS. Alguns dos headers mais
      frequentemente utilizados são:

      stdio.h  .. getchar, scanf, fopen, etc.
      fcntl.h  .. open, close, etc.
      math.h   .. sin, atan, etc.

      Sempre que se usam num programa C funções de biblioteca, há
      necessidade de incluir o header específico que contém a
      declaração do protótipo dessa função e eventualmente de macros de
      que ela necessite, SENDO NECESSÁRIO CONSULTAR OS MANUAIS DO
      COMPILADOR UTILIZADO.



      Notas ao capítulo sobre operadores
      ----------------------------------


      [pg 1] A constante 0x48 (última linha da página) significa "48
      hexadecimal", ou seja, 72 decimal. Constantes iniciadas por um
      dígito 0 (zero) subentende-se que estão na base 8, e constantes
      iniciadas por "0x" subentende-se que estão na base 16.


      [pg 3] A letra L após a constante 30 na declaração "long l = 30L"
      especifica que a constante é "lng". Na prática, explicita-se que
      uma constante é long apenas quando ela ultrapassa o valor máximo
      de uma constante int. Por exemplo: uma constante que valha 100000
      deve ser explicitamente declarada long quanto usa-se ints de 16
      bits.


      [pg 3] A nota sobre extensão de sinal no final da página
      significa, na prática, que as atribuições

        c = -1;
        i = c;

      onde c é do tipo char e i do tipo int podem resultar (em i) o
      valor -1 ou o valor 255. Para entender exatamente o que está
      acontecendo aqui é necessário conhecer a notação de complemento
      de dois.


      [pg 4] A atribuição "c = ' 62'" significa que a variável c
      receberá o valor 62 octal. Isso não é um modo muito usual de
      proceder. Melhor seria "c = 062" ou então "c = '\62'".


      [pg 7] A "precedência" indica a ordem em que são executados os
      diferentes operadores. Por exemplo: na expressão 3 + 2 * 5 a
      multiplicação será feita antes da adição. Quando um mesmo
      operador ocorre mais de uma vez numa expresão, é a
      "associatividade" indica a ordem em que eles serão
      executados. Por exemplo, na expressão 3 + 4 + 5 a adição 3 + 4
      será feita em primeiro lugar (associatividade da esquerda para a
      direita).




      Notas ao capítulo sobre controle de fluxo
      -----------------------------------------


      [pg 3] é raro usar-se "goto" em C, não tanto por preconceito (o
      goto tem uma péssima fama em computação), mas por ser
      desnecessário.



      Notas ao capítulo sobre apontadores
      -----------------------------------


      [pg 1] Em C, o uso de apontadores é essencial por ser o único
      modo de se conseguir fazer passagem por referência. Como toda
      passagem de parâmetros em C é feita por valor, o único modo de se
      simular passagem por referência é passar como parâmetro um
      apontador para a variável que se deseja passar por
      referência. Considere por exemplo a função abaixo:

         void incrementa(int *x) {

           ++(*x);
         }

      A chamada "incrementa(&y)" onde y é uma variável inteira
      produzirá o efeito de incrementar y de uma unidade.


      [pg 5] A representação em 16 bits do inteiro 258 é

                     0000 0001  0000 0002

      O armazenamento na memória desses 16 bits exige a alocação de 2
      bytes. Num deles será armazenado os 8 bits mais significativos
      (00000001) e no outro os menos significativos (00000002). Esses
      dois bytes serão dois bytes contíguos na memória, e um deles terá
      um endereço menor (de uma unidade) que o outro. Em algumas
      plataformas, os 8 bits menos significativos são armazenados no
      byte de endereço menor, em outras no byte de endereço maior. Esse
      detalhe deve ser levado em conta quando se pretende escrever
      programas portáveis.


      [pg 6] Um exemplo que pode esclarecer a relação entre apontadores
      e vetores é a construção

        char *s = "bambi";

      frequentemente usada em C. O que ela quer significar? Quando o
      compilador encontra a string "bambi" no programa, ele alocará 6
      bytes para armazenar cada um dos caracteres da string colocando
      um caractere '\0' no final. Ao mesmo tempo, ele irá criar a
      variável s, com tipo apontador de caracter e inicializá-la com o
      endereço do byte que armazena o primeiro "b". A partir daí,
      pode-se executar por exemplo a chamada "mensaje(s)", onde mensaje
      é a função da página 16 da introdução (repare que a declaração
      "char s[]" é equivalente a "char *s").


      [pg 6] (exercício) Escreva uma função que receba como parâmetro
      uma string e devolva o tamanho dela. Note que as strings em C
      habitualmente são terminadas pelo caracter de código 0 (veja a
      página 16 da introdução).


      [pg 7] (exercício) Escreva um programa chamado meunome que
      imprime a mensagem "Muito prazer, " seguida do primeiro argumento
      da linha de comandos.


      [pg 9] Na medida em que um apontador é também uma variável,
      pode-se ter um apontador para um apontador. A declaração "char
      *argv[]" equivale a "char **argv" e significa que argv é um
      apontador de um apontador de char. Lembrando que um apontador de
      char é, num certo sentido, equivalente a uma string, temos que
      argv é um apontador de strings ou equivalentemente um vetor de
      strings, cujas entradas são os argumentos da linha de comandos.


      Notas ao capítulo de acesso a arquivos
      --------------------------------------

      [pg 4] Altere o programa da página 4 para que ele receba na linha
      de comandos dois nomes de arquivos, ao invés de ser chamado com
      redirecionamento de E/S (i.e. para que possamos usá-lo da forma
      "copia a.txt b.txt", como o mesmo efeito de um "COPY A.TXT B.TXT"
      executado da linha de comandos do DOS).

      [pg 6] No MS-DOS, arquivos binários não podem ser acessados
      através da interface de alto nível fopen/fgetc/fputc/fclose, use
      a de baixo nível open/read/write/close no lugar. Por sua vez
      (ainda no MS-DOS), é conveniente acessar arquivos texto através
      da interface de alto nível. Tanto num caso quanto no outro o
      problema é o terminador de arquivos-texto Ctrl-Z do MS-DOS.

      [pg 10] Procure não usar funções como feof, que (na minha
      opinião) dificultam a leitura do programa. Use, no seu lugar, um
      teste equivalente ao daquele do programa da página 4, como no
      exemplo que segue:

            F = fopen("arquivo.txt","r");
            while ((c = getc(F)) != EOF) {
             ...
            }
            fclose(F);


      Apêndice: estruturas
      --------------------

      O uso de estruturas permite organizar melhor os dados manipulados
      por um programa. Exemplo:

        typedef struct {
          float r,i;
        }complex;

        void sum(complex *z,complex *x,complex *y) {
          (*z).r = (*x).r + (*y).r;
          (*z).i = (*x).i + (*y).i;
        }


      Apêndice: compilação em separado
      --------------------------------

      A diretiva #include do preprocessador de macros da linguagem C
      permite que se faça uma modularização "lógica"de um programa:


        +------------------------------+
        | #define N 5                  |
        | int x[N] = {1,2,0,-1,2};     |
        +------------------------------+
                                 var.c

        +------------------------------+
        | int sum(int x,int y) {       |
        |   return(x+y);               |
        | }                            |
        +------------------------------+
                                 sum.c

        +------------------------------+
        | #include <stdio.h>           |
        | #include "var.c"             |
        | #include "sum.c"             |
        | void main() {                |
        |   int i;                     |
        |   for (i=1;i<N;++i)          |
        |     x[i] = sum(x[i-1],x[i]); |
        |   printf("%d\n",x[N-1]);     |
        | }                            |
        +------------------------------+
                                main.c


      Note que #include com aspas (e.g #include "var.c") faz com que o
      arquivo a incluir seja procurado no diretório corrente e, com <>
      (e.g. #include <stdio.h>) nos diretórios de inclusões, que
      dependem da plataforma e do compilador utilizados.

      A compilação de main.c provoca a compilação, de uma só vez, dos
      três arquivos, isto é, de todo o programa. Isso não é uma boa
      idéia. O ideal é que cada um dos arquivos seja independentemente
      compilável, e gere um arquivo objeto intermediário (a
      "linkagem" de todos eles gerará o executável):


           +--------+   C    +----------+
           | var.c  |------->| var.obj  |------>+
           +--------+        +----------+       |
                                                |
           +--------+   C    +----------+       | L  +----------+
           | sum.c  |------->| sum.obj  |------>+--->| main.exe |
           +--------+        +----------+       |    +----------+
                                                |
           +--------+   C    +----------+       |
           | main.c |------->| main.obj |------>+
           +--------+        +----------+


      A vantagem é que uma alteração em sum.c (por exemplo) poderá não
      exigir a recompilação de todo o programa, mas apenas de sum.c A
      viabilização disso exige que um módulo (arquivo) possa conhecer tudo
      (variáveis, macros, tipos, funções) que os outros definem e
      oferecem aos outros. Isso dá origem aos protótipos de funções e
      às declarações de variáveis externas. O modo usual de se fazer
      isso é criar, para cada arquivo .c, um arquivo "header" .h que
      contém os protótipos e declarações de variáveis externas:


        +------------------------------+
        | #define N 5                  |
        | extern int x[];              |
        +------------------------------+
                                 var.h

        +------------------------------+
        | #include "var.h"             |
        | int x[N] = {1,2,0,-1,2};     |
        +------------------------------+
                                 var.c

        +------------------------------+
        | int sum(int x,int y);        |
        +------------------------------+
                                 sum.h

        +------------------------------+
        | .. como antes ..             |
        +------------------------------+
                                 sum.c

        +------------------------------+
        | #include <stdio.h>           |
        | #include "var.h"             |
        | #include "sum.h"             |
        | void main() {                |
        |   .. como antes ..           |
        | }                            |
        +------------------------------+
                                main.c

      Agora cada arquivo deverá ser compilado separadamente e, uma vez
      que todos estejam compilados, os arquivos objeto poderão ser
      linkados para formar o executável. Normalmente trabalha-se com
      alguma ferramenta que conhece os módulos que compõe o programa e
      capaz de analisar, num determinado momento, quais módulos estão
      compilados e quais não, ou quais necessitam ser recompilados em
      virtude de alterações nos fontes posteriores às suas compilações,
      de forma a minimizar o esforço de produção do executável. No
      Unix, essa ferramenta é o "make". No TURBOC, é o "project".

      O uso de #include's produz com certa facilidade os fenômenos da
      dupla inclusão e da circularidade:


        +-------------------+         +-------------------+ 
        | #include "arq.h"  |         | #include "y.h"    | 
        | ...               |         | ...               | 
        +-------------------+         +-------------------+ 
                        x.h                           x.h
                                                            
        +-------------------+         +-------------------+ 
        | #include "arq.h"  |         | #include "x.h     | 
        | #include "x.h"    |         | ...               | 
        | ...               |         +-------------------+ 
        +-------------------+                         y.h
                        y.c


      A compilação, nesses casos, produz erros, principalmente de
      redefinição de tipos. Há basicamente duas maneiras de lidar com
      essas situações. Uma é disciplinar o uso de #include's (exige-se,
      basicamente, que arquivos .h não tenham #include's). A outra
      (mais usual) é usar a diretiva #ifndef da seguinte forma:

        +------------------+
        | #ifndef ARQ      |
        | #define ARQ      |
        | ...              |
        | #endif           |
        +------------------+

      Assim, se na compilação de um módulo o arquivo arq.h for aberto
      para uma segunda compilação, ele simplesmente não será
      recompilado, pois o símbolo ARQ já estará definido.

      Perguntas:

      1. A compilação de um "header" produz código? ("produzir código"
      significa provocar alocação de espaço para variáveis ou gerar
      instruções de máquina correspondentes a comandos da linguagem
      compilada. A compilação de um protótipo, por exemplo, apenas
      informa ao compilador a existência de uma determinada função, do
      seu tipo e dos parâmetros que ela recebe).

      2. Num programa modularizado com "headers", é razoável exitir
      inclusões de arquivos .c?

      3. Das duas estratégias dadas para evitar circularidade e dupla
      inclusão, qual é a que exige (ao menos em princípio) menor tempo
      de compilação?