Previous Up Next
Programação de Computadores em C

Capítulo 3  Primeiros Problemas

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:

  1. dado um número inteiro, retornar o seu quadrado;
  2. dados dois números inteiros, retornar a soma dos seus quadrados;
  3. dados três números inteiros, determinar se são todos iguais ou não;
  4. dados três números inteiros, a, b e c, determinar se eles podem representar os lados de um triângulo ou não (isso é, se existe um triângulo com lados de comprimentos iguais a a, b e c).
  5. dados dois números inteiros, retornar o máximo entre eles;
  6. dados três números inteiros, retornar o máximo entre eles.

3.1  Funções sobre Inteiros e Seleção


/****************************** * * * 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:

  1. Inicialmente é especificado o tipo do valor fornecido como resultado, em uma chamada (aplicação) da função.

    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).

  2. Em seguida vem o nome da função, e depois, entre parênteses, a lista dos seus parâmetros (que pode ser vazia).

    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.

  3. Finalmente, é definido o corpo do método, que consiste em um bloco, ou seja, uma sequência de comandos, delimitada por “{” (abre-chaves) e “}” (fecha-chaves).

A execução do corpo do método quadradoreturn   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:

return e;

é 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:

tresIguais (e1,e2,e3)

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
OperadorSignificadoExemploResultado
==Igual a1 == 1verdadeiro
!=Diferente de1 != 1falso
< Menor que1 < 1falso
> Maior que1 > 1falso
<=Menor ou igual a1 <= 1verdadeiro
>=Maior ou igual a1 >= 1verdadeiro


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:

e ? e1 : e2

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:

int max (int a, int b) { return (a >= b ? a : b); } int max3 (int a, int b, int c) { return (a >= b ? max(a,c) : max(b,c)); }

3.2  Entrada e Saída

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

#include <stdio.h>

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:

int x = 10; printf("Resultado = %d", x);

faz com que o valor da variável inteira x (10) seja impresso em notação decimal, precidido da sequência de caracteres "Resultado = ", ou seja, faz com que seja impresso:

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:

int x = 10; printf("Resultado = %d", x);

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 "Resultado = ", ou seja, faz com que seja impresso:

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:

scanf("%d", &a);

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 "%d" 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).

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:

scanf("%d%d", &a, &b);

Podemos fazer agora nosso primeiro programa, que lê dois inteiros e imprime o maior dentre eles, como a seguir:

#include <stdio.h> int max (int a, int b) { return (a>=b ? a : b); } int main() { int v1, v2; printf("Digite dois inteiros "); scanf("%d%d", &v1, &v2); printf("Maior dentre os valores digitados = %d\n", max (v1, v2)); }

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 "interface.h", sendo interface.h o nome do arquivo).

A diretiva #include <stdio.h> é a diretiva mais comumente usada em programas C.

3.2.1  Entrada e Saída em Arquivos via Redirecionamento

É 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 

3.2.2  Especificações de Formato

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 controleTipo
%d (ou %i)int
%cchar
%ffloat
%lfdouble
%schar*

%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 "0x", e como octal quando precedida de "0". Por exemplo, a cadeia de caracteres "031" é lida como 31 usando %d, mas como 25 usando %i (25 = 3 × 8 + 1).

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:

"%n.pt"

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:

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
#include <stdio.h> int main () { char c; int fim = scanf("%c", &c); while (fim != EOF) { printf("%c", c); fim = scanf("%c", &c); } return 0; }
ecoar2.c
#include <stdio.h> int main() { int c = getchar(); while (c != EOF) { printf("%c", c); c = getchar(); } return 0; }

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.

3.3  Números

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:

sizeof(short) ≤ sizeof(int) ≤ sizeof(long) sizeof(short) ≥ 16 bits} sizeof(int)} ≥ 16 bits sizeof(long) ≥ 32 bits sizeof(long long int}) ≥ 64 bits sizeof(float) ≤ sizeof(double) ≤ sizeof(long double)

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:

short s = 10;

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

3.3.1  Consequências de uma representação finita

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.

3.4  Caracteres

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:

caractereindica
'\n'terminação de linha
'\t'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:

int minusc (char x) { return (x >= 'a') && (x <= 'z'); } int maiusc (char x) { return (x >= 'A') && (x <= 'Z'); } int digito (char x) { return (x >= '0') && (x <= '9'); }

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:

char minusc_maiusc (char x) { int d = 'A' - 'a'; return (minusc(x)) ? x+d : x; }

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:

minusc_maiusc ('b')

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:

'\x0000'

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.

3.5  Constantes

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.

3.6  Enumerações

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:

enum TipoTriang { NaoETriang, Equilatero, Isosceles, Escaleno }

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):

3.7  Ordem de Avaliação de Expressões

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
OperadorSignificadoExemploResultado
+Adição2 + 13
  2 + 1.03.0
  2.0 + 1.03.0
-Subtração2 - 11
  2.0 - 11.0
  2.0 - 1.01.0
-Negação-1-1
  -1.0-1.0
*Multiplicação2 * 36
  2.0 * 36.0
  2.0 * 3.06.0
/Divisão5 / 22
  5 / 2.02.5
  5.0 / 2.02.5
%Resto5 % 21
  5.0 % 2.01.0



Tabela 3.3: Precedência de operadores
Precedência maior
OperadoresExemplos
*, /, %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.

int main () { int i = 10, int j = (i=2) * i; printf("%d", j); }

3.8  Operações lógicas

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
OperadorSignificado
!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:

0 && (0/0 == 0)

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,

0 & (0/0 == 0)

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 ^.

3.9  Programas e Bibliotecas

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:

int main (void)

ou

int main (int argc, char* argv[])

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):

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.

3.10  Conversão de Tipo

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.

3.11  Exercícios Resolvidos

  1. Escreva um programa que leia três valores inteiros e imprima uma mensagem que indique se eles podem ou não ser lados de um triângulo e, em caso positivo, se o triângulo formado é equilátero, isósceles ou escaleno.

    Soluçã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; } char* show (enum TipoTriang t) { return (t==Equilatero ? "equilatero" : t==Isosceles ? "isosceles" : "escaleno"); } int main() { int a, b, c; printf("Digite 3 valores inteiros: "); scanf("%d%d%d", &a, &b, &c); enum Tipotriang t = tipoTriang(a, b, c); printf("Os valores digitados "); if (t == NaoETriang) printf("nao podem formar um triangulo\n"); else printf("formam um triangulo \%s\n", show(t)); }

    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.

  2. Simplifique a definição da função 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.
  3. Como vimos na Seção 3.8, a avaliação da expressão 0 && 0/0 == 0 tem resultado diferente da avaliação da expressão 0 & 0/0 == 0. Construa duas expressões análogas, usando || e |, para as quais a avaliação fornece resultados diferentes.

    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.

  4. Defina os operadores lógicos && 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:

    a ? b : 0

    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) é:

    a ? 1 : b

3.12  Exercícios

  1. Sabendo que a ordem de avaliação de expressões em C é da esquerda para a direita, respeitando contudo a precedência de operadores e o uso de parênteses, indique qual é o resultado da avaliação das seguintes expressões (consulte a Tabela 3.3, se necessário):
    1. 2 + 4 - 3
    2. 4 - 3 * 5
    3. (4 - 1) * 4 - 2
    4. 2 >= 1 && 2 != 1
  2. A função 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.
    int eQuadrado (int a, int b, int c, int d) { // todos os valores são positivos, e iguais entre si return (a>0) && (b>0) && (c>0) && (d>0) && (a==b && b==c && c==d); }

    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.

  3. A seguinte definição de função determina se um dado valor inteiro positivo representa um ano bissexto ou não. No calendário gregoriano, usado atualmente, um ano é bissexto se for divisível por 4 e não for divisível por 100, ou se for divisível por 400.
    int bissexto (int ano) { return ((ano % 4 == 0 && ano % 100 != 0) || ano % 400 == 0 ); }

    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.

  4. Defina uma função 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).

  5. Defina uma função 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).

  6. Considere a seguinte definição, que associa a pi o valor 3.1415:
    final float pi = 3.1415f;

    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).

  7. Defina uma função que, dados cinco números inteiros, retorna verdadeiro (inteiro diferente de zero) se o conjunto formado pelos 2 últimos números é um subconjunto daquele formado pelos 3 primeiros, e falso em caso contrário.

    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).

  8. Defina uma função que, dado um valor inteiro não-negativo que representa a nota de um aluno em uma disciplina, retorna o caractere que representa o conceito obtido por esse aluno nessa disciplina, de acordo com a tabela:
    NotaConceito
    0 a 59R
    60 a 74C
    75 a 89B
    90 a 100A

    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).

  9. Defina uma função que, dados dois caracteres, cada um deles um algarismo, retorna o maior número inteiro que pode ser escrito com esses dois algarismos. Você não precisa considerar o caso em que os caracteres dados não são algarismos.

    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).

  10. Escreva uma função que, dados um número inteiro e um caractere — representando respectivamente a altura e o sexo de uma pessoa, sendo o sexo masculino representado por ’M’ ou ’m’ e o sexo feminino representado por ’F’ ou ’f’ —, retorna o peso supostamente ideal para essa pessoa, de acordo com a tabela:
      
    homensmulheres
    (72,7 × altura) − 58(62,1 × altura) − 44,7

    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).


Previous Up Next