Abordamos a seguir, nos Capítulos 5 a 7, a definição e o uso de estruturas de dados, que são valores compostos por valores mais simples: arranjos (capítulo 5), ponteiros (capítulo 6) e registros (ou, como são chamados em C, estruturas, capítulo 7). Uma introdução à definição e uso de estruturas de dados encadeadas, formadas com o uso de registros com ponteiros, é apresentada na seção 7.
Arranjos são estruturas de dados homogêneas, devido ao fato de que os componentes têm que ser todos de um mesmo tipo, enquanto estruturas de dados encadeadas e registros em geral são estruturas de dados heterogêneas, que podem envolver componentes de vários tipos, diferentes entre si. Como veremos na seção 5.5, cadeias de caracteres são, na linguagem C, arranjos terminados com um caractere especial (o caractere ’\0’).
Arranjos são estruturas de dados muito usadas em programas. Um arranjo é uma forma de representar uma função finita (função de domínio finito) — que em geral é vista como uma tabela ou uma seqüência finita de valores — com a característica de que o acesso aos seus componentes podem ser feitos de modo eficiente. Esta seção aborda a definição e uso de arranjos na linguagem C.
Um arranjo é uma estrutura de dados formada por um certo número finito de componentes (também chamados de posições do arranjo) de um mesmo tipo, sendo cada componente identificado por um índice.
Um arranjo tem um tamanho, que é o número de componentes do arranjo. A uma variável de tipo arranjo de um tipo T e tamanho n correspondem n variáveis de tipo T.
Em C, os índices de um arranjo são sempre inteiros que variam de 0 a n-1, onde n é o tamanho do arranjo.
Se v é uma expressão que representa um
arranjo de tamanho n, e i é uma expressão de tipo int
com valor entre 0 e n-1, então v[i]
representa um componente do arranjo v.
Nota sobre uso de índices fora do limite em C: A linguagem C não especifica que, em uma operação de indexação (uso de um valor como índice) de um arranjo, deva existir uma verificação de que esse índice é um índice válido. A linguagem simplesmente deixa a responsabilidade para o programador. Se o índice estiver fora dos limites válidos em uma indexação, uma área de memória distinta da área alocada para o arranjo será usada, ou o programa é interrompido, com uma mensagem de que um erro ocorreu devido a um acesso ilegal a uma área de memória que não pode ser usada pelo processo corrente. Se o índice for inválido mas estiver dentro da área reservada ao processo corrente, nenhum erro em tempo de execução será detectado. O erro devido a um acesso ilegal a uma área de memória não reservada ao processo corrente provoca a emissão da mensagem segmentation fault, e a interrupção da execução do processo corrente. O motivo de não existir verificação de que um índice de um arranjo está ou não entre os limites desse arranjo é, obviamente, eficiência (i.e. evitar gasto de tempo de execução). O programador deve estar ciente disso e atento de modo a evitar erros (i.e. evitar o uso de índices fora dos limites válidos em indexações de arranjos).
A eficiência que existe no acesso a componentes de um arranjo se deve ao fato de que arranjos são geralmente armazenados em posições contíguas da memória de um computador, e o acesso à i-ésima posição é feito diretamente, sem necessidade de acesso a outras posições. Isso espelha o funcionamento da memória de computadores, para a qual o tempo de acesso a qualquer endereço de memória é o mesmo, ou seja, independe do valor desse endereço. Um arranjo é, por isso, chamado de uma estrutura de dados de acesso direto (ou acesso indexado). Ao contrário, em uma estrutura de dados de acesso sequencial, o acesso ao i-ésimo componente requer o acesso aos componentes de índice inferior a i.
Em C, arranjos (valores de tipo arranjo) podem ser criados no instante da declaração de variáveis do tipo arranjo. Note, no entanto, que uma variável que representa um arranjo dinâmico em C armazena não um valor de tipo arranjo mas o endereço do primeiro componente do arranjo.
A versão C-99 da linguagem permite a declaração de arranjos dentro de funções com tamanho que é conhecido apenas dinamicamente.
É comum que uma variável de tipo arranjo tenha um tamanho conhecido apenas dinamicamente (em tempo de execução), como por exemplo quando é parâmetro de uma função. O tamanho, nesse caso, deve ser especificado como um parâmetro adicional na chamada à função (a função deve ter um parâmetro adicional que representa o tamanho).
O tamanho de um arranjo não faz parte do seu tipo (em geral, esse tamanho não é conhecido estaticamente), mas não pode ser modificado. Considere os seguintes exemplos:
|
As declarações acima criam
as variáveis ai e ac. A primeira é um arranjo de 3
inteiros e a segunda um arranjo de 4 caracteres.
A declaração dessas variáveis de tipo arranjo envolvem também a
criação de um valor de tipo arranjo. Na declaração de ai, é
criada uma área de memória com tamanho igual ao de 3 variáveis de tipo
int. Similarmente, na declaração de ac, é criada uma área
de memória com tamanho igual ao de 4 variáveis de tipo char. Os
valores contidos nessas áreas de memória não são conhecidos: são
usados os valores que estão já armazenados nessas áreas de memória.
A função malloc, definida na biblioteca stdlib, aloca
dinamicamente uma porção de memória de um certo tamanho, passado como
argumento da função, e retorna o endereço da área de memória alocada.
Esse endereço é retornado com o valor de um ponteiro para o primeiro
componente do arranjo. Ponteiros são abordados mais detalhadamente na
seção 6. Por enquanto, considere apenas que malloc
aloca uma área de memória — na área de memória dinâmica do
processo corrente, chamada em inglês de heap — que será
usada tipicamente por meio da operação de indexação do
arranjo. Considere os seguintes comandos:
|
A chamada malloc(3*sizeof(int)) aloca uma área de memória de
tamanho 3× x, se x é o tamanho da unidade de memória ocupada
por valores de tipo int.
Similarmente, a chamada malloc(4 * sizeof(char)) alloca uma área
de memória de tamanho 4*(sizeof(char)), sendo sizeof(char)
o tamanho de uma área de memória ocupada por um valor de tipo
char.
Após a atribuição à variável pi acima, pi contém o
endereço da primeira posição de um arranjo com 3 componentes de tipo
int. Analogamente, após a atribuição à variável pc acima,
ac contém o endereço da primeira posição de um arranjo com 4
componentes de tipo char.
No caso da declaração de variáveis com um tipo que é explicitamente
indicado como sendo um tipo arranjo, para o qual o tamanho é indicado
explicitamente (como no exemplo anterior das variáveis pi e
pc), o arranjo não é alocado na área dinâmica mas na área de
pilha da função na qual a declaração ocorre (no caso, na área de
memória alocada quando a execução da função main é iniciada).
O tamanho de uma área de memória alocada para variáveis de um tipo
pode ser obtido em C por meio do uso da função predefinida
sizeof. A palavra reservada sizeof pode ser seguida de um
nome de tipo (como nos exemplos acima) ou por uma expressão. O
resultado retornado pela avaliação de sizeof é o tamanho em
bytes da área alocada para variáveis do tipo ou expressão usada como
“argumento”. No caso de uso de um tipo, ele deve ser colocado entre
parênteses, mas quando uma expressão é usada, ela pode seguir
sizeof sem necessidade de parênteses (respeitando-se a
precedência de operadores e chamadas de funções).
Por exemplo, depois da atribuição acima, sizeof pi retorna o
mesmo que sizeof(3 * sizeof(int)).
A linguagem C usa colchetes em declaração de arranjos após o nome da variável; por exemplo, a declaração:
|
declara uma variável a de tipo arranjo, mas o tipo int
ocorre antes e a indicação de que esse tipo é um arranjo de
componentes de tipo int ocorre após o nome da variável.
Isso é feito com o intuito de permitir declarações um pouco mais
sucintas, como a seguinte, que cria uma variável b de tipo
int e um arranjo a com componentes de tipo int:
|
Como exemplo do uso de arranjo criados dinamicamente, vamos considerar o problema de ler um inteiro não-negativo n, em seguida n valores inteiros e imprimir os n inteiros na ordem inversa à que foram lidos.
Por exemplo, se forem lidos o inteiro 3, em seguida três inteiros i1, i2 e i3, o programa deve imprimir i3, i2, i1, nesta ordem. Uma solução é mostrada na Figura 5.1.
/*************************************************************** * Lê n, depois n inteiros do dispositivo de entrada padrão * * e imprime os n inteiros lidos em ordem inversa. * ***************************************************************/ #include stdio.h #include stdlib.h int main() { int *arr, i=0, n; scanf("%d", &n); arr = malloc(n * sizeof(int)); for (i=0; i<n; i++) scanf("%d", &arr[i]); for (i--; i>=0; i--) printf("%d ", arr[i]); }
Figura 5.1: Exemplo de uso de comando forpara percorrer arranjo
O programa da Figura 5.1 ilustra a operação básica de “percorrer” um arranjo para realização de alguma operação sobre os valores armazenados no arranjo.
O programa declara o tipo da variável arr não como um arranjo de
componentes de tipo int, mas como um ponteiro para variáveis do
tipo int. Isso ocorre porque o tamanho do arranjo só é conhecido
dinamicamente.
A alocacão de memória feita pela função malloc ocorre na área de
memória dinâmica. A função retorna o endereço inicial da área de
memória alocada.
A operação de indexação de arranjos funciona no caso de variáveis ou valores de tipo ponteiro exatamente como ocorre no caso de variáveis e valores de tipo arranjo. Mais detalhes sobre a relação entre arranjos e ponteiros em C estão na seção 6.
Outra observação importante é referente à necessidade de se especificar o tamanho do arranjo, ou seja, o número de valores inteiros a ser digitado, antes da leitura dos valores inteiros. Para evitar isso, há duas opções: i) não usar um arranjo, mas uma estrutura de dados encadeada, como descrito na seção 7, ou ii) adotar um certo valor como máximo para o número de valores a serem digitados (de modo a alocar um arranjo com tamanho igual a esse número máximo). Essa opção tem as desvantagens de que um número máximo pode não ser conhecido estaticamente ou ser difícil de ser determinado, e o uso de um valor máximo pode levar a alocação desnecessária de área significativa de memória, que não será usada (ou seja, o máximo pode ser um valor muito maior do que o de fato necessário). No caso de uso de um arranjo, é possível testar se o número máximo foi alcançado e realocar um arranjo maior caso isso ocorra, mas essa é uma operação dispendiosa em termos de esforço de programação e tempo de execução (requer não só testes para verificar se o número máximo foi alcançado mas, também, nesse caso, eventual necessidade de cópia de todo o arranjo para outra área de memória).
Para percorrer um arranjo, é usado tipicamente um comando for, pois o formato desse comando é apropriado para o uso de uma variável que controla a tarefa de percorrer um arranjo: iniciação do valor da variável usada para indexar o arranjo, teste para verificar se seu valor é ainda um índice válido do arranjo, e atualização do valor armazenado na variável.
A Figura 5.2 apresenta exemplos de
funções que realizam operações básicas bastante comuns sobre arranjos
de componentes de um tipo específico, int. As funções são: i)
preencher todos os componentes de um arranjo, passado como argumento,
com um valor, também passado como argumento, e ii) testar igualdade de
arranjos, passados como argumentos da função.
A primeira operação é uma função com “efeito colateral”. Isso significa que ela não é uma função que tem como domínio e contra-domínio os tipos anotados na definição da função, mas precisa, para poder ser considerada como função, que o domínio e contra-domínio abranjam (além dos parâmetros explicitamente indicados na definição da função) também uma forma de representação do estado da computação. O estado da computação pode ser representado na forma de uma função que associa variáveis a valores armazenados nessas variáveis.
void preenche(int arr[], int tam, int valor) { // Preenche todas as posicoes de arr com valor int i; for (i=0; i<tam; i++) arr[i] = valor; } int iguais(int arr1[], int tam1, int arr2[], int tam2) { // Retorna verdadeiro sse arr1 = arr2, componente a componente. int i; if (tam1 == tam2) { for (i=0; i<tam1; i++) if (arr1[i] != arr2[i]) return 0; return 1; } else return 0; }
Figura 5.2: Operações comuns em arranjos
Nas duas funções acima, preenche e iguais, o tamanho do
arranjo (número de posições alocadas) é um parâmetro da função. Isso é
feito para evitar o uso da função predefinida sizeof de C com
argumento que é um arranjo alocado dinamicamente: esta função,
anteriormente à definição do padrão C-99, só podia ser usada
quando o tamanho do arranjo era conhecido em tempo de compilação.
Note que o uso de == não é adequado para comparar igualdade de dois arranjos em C (i.e. comparar se dois arranjos têm o mesmo número de componentes e os conteúdos dos componentes em cada índice dos dois arranjos são iguais). O teste de igualdade de valores de tipo arranjo com == se refere a comparação apenas de ponteiros (i.e. comparação entre se os endereços da primeira posição do primeiro e do segundo arranjo são iguais).
O programa da Figura 5.3 ilustra o uso da função
iguais definida acima, escrevendo um programa que lê, nesta
ordem, um valor inteiro positivo n, 2 × n valores
inteiros, e em seguida imprime o resultado de testar se os n
primeiros dos 2 × n valores lidos são iguais aos n
últimos (ou seja, se o primeiro é igual ao n-ésimo mais um, o
segundo é igual ao n-ésimo mais dois, etc.).
#include <stdio.h> #include <stdlib.h> int main() { int *arr1, *arr2, n1, n2, i; printf("Digite inteiro positivo n1, n1 valores inteiros, "); printf("inteiro positivo n2, n2 valores inteiros\n"); scanf("%d", &n1); arr1 = malloc(n1 * sizeof(int)); for (i=0; i<n1; i++) scanf("%d", &(arr1[i])); scanf("%d", &n2); arr2 = malloc(n2 * sizeof(int)); for (i=0; i<n2; i++) scanf("%d", &(arr2[$i$])); printf("Sequencia 1 de valores inteiros e' \%s sequencia 2 de valores inteiros.", iguais(arr1,n1,arr2,n2) ? "igual a" : "diferente da"); return 0; }
Figura 5.3: Exemplo de uso de função que testa igualdade entre arranjos
Em C, uma cadeia de caracteres — em inglês, um string — é
um arranjo de caracteres que segue a convenção de que o último
componente é o caractere (chamado de caractere nulo). O
caractere '\0' é inserido automaticamente em literais de tipo
cadeia de caracteres, escritos entre aspas duplas. '\0'
Por exemplo:
|
é o mesmo que:
|
Note que são 5 componentes no arranjo str: o último componente,
de índice 4, contém o caractere , e é inserido
automaticamentre no literal de tipo cadeia de caracteres, e o tamanho
do arranjo deve ser igual ao número de caracteres no literal mais 1.'\0'
O uso de cadeias de caracteres em C, e em particular a operação
de ler uma cadeia de caracteres, deve levar em conta de que toda
variável que é uma cadeia de caracteres deve ter um tamanho fixo. Para
ler uma cadeia de caracteres, é necessário primeiro alocar espaço
— um tamanho máximo — para armazenar os caracteres que vão ser
lidos. No entanto, no caso de passagem de parâmetros para a função
main (veja seção 3.9), o sistema aloca,
automaticamente, cadeias de caracteres de tamanho suficiente para
armazenar os caracteres passados como parâmetros para a função
main).
O uso de cadeias de caracteres em C envolve muitas vezes o uso de
funções definidas na biblioteca string, dentre as quais
destacamos as seguintes:
| Assinatura | Significado |
int strlen (char *s) |
Retorna tamanho da cadeia s, excluindo caractere nulo no final de s
|
char* strcpy (char *dest, const char *fonte) |
Copia cadeia apontada por fonte, incluindo caractere que indica
terminação da cadeia, para dest; retorna fonte |
char* strcat (char *dest, const char *fonte) |
Insere cadeia apontada por fonte, incluindo caractere que indica
terminação da cadeia, no final da cadeia apontada por dest, sobrepondo primeiro caractere
de fonte com caractere nulo que termina dest; retorna cadeia dest atualizada |
int strcmp (const char *s1, const char *s2)
| Comparação lexicográfica entre cadeias
de caracteres, sendo retornado 0 se as cadeias são iguais, caractere a caractere, valor negativo se
s1<s2, lexicograficamente, e positivo caso contrário. |
O uso do atributo const na declaração de parâmetros em
assinaturas de funções acima significa que o parâmetro não é
modificado no corpo da função, e é usado por questão de legibilidade.
A comparação do conteúdo de duas cadeias de caracteres não pode ser
feita com o operador ==, pois esse operador, se aplicado a
valores de tipo char*, ou char[], testa igualdade de
ponteiros, e não do conteúdo apontado pelos ponteiros.
Em C, podem ocorrer erros difíceis de serem detectados se não for seguida a convenção de que uma cadeia de caracteres termina com o caractere nulo.
As funções acima não devem ser usadas se houver sobreposição de cadeias fonte e destino, sendo o comportamento dessas funções não especificado nesses casos.
A comparação lexicográfica entre duas cadeias s1 e s2
termina assim que ocorre uma diferença, e resulta em que s1 < s2
se o tamanho de s1 é menor que o de s2 ou se o caractere
diferente de s1 é menor do que o s2. A comparação entre
caracteres é feita pelo valor da representação no código ASCII. Por
exemplo: é menor que "abc" e que "ac", e maior
que "aa" e "a"."ab"
As
seguintes funções da biblioteca stdlib podem ser usadas para
converter uma cadeia de caracteres em um valor numérico:
| Assinatura | Significado |
int atoi (char *) | Converte cadeia de caracteres para valor de tipo int |
double atof (char *) | Converte cadeia de caracteres para valor de tipo double |
long atol (char *) | Converte cadeia de caracteres para valor de tipo long
|
Os significados são ilustrados em comentários no seguinte exemplo:
|
Note que:
Para conversão de um valor numérico em uma cadeia de caracteres, a
função sprintf, similar a printf, pode ser usada. Um uso
de sprintf tem o seguinte formato:
|
onde formato é um literal de tipo cadeia de caracteres de
controle da operação de escrita na cadeia de caracteres str e
v1,…,vn são argumentos (valores a serem escritos).
As especificações de controle em formato são feitas como no caso
de printf, usando o caractere % seguido de outro
caractere indicador do tipo de conversão a ser realizada.
A função sprintf tem um comportamento semelhante ao de
printf, com a diferença de que os valores são escritos na cadeia
de caracteres str, em vez de no dispositivo de saída padrão.
O tamanho da cadeia str deve ser suficiente para conter todos os
resultados da conversão dos valores para uma cadeia de caracteres.
sprintf não pode ser usada quando se deseja o valor da cadeia de
caracteres correspondente a um inteiro (pois sprintf é um
comando, não retorna nenhum valor). Quando um valor é desejado, uma
função pode ser definida pelo programador, ou pode ser usada a
função itoa, disponível em grande parte das
implementações da biblioteca stdlib, embora não seja
definida na linguagem C padrão (ANSI C). A
função itoa tem a seguinte assinatura:
|
itoa converte valor para uma cadeia de caracteres,
terminada com o caractere NULL, usando a base base,
armazena essa cadeia em str, e retorna str.
Se a base for 10 e valor for negativo, a cadeia resultante é
precedida do sinal ’-’. Em qualquer outro caso, simplesmente se
supõe que o valor é positivo.
str deve ser um arranjo com um tamanho grande o suficiente para
conter a cadeia.
A função main pode receber como argumento várias cadeias de
caracteres, separadas entre si por espaços ou outros caracteres
delimitadores de palavras (como tab, i.e. ). O interpretador
da linha de comandos do sistema operacional, quando chamado com um
nome de um programa seguido de uma cadeia de caracteres, percorre essa
cadeia de caracteres colocando cada sequência de caracteres, separada
da seguinte por um ou mais delimitadores, em uma posição de um arranjo
que é passado como argumento para a função '\t'main, precedido do
número de valores contidos neste arranjo.
Por exemplo, para um programa com nome prog, o comando:
| prog abc xy 123 |
faz com que o interpretador da linha de comandos percorra a cadeia de
caracteres "abc xy 123" e coloque cada sequência de caracteres
que está separada da seguinte por um ou mais delimitadores em uma
posição de um arranjo, e passe como argumento para o método main
do programa (prog) o valor 3 — que é igual ao tamanho do
arranjo (número de cadeias de caracteres) — e esse arranjo. Assim, o
arranjo passado contém "abc" na variável de índice 0,
"xy" na variável de índice 1 e "123" na variável de
índice 2.
Em princípio, se poderia pensar que uma desvantagem de usar scanf para leitura de cadeias de caracteres é que não se pode ler cadeias de caracteres que contenham espaços (e caracteres de terminação de linha, ’\n’). No entanto, é possível ler espaços e e quebras de linhas usando scanf. O formato de controle da leitura usado no scanf pode ser usado para especificar tanto os caracteres válidos que podem ou não podem fazer parte da cadeia de caracteres lido. Por exemplo:
|
O formato %[A-Z]s especifica que apenas letras maiúsculas (de
’A’ a ’Z’) podem ser armazenadas em str. Ou seja, a
leitura de caracteres realizada em uma chamada a scanf irá terminar
quando um caractere que não seja uma letra maiúscula for encontrado.
Por exemplo, ao digitar-se , apenas "XPto" vai ser
armazenado em "XP"str, após o comando
scanf("%[A-Z]s", str); acima. A leitura
termina ao encontrar a letra minúscula t.
É possível também inserir o caractere ’\n’ como entrada válida a ser lida e armazenada na cadeia de caracteres; por exemplo:
|
faz com que letras maiúsculas ou minúsculas, dígitos de ’0’ a
’9’ e o caratere de terminação de linha ’\n’
possam ser lidos e armazenados em str. Note: o uso da tecla
ENTER (ou RETURN) não irá encerrar a leitura; a leitura
será terminada apenas se for digitado algum caractere que não seja uma
letra (maiúscula ou minúscula), um dígito ou o caractere de terminação
de linha (com entrada feita com as teclas ENTER ou RETURN).
Os caracteres que podem ser lidos e armazenados na cadeia de
caracteres podem também ser especificados como todos aqueles que
precedem, na tabela ASCII, um determinado caractere, que deve suceder
o caractere ; por exemplo:'^'
|
O comando scanf("%[^\n]s", str); acima faz com que os os
caracteres lidos, com código na tabela ASCII menor que ,
sejam armazenados em '\n'str (até que um caractere com código na
tabela ASCII igual ou superior a seja lido).'\n'
Resumindo: o caractere indica que a cadeia de caracteres
lida pode conter todos os caracteres que estejam na tabela ASCII
entre '[' e '['. Se o primeiro caractere depois de
']' for '[', no entanto, então a cadeia de caracteres
lida pode conter todos os caracteres que estejam na tabela ASCII
até (e não incluindo) o primeiro caractere que segue o '^'.'^'
Por exemplo, especifica que todos os caracteres
anteriores a "%[^]ABC]s" e mais ’A’, ’B’ e ’C’ podem
fazer parte da cadeia lida. Para ser incluído, um hífen deve ser o
último caractere da cadeia de caracteres que segue o ']' (i.e. o
caractere anterior ao '['. Por exemplo, ']'
indica que podem ser lidos os caracteres ’A’, ’B’, ’C’ e "%[ABC-]s". Se ocorrer entre dois caracteres dentro do
formato que inicia com o caractere '-', o hífen indica a faixa
entre esses dois caracteres. Por exemplo: '['
indica letras maiúsculas (de ’A’ a ’Z’), letras
minúsculas (de ’a’ a ’z’) e dígitos (de ’0’ a
’9’)."%[A-Za-z0-9]s\"
A cadeia de caracteres lida por scanf sempre termina com o caractere
.
'\0'
Por exemplo, para a entrada:
| 10 |
| 112223a |
a saída deve ser:
| 1 aparece 2 vezes |
| 2 aparece 3 vezes |
| 3 aparece 1 vez |
| a aparece 1 vez |
A ordem de impressão dos caracteres e sua frequência não é importante, mas cada caractere deve aparecer, com sua frequência de ocorrência, apenas uma vez na saída, e somente se essa frequência for diferente de zero.
Solução: Cada caractere é representado no código ASCII por um valor inteiro compreendido entre 0 e 127. Para armazenar a informação sobre o número de ocorrências de cada um desses caracteres, podemos usar um arranjo de valores inteiros com 128 componentes, fazendo corresponder a cada caractere representado pelo número inteiro i a posição i desse arranjo. Tal arranjo pode ser declarado da seguinte forma:
|
Um trecho de programa que armazena em contchar[i] o número de
ocorrências de cada caractere de uma cadeia de caracteres
str — onde o fim da cadeia str é indicado pela existência
do caractere — pode ser escrito como a seguir:'\0'
|
O uso do atributo const na declaração do parâmetro str da
função contFreqChar é usado apenas por questão de legibilidade,
para indicar que a cadeia de caracteres str não é modificada no
corpo da função.
Um exemplo de definição de uma função main que usa a função
contFreqChar é mostrada a seguir. Os valores de entrada são
lidos, a função contFreqChar é chamada e a frequência de cada
caractere que ocorre na cadeia de caracteres especificada na entrada é
impressa.
|
Solução: Um caractere alfanumérico é um caractere que pode ser um algarismo ou uma letra. A solução consiste em, inicialmente, determinar a freqüência de ocorrência de cada caractere alfanumérico, de maneira análoga à do exercício anterior. Ou seja, a solução armazena cria arranjos com componentes correspondentes a cada caractere alfanumérico. Cada componente contém a frequência de ocorrência do caractere alfanumérico correspondente. Em seguida, o índice do componente de maior valor (isto é, maior frequência) desse arranjo é determinado e o caractere alfanumérico correspondente a esse índice é impresso. Vamos usar três arranjos, um arranjo para dígitos — com índices de 0 a 9 —, e os outros para letras minúsculas e maiúsculas. O índice do arranjo de dígitos correspondente a um dígito d é obtido por d - ’0’, usando o fato de que a representação dos dígitos são crescentes, a partir do valor da representação do caractere ’0’. Analogamente para letras minúsculas e maiúsculas, que têm valores de representação crescentes a partir, respectivamente, dos valores das representações dos caractere ’a’ e ’A’.
|
A função maisFreq recebe como argumento uma cadeia de
caracteres, em que cada caractere representa um algarismo, e retorna o
algarismo mais freqüente nessa cadeia. Quando existir mais de um
algarismo com a maior freqüência de ocorrência, o método
retorna, dentre esses, aquele que ocorre primeiro na cadeia. Por
exemplo, maisFreq("005552") retorna ’5’, e
maisFreq("110022") retorna ’1’.
Solução: Usamos uma cadeia de caracteres de até 1001
dígitos para armazenar inteiros que podem ter até 1000 dígitos, e
mais o caractere para indicar terminação da cadeia. A
soma dos dígitos de um inteiro de até 1000 dígitos sempre pode ser
armazenada em um valor de tipo '\0'int.
|
inverte que estenda o Exercício
5 da seção 3.12 para
qualquer cadeia de caracteres s. A função inverte tem
como parâmetro adicional (além da cadeia de caracteres) o tamanho
n da cadeia passada como argumento. O programa que chama a
função inverte deve ler, antes de cada cadeia de
caracteres, um valor que, deve-se supor, é maior que o tamanho da
cadeia.decPraBin que receba um número
inteiro não-negativo como argumento e retorne uma cadeia de
caracteres que é igual à representação desse
número em notação binária.Por exemplo, ao receber o número inteiro 8, a função deve retornar "1000".
Para calcular o tamanho da cadeia de caracteres a ser alocada,
defina e use uma função numDiv2 que, ao receber um
número inteiro positivo n como argumento, retorne m+1 tal
que 2m ≤ n < 2m+1. Esse número (m+1) é igual ao
número de vezes que n pode ser dividido por 2 até que o
quociente da divisão seja zero. Por exemplo, 23 ≤ 8 < 24, e
4 é o número de caracteres da cadeia "1000",
necessários para representação de 8 na base 2.
Note que, para cada n, deve ser alocada uma cadeia de
caracteres de tamanho tam+2, onde tam é o
resultado de numDiv2(n), para conter, além dos caracteres
necessários para representação de n na base 2, o caractere
(usado em C para terminação de cadeias de
caracteres).'\0'
Escreva um programa que leia vários números inteiros positivos
do dispositivo de entrada padrão e imprima, para cada inteiro
lido, a sua representação em notação binária, usando a
função definida no item anterior (o programa que contém a
função main deve conter também a definição das
funções definidas acima).
A execução deve terminar quando um inteiro negativo ou zero for lido.
int) correspondente, em notação
decimal.Por exemplo, ao receber o número inteiro 1000, a função deve retornar o valor 8.
Seu programa pode ler b como um valor inteiro — e supor que o
valor lido pode ser armazenado como um valor de tipo int —
ou como uma cadeia de caracteres — e supor que o tamanho máximo da
cadeia é de 30 dígitos binários.
No caso de leitura como um valor de tipo int, cada dígito deve
ser obtido como resto de divisão por 10. Por exemplo, para
obter cada dígito de 101, obtenha o 1 mais à direita
como resto da divisão de 101 por 10; depois obtenha o
quociente da divisão de 101 por 10 (que é igual a
10) e repita o processo.
Escreva um programa que leia, do dispositivo de entrada padrão,
várias cadeias de caracteres que representam números inteiros
positivos em notação binária, e imprima, para cada valor lido, a
sua representação em notação decimal, usando a função definida acima
(o programa que contém a função main deve conter também a
definição da função definida acima).
A execução deve terminar com o fim dos dados de entrada.
scanf retornar -1, devido ao fato de o usuário digitar
Control-d (no Unix) ou Control-z (no Windows).Por exemplo, se a entrada for:
| 1000 |
| abcdefghijklmnopqrstuvwxyz123456789 |
| nopqr |
| 789 |
| xya |
| abc |
a saída deve ser uma mensagem como a seguir:
| nopqr Sim |
| 789 Sim |
| xya Nao |
| abc Sim |
Não se esqueça de considerar que, em C, cadeias de caracteres
são armazenadas de modo a terminar com o caractere .'\0'
Por exemplo, para a entrada:
| 100 |
| abci adefghiabaf |
A saída deve ser:
| defghf |
Considere o trecho de programa a seguir:
|
Após a execução desse trecho de programa, a representa
um arranjo com quatro componentes, sendo cada componente um arranjo
com três componentes. Tal arranjo é algumas vezes chamado de uma
matriz, no caso uma matriz 4 por 3. Um arranjo de arranjos é
também chamado de arranjo multidimensional.
Um arranjo pode ter como componentes arranjos de tamanhos diferentes, alocados dinamicamente, como ilustra o exemplo a seguir.
|
O arranjo a é um arranjo, alocado dinamicamente, de tamanho 2,
contendo dois arranjos alocados dinamicamente, sendo que o primeiro
deles, a[0], tem 10 componentes, enquanto o segundo, a[1],
tem 40 componentes.
O modo mais simples, mas não muito flexível, de passar um arranjo multidimensional como parâmetro é usar tipos estáticos na declaração do parâmetro e do argumento. Por exemplo, podemos declarar:
|
A definição da função f acima só funciona para argumentos que
são arranjos formados por 2 arranjos de 3 inteiros. Se quisermos usar
argumentos cujo tipo são arranjos de tamanhos estáticos, só podemos
deixar a primeira dimensão (o número de linhas) não especificado, em
C; veja o exemplo a seguir:
|
Ao definir um parâmetro para f de tipo que é um ponteiro para
ponteiros de valores de tipo int, não podemos usar a, de tipo
int [2][3], como argumento (porque a não é um arranjo de
ponteiros; cada componente de a não é um ponteiro, mas um
arranjo de 3 inteiros). Uma maior flexibilidade para o uso de f
requer que o tipo do parâmetro seja um ponteiro para ponteiros, e o
argumento seja um arranjo dinâmico, como mostrado a seguir:
|
Variáveis devem em geral ser inicializadas na sua declaração. Do contrário, o valor armazenado será definido pelo valor que estiver na memória, durante a execução do programa, quando a variável é criada. Isso pode provocar a ocorrência de erros em outras partes do programa, que podem ser difíceis de detectar.
Para variáveis de tipo arranjo, existe uma notação especial em C, que infelizmente só pode ser usada em declarações de variáveis, que consiste em enumerar, entre os caracteres ’’ e ’’, todos os valores componentes do arranjo, separados por vírgulas.
Por exemplo, pode-se escrever:
|
para declarar uma variável diasDaSemana que é um arranjo de 7
componentes, sendo cada componente uma cadeia de caracteres. O tamanho
do arranho não precisa ser especificado, sendo determinado
automaticamente de acordo com o número de componentes especificado no
valor usado para inicialização do arranjo. Ou seja, a declaração acima
é equivalente pode ser feita como a seguir:
|
No entanto, se o tamanho for explicitamente especificado, como acima, a variável de tipo arranjo não poderá posteriormente conter um arranjo com tamanho diferente de 7, ou seja, a variável só poderá ser indexada com valores entre 0 e 6.
Em casos em que não se pode inicializar um arranjo no instante de sua
declaração (pois o valor inicial ou o tamanho de um arranjo ainda não
são conhecidos), é em geral conveniente inicializar a variável de tipo
arranjo com o valor NULL (ponteiro nulo).
Solução: A solução define uma função
calcNotas que recebe um arranjo com as notas de cada aluno em
cada tarefa, e retorna um arranjo com as notas finais de cada aluno.
Em C, o arranjo passado como argumento, alocado dinamicamente,
é um ponteiro para ponteiros-para-inteiros.
Outra função, chamada media, recebe um arranjo de inteiros
(em C, um ponteiro para inteiros) e o tamanho do arranjo, e
retorna um valor de tipo float que é igual à média dos
inteiros contidos no arranjo.
Na função main, um arranjo de notas de cada aluno em cada
avaliação é preenchido com valores lidos, o método
calcNotas é chamado para cálculo das notas finais, e as notas
finais calculadas, assim como a média, calculada por chamada à
função media, são impressas.
As notas de cada aluno são representadas em C como ponteiros para
ponteiros-para-inteiros. Para i de 1 a n, as notas do aluno i
são armazenadas em um arranjo notas[i-1] (pois o
índice de arranjos na linguagem C começa com zero), e para cada
tarefa j de 1 a k a nota do aluno i na tarefa j é armazenada
em notas[i-1][j-1].
|
A empresa tem dados armazenados sobre o número de vendas realizadas em cada loja. Não importa qual tipo de material, a empresa está interessada apenas no número de unidades vendidas.
A empresa quer um programa que leia, do dispositivo de entrada padrão, o valor de n, em seguida n valores v1, …, vn que correspondem ao número de unidades vendidas em um mês nas lojas de 1 a n, respectivamente, e imprima, no dispositivo de saída padrão, quais foram as lojas de 1 a n nas quais o número de unidades vendidas foi maior ou igual à média de unidades vendidas em suas lojas.
Um quadrado mágico é uma matriz quadrada na qual a soma dos números em cada linha, coluna e diagonal é igual.
A eleição tem um vencedor se o número de votos em branco mais o número de votos nulos é menor do que 50% do total de votos, sendo vencedores, nesse caso, todos os candidatos com número de votos igual ao maior número de votos.
Escreva um programa que leia um número positivo n, que indica o número de candidatos de uma eleição, em seguida leia cada um dos votos de uma eleição, e determine se há um vencedor e, em caso positivo, determine o número de vencedores da eleição, quais são esses vencedores e o número de votos dos mesmos.
Dica: use um arranjo de n posições com elementos que são arranjos de k valores inteiros, sendo um valor igual a zero indicativo de ausência de valor naquela posição.
Exemplo: Considere a entrada:
| 4 5 12 13 15 16 17 |
Para essa entrada, a saída deve ser como a seguir:
| Valores divisiveis por 4: 12 16 |
| Valores com resto da divisao por 4 igual a 1: 13 17 |
| Valores com resto da divisao por 4 igual a 2: |
| Valores com resto da divisao por 4 igual a 3: 15 |
Por exemplo, o valor contido na terceira coluna da linha correspondente a n=5 no Triângulo de Pascal é 10. Ele é igual a 6+4: 6 é o valor na mesma coluna da linha anterior (correspondente a n=4), e 4 o valor anterior a 6 nesta mesma linha (correspondente a n=4).
Por exemplo, para a entrada:
11 2 -2 -7 3 14 10 -3 9 -6 4 1
a saída deve ser 33 3.
Isso porque a sub-sequência que tem soma máxima é 3 14 10 -3 9.
O programa deve imprimir, para cada n lido, uma linha com os valores vi, vi+k, vi+2× k, …, vi+p× k tais que i+p × k ≤ m. Os valores devem ser separados por espaços e impressos no dispositivo de saída padrão.
Por exemplo, para a entrada:
| 10 11 25 33 40 50 69 73 85 96 101 3 2 7 0 |
a saída deve ser:
| 33 50 73 |
Isso ocorre porque o primeiro valor impresso é v3 (i=3), o seguinte é v5 (k=2, 5 = i+k), e o seguinte e último é v7 (m = 7 = i+2 × k).
Escreva o seu programa usando a função capitaliza, definida
abaixo para você. Essa função recebe um valor de tipo char e,
se o valor for uma letra minúscula, retorna a letra maiúscula
correspondente, senão o próprio valor recebido. A função
capitaliza usa uma função minusc, que você deve definir,
que recebe um valor de tipo char e retorna se esse valor é ou
não uma letra minúscula, ou seja, se é ou não um valor maior ou
igual a ’a’ e menor ou igual a ’z’.
|
Por exemplo, se a entrada for:
| 10 |
| ab1 |
| 10 ab*cd! |
| 0 |
a saída deve ser:
| AB1 |
| AB*CD! |
Você pode considerar, para simplificar o problema, que:
'\t', '\n' ou EOF.A entrada termina com EOF (caractere indicador de fim dos
dados de entrada: em entrada interativa, Control-z seguido
de Enter no Windows, ou Control-d no Linux).
Dica: Use a função getchar para ler um caractere, e use o
valor retornado por getChar para verificar fim dos dados de
entrada: getChar retorna o valor EOF (igual a -1)
para indicar fim dos dados.
Dica: para contar o número de palavras, use uma variável booleana para indicar se a posição corrente de leitura corresponde a uma posição interna a uma palavra ou externa. Uma posição fora de uma palavra é uma posição correspondente a um delimitador.
Lembre-se que, em C, uma variável booleana é uma variável inteira: o valor 0 representa falso e qualquer valor diferente de zero verdadeiro
Cada funcionário do armazém tem salário mensal estipulado como 20% do total da receita que ele consegue vender, mais um bônus igual a 10% da receita obtida com a mercadoria que teve a maior receita com as vendas.
Escreva um programa em C para calcular o valor do salário de um funcionário em um determinado mês; o programa deve ler o valor n que indica o número de mercadorias, em seguida uma sequência de pares i seguido de vi, sendo i um número de mercadoria (de 1 a n) e vi uma receita obtida no mês pelo funcionário por uma operação de venda da mercadoria i, e imprimir o salário desse funcionário, nesse mês.
Por exemplo, se a entrada for:
| 3 |
| 1 10.0 |
| 2 20.0 |
| 3 30.0 |
| 1 30.0 |
a saída deve ser: 22.0
Isso ocorre porque o total das vendas foi 90.0 e a maior receita por mercadoria, obtida com a mercadoria 1, foi 40.0 (30.0 + 10.0), e 90.0 * 0.2 (vinte por cento do total de vendas) somado com 0.1 * 40.0 (dez por cento da total de vendas com a mercadoria mais vendida) é igual a 22.0.
Existe uma vasta literatura sobre algoritmos e estruturas de dados em computação, que estendem o que foi abordado neste capítulo principalmente com o estudo mais detalhado de algoritmos para busca, inserção, remoção e ordenação de valores nessas estruturas de dados. Além de aspectos de implementação de estruturas de dados e de operações para manipulação das mesmas, essa literatura aborda em geral diferentes aplicações dos algoritmos e discute também aspectos de eficiência (ou complexidade, como é usual dizer em computação) de algoritmos.
Livros didáticos dedicados a esses temas incluem [27, 22, 15, 19], os dois primeiros já traduzidos para a língua portuguesa, o quarto escrito em português e o terceiro merece ser destacado. [9] é exemplo de um livro que adota a abordagem de programação funcional.