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?