Prefácio
Nos meus
comentários, me referi várias vezes ao livro de Andrew Tanenbaum Operating Systems Design and Implementation, sua
primeira edição, e como C é representado nele. E esses comentários sempre foram de interesse. Decidi que era hora de publicar uma tradução desta introdução em C. Ainda é relevante. Embora certamente haja quem não tenha ouvido falar sobre a linguagem de programação
PL / 1 , e talvez até sobre o sistema operacional
Minix .
Essa descrição também é interessante do ponto de vista histórico e para entender até que ponto a linguagem C foi desde o nascimento e o setor de TI como um todo.
Quero fazer imediatamente uma reserva de que meu segundo idioma é o francês:

Mas isso é compensado por 46 anos de
experiência em programação .
Então, vamos começar, é a vez de Andrew Tanenbaum.
Introdução à linguagem C (pp. 350 - 362)
A linguagem de programação C foi criada por Dennis Ritchie, da AT&T Bell Laboratories, como uma linguagem de programação de alto nível para o desenvolvimento do sistema operacional UNIX. Atualmente, o idioma é amplamente utilizado em vários campos. C é especialmente popular entre os programadores de sistemas, pois permite escrever programas de maneira simples e concisa.
O livro principal que descreve a linguagem C é o livro de Brian Kernigan e Dennis Ritchie, The C Programming Language (1978). Os livros na língua C foram escritos por Bolon (1986), Gehani (1984), Hancock e Krieger (1986), Harbison e Steele (1984) e muitos outros.
Neste apêndice, tentaremos dar uma introdução bastante completa ao C, para que aqueles que estão familiarizados com idiomas de alto nível, como Pascal, PL / 1 ou Modula 2, possam entender a maior parte do código MINIX fornecido neste livro. Os recursos C que não são usados no MINIX não são discutidos aqui. Numerosos pontos sutis omitidos. A ênfase está na leitura de programas em C, em vez de escrever código.
A.1 Noções básicas de linguagem C
Um programa C consiste em um conjunto de procedimentos (geralmente chamados de funções, mesmo que eles não retornem valores). Esses procedimentos contêm declarações, operadores e outros elementos que juntos informam ao computador o que fazer. A Figura A-1 mostra um pequeno procedimento no qual três variáveis inteiras são declaradas e atribuídas valores. O nome do procedimento é principal. O procedimento não possui parâmetros formais, conforme indicado pela ausência de identificadores entre os colchetes atrás do nome do procedimento. O corpo do procedimento é colocado entre chaves ({}). Este exemplo mostra que C possui variáveis e que essas variáveis devem ser declaradas antes do uso. C também possui operadores, neste exemplo, são operadores de atribuição. Todas as instruções devem terminar com ponto e vírgula (ao contrário de Pascal, que usa dois pontos entre as instruções, não depois delas).
Os comentários começam com os caracteres "/ *" e terminam com os caracteres "* /" e podem se estender por várias linhas.
main () { int i, j, k; i = 10; j = i + 015; k = j * j + 0xFF; } . Al. .
O procedimento contém três constantes. Constante 10 na primeira tarefa
é uma constante decimal comum. A constante 015 é uma constante octal
(igual a 13 em decimal). As constantes octais sempre começam em zero. A constante 0xFF é uma constante hexadecimal (igual a 255 decimal). As constantes hexadecimais sempre começam com 0x. Todos os três tipos são usados em C.
A.2 Tipos de dados básicos
C possui dois tipos principais de dados (variáveis): um número inteiro e um caractere, declarados como int e char, respectivamente. Não há variável booleana separada. A variável int é usada como uma variável booleana. Se essa variável contiver 0, significa falso / falso e qualquer outro valor significa verdadeiro / verdadeiro. C também possui tipos de ponto flutuante, mas o MINIX não os utiliza.
Você pode aplicar "adjetivos" curtos, longos ou não assinados a um tipo int que define um intervalo de valores (intervalo dependente do compilador). A maioria dos processadores 8088 usa números inteiros de 16 bits para int e short int e números inteiros de 32 bits para int longo. Inteiros não assinados (int não assinado) no processador 8088 têm um intervalo de 0 a 65535, e não de -32768 a +32767, como é o caso de números inteiros comuns (int). Um personagem leva 8 bits.
O especificador de registro também é permitido para int e char, e é uma dica para o compilador de que a variável declarada deve ser colocada no registro para que o programa funcione mais rapidamente.
Alguns anúncios são mostrados na fig. A - 2.
int i; short int z1, z2; / * */ char c; unsigned short int k; long flag_poll; register int r; . -2. .
A conversão entre tipos é permitida. Por exemplo, o operador
flag_pole = i;
permitido mesmo que eu seja do tipo int e flag_pole seja longo. Em muitos casos
é necessário ou útil forçar conversões entre tipos de dados. Para a conversão forçada, basta colocar o tipo de destino entre colchetes na frente da expressão para conversão. Por exemplo:
( (long) i);
instrui a converter o número inteiro i para muito antes de passá-lo como um parâmetro para o procedimento p, que espera o parâmetro longo.
Ao converter entre tipos, preste atenção ao sinal.
Ao converter um caractere em um número inteiro, alguns compiladores tratam os caracteres como assinados, ou seja, de - 128 a +127, enquanto outros os tratam como
não assinado, ou seja, de 0 a 255. No MINIX, expressões como
i = c & 0377;
que converte de (caractere) em um número inteiro e executa um AND lógico
(e comercial) com a constante octal 0377. O resultado é que os altos 8 bits
são definidos como zero, forçando c a ser considerado como um número não assinado de 8 bits, no intervalo de 0 a 255.
A.3 Tipos de compostos e ponteiros
Nesta seção, examinaremos quatro maneiras de criar tipos de dados mais complexos: matrizes, estruturas, uniões e ponteiros. Uma matriz é uma coleção / conjunto de elementos do mesmo tipo. Todas as matrizes em C começam com o elemento 0.
Anúncio
int a [10];
declara uma matriz a com 10 números inteiros a serem armazenados nos elementos da matriz de [0] a [9]. Segundo, matrizes podem ter três ou mais dimensões, mas não são usadas no MINIX.
Uma estrutura é uma coleção de variáveis, geralmente de vários tipos. A estrutura em C é semelhante ao registro em Pascal. Operador
struct {int i; char c;} s;
declara s como uma estrutura contendo dois membros, o inteiro ie o caractere c.
Para atribuir o membro i da estrutura s a 6, escreva a seguinte expressão:
si = 6;
onde o operador de ponto indica que o elemento i pertence à estrutura s.
Um sindicato também é um conjunto de membros, semelhante a uma estrutura, exceto que a qualquer momento apenas um deles pode estar em um sindicato. Anúncio
union {int i; char c;} u;
significa que você pode ter um número inteiro ou caractere, mas não ambos. O compilador deve alocar espaço suficiente para a combinação, para que possa acomodar o maior elemento de combinação (do ponto de vista da memória ocupada). As uniões são usadas apenas em dois locais no MINIX (para definir uma mensagem como uma união de várias estruturas diferentes e para definir um bloco de disco como uma união de um bloco de dados, bloco de nó i, bloco de catálogo, etc.).
Ponteiros são usados para armazenar endereços de máquinas em C. Eles são usados com muita, muita frequência. Um asterisco (*) é usado para indicar um ponteiro nos anúncios. Anúncio
int i, *pi, a [10], *b[10], **ppi;
declara um número inteiro i, um ponteiro para um número inteiro pi, uma matriz a de 10 elementos, uma matriz b de 10 ponteiros para números inteiros e um ponteiro para um ponteiro ppi para um número inteiro.
As regras de sintaxe exata para declarações complexas que combinam matrizes, ponteiros e outros tipos são um tanto complexas. Felizmente, o MINIX usa apenas declarações simples.
A Figura A-3 mostra a declaração de uma matriz z de estruturas de tabela struct, cada uma das quais possui
três membros, número inteiro i, ponteiro cp para caractere e caractere c.
struct table { int i; / * */ char *cp, c; } z [20]; . - 3. .
Matrizes de estruturas são comuns no MINIX. Além disso, a tabela de nomes pode ser declarada como uma estrutura de tabela struct que pode ser usada em declarações subsequentes. Por exemplo
register struct table *p;
declara p um ponteiro para uma estrutura de tabela struct e sugere salvá-lo
em registro. Durante a execução do programa, p pode indicar, por exemplo, z [4] ou
para qualquer outro elemento em z, todos os 20 elementos dos quais são estruturas do tipo struct table.
Para fazer um ponteiro para z [4], basta escrever
p = &z[4];
onde o "e comercial" como operador unário (monádico) significa "pegue o endereço do que se segue". Copie o valor do membro i para a variável inteira n
a estrutura apontada por p pode ser feita da seguinte maneira:
n = p->i;
Observe que a seta é usada para acessar um membro da estrutura por meio de um ponteiro. Se usarmos a variável z, devemos usar o operador dot:
n = z [4] .i;
A diferença é que z [4] é uma estrutura e o operador de ponto seleciona os elementos
diretamente de tipos compostos (estruturas, matrizes). Usando ponteiros, não selecionamos um participante diretamente. O ponteiro instrui você a selecionar primeiro uma estrutura e somente depois selecionar um membro dessa estrutura.
Às vezes, é conveniente atribuir um nome a um tipo composto. Por exemplo:
typedef unsigned short int unshort;
define unshort como curto não assinado (número inteiro curto não assinado). Agora unshort pode ser usado no programa como o tipo principal. Por exemplo
unshort ul, *u2, u3[5];
declara um número inteiro sem sinal curto, um ponteiro para um número inteiro sem sinal curto e
uma matriz de números inteiros curtos e não assinados.
A.4 Operadores
Os procedimentos em C contêm declarações e declarações. Já vimos as declarações, agora vamos considerar os operadores. O objetivo dos operadores condicionais e de loop é essencialmente o mesmo que em outros idiomas. A Figura A - 4 mostra vários exemplos deles. A única coisa que vale a pena prestar atenção é que os chavetas são usadas para agrupar operadores, e a instrução while tem duas formas, a segunda das quais é semelhante à declaração repetida de Pascal.
C também possui uma declaração for, mas não se parece com uma declaração for em qualquer outro idioma. A instrução for tem o seguinte formato:
for (<>; <>; <>) ;
O mesmo pode ser expresso através da instrução while:
<> while(<>) { <>; <> }
Como exemplo, considere a seguinte declaração:
for (i=0; i <n; i = i+l) a[i]=0;
Este operador define os primeiros n elementos da matriz a como zero. A execução do operador começa definindo i como zero (isso é feito fora do loop). Em seguida, o operador é repetido até i <n, enquanto executa a atribuição e o aumento de i. Obviamente, em vez de o operador atribuir um valor ao elemento atual de uma matriz zero, pode haver um operador composto (bloco) entre colchetes.
if (x < 0) k = 3; if (x > y) { i = 2; k = j + l, } if (x + 2 <y) { j = 2; k = j - 1; } else { m = 0; } while (n > 0) { k = k + k; n = n - l; } do { / * while */ k = k + k; n = n - 1; } while (n > 0); . A-4. if while C.
C também possui um operador semelhante ao operador de caso em Pascal. Esta é uma declaração de opção. Um exemplo é mostrado na Figura A-5. Dependendo do valor da expressão especificada na opção, uma ou outra instrução de caso é selecionada.
Se a expressão não corresponder a nenhuma das instruções de caso, a instrução padrão será selecionada.
Se a expressão não estiver associada a nenhuma instrução case e a instrução padrão estiver ausente, a execução continuará na próxima instrução após a instrução switch.
Deve-se observar que, para sair do bloco do caso, use a instrução break. Se não houver instrução de interrupção, o próximo bloco de caso será executado.
switch (k) { case 10: i = 6; break; case 20: i = 2; k = 4; break; / * default* / default: j = 5; } . A-5. switch
A instrução break também atua dentro dos loops for e while. Deve-se lembrar que, se a instrução break estiver dentro de uma série de loops aninhados, a saída será apenas um nível acima.
Uma declaração relacionada é a instrução continue, que não sai do loop,
mas causa a conclusão da iteração atual e o início da próxima iteração
imediatamente. Este é essencialmente um retorno ao topo do loop.
C possui procedimentos que podem ser chamados com ou sem parâmetros.
Segundo Kernigan e Ritchie (p. 121), não é permitido transferir matrizes,
estruturas ou procedimentos como parâmetros, embora passe indicadores para tudo isso
permitido. Existe ou não um livro (ele aparecerá na minha memória: - “Se houver vida em Marte, se não houver vida em Marte”), muitos compiladores C permitem estruturas como parâmetros.
O nome da matriz, se for escrito sem um índice, significa um ponteiro para uma matriz, o que simplifica a transferência de um ponteiro da matriz. Assim, se a é o nome de uma matriz de qualquer tipo, pode ser passado para g escrevendo
g();
Esta regra se aplica apenas a matrizes; essa regra não se aplica a estruturas.
Os procedimentos podem retornar valores executando uma instrução de retorno. Essa instrução pode conter uma expressão, cujo resultado será retornado como o valor do procedimento, mas o chamador pode ignorar com segurança o valor de retorno. Se o procedimento retornar um valor, o valor do tipo será gravado antes do nome do procedimento, conforme mostrado na Fig. A-6. Como os parâmetros, os procedimentos não podem retornar matrizes, estruturas ou procedimentos, mas podem retornar ponteiros para eles. Esta regra foi projetada para uma implementação mais eficiente - todos os parâmetros e resultados sempre correspondem a uma palavra de máquina (na qual o endereço está armazenado). Compiladores que permitem o uso de estruturas como parâmetros geralmente também permitem seu uso como valores de retorno.
int sum (i, j) int i, j ; { return (i + j); } . -6. , .
C não possui E / S incorporada. A entrada / saída é implementada chamando as funções da biblioteca, as mais comuns são ilustradas abaixo:
printf («x=% dy = %oz = %x \n», x, y, z);
O primeiro parâmetro é a sequência de caracteres entre aspas (na verdade, essa é uma matriz de caracteres).
Qualquer caractere que não seja uma porcentagem é simplesmente impresso como está.
Quando ocorre uma porcentagem, o seguinte parâmetro é impresso no formulário definido pela letra após a porcentagem:
d - imprime como um número inteiro decimal
o - imprime como um número inteiro octal
u - imprime como um número inteiro decimal não assinado
x - imprime como um número inteiro hexadecimal
s - imprime como uma sequência de caracteres
c - imprimir como um caractere
As letras D, 0 e X também são permitidas para impressão decimal, octal e hexadecimal de números longos.
A.5 Expressões
Expressões são criadas pela combinação de operandos e operadores.
Operadores aritméticos, como + e -, e operadores relacionais, como <
e> semelhantes aos seus colegas em outros idiomas. Operador%
módulo usado. Vale a pena notar que o operador de igualdade é == e o operador de desigualdade é! =. Para verificar se aeb são iguais, você pode escrever assim:
if (a == b) <>;
C também permite combinar o operador de atribuição com outros operadores, portanto
a += 4;
equivalente a gravação
= + 4;
Outros operadores também podem ser combinados dessa maneira.
C possui operadores para manipular bits de uma palavra. São permitidos turnos e operações lógicas bit a bit. Os operadores de turno esquerdo e direito são <<
e >>, respectivamente. Operadores lógicos bit a bit &, | e ^, que são lógicos AND (AND), incluindo OR (OR) e OR exclusivo (XOP), respectivamente. Se i tiver o valor 035 (octal), a expressão i & 06 terá o valor 04 (octal). Outro exemplo, se i = 7, então
j = (i << 3) | 014;
e obtenha 074 para j.
Outro grupo importante de operadores são os operadores unários, cada um dos quais aceita apenas um operando. Como operador unário, & comercial & obtém o endereço de uma variável.
Se p é um ponteiro para um número inteiro e i é um número inteiro, o operador
p = &i;
calcula o endereço ie armazena-o na variável p.
O oposto de pegar um endereço é um operador que pega um ponteiro como entrada e calcula o valor nesse endereço. Se atribuirmos o endereço i ao ponteiro p, então * p terá o mesmo significado que i.
Em outras palavras, como operador unário, um asterisco é seguido por um ponteiro (ou
expressão que fornece um ponteiro) e retorna o valor do elemento para o qual ele aponta. Se eu tiver um valor 6, o operador
j = *;
atribuirá j o número 6.
O operador! (o ponto de exclamação é o operador de negação) retorna 0 se o operando for diferente de zero e 1 se o operador for 0.
É usado principalmente em instruções if, por exemplo
if (!x) k=0;
verifica o valor de x. Se x é zero (falso), então k recebe o valor 0. Na verdade, o operador! cancela a condição seguinte, assim como o operador não em Pascal.
O operador ~ é um operador de complemento bit a bit. Cada 0 em seu operando
se torna 1 e cada 1 se torna 0.
O operador sizeof relata o tamanho do seu operando em bytes. Em relação a
uma matriz de 20 números inteiros a em um computador com números inteiros de 2 bytes, por exemplo sizeof a terá um valor de 40.
O último grupo de operadores são os operadores de aumento e redução.
Operador
++;
significa um aumento na p. Quanto p aumentará depende do seu tipo.
Inteiros ou caracteres aumentam em 1, mas ponteiros aumentam em
o tamanho do objeto apontado dessa maneira, se a é uma matriz de estruturas ep é um ponteiro para uma dessas estruturas, e escrevemos
p = &a[3];
para fazer p apontar para uma das estruturas da matriz, depois de aumentar p
irá apontar para um [4] não importa o tamanho das estruturas. Operador
p--;
semelhante ao operador p ++, exceto pelo fato de diminuir em vez de aumentar o valor do operando.
Em declaração
n = k++;
onde ambas as variáveis são números inteiros, o valor original de k é atribuído a n e
somente então k aumenta. Em declaração
n = ++ k;
k aumenta primeiro e, em seguida, seu novo valor é armazenado em n.
Assim, um operador ++ (ou -) pode ser escrito antes ou depois do seu operando, o que resulta em vários valores.
A última afirmação é essa? (ponto de interrogação) que seleciona uma das duas alternativas
separados por dois pontos. Por exemplo, um operador,
i = (x < y ? 6 : k + 1);
compara x com y. Se x é menor que y, então eu obtém o valor 6; caso contrário, a variável i obtém o valor k + 1. Os colchetes são opcionais.
A.6 Estrutura do programa
Um programa C consiste em um ou mais arquivos que contêm procedimentos e declarações.
Esses arquivos podem ser compilados individualmente em arquivos de objetos, que são então vinculados entre si (usando o vinculador) para formar um programa executável.
Ao contrário do Pascal, as declarações de procedimentos não podem ser aninhadas, portanto, todas elas são gravadas no "nível superior" no arquivo do programa.
É permitido declarar variáveis fora dos procedimentos, por exemplo, no início do arquivo antes da primeira declaração do procedimento. Essas variáveis são globais e podem ser usadas em qualquer procedimento ao longo do programa, a menos que a palavra-chave estática preceda a declaração. Nesse caso, essas variáveis não podem ser usadas em outro arquivo. As mesmas regras se aplicam aos procedimentos. Variáveis declaradas dentro de um procedimento são locais para o procedimento.
O procedimento pode acessar a variável inteira v declarada em outro arquivo (desde que a variável não seja estática), declarando-a externa:
extern int v;
Cada variável global deve ser declarada exatamente uma vez sem o atributo externo para alocar memória para ela.
Variáveis podem ser inicializadas quando declaradas:
int size = 100;
Matrizes e estruturas também podem ser inicializadas. Variáveis globais que não são explicitamente inicializadas recebem um valor padrão zero.
A.7 Pré-processador C
Antes de o arquivo de origem ser transferido para o compilador C, ele é processado automaticamente
um programa chamado pré-processador. É a saída do pré-processador, não
O programa original é alimentado na entrada do compilador. O pré-processador executa
Três conversões básicas em um arquivo antes de passá-lo ao compilador:
1. Inclusão de arquivos.
2. Definição e substituição de macros.
3. Compilação condicional.
Todas as diretivas de pré-processador começam com um sinal de número (#) na 1ª coluna.
Quando uma diretiva de exibição
#include "prog.h"
atendido pelo pré-processador, inclui o arquivo prog.h, linha por linha, em
o programa a ser passado para o compilador. Quando a diretiva #include é escrita como
#include <prog.h>
o arquivo incluído é pesquisado no diretório / usr / include em vez do diretório de trabalho. É prática comum em C agrupar as declarações usadas por vários arquivos em um arquivo de cabeçalho (geralmente com o sufixo .h) e incluí-las sempre que necessário.
O pré-processador também permite definições de macro. Por exemplo
#define BLOCK_SIZE 1024
define a macro BLOCK_SIZE e atribui a ele um valor de 1024. A partir de agora,cada ocorrência de uma sequência de 10 caracteres "BLOCK_SIZE" no arquivo serásubstituída por uma sequência de 4 caracteres "1024" antes que o compilador veja o arquivo com o programa. Por convenção, os nomes de macro são escritos em maiúsculas. Macros podem ter parâmetros, mas na prática poucos o fazem.O terceiro recurso do pré-processador é a compilação condicional. O MINIX possui várioslocais onde o código é escrito especificamente para o processador 8088, e esse código não deve ser incluído ao compilar para outro processador. Essas seções são assim: #ifdef i8088 < 8088> #endif
Se o caractere i8088 estiver definido, as instruções entre as duas diretivas de pré-processador #ifdef i8088 e #endif serão incluídas na saída do pré-processador; caso contrário, eles são ignorados. Chamando o compilador com o comando cc -c -Di8088 prog.c
ou incluindo uma declaração no programa #define i8088
definimos o símbolo i8088, para que todo o código dependente do 8088 seja incluído. À medida que o MINIX se desenvolve, ele pode adquirir um código especial para 68000s e outros processadores que também serão processados.Como um exemplo de como o pré-processador funciona, considere o programa Fig. A-7 (a). Inclui um arquivo prog.h, cujo conteúdo é o seguinte: int x; #define MAXAELEMENTS 100
Imagine que o compilador foi chamado por um comando cc -E -Di8088 prog.c
Após o arquivo ter passado pelo pré-processador, a saída será mostrada na Fig. A-7 (b).É essa saída, e não o arquivo de origem, que é fornecida como entrada para o compilador C.
Observe que o pré-processador fez seu trabalho e excluiu todas as linhas começando com o sinal #. Se o compilador fosse chamado assim cc -c -Dm68000 prog.c
então outra impressão seria incluída. Se fosse chamado assim: cc -c prog.c
então nenhuma impressão seria incluída. (O leitor pode refletir sobre o que aconteceria se o compilador fosse chamado com os dois sinalizadores -D.A.8 Expressões idiomáticas
Nesta seção, examinaremos várias construções típicas de C, mas não comuns em outras linguagens de programação. Primeiro, considere o loop: while (n--) *p++ = *q++;
As variáveis peq são geralmente ponteiros de caracteres, e n é um contador. O loop copia a cadeia de caracteres n de onde q aponta para onde p aponta. A cada iteração do loop, o contador diminui até atingir 0 e cada um dos ponteiros aumenta, de modo que apontam sequencialmente para as células de memória com um número maior.Outro design comum: for (i = 0; i < N; i++) a[i] = 0;
que define os primeiros N elementos de a como 0. Uma maneira alternativa de escrever esse loop é a seguinte: for (p = &a[0]; p < &a[N]; p++) *p = 0;
Nesta instrução, o ponteiro inteiro p é inicializado para apontar para o elemento zero da matriz. O loop continua até que p atinja o endereço do enésimo elemento da matriz. Uma construção de ponteiro é muito mais eficiente que uma construção de matriz e, portanto, geralmente é usada.Os operadores de atribuição podem aparecer em locais inesperados. Por exemplo
if (a = f (x)) < >;
primeiro chama a função f, depois atribui o resultado da chamada da função a e,finalmente, verifica se é verdadeiro (diferente de zero) ou falso (zero). Se a não for igual a zero, a condição será satisfeita. Operador
if (a = b) < >;
Além disso, primeiro, o valor da variável b da variável ae depois verifica a se o valor é diferente de zero. E esse operador é completamente diferente de if (a == b) < >;
que compara duas variáveis e executa o operador se forem iguais.Posfácio
Isso é tudo. Você não vai acreditar o quanto eu gostei de preparar este texto. Quanto me lembrei de útil da mesma linguagem C. Espero que você também goste de mergulhar no maravilhoso mundo da linguagem C.