Neste capítulo, introduzimos um primeiro conjunto de problemas, bastante simples, e exploramos um raciocínio também bastante simples de construção de algoritmos. A solução de cada um desses problemas é expressa na forma de uma definição de função, em C.
Esses primeiros problemas têm o propósito de ilustrar os mecanismos de definição e uso de funções em programas e o uso de comandos de seleção. Além disso, visam também introduzir operações sobre valores inteiros e alguns aspectos sintáticos da linguagem.
O nosso primeiro conjunto de problemas é especificado a seguir:
/****************************** * * * Primeiros exemplos * * * * Definições de funções * *-----------------------------*/ int quadrado (int x) { return x*x; } int somaDosQuadrados (int x, int y) { return (quadrado(x) + quadrado(y)); } int tresIguais (int a, int b, int c) { return ((a==b) && (b==c)); } int eTriang (int a, int b, int c) { // a, b e c positivos e // cada um menor do que a soma dos outros dois return (a>0) && (b>0) && (c>0) && (a<b+c) && (b<a+c) && (c<a+b); } int max (int a, int b) { if (a >= b) return a; else return b; } int max3 (int a, int b, int c) { return (max (max (a, b), c)); }
Figura 3.1: Definições de funções: primeiros exemplos em C
A Figura 3.1 apresenta definições de funções para solução de cada um dos problemas relacionados acima. Cada uma das funções opera apenas sobre valores inteiros e tem definição bastante simples, baseada apenas no uso de outras funções, predefinidas na linguagem, ou definidas no próprio programa.
O programa começa com um comentário. Comentários são adicionados a um programa para tornar mais fácil a sua leitura. Eles não têm nenhum efeito sobre o comportamento do programa, quando esse é executado. Nos exemplos apresentados neste livro, comentários são muitas vezes omitidos, uma vez que a função e o significado dos programas são explicados ao longo do texto.
Existem dois tipos de comentários em C. O primeiro começa com
os caracteres /* e termina com */ — qualquer sequência
de caracteres entre /* e */ faz parte do comentário. O
comentário no início do nosso programa é um exemplo desse
tipo de comentário. O segundo tipo de comentário começa com
os caracteres // em uma dada linha e termina no final dessa
linha. Um exemplo é o comentário usado na definição da
função eTriang.
A definição de cada função obedece à seguinte estrutura padrão de declarações de funções:
O tipo do valor retornado por cada função declarada acima é
int (os nomes das funções são quadrado,
somaDosQuadrados, tresIguais, max e
max3).
A especificação de um parâmetro consiste em um tipo, seguido do nome do parâmetro. A especificação de cada parâmetro é separada da seguinte por uma vírgula.
Por exemplo, a declaração de somaDosQuadrados especifica que
essa função tem dois parâmetros: o primeiro tem tipo int e
nome x, o segundo também tem tipo int e nome
y.
A execução do corpo do método quadrado — return x*x; — retorna o quadrado do valor (x) passado como
argumento em uma chamada a esse método.
O valor retornado pela execução de uma chamada a um método é
determinado pela expressão que ocorre como argumento do comando
return, que deve ocorrer no corpo desse método. O efeito da
execução de um comando da forma:
|
é o de avaliar a expressão e, obtendo um determinado valor, e finalizar a execução do método, retornando esse valor.
Uma função definida em um programa pode ser usada do mesmo modo
que funções predefinidas na linguagem. Por exemplo, a
função quadrado é usada na definição da
função somaDosQuadrados, assim como o operador predefinido
+, que representa a operação (ou função) de
adição de inteiros. A avaliação da expressão
quadrado(3) + quadrado(4) consiste em avaliar a expressão
quadrado(3), o que significa executar o comando return 3*3, que retorna 9 como resultado da chamada
quadrado(3); em seguida, avaliar a expressão
quadrado(4), de maneira análoga, retornando 16; e
finalmente avaliar a expressão 9 + 16, o que fornece o
resultado 25.
O número e o tipo dos argumentos
em uma chamada de método devem corresponder ao número e tipo
especificados na sua definição. Por exemplo, em uma chamada a
tresIguais, devem ser especificadas três expressões e1, e2
e e3, como a seguir:
|
Essa chamada pode ser usada em qualquer contexto que requer um valor
de tipo int ou, em outras palavras, em qualquer lugar onde uma
expressão de tipo int pode ocorrer.
O corpo do método tresIguais usa o operador &&, que
representa a operação booleana “ê”. Três valores a, b
e c são iguais, se a é igual a b e b é igual a
c, o que é expresso pela expressão (a==b) &&
(b==c).
Como vimos anteriormente, o símbolo == é usado para comparar a igualdade de dois valores. A avaliação de uma expressão da forma “e1 == e2” tem resultado verdadeiro (em C, qualquer valor inteiro diferente de zero) se os resultados da avaliação de e1 e e2 são iguais, e falso (em C, o inteiro zero), caso contrário.
A operação de desigualdade é representada por !=. Outras operações de comparação (também chamadas operações relacionais) são apresentadas na Tabela 3.1.
Tabela 3.1: Operadores de comparação
Operador Significado Exemplo Resultado == Igual a 1 == 1 verdadeiro != Diferente de 1 != 1 falso < Menor que 1 < 1 falso > Maior que 1 > 1 falso <= Menor ou igual a 1 <= 1 verdadeiro >= Maior ou igual a 1 >= 1 verdadeiro
O operador && é também usado na expressão que determina o
valor retornado pela função eTriang\/. O valor dessa
expressão será falso (zero) ou verdadeiro (diferente de zero),
conforme os valores dos parâmetros da função — a,
b e c — constituam ou não lados de um
triângulo. Isso é expresso pela condição: os valores são positivos
— (a>0) && (b>0) && (c>0) — e cada um deles é menor do que a
soma dos outros dois — (a<b+c) && (b<a+c) && (c<a+b).
O operador &&, assim como outros operadores lógicos que podem
ser usados em C, são descritos na Seção 3.8.
O método max retorna o máximo entre dois valores, passados
como argumentos em uma chamada a essa função, usando um comando
“if”. O valor retornado por uma chamada a max(a,b) é o
valor contido em a, se o resultado da avaliação da
expressão a>=b for verdadeiro, caso contrário o valor
contido em b.
Finalmente, no corpo da definição de max3, o valor a ser
retornado pelo método é determinado pela expressão
max(max(a,b),c), que simplesmente usa duas chamadas ao método
max, definido anteriormente. O resultado da avaliação
dessa expressão é o máximo entre o valor contido em c e o
valor retornado pela chamada max(a,b), que é, por sua vez, o
máximo entre os valores dados pelos argumentos a e b.
Uma maneira alternativa de definir as funções max e
max3 é pelo uso de uma expressão condicional, em vez
de um comando condicional. Uma expressão condicional tem a forma:
|
onde e é uma expressão booleana e e1 e e2 são
expressões de um mesmo tipo. O resultado da avaliação de
e ? e1 : e2 é igual ao de e1 se a
avaliação de e for igual a verdadeiro, e igual ao de e2, em
caso contrário. Lembre que em C “verdadeiro” significa um valor
inteiro diferente de zero. As funções max e max3
podem ser então definidas como a seguir:
|
Sob o ponto de vista de um usuário, o comportamento de um programa é determinado, essencialmente, pela entrada e pela saída de dados desse programa.
Nesta seção, apresentamos uma introdução aos mecanismos de entrada e saída (E/S) de dados disponíveis na linguagem C, abordando inicialmente um tema relacionado, que é o uso de cadeias de caracteres (chamados comumente de strings em computação).
O suporte a caracteres em C é apresentado mais detalhadamente na
seção 3.4 (isto é, a seção descreve como caracteres são
tratados na linguagem C, apresentando o tipo char e a relação
desse tipo com tipos inteiros em C), e o suporte a sequências
(ou cadeias) de caracteres é apresentado na seção 5.5.
Em C, assim como em grande parte das linguagens de programação,
os mecanismos de E/S não fazem parte da linguagem propriamente dita,
mas de uma biblioteca padrão, que deve ser implementada por todos os
ambientes para desenvolvimento de programas na linguagem. Essa
biblioteca é chamada de stdio.
A biblioteca stdio provê operações para entrada de dados no dispositivo de entrada padrão (geralmente o teclado do
computador), e de saída de dados no dispositivo de saída
padrão (geralmente a tela do computador).
Qualquer programa que use a biblioteca stdio para realizar
alguma operação de entrada ou saída de dados deve incluir a linha
|
antes do primeiro uso de uma função definida na biblioteca.
Vamos usar principalmente duas funções definidas na biblioteca
stdio, respectivamente para entrada e para saída de dados:
scanf e printf.
Um uso da função printf tem o seguinte formato:
| int printf( str, v1, …, vn ) |
onde str é um literal de tipo string (cadeia de caracteres), escrita entre aspas duplas, e v1,…,vn são argumentos (valores a serem impressos).
A sequência de caracteres impressa em uma chamada a printf é
controlada pelo parâmetro str, que pode conter especificações de
controle da operação de saída de dados. Essas especificações de
controle contêm o caractere % seguido de outro caractere
indicador do tipo de conversão a ser realizada.
Por exemplo:
|
faz com que o valor da variável inteira x (10) seja
impresso em notação decimal, precidido da sequência de caracteres
, ou seja, faz com que seja impresso:"Resultado = "
| Resultado = 10 |
Existem ainda os caracteres x,o,e,f,s,c, usados para leitura e
impressão de, respectivamente, inteiros em notação hexadecimal e
octal, valores de tipo double, com (e e sem (f)
parte referente ao expoente, cadeias de caracteres (strings) e
caracteres.
Um número pode ser usado, antes do caractere indicador do tipo de conversão, para especificar um tamanho fixo de caracteres a ser impresso (brancos são usados para completar este número mínimo se necessário), assim como um ponto e um número, no caso de impressão de valores de ponto flutuante, sendo que o número indica neste caso o tamanho do número de dígitos da parte fracionária.
A sequência de caracteres impressa em uma chamada a printf é
igual a str mas pode conter o que são chamadas especificações de formato. Especificações de formato contêm o
caractere % seguido de outro caractere indicador de um tipo de
conversão que deve ser relizada. Especificações de formato podem
também ocorrer em chamadas a scanf, para conversão do valor
lido, como mostramos a seguir.
Por exemplo:
|
faz com que o valor da variável inteira x (igual a 10) seja
impresso em notação decimal, precedido da sequência de caracteres
, ou seja, faz com que seja impresso:"Resultado = "
| Resultado = 10 |
Neste exemplo poderíamos ter impresso diretamente a sequência de
caracteres "Resultado = 10), mas o intuito é ilustrar o uso de
%d, que será explorado depois de introduzirmos a função
scanf de entrada de dados, a seguir.
Usos da função scanf seguem o seguinte formato:
| int scanf( str, v1, …, vn ) |
onde str é um literal de tipo string (cadeia de caracteres), escrito entre aspas duplas, e v1,…,vn são argumentos (valores a serem impressos).
Para ler um valor inteiro e armazená-lo em
uma variável, digamos a, a função scanf pode ser usada,
como a seguir:
|
Esse comando deve ser entendido como: leia um valor inteiro do
dispositivo de entrada entrada padrão e armazene esse valor na
variável a.
O dispositivo de entrada padrão é normalmente o teclado, de modo que a operação de leitura interrompe a execução do programa para esperar que um valor inteiro seja digitado no teclado, seguido da tecla de terminação de linha (Enter).
O uso de em uma especifação de formato indica, como
explicado acima, que deve ser feita uma conversão do valor digitado,
em notação decimal, para um valor inteiro correspondente
(representado como uma sequência de bits)."%d"
O
caractere & siginifica que o segundo argumento é o endereço da
variável a (e não o valor armazenado nessa variável). Ou seja,
&a deve ser lido como “endereço de a”, ou “referência
para a. Esse assunto é abordado mais detalhadamente na seção
6.
A execução do comando scanf acima consiste em uma espera, até
que um valor inteiro seja digitado no teclado (se o dispositivo de
entrada padrão for o teclado, como ocorre se não houver
redirecionamento do dispositivo de entrada padrão, como explicado na
seção 3.2.1) seguido do caractere de terminação de
linha (Enter), e no armazenamento do valor inteiro digitado na
variável a.
Para ler dois valores inteiros e armazená-los em duas variáveis
inteiras a e b, a função scanf pode ser usada como a
seguir:
|
Podemos fazer agora nosso primeiro programa, que lê dois inteiros e imprime o maior dentre eles, como a seguir:
|
A primeira linha deste nosso primeiro programa indica que o arquivo
stdio.h deve ser lido e as definições contidas neste arquivo
devem ser consideradas para compilação do restante do programa. Vamos
chamar essa primeira linha de uma diretiva de pre-processamento.
Além de “ler valores”, isto é, além de modificar valores armazenados
em variáveis, uma chamada à função scanf também retorna um
valor. Esse valor é igual ao número de variáveis lidas, e pode ser
usado para detectar fim dos dados de entrada a serem lidos (isto é, se
não existe mais nenhum valor na entrada de dados a ser lido). No
Capítulo 4 mostraremos exemplos de leitura de
vários inteiros até que ocorra uma condição ou até que não haja mais
valores a serem lidos.
Toda diretiva de preprocessamento começa com o caractere #, e deve ser inserida na primeira coluna de uma linha. A linguagem C permite usar espaços em branco e dispor comandos e declarações como o programador desejar (no entanto, programadores usam tipicamente convenções que têm o propósito de homegeneizar a disposição de trechos de programas de modo a facilitar a leitura). Uma diretiva de preprocessamento no entanto é uma exceção a essa regra de dispor livremente espaços, devendo começar sempre na primeira coluna de uma linha. Após o caractere # vem o nome do arquivo a ser lido, o qual deve ter uma extensão .h, entre os caracteres < e >, no caso de um arquivo de uma biblioteca padrão.
Para arquivos com extensão .h definido pelo programador, o nome
do arquivo deve ser inserido entre aspas duplas (como por exemplo em
, sendo "interface.h"interface.h o nome do arquivo).
A diretiva #include <stdio.h> é a diretiva mais comumente usada
em programas C.
É muitas vezes necessário ou mais adequado que os dados lidos por um programa estejam armazenados em arquivos, em vez de serem digitados repetidamente por um usuário em um teclado, e sejam armazenados em arquivos após a execução de um programa, permanecendo assim disponíveis após a execução desse programa.
Uma maneira simples de fazer entrada e saída em arquivos é através de redirecionamento, da entrada padrão no caso de leitura, ou da saída padrão no caso de impressão, para um arquivo. Em outras palavras, o redirecionamento da entrada padrão especifica que os dados devem ser lidos de um arquivo, em vez de a partir do teclado, e o redirecionamento da saída padrão especifica que os dados devem ser impressos em um arquivo, em vez de serem mostrados na tela do computador. A desvantagem desse esquema é que a entrada e saída de dados devem ser redirecionados para um único arquivo de entrada e um único arquivo de saída, ambos determinados antes da execução do programa.
As operações de entrada e de saída de dados scanf e
printf funcionam normalmente, mas acontecem em arquivos, para os
quais a entrada ou saída foi redirecionada no momento da chamada ao
sistema operacional para iniciação do programa.
Para especificar o redirecionamento da entrada para um arquivo selecionado, o programa é iniciado com uma chamada que inclui, além do nome do programa a ser iniciado, o caractere < seguido do nome do arquivo de entrada que deve substituir o dispositivo de entrada padrão:
| programa < arquivoDeEntrada |
As operações de entrada de dados, que seriam feitas a partir do dispositivo de entrada padrão (usualmente o teclado), são realizadas então a partir do arquivo de nome arquivoDeEntrada.
O nome desse arquivo pode ser uma especificação completa de arquivo.
De modo similar, podemos redirecionar a saída padrão:
| programa > arquivoDeSaida |
Ao chamarmos programa dessa forma, a saída de dados vai ser feito em um arquivo que vai ser criado com o nome especificado, no caso arquivoDeSaida, em vez de a saída aparecer na tela do computador.
Podemos é claro fazer o redirecionamento tanto da entrada quanto da saída:
| programa < arquivoDeEntrada > arquivoDeSaida |
Um exemplo de iniciação de programa.exe a partir da interface
de comandos do DOS com especificação completa de arquivos (em
um computador com um sistema operacional Windows), para entrada de
dados a partir do arquivo
c:\temp\dados.txt e
saída em um arquivo
c:\temp\saida.txt deve
ser feito como seguir:
programa.exe < c:\temp\dados.txt > c:\temp\saida.txt |
A letra que segue o caractere % em uma especificação de formato especifica qual conversão deve ser realizada. Várias letras podem ser usadas. As mais comuns são indicadas abaixo. Para cada uma é indicado o tipo do valor convertido:
| Especificação de controle | Tipo |
| %d (ou %i) | int |
| %c | char |
| %f | float |
| %lf | double |
| %s | char* |
%e pode também ser usado, para converter um valor em notação científica. Números na notação científica são escritos na forma a × 10b, onde a parte a é chamada de mantissa e b de expoente.
%x e %o podem ser usados para usar, respetivamente, notação hexadecimal e octal de cadeias de caracteres (dígitos), para conversão de um valor inteiro em uma cadeia de caracteres.
As letras lf em %lf são iniciais de long float.
%d e %i são equivalentes para saída de dados, mas são
distintos no caso de entrada, com scanf. %i considera a
cadeia de caracteres de entrada como hexadecimal quando ela é
precedida de , e como octal quando precedida de "0x".
Por exemplo, a cadeia de caracteres "0" é lida como 31
usando %d, mas como 25 usando %i (25 = 3 × 8
+ 1)."031"
Em uma operação de saída, podem ser especificados vários parâmetros de controle. A sintaxe de uma especificação de formato é bastante elaborada, e permite especificar:
|
onde t é uma letra que indica o tipo da conversão (que pode ser d, i, c, s etc., como vimos acima), e n, p especificam um tamanho, como explicado a seguir:
Por exemplo, o programa:
|
imprime:
| 1 | 2345 |
| 12 |
Ou seja, no segundo printf um espaço é inserido antes de 12 para completar 3 caracteres (no primeiro printf o uso do 3 em %3d não tem efeito).
scanf antes do valor a
ser impresso.Por exemplo, printf("%*d", 5, 10) imprime
com tamanho mínimo 5."10"
Por exemplo, o programa:
|
imprime:
| 12345 |
| 12 |
| 12.340 |
| 1234.568 |
| abc |
scanf antes do valor a ser impresso.Por exemplo, printf("%*s", 3, "abcde") imprime
"abc".
Nota sobre uso do formato %c com scanf em programas C executando sob o sistema operacional Windows, em entrada de dados interativa:
O formato %c não deve ser usado com scanf em programas
C executando sob o sistema operacional Windows; em vez disso,
deve-se usar getChar. Para entender porque, considere as
execuções dos dois programas a seguir, que vamos chamar de ecoar1.c e ecoar2.c:
| ecoar1.c | |
|
| ecoar2.c | |
|
As execuções de ecoar1.c e ecoar2.c não são
equivalentes em entrada interativa, no sistema operacional
Windows: nesse caso, o valor retornado por scanf("%c", &c) só é
igual a -1 quando se pressiona Control-Z seguido de Enter duas vezes no início de uma linha. A razão para tal
comportamento é misteriosa, mas provavelmente trata-se de algum erro
na imnplementação da função scanf.
A área de memória reservada para armazenar um dado valor em um
computador tem um tamanho fixo, por questões de custo e
eficiência. Um valor de tipo int em C é usualmente
armazenado, nos computadores atuais, em uma porção de memória
com tamanho de 32 ou 64 bits, assim como um número de ponto
flutuante — em C, um valor de tipo float ou
double.
Também por questões de eficiência (isto é, para minimizar tempo
ou espaço consumidos), existem qualificadores que podem ser usados
para aumentar ou restringir os conjuntos de valores numéricos
inteiros e de números de ponto flutuante que podem ser armazenados ou
representados por um tipo numérico em C. Os qualificadores podem ser:
short e long.
O tipo char, usado para representar caracteres, é também
considerado como tipo inteiro, nesse caso sem sinal. O qualificador
unsigned também pode ser usado para tipos inteiros, indicando
que valores do tipo incluem apenas inteiros positivos ou zero.
A definição da linguagem C não especifica qual o tamanho do espaço
alocado para cada variável de um tipo numérico específico (char,
short, int, long, float ou double), ou
seja, a linguagem não especifica qual o número de bits alocado. No
caso de uso de um qualificador (short ou long), o nome
int pode ser omitido.
Cada implementação da linguagem
pode usar um tamanho que julgar apropriado. As únicas condições
impostas são as seguintes. Elas usam a função sizeof,
predefinida em C, que retorna o número de bytes de um nome de tipo,
ou de uma expressão de um tipo qualquer:
|
Implementações usam comumente 32 ou 64 bits para variáveis de tipo
int, 64 ou 128 bits para long int e 16 ou 32 bits para
variáveis de tipo short int.
Um numeral inteiro é do
tipo long se ele tiver como sufixo a letra L, ou l
(mas a letra L deve ser preferida, pois a letra l se
parece muito com o algarismo 1). Do contrário, o numeral é
do tipo int. Não existem numerais do tipo short.
Entretanto, um numeral do tipo int pode ser armazenado em uma
variável do tipo short, ocorrendo, nesse caso, uma conversão
implícita de int para short. Por exemplo, no comando
de atribuição:
|
o valor 10, de tipo int, é convertido para o tipo
short e armazenado na variável s, de tipo
short. Essa conversão obtém apenas os n bits mais à
direita (menos significativos) do valor a ser convertido, onde n é
o número de bits usados para a representação de valores do
tipo para o qual é feita a conversão, sendo descartados os bits
mais à esquerda (mais significativos) restantes.
Números inteiros podem também ser representados nos sistemas de numeração hexadecimal e octal. No primeiro caso, o numeral deve ser precedido dos caracteres 0x ou OX, sendo representado com algarismos hexadecimais: os números de 0 a 15 são representados pelos algarismos 0 a 9 e pelas letras de a até f, ou A até F, respectivamente. Um numeral octal é iniciado com o dígito 0 (seguido de um ou mais algarismos, de 0 a 7).
Números de ponto flutuante são números representados com uma parte inteira (mantissa) e outra parte fracionária, como, por exemplo:
| 2.0 3.1415 1.5e-3 7.16e1 |
Um ponto decimal é usado para separar a parte inteira (mantissa) da parte fracionária. Um expoente na base 10 pode (ou não) ser especificado, sendo indicado pela letra e, ou E, seguida de um inteiro, opcionalmente precedido de um sinal (+ ou -). Os dois últimos exemplos, que contêm também um expoente de 10, representam, respectivamente, 1.5 × 10−3 e 7.16 × 101.
Um sufixo, f ou F, como em 1.43f, indica um valor do
tipo float, e a ausência do sufixo, ou a especificação
de um sufixo d ou D, indica um valor do tipo double.
Exemplos de numerais de tipo float:
| 2e2f 4.f .5f 0f 2.71828e+4f |
Exempos de numerais de tipo double:
| 2e2 4. .5 0.0 1e-9d |
Como números são representados em um computador com um número
fixo de bits, a faixa de valores representáveis de cada tipo
numérico é limitada. O uso de uma operação que retorna um
valor positivo maior do que o maior inteiro representável em uma
variável de tipo int constitui, em geral, um erro. Esse tipo de
erro é chamado, em computação, de overflow, ou seja,
espaço insuficiente para armazenamento.
Em C, um erro de overflow não é detectado, e o programa continua a sua execução: como o valor que causou a ocorrência de overflow não pode ser representado no espaço reservado para que ele seja armazenado, o programa usa então um outro valor (incorreto). Quando ocorre overflow, por exemplo, na adição de dois números inteiros positivos, o resultado é um número negativo. O inverso também é verdadeiro: quando ocorre overflow na adição de dois números inteiros negativos, o resultado é um número positivo.
Valores do tipo char, ou caracteres, são usados para
representar símbolos (caracteres visíveis), tais como
letras, algarismos etc., e caracteres de controle, usados para indicar
fim de arquivo, mudança de linha, tabulação etc.
Cada caractere é representado, em um computador, por um determinado valor (binário). A associação entre esses valores e os caracteres correspondentes constitui o que se chama de código.
O código usado para representação de caracteres em C é o chamado código ASCII (American Standard Code for Information Interchange). O código ASCII é baseado no uso de 8 bits para cada caractere.
Caracteres visíveis são escritos em C entre aspas simples. Por exemplo: ’a’, ’3’, ’*’, ’ ’, ’%’ etc. É preciso notar que, por exemplo, o caractere ’3’ é diferente do numeral inteiro 3. O primeiro representa um símbolo, enquanto o segundo representa um número inteiro.
Caracteres de controle e os caracteres ’ e \ são escritos usando uma sequência especial de caracteres. Por exemplo:
| caractere | indica |
| terminação de linha |
| tabulação |
| o caractere |
\\ | o caractere \
|
“Tabulação” significa o uso de um certo número de espaços, para alcançar a próxima posição de uma linha que é dividida em posições fixas, tipicamente de 8 em 8 caracteres.
Um valor do tipo char pode ser usado em C como um valor
inteiro. Nesse caso, ocorre uma conversão de tipo implícita,
tal como no caso de conversões entre dois valores inteiros de tipos
diferentes (como, por exemplo, short e int). O valor
inteiro de um determinado caractere é igual ao seu “código
ASCII” (isto é, ao valor associado a esse caractere no código ASCII).
Valores de tipo char incluem apenas valores não-negativos. O
tipo char é, portanto, diferente do tipo short, que
inclui valores positivos e negativos. Conversões entre esses tipos,
e conversões de tipo em geral, são abordadas na Seção
3.10.
As seguintes funções ilustram o uso de caracteres em C:
|
Essas funções determinam, respectivamante, se o caractere passado como argumento é uma letra minúscula, uma letra maiúscula, ou um algarismo decimal (ou dígito).
A função a seguir ilustra o uso de caracteres como inteiros, usada na transformação de letras minúsculas em maiúsculas, usando o fato de que letras minúsculas e maiúsculas consecutivas têm (assim como dígitos) valores consecutivos no código ASCII:
|
Note que o comportamento dessa função não depende dos valores usados para representação de nenhuma letra, mas apenas do fato de que letras consecutivas têm valores consecutivos no código ASCII usado para sua representação. Por exemplo, a avaliação de:
|
tem como resultado o valor dado por ’b’ + (’A’ - ’a’), que é igual a ’A’ + 1, ou seja, ’B’.
Como mencionado na seção 3.4, caracteres podem ser expressos também por meio do valor da sua representação no código ASCII. Por exemplo, o caractere chamado nulo, que é representado com o valor 0, pode ser denotado por:
|
Nessa notação, o valor associado ao caractere no código ASCII (“código ASCII do caractere”) é escrito na base hexadecimal (usando os algarismos hexadecimais 0 a F, que correspondem aos valores de 0 a 15, representáveis com 4 bits), precedido pela letra x minúscula.
Valores inteiros podem ser usados em um programa para representar valores de conjuntos diversos, segundo uma convenção escolhida pelo programador em cada situação. Por exemplo, para distinguir entre diferentes tipos de triângulos (equilátero, isósceles ou escaleno), podemos usar a convenção de que um triângulo equilátero corresponde ao inteiro 1, isósceles ao 2, escaleno ao 3, e qualquer outro valor inteiro indica “não é um triângulo”.
Considere o problema de definir uma função tipoTriang que,
dados três números inteiros positivos, determina se eles podem
formar um triângulo (com lados de comprimento igual a cada um desses
valores) e, em caso positivo, determina se o triângulo formado
é um triângulo equilátero, isósceles ou escaleno. Essa
função pode ser implementada em C como na Figura 3.5.
const int NaoETriang = 0; const int Equilatero = 1; const int Isosceles = 2; const int Escaleno = 3; int tipoTriang (int a, int b, int c) { return (eTriang(a, b, c) ? (a == b && b == c) ? Equilatero : (a == b || b == c || a == c) ? Isosceles : Escaleno) ) : NaoETriang; }
Figura 3.2: Constantes em C
Para evitar o uso de valores inteiros (0, 1, 2 e
3, no caso) para indicar, respectivamente, que os lados não
formam um triângulo, ou que formam um triângulo equilátero,
isósceles ou escaleno, usamos nomes correspondentes —
NaoETriangulo, Equilatero, Isosceles e
Escaleno. Isso torna o programa mais legível e mais fácil
de ser modificado, caso necessário.
O atributo const, em uma declaração de variável, indica
que o valor dessa variável não pode ser modificado no programa,
permanecendo sempre igual ao valor inicial especificado na
declaração dessa variável, que tem então que ser
especificado. Essa “variável” é então, de fato, uma constante.
Em vez de atribuir explicitamente nomes a valores numéricos componentes de um mesmo tipo de dados, por meio de declarações com o atributo const, como foi feito na seção anterior, podemos declarar um tipo enumeração. Por exemplo, no caso das constantes que representam tipos de triângulos que podem ser formados, de acordo com o número de lados iguais, podemos definir:
|
Por exemplo, o programa da seção anterior (Figura 3.5) pode
então ser reescrito como mostrado na Figura 3.6. Em C, a
palavra reservada enum deve ser usada antes do nome do tipo
enumeração.
enum TipoTriang { NaoETriang, Equilatero, Isosceles, Escaleno }; enum TipoTriang tipoTriang (int a, int b, int c) { return (eTriang(a,b,c) ? (a == b && b == c) ? Equilatero : (a == b || b == c || a == c) ? Isosceles : Escaleno ) : NaoETriang; }
Figura 3.3: Tipo-Enumeração
O uso de tipos-enumerações, como no programa da Figura 3.6, apresenta as seguintes vantagens, em relação ao uso de nomes de constantes (como no programa da Figura 3.5):
Por exemplo, no programa da Figura 3.6, o tipo do
resultado da função tipoTriang é TipoTriang, em vez de
int. Isso documenta o fato de que o valor retornado pela
função é um dos tipos de triângulo especificados na declaração do
tipo TipoTriang (i.e. um dos valores NaoETriang,
Equilatero, Isosceles, Escaleno).
Por exemplo, no programa da Figura 3.6, é garantido que um literal usado para denotar o valor retornado pela função não é um inteiro qualquer, mas sim um dos valores definidos no tipo-enumeração.
Uma expressão em C é sempre avaliada da esquerda para a direita, respeitando-se contudo a precedência predefinida para operadores e a precedência especificada pelo uso de parênteses.
O uso de parênteses em uma expressão é, em alguns casos, opcional, apenas contribuindo para tornar o programa mais legível. Por exemplo, a expressão 3+(5*4) é equivalente a 3+5*4, uma vez que o operador de multiplicação (*) tem maior precedência do que o de adição (+) (o que resulta na avaliação da multiplicação antes da adição). Outro exemplo é 3<5*4, que é equivalente a 3<(5*4).
Em outros casos, o uso de parênteses é necessário, tal como, por exemplo, na expressão 3*(5+4) — a ausência dos parênteses, nesse caso, mudaria o resultado da expressão (pois 3*5+4 fornece o mesmo resultado que (3*5)+4).
Os operadores aritméticos em C são mostrados na Tabela 3.2. A precedência desses operadores, assim como de outros operadores predefinidos usados mais comumente, é apresentada na Tabela 3.3. O estabelecimento de uma determinada precedência entre operadores tem como propósito reduzir o número de parênteses usados em expressões.
Tabela 3.2: Operadores aritméticos em C
Operador Significado Exemplo Resultado + Adição 2 + 1 3 2 + 1.0 3.0 2.0 + 1.0 3.0 - Subtração 2 - 1 1 2.0 - 1 1.0 2.0 - 1.0 1.0 - Negação -1 -1 -1.0 -1.0 * Multiplicação 2 * 3 6 2.0 * 3 6.0 2.0 * 3.0 6.0 / Divisão 5 / 2 2 5 / 2.0 2.5 5.0 / 2.0 2.5 % Resto 5 % 2 1 5.0 % 2.0 1.0
Tabela 3.3: Precedência de operadores
Precedência maior Operadores Exemplos *, /, % i * j / k ≡ (i * j) / k +, - i + j * k ≡ i + (j*k) >, <, >=, <= i < j + k ≡ i < (j+k) ==,!= b == i < j ≡ b == (i < j) & b & b1 == b2 ≡ b & (b1 == b2) ^ i ^ j & k ≡ i ^ (j & k) | i < j | b1 & b2 ≡ (i < j) | (b1 & b2) && i + j != k && b1 ≡ ((i + j) != k) && b1 || i1 < i2 || b && j < k ≡ (i1<i2) || (b && (j<k)) ? : i < j || b ? b1 : b2 ≡ ((i<j) || b) ? b1 : b2 Precedência menor
Note que a divisão de um valor inteiro (de tipo int, com ou sem
qualificadores) por outro valor inteiro, chamada em computação de divisão inteira, retorna em C o quociente da divisão (a parte
fracionária, se existir, é descartada). Veja o exemplo da Tabela
3.2: 5/2 é igual a 2. Para obter
um resultado com parte fracionária, é necessário que pelo menos um dos
argumentos seja um valor de ponto flutuante.
O programa a seguir ilustra que não é uma boa prática de programação escrever programas que dependam de forma crítica da ordem de avaliação de expressões, pois isso torna o programa mais difícil de ser entendido e pode originar resultados inesperados, quando esse programa é modificado.
A execução do programa abaixo imprime 4 em vez de 20. Note como o resultado depende criticamente da ordem de
avaliação das expressões, devido ao uso de um comando de
atribuição como uma expressão. Nesse caso, o comando de
atribuição ($i$=2) é usado como uma expressão (que
retorna, nesse caso, o valor 2), tendo como “efeito
colateral” modificar o valor armazenado na variável
i.
|
Existem operadores lógicos (também chamados de operadores booleanos) — que operam com valores falso ou verdadeiro, representados em C como inteiros zero e diferente de zero — predefinidos em C, que são bastante úteis. Eles estão relacionados na Tabela 3.4 e são descritos a seguir.
Tabela 3.4: Operadores lógicos em C
Operador Significado ! Negação && Conjunção (não-estrita) (“ê”) || Disjunção (não-estrita) (“ou”)
A operação de negação (ou complemento) é representada por !; ela realiza a negação do argumento (de modo que um valor falso (zero) fique verdadeiro (diferente de zero), e o inverso também é verdadeiro: a negação de um valor verdadeiro em C (diferente de zero) é falso (zero em C).
A operação de conjunção lógica é representada por && (lê-se “ê”): e1 && e2 é verdadeiro (diferente de zero) se somente se a avaliação de cada uma das expressões, e1 e e2 tem como resultado o valor verdadeiro.
O operador && é um operador não-estrito, no segundo argumento; isso significa que a avaliação de e1 && e2 pode retornar um resultado verdadeiro mesmo que a avaliação de e2 não retorne nenhum valor válido; por exemplo:
|
retorna falso (0).
Note que o operador de conjunção bit-a-bit, &, é um operador estrito (nos dois argumentos), ou seja, a avaliação de e1 & e2 retorna um resultado se e somente se a avaliação de e1 e de e2 retornam. Por exemplo,
|
provoca a ocorrência de um erro (pois 0/0 provoca a ocorrência de um erro).
A operação de conjunção não-estrita é definida como a seguir: o valor de e1 && e2 é igual ao de e1 se esse for falso (0); caso contrário, é igual ao valor de e2. Dessa forma, e2 só é avaliado se a avaliação de e1 fornecer valor verdadeiro (diferente de zero).
Ao contrário, a avaliação de e1 & e2 sempre envolve a avaliação tanto de e1 quanto de e2.
Analogamente, a operação de disjunção lógica é representada por || (lê-se “ou”): e1 || e2 é igual a falso somente se a avaliação de cada uma das expressões, e1 e e2, tem como resultado o valor falso.
Observações análogas às feitas anteriormente, para os operadores de conjunção bit-a-bit & e conjunção lógica não-estrita &&, são válidas para os operadores de disjunção bit-a-bit | e disjunção lógica não-estrita ||.
Nesse caso, temos que e1 || e2 é igual a verdadeiro (diferente de zero) se e1 for igual a verdadeiro, e igual a e2 em caso contrário.
Existe ainda predefinido em C o operador ou-exclusivo bit-a-bit ^.
Um programa en C consiste em uma sequência de uma ou mais definições
de funções, sendo que uma dessas funções, de nome main, é a
função que inicia a execução do programa. A definição de uma função de
nome main deve sempre estar presente, para que uma sequência de
definições de funções forme um programa C.
A assinatura — ou interface — de uma função é uma definição de uma função que omite o corpo (sequência de comandos que é executada quando a função é chamada) da função, mas especifica o nome, o tipo de cada argumento e do resultado da função.
Em uma assinatura, os nomes dos argumentos são opcionais (mas os tipos são necessários).
A assinatura da função main de um programa C deve ser:
|
ou
|
A primeira assinatura especifica que a função main não tem
parâmetros e o tipo do resultado é int. Esse valor é usado para
especificar, para o sistema operacional, que a função foi executada
normalmente, sem causar nenhum erro (nesse caso, o valor zero é
retornado), ou para especificar que a execução da função provocou a
ocorrência de algum erro (nesse caso, um valor diferente de zero é
retornado). Como o sistema operacional pode usar uma convenção
diferente para indicar a presença ou ausência de erro, é boa prática
usar as constantes EXIT_SUCCESS e EXIT_FAILURE, definidos
na biblioteca stdlib, para indicar respectivamente sucesso e
falha da execução.
A linguagem C permite o uso de funções de bibliotecas, que requerem o uso de diretivas de preprocessamento. Como mencionado na seção 3.2, Uma diretiva de preprocessamento começa com o caractere #, e deve ser inserida na primeira coluna de uma linha. Uma diretiva de preprocessamento deve começar sempre na primeira coluna de uma linha. Após o caractere # vem o nome do arquivo a ser lido, o qual deve ter uma extensão .h, entre os caracteres < e >, no caso de um arquivo de uma biblioteca padrão. Para arquivos com extensão .h definido pelo programador, o nome do arquivo deve ser inserido entre aspas duplas (como por exemplo em "interface.h", sendo interface.h o nome do arquivo).
A diretiva #include <stdio.h> é a diretiva mais comumente usada
em programas C.
Existem muitas funções definidas em bibliotecas da linguagem C, e a questão, bastante comum para iniciantes no aprendizado de programação em uma determinada linguagem, de quando existe ou não uma função definida em uma biblioteca, só pode ser respondida em geral por experiência ou pesquisa em textos sobre a linguagem e as bibliotecas específicas. As bibliotecas mais comuns usadas em programas C são as seguintes (são incluídas a assinatura e uma descrição sucinta de algumas funções de cada biblioteca):
stdio: contém funções para entrada e saída em
dispositivos padrão, como printf e scanf (descritas
na seção 3.2).string: contém funções para manipulação de strings (seção 5.5).ctype: contém funções sobre valores de tipo char, como:
int isdigit(char): retorna verdadeiro (diferente de zero) se o argumento é um dígito (de ’0’ a ’9’),
isalpha(char): retorna verdadeiro se o argumento é uma letra,
int isalnum(char): retorna verdadeiro se o argumento é uma letra ou um dígito,
islower(char): retorna verdadeiro se o argumento é uma letra minúscula,
int isupper(char): retorna verdadeiro se o argumento é uma letra maiúscula
math: contém funções matemáticas, como:
double cos(double): retorna o cosseno do argumento,
double sen(double): retorna o seno do argumento,
double exp(double): retorna e elevado ao argumento, onde e é a base do logarítmo natural (constante de Euler),
double fabs(double): retorna o valor absoluto do argumento,
double sqrt(double): retorna a raiz quadrada do argumento,
double pow(double a, double b): retorna ab (a elevado a b),
double pi: uma representação aproximada do número irracional π.
stdlib: contém funções diversas, para diversos fins, como:
int abs(int): retorna o valor absoluto do argumento,
long labs(long): retorna o valor absoluto do argumento,
void srand(unsigned int): especifica a semente do gerador de números pseudo-aleatórios rand,
int rand(): retorna um número pseudo-aleatório entre 0 e RAND_MAX,
int RAND_MAX: valor maior ou igual a 32767, usado pelo gerador de números pseudo-aleatórios rand,
atoi, atol, atof convertem, respectivamente, string para valor
de tipo int, long int e double (veja seção 5.5),
malloc aloca memória dinamicamente (seção 6);
int EXIT_SUCCESS: valor usado para indicar que a execução de um programa ocorreu com sucesso,
int EXIT_FAILURE: valor usado para indicar que ocorreu alguma falha na execução de um programa.
Na seção 3.9 mostraremos como um programa em C pode ser dividido em várias unidades de compilação, cada uma armazenada em um arquivo, e como nomes definidos em uma unidade de compilação são usados em outra unidade.
Conversões de tipo podem ocorrer, em C, nos seguintes casos:
Em geral há converesão automática (implícita) quando um valor é de um
tipo numérico t0 que tem um conjunto de valores contido em outro
tipo numérico t. Neste caso, o valor de tipo t0 é convertido
automaticamente, sem alteração, para um valor de tipo t. Isso
ocorre, por exemplo, quando t0 é char ou short ou
byte e t é igual a int.
Uma conversão de tipo explícita tem a seguinte sintaxe:
| (t) e |
onde t é um tipo e e uma expressão. A conversão de tipo indica que o valor resultante da avaliação da expressão e deve ser convertido para o tipo t. Em geral, a conversão de tipo pode resultar em alteração de um valor pelo truncamento de bits mais significativos (apenas os bits menos significativos são mantidos), no caso de conversão para um tipo t cujos valores são representados por menos bits do que valores do tipo da expressão e. No caso de uma conversão para um tipo t cujos valores são representados por mais bits do que valores do tipo da expressão e, há uma extensão do bit mais à esquerda do valor de e para completar os bits da representação desse valor como um valor do tipo t.
Solução:
|
O valor retornado pela função show é um string. Em C,
um string é um ponteiro para um valor de tipo char (ou,
equivalentemente em C, um arranjo de caracteres). Isso será explicado
mais detalhadamente na seção 5.5.
tipoTriang de modo
a utilizar um menor número de operações (de igualdade e de
disjunção), supondo que, em toda chamada a essa função, os
argumentos correspondentes aos parâmetros a, b e c
são passados em ordem não-decrescente.Solução:
| 1 || | 0/0 == 0 |
| 1 | | 0/0 == 0 |
A avaliação da primeira expressão fornece resultado verdadeiro, e a avaliação da segunda provoca a ocorrência de um erro.
&& e || usando uma
expressão condicional. Solução: Para quaisquer expressões booleanas a e
b, a operação de conjunção a && b tem o mesmo
comportamento que:
|
Note que o valor da expressão é falso quando o valor de a
é falso, independentemente do valor de b (ou mesmo de a
avaliação de b terminar ou não, ou ocasionar ou não um
erro).
Antes de ver a resposta abaixo, procure pensar e escrever uma
expressão condicional análoga à usada acima, mas que tenha o
mesmo comportamento que a || b.
A expressão desejada (que tem o mesmo significado que a || b)
é:
|
eQuadrado, definida a seguir, recebe como
argumentos quatro valores inteiros e retorna verdadeiro se esses
valores podem formar os lados de um quadrado, e falso em caso
contrário.
|
Escreva um programa que leia quatro valores inteiros e imprima uma mensagem indicando se eles podem ou não ser tamanhos dos lados de um retângulo, usando função que, dados quatro números inteiros, retorna verdadeiro se eles podem representar lados de um retângulo, e falso em caso contrário.
|
Reescreva a definição de bissexto de maneira a usar uma
expressão condicional em vez de usar, como acima, os operadores
lógicos && e ||.
Escreva um programa que leia um valor inteiro e responda se ele é ou não um ano bissexto, usando a função definida acima com a expressão condicional.
somaD3 que, dado um número
inteiro representado com até três algarismos, fornece como
resultado a soma dos números representados por esses
algarismos. Exemplo: somaD3(123) deve fornecer resultado 6.Escreva um programa que leia um valor inteiro, e imprima o resultado
da soma dos algarismos do número lido, usando a função
somaD3. Note que você pode supor que o inteiro lido contém no
máximo 3 algarismos (o seu programa deve funcionar corretamente apenas
nesses casos).
inverteD3 que,
dado um número representado com até três algarismos, fornece
como resultado o número cuja representação é obtida
invertendo a ordem desses algarismos. Por exemplo: o resultado de
inverteD3(123) deve ser 321.Escreva um programa que leia um valor inteiro, e imprima o resultado
de inverter os algarismos desse valor, usando a função
inverteD3. Note que você pode supor que o inteiro lido contém no
máximo 3 algarismos (o seu programa deve funcionar corretamente apenas
nesses casos).
pi o
valor 3.1415:
|
Use essa definição do valor de pi para definir uma
função que retorna o comprimento aproximado de uma
circunferência, dado o raio.
Escreva um programa que leia um número de ponto flutuante, e imprima o comprimento de uma circunferência que tem raio igual ao valor lido, usando a função definida acima. Por simplicidade, você pode supor que o valor lido é um número de ponto flutuante algarismos (o seu programa deve funcionar corretamente apenas nesse caso).
Escreva um programa que leia 5 valores inteiros, e imprima o resultado de determinar se o conjunto formado pelos 2 últimos é um subconjunto daquele formado pelos três primeiros, usando a função definida acima. Por simplicidade, você pode supor que os valores lidos são todos inteiros (o seu programa deve funcionar corretamente apenas nesse caso).
| Nota | Conceito |
| 0 a 59 | R |
| 60 a 74 | C |
| 75 a 89 | B |
| 90 a 100 | A |
Escreva um programa que leia um valor inteiro, e imprima a nota correspondente, usando a função definida acima. Por simplicidade, você pode supor que o valor lido é um valor inteiro (o seu programa deve funcionar corretamente apenas nesse caso).
Escreva um programa que leia 2 caracteres, cada um deles um algarismo, e imprima o maior número inteiro que pode ser escrito com esses dois algarismos. Por simplicidade, você pode supor que os valores lidos são de fato um caractere que é um algarismo (o seu programa deve funcionar corretamente apenas nesse caso).
|
Escreva um programa que leia um número inteiro e um caractere, que você pode supor que sejam respectivamente o peso de uma pessoa e um dos caracteres dentre ’M’, ’m’, ’F’, ’f’, e imprima o peso ideal para uma pessoa, usando a função definida acima. Você pode supor que os dados de entrada estão corretos (o seu programa deve funcionar corretamente apenas nesse caso).