Muitas linguagens possuem tipos de dados ou bibliotecas que implementam números inteiros arbitrariamente grandes ("big integers"). Neste exercício-programa você criará uma implementação de "inteiros grandes" para a linguagem C. O objetivo do exercício-programa é representar números inteiros de comprimento (quase) arbitrário e implementar operações aritméticas com esses números. Você implementará as seguintes operações aritméticas com "inteiros grandes": + (adição), - (subtração), * (multiplicação), / (divisão inteira) e % (resto da divisão inteira). Você construirá também uma calculadora simplificada que efetua essas operações.
Como os números que vamos manipular podem ser muito grandes, eles não cabem em variáveis do tipo int
ou long
.
Assim, para representar cada número, vamos utilizar um vetor inteiro que
armazena em cada posição um dígito do número.
Os valores dos dígitos decimais do número serão armazenados a partir da posição 1 do vetor, do dígito menos significativo para o mais significativo. A posição 0 do vetor conterá as informações de sinal e de comprimento (quantidade de dígitos decimais) do número, no formato especificado a seguir.
#define MAX_DIGITOS 50
int
s com MAX_DIGITOS + 1
posições.
MAX_DIGITOS
) não são usadas.
MAX_DIGITOS
), da seguinte forma:
-15 | 4 | 3 | 7 | 2 | 8 | 3 | 2 | 9 | 4 | 8 | 3 | 9 | 1 | 2 | 3 | ? | ? | ... | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
Note que o número de posições do vetor que armazena um "inteiro grande" deve
ser MAX_DIGITOS + 1
, já que a posição zero tem um uso especial.
No exemplo acima, note que o -15 na posição zero indica que o "inteiro grande"
armazenado no vetor é negativo, que o valor do dígito mais significativo desse
"inteiro grande" está na posição 15 do vetor, e que esse "inteiro grande" tem
15 dígitos decimais.
Note, ainda, que o comprimento dos nossos "inteiros grandes" não é completamente arbitrário, pois ele é limitado pela constante MAX_DIGITOS
. Como é bem fácil aumentar o valor dessa constante, no início deste enunciado falamos em números inteiros de "comprimento (quase) arbitrário". Quando estudarmos alocação dinâmica de memória (um pouco mais adiante, nas aulas de MAC0122), veremos como se poderia alocar dinamicamente o vetor com os dígitos de um "inteiro grande" e, dessa forma, eliminar a limitação sobre o comprimento dos "inteiros grandes".
void zera(int num[]);
num
o "inteiro grande" 0 (zero).
int soma(int num1[], int num2[], int res[]);
num1
com o armazenado no vetor num2
. Coloca o resultado (que é outro "inteiro grande")
no vetor res
. No caso de overflow (estouro da capacidade, ou seja, vai-um quando da soma dos dígitos mais significativos), a função deve devolver o valor 0 (zero). Caso contrário, a função deve devolver o valor 1 (um).
soma_mesmo_sinal
ou à função soma_sinais_opostos
(vide abaixo), dependendo dos sinais dos dois números.
int soma_mesmo_sinal(int num1[], int num2[], int res[]);
num1
e num2
dois "inteiros grandes" com o mesmo sinal (ambos positivos ou ambos negativos) que são as parcelas a serem somadas. A função efetua a soma e coloca o resultado (que é outro "inteiro grande") no vetor res
. Note que esta função supõe que ambas as parcelas sejam positivas ou ambas sejam negativas. Ela deve calcular |num1|+|num2|
e acertar o sinal de res
. No caso de overflow (vai-um quando da soma dos dígitos mais significativos), a função deve devolver o valor 0 (zero). Caso contrário, a função deve devolver o valor 1 (um).
void soma_sinais_opostos(int num1[], int num2[], int res[]);
num1
e num2
dois "inteiros grandes" com sinais opostos e tais que o módulo do primeiro (num1
) seja maior ou igual ao módulo do segundo (num2
). A função soma os dois números e coloca o resultado (que é outro "inteiro grande") no vetor res
. Como as duas parcelas têm sinais opostos, a operação efetuada é na verdade uma subtração: a função deve calcular |num1|-|num2|
e acertar o sinal de res
.
int subtrai(int num1[], int num2[], int res[]);
num2
do armazenado no vetor num1
(ou seja, calcula num1-num2
). Coloca o resultado no vetor res
.
No caso de overflow (vai-um quando da soma dos dígitos mais significativos), a função deve devolver o valor 0 (zero). Caso contrário, a função deve devolver o valor 1 (um).
Dica: Observe que o que esta função faz é somar num1
com o
oposto de num2
. Implemente-a com uma chamada à função soma
.
int multiplica(int num1[], int num2[], int res[]);
num1
pelo armazenado no vetor num2
. Coloca o produto (que é outro "inteiro grande") no vetor
res
. Para obter o produto, esta função emprega o
algoritmo usual de multiplicação de dois números. (É claro que deve ser
usada a função soma_mesmo_sinal
e a função
multiplica_digito
, cujo protótipo aparece abaixo.) No caso
de overflow (vai-um quando da soma dos dígitos mais
significativos), a função deve devolver o valor 0 (zero). Caso
contrário, a função deve devolver o valor 1 (um).
int multiplica_digito(int num[], int d, int k, int res[]);
d
e por 10k
o "inteiro grande" armazenado no vetor num
. Coloca o resultado (que é outro "inteiro grande") no vetor res
. Esta função pressupõe que o inteiro d
satifaz 0 < d
<= 9 e que o inteiro k
é não negativo. No caso de overflow (vai-um quando da multiplicação pelo dígito mais significativo) a função deve devolver o valor 0 (zero). Caso
contrário, a função deve devolver o valor 1 (um).
int divide(int num1[], int num2[], int quoc[], int resto[]);
num1
pelo armazenado no vetor num2
. Coloca o quociente inteiro no vetor quoc
e o resto no vetor resto
. Para efetuar a divisão, esta função deve fazer subtrações sucessivas, chamando a função soma_sinais_opostos
e a função soma1
cujo protótipo aparece abaixo. No caso do divisor (o "inteiro grande" no vetor num2
) ser zero, a função deve devolver o valor 0 (zero). Caso contrário, a função deve devolver o valor 1 (um).
void copia(int destino[], int origem[]);
origem
e o copia para o vetor destino
.
int soma1(int num[]);
num
.
Devolve 0 (zero) em caso de overflow e 1 (um) caso contrário.
int compara_modulo(int num1[], int num2[]);
num1
e num2
.
Devolve o valor 1 (um) se o módulo do "inteiro grande" em num1
for maior do que o módulo do "inteiro grande" em num2
.
Devolve 0 (zero) se o módulo do "inteiro grande" em num1
for igual ao módulo do "inteiro grande" em num2
.
Devolve o valor -1 (menos um) se o módulo do "inteiro grande"
em num1
for menor do que o módulo do "inteiro grande" em num2
.
num1
e num2
com "inteiros grandes": soma
, soma_mesmo_sinal
, soma_sinais_opostos
, subtrai
, multiplica
, divide
e compara_modulo
. Nenhuma dessas funções deve alterar o conteúdo do vetor num1
ou do vetor num2
. Em outras palavras, no retorno de uma chamada a qualquer uma dessas funções, os vetores num1
e num2
devem continuar contendo os mesmos "inteiros grandes" que eles continham quando a função foi chamada. Se alguma dessas funções fizer alguma modificação no conteúdo do vetor num1
ou do vetor num2
, a modificação tem de ser temporária: antes de retornar,
a função deve restaurar o conteúdo original do vetor modificado.
De modo análogo, a função multiplica_digito
não deve alterar o conteúdo do vetor num
e a função copia
não deve alterar o contetúdo do vetor origem
.
A saída do programa será outra sequência de linhas (uma para cada linha da entrada) com os resultados das operações especificadas pelas linhas da entrada. Se ocorrer overflow ou divisão por zero, o programa deve imprimir uma mensagem de erro.
Aqui você encontra um exemplo de arquivo de entrada. Teste o seu programa com esse arquivo!
Aqui você encontra um arquivo com a saída do programa para o exemplo de entrada acima.
Digite o nome do arquivo de entrada: entrada1.txt Digite o nome do arquivo de saida: saida1.txtSe o nome do arquivo de entrada for vazio (isso acontecerá caso o operador digite a tecla "Entra" quando o programa pedir o nome do arquivo de entrada, sem digitar nada antes da tecla "Entra"), o programa considerará que a entrada é a "entrada padrão" (
stdin
, normalmente associada ao teclado). De modo análogo, se o nome do arquivo de saída for vazio, o programa considerará que a saída é a "saída padrão" (stdout
, normalmente associada à tela).
Se o nome do arquivo de entrada for não vazio, o programa lerá os dados de entrada do arquivo especificado. Se o nome do arquivo de saída for não vazio, o programa escreverá os dados de saída no arquivo especificado.
Certifique-se que o seu arquivo de entrada não contém linhas em branco no final, pois isso pode fazer diferença para o seu programa. Certifique-se também que todas as linhas do arquivo de entrada (inclusive a última) são terminadas por um caractere de fim de linha (new line).
#define MAX_NOME_ARQ 256 ... /*----- Declaracoes das variaveis para leitura e gravacao em arquivos -----*/ char buffer[MAX_NOME_ARQ]; /* para ler os nomes dos arquivos */ char nome_arq_entrada[MAX_NOME_ARQ] = ""; /* nome do arquivo de entrada */ char nome_arq_saida[MAX_NOME_ARQ]= ""; /* nome do arquivo de saida */ FILE *entrada; /* arquivo de entrada aberto */ FILE *saida; /* arquivo de saida aberto */ ... /*----- Primeiros comandos do seu programa -----*/ /* Leitura do nome do arquivo de entrada */ printf("Digite o nome do arquivo de entrada: "); fgets(buffer, MAX_NOME_ARQ, stdin); sscanf(buffer, "%s", nome_arq_entrada); /* Leitura do nome do arquivo de saida */ printf("Digite o nome do arquivo de saida: "); fgets(buffer, MAX_NOME_ARQ, stdin); sscanf(buffer, "%s", nome_arq_saida); /* Se o nome do arquivo de entrada for nao vazio, abre o arquivo de entrada para leitura ("r") */ if (nome_arq_entrada[0] != '\0') { entrada = fopen(nome_arq_entrada, "r"); if (entrada == NULL) { printf("Arquivo de entrada nao encontrado!\n"); exit(1); } } else { entrada = stdin; } /* Se o nome do arquivo de saida for nao vazio, abre o arquivo de saida para escrita ("w") */ if (nome_arq_saida[0] != '\0') { saida = fopen(nome_arq_saida, "w"); if (saida == NULL) { printf("Erro na abertura do arquivo de saida!\n"); exit(1); } } else { saida = stdout; }
Tendo feito isso, a variável entrada
se referirá ao arquivo de
entrada e a variável saida
se referirá ao arquivo de saída.
Note que as variáveis entrada
e saida
tem um tipo
especial (FILE *
), que representa um arquivo aberto. É esse o tipo
devolvido pela função fopen
, que abre um arquivo para leitura
("r"
) ou para escrita ("w"
). Observe, ainda, que
esse é o único trecho do programa que usa os nomes dos arquivos. Esses nomes
só são usados para abrir os arquivos. Daí para a frente, qualquer referência
ao arquivo de entrada será feita por meio da variável entrada
e
qualquer referência ao arquivo de saída será feita por meio da variável
saida
.
No restante do seu programa utilize a função fscanf
para ler
dados do arquivo de entrada e a função fprintf
para escrever
dados no arquivo de saída. Essas funções (que também estão na biblioteca
<stdio.h>
) funcionam de modo semelhante ao
scanf
e ao printf
. A única diferença é que elas
recebem um parâmetro a mais, que especifica o arquivo do qual o
fscanf
lê os dados ou no qual o fprintf
escreve os
dados. Para informar ao fscanf
qual é o arquivo de entrada, passe
como parâmetro a variável entrada
. Para informar ao
fprintf
qual é o arquivo de saída, passe como parâmetro a variável
saida
.
Caso o nome do arquivo de entrada seja vazio, a variável entrada
conterá o valor stdin
, que representa a "entrada padrão"
(normalmente associada ao teclado). Nessa situação, quando você usar
fscanf(entrada, ...);para ler dados da entrada, na realidade você estará executando uma chamada
fscanf(stdin, ...)
, que é exatamente equivalente a uma
chamada scanf(...)
.
De modo análogo, caso o nome do arquivo de saída seja vazio, a variável
saida
conterá o valor stdout
, que representa a "saída
padrão" (normalmente associada à tela). Nessa situação, quando você usar
fprintf(saida, ...);para escrever dados na saída, na realidade você estará executando uma chamada
fprintf(stdout, ...)
, que é exatamente equivalente a uma
chamada printf(...)
.
Neste exercício-programa o arquivo de entrada deve ser lido caractere a
caractere (utilizando-se o formato de leitura "%c"
), pois os
números são muito grandes para serem lidos com formatos numéricos como "%d"
ou "%ld"
. Seu programa deve ler cada
número como uma sequência de caracteres e converter o número para o formato
de "inteiro grande".
Caso a entrada não seja a "entrada padrão", seu programa deve fechar o arquivo de entrada depois de ler todos os dados. Faça isso com a seguinte chamada:
fclose(entrada);
De modo análogo, caso a saída não seja a "saída padrão", seu programa deve fechar o arquivo de saída depois de gravar todos os dados. Faça isso com a seguinte chamada:
fclose(saida);
int le_linha_arquivo(FILE *arq, int num1[], int num2[], char *op);
arq
.
Se não houver próxima linha, devolve o valor 0 (zero). Caso contrário
lê todos os caracteres da linha, inclusive o caractere new line
presente ao final da linha. Neste caso a função armazena no vetor
num1
o primeiro "inteiro grande" presente na linha lida,
armazena em *op
o operador (um caractere) presente na linha,
armazena no vetor num2
o segundo "inteiro grande" presente
na linha, e devolve o valor 1 (um).
Esta função deve ser usada no programa principal, para saber se existe mais uma linha no no arquivo de entrada e para ler essa linha. Seu programa principal conterá uma repetição como esta:
while (le_linha_arquivo(entrada, num1, num2, &op)) { /* efetua a operação especificada na linha lida */ ... }
void escreve_numero_arquivo(FILE *arq, int num[]);
arq
o "inteiro grande" armazenado
no vetor num
.
/*************************************************************/ /* Aluno: Fulano de Tal */ /* Número USP: 12345678 */ /* Curso: ... */ /* Exercicio-Programa 1 -- Aritmética com "Inteiros Grandes" */ /* MAC0122 -- 2009 -- IME/USP, turma XX -- Prof. YYYYY */ /* Compilador: ... (gcc ou Code::Blocks) versão ... */ /* Sistema Operacional: ... */ /*************************************************************/
gcc
ou o Code::Blocks
(que na verdade chama o gcc
para fazer a compilação). Caso você use diretamente o gcc
, passe ao compilador (na linha de comando) as seguintes opções:
-Wall -ansi -pedantic -O2 -U_FORTIFY_SOURCECaso você use o
Code::Blocks
, entre em Settings -> Compiler and debugger... -> Compiler settings -> Compiler Flags, selecione as quatro opcões correspondentes
a -Wall
, -ansi
, -pedantic
e -O2
, e clique em OK. Entre também em Settings -> Compiler and debugger... -> Compiler settings -> Other options, digite -U_FORTIFY_SOURCE
na caixa de texto Other options e clique OK.
ep1-<seu-número-USP>.c
. Exemplo: Se seu número USP for 12345678, você deverá entregar um arquivo com o nome ep1-12345678.c
. (Note que não há espaços no nome do arquivo.)