Você está na página 1de 12

Erros Comuns detectados em avaliações:

Funções sem argumento ou com número de argumentos incompatíveis

Seja obter, dado um valor x o proximo valor de x, x sendo inteiro.

Se escrever f=x+1

o que está sendo definido é uma variável f, que depende de outra variável x. Note que x não
está definido.

Se escrever x=x+1

o valor de x estará sendo atualizado para o valor seguinte. Note que x não está definido
inicialmente.

Se escrever f(x)=x+1

o que está sendo definido é uma função, cujo argumento é x, que irá devolver o próximo valor
de x. Neste caso, como x é um argumento, x está definido. Esta é a expressão correta. A função
deve ser chamada com um argumento válido, por exemplo f(3). Devolverá o valor 4.

Se escrever f2(x,y)=x+1

o que está sendo definido é uma função com 2 argumentos sendo que apenas o 1o (x) é
utilizado. É estranho, mas não estaria errado.

Se escrever f2(x)=x+y
o que está sendo definido é uma função com 1 argumento x, e uma variável y, interna à
função, que não está definida. Isto está errado. Todas as variáveis devem estar definidas.

Se escrever f2(x,y)=x+y

o que está sendo definido é uma função com 2 argumentos sendo que ambos são utilizados.
Esta é a expressão correta. A função deve ser chamada com argumentos válidos, por exemplo
f2(3,1). Devolverá o valor 4. Não pode ser chamada com apenas 1 argumento.

Funções definidas com argumentos errados ou usadas com número de argumentos errados

Uma função não pode ser definida com argumentos que não sejam exclusivamente variáveis,
também chamados de parametros. Na linguagem C, tanto a função quanto seus parametros
devem ter o seu tipo definido. Uma função não pode ser usada ou chamada com número de
argumentos diferentes da sua definição, e deve respeitar a ordem e o tipo dos argumentos. No
exemplo (a), evidenciamos formas corretas de se definir e usar as funções e nos demais,
formas equivocadas.

Sejam as funções f e g definidas em matemática como:

a) f(x) = x + 1 e g(x,y) = x || y.

Neste caso, está claro que f(x) possui um parametro x que seria do tipo número pois a
expressão que usa x utiliza o operador +, de soma, que é usado para números. Também está
claro que a função g(x,y) possui 2 parametros x e y, ambos booleanos, pois a expressão que os
relaciona usa o operador booleano ou-lógico, simbolizado por ||. No caso da função f(x) o que
não está claro ainda é qual o tipo de número que é utilizado, podendo ser natural (conjunto
N), inteiro (conjunto Z), racional (conjunto Q) ou real (conjunto R). Geralmente, o conjunto
adequado está atribuido pelo contexto. Para explicitar melhor, usa-se uma notação
matemática mais explícita que evidencia o conjunto da forma:

f(N) → N {x → x+1} ou g(B,B) → B {x,y → x || y},


onde denomino aqui B como o conjunto dos booleanos que só possui 2 elementos: verdadeiro
(associado a V ou 1) e falso (associado a F ou 0). Na linguagem C, ambos podem usar o tipo int
e as definições ficariam como:

int f(int x) {return x+1;} e int g(int x ,int y) {return x||y?1:0;}

onde na tradução para a linguagem C, no caso da expressão booleana associada à função


g(x,y), usamos um ternário para retornar 1 se esta for verdadeira e 0 se for falsa. Se a função
f(x) fosse de outro conjunto numérico mais abrangente, por exemplo dos reais, então ela seria
definida matemáticamente como:

f(R) → R {x → x+1}

porém traduzida para a linguagem C como:

float f(float x) {return x+1.0;}

Nota-se que para ser evidenciado que a função opera com float, o numero 1 foi escrito como
1.0, que é a notação correta para um número do tipo float, mesmo que este não tenha parte
fracionária (neste caso é 0).

Caso sejam utilizadas 2 funções que usem ou retornem tipos diferentes, mesmo que o código
seja o mesmo, seus nomes também devem ser diferentes. Se fosse usar a versão de f para o
tipo int e a versão de f para o tipo float, seus nomes devem ser diferentes, por exemplo:
f_int(x) para o tipo int e f_float(x) para o tipo float. Assim,

float f_float(float x) {return x+1.0;} e int f_int(int x) {return x+1;}

Para que 2 funções sejam consideradas iguais, é necessário que seus tipos de parametros de
entrada, na mesma ordem, sejam do mesmo tipo, seu tipo de retorno sejam iguais e que seus
códigos sejam iguais. Assim, se tivermos:
int f1(int x,int y) {return x+y;}

float f2(int x,float y) {return x+y;}

float f3(float x,float y) {return x+y;}

float f4(float x,int y) {return x+y;}

embora todas tenham o mesmo código, todas são diferentes pois os parametros ou tipos de
retorno diferem de alguma maneira. As únicas que estão matemáticamente e
computacionalemnte corretas seriam as funções f1 e f3, pois somam valores do mesmo tipo.
No caso das funções f2 e f4, o parametro do tipo int é convertido para float pela linguagem C,
de modo que a soma seja feita com ambos os valores do mesmo tipo. Na linguagem Pascal,
por exemplo, haveria um erro de sintaxe sinalizado, obrigando a que a conversão do tipo int
para o float seja feita explicitamente, pelo programador, tanto no caso de f2 quanto de f4.
Versões de f2 e f4 mais corretas seriam, ainda em C:

float f2(int x,float y) {return (float) x+y;} e float f4(float x,int y) {return x+ (float) y;}

onde a conversão foi forçada precedendo a variável do tipo int com (float).

Agora vamos considerar o uso das 2 funções. Os argumentos podem ser expressões quaisquer,
desde que sejam do mesmo tipo do parametro definido na função e que o resultado da
expressão seja definido, isto é, que não resultem em situações do tipo: divisão por zero, valor
indefinido por conta de valores de variáveis não inicializadas que possam compor a expressão,
valor que extrapole o limite de armazenamento do valor correspondente ao tipo, tais como
overflow.

Por exemplo, se temos uma variável p=2 e uma função h(int) → int disponíveis, podemos
chamar as funções f2(x,y) e g(x,y) por meio de:

f2(2,3.0) , f2(1+p,(float) 3) , f2((int) 2.0,3.0) , f2(h(1),g(1<2,!0)?:3.0:0.0)

que estariam corretos, pois as expressões retornam valores definidos do tipo esperado para os
parametros de entrada.
b) f(x+1) = x+1

embora esta definição matemática seja aceita, pois informa como o argumento x+1 é
transformado _ neste caso, entra x+1, devolve x+1 _, esta definição não é permitida em
computação. Só podemos definir uma função com o argumento sendo uma variavel, não uma
expressão. Então ao invés de definir a função da forma acima, devemos usar: f(x) = x, que seria
o equivalente. Em C ficaria, se o argumento fosse inteiro:

int f(int x) {return x;} [b.1]

Não podendo ser escrito então:

int f((int) x+1) {return x+1;} [b.2]

Agora, podemos usar a função com o argumento x+1, ou seja podemos chamar a função
definida em [b.1] com o argumento x+1, resultando em f(x+1), desde que x tenha sido ele
próprio definido préviamente _ o x que aparece aqui não tem nada a ver com o x usado na
definição da função em b.1 _.

c) f(x,2)=x+1

Neste caso a função f teria 2 argumentos, que em matemática poderia ser aceita informando
que se o primeiro argumento for x e o segundo argumento for 2, então a função retorna o
valor de x acrescido de 1. No caso da computação esta definição não seria aceita, devendo
escrevê-la de outra forma, podendo ser:

f(x,y)=y==2?x+1:1/0, ou em C como:

int f(int x,int y) {return y==2?x+1:1/0;}


Neste caso, como não sabemos qual seria o valor da função caso o valor de y fosse diferente
de 2, optamos por retornar uma expressão (1/0), que pararia o programa, como se algo não
previsto tivesse ocorrido. Na verdade, a definição matemática estaria incompleta.

d) f(x,y) = x+y e g(y)=f(2)+y e g(3,4)

Neste caso a funcão f possui 2 argumentos x e y, e devolve a soma deles. Tudo ok. Já a função
g possui um argumento y e devolve uma expressão que soma a este y o valor de f(2). Temos
um problema, pois f foi definida com 2 argumentos e está sendo usada com 1 apenas. Este é
um erro tanto de matemática quanto de computação. O número de argumentos deve ser
igual, tanto na definição, quanto na chamada. Existem linguagens de computação que aceitam
isto, mas existe de fato uma definição da função para cada número de argumentos, então esta
pode ser chamada com um número de argumentos diferente, desde que haja uma definição
correspondente. Não é o caso da linguagem C. Já em g(3,4) o erro é que a função g teria sida
definida com 1 argumento apenas e na sua chamada, são usados 2. Neste caso, o número de
argumentos na chamada excede o número de argumentos na sua definição.

Funções recursivas incompletas/não convergentes

Seja a função:

f(x)=x<10?1:f(x*2)+1

Trata-se de uma função recursiva pois ela é usada na sua própria definição. Ela possui 2 casos:
1 caso terminal (quando x<10) ao devolver 1; 1 caso genérico que devolve f(2*x)+1. O
problema neste caso é que o argumento da função é maior que o argumento que entrou. Se x
for 21, a função será chamada com o argumento 42. Neste caso a função será chamada
novamente como o argumento 84, e assim sucessivamente. Note que o caso que seria o
terminal, para onde a função deveria ir em algum momento não é alcançado e a função não
para nunca. O argumento de chamada deve ir no sentido de convergir para o caso terminal.
Então a função recursiva não está correta.

se escrever f(x)=x<10?1:x*2+1
Neste caso, a função não é recursiva pois ela não aparece na expressão. Se x<10, irá devolver
1, senão irá devolver x*2+1.

se escrever f(x)=x<10?1:f2(x*2)+1

Neste caso, a função não é recursiva pois ela não aparece na expressão. Se x<10, irá devolver
1, senão irá devolver f2(x*2)+1. A função f2(x) deverá ter sido definida previamente, senão não
seria conhecida ao ser ativada e não teria como f(x) devolver um valor se x>=10.

se escrever f(x)=f(x*2)+1

Seria uma função recursiva, mas que não possui caso terminal. Uma função recursiva deve ter
pelo menos 2 casos, 1 terminal e 1 genérico. Neste caso, a função não para nunca.

se escrever f(x)=x<10?1:2*f(x/2)+1

seria uma função recursiva, com 1 caso terminal e 1 genérico, sendo que no caso genérico, o
argumento da chamada seguinte é menor que o da chamada atual, então a função tende para
o caso terminal. A função estaria correta. f(21) iria chamar f(10) que iria chamar f(5) que iria
devolver 5, pois 5<10. O cálculo seria: f(21)=2*f(10)+1, f(10)=2*f(5)+1, f(5)=5. Inserindo os
valores obtidos de volta nas expressões, ficaria com: f(10)=2*5+1=11 e f(21)=2*11+1=23.

se escrever f(x)=x==1?1:2*f(x/2)+1

Aparentemente, tem 2 casos, 1 generico e 1 terminal. No genérico, o argumento da função


decresce. Mas será que irá convergir sempre para 1, que seria o valor terminal? Se calcularmos
f(2), obtemos: f(2)=2*f(1)+1 e f(1)=1, ou seja, f(2)=2*1+1=3. Agora, se calcularmos f(3),
obtemos: f(3)=2*f(1)+1=3. Aparentemente está tudo ok. Agora, se calcularmos f(0), o que
aconterá? f(0)=2*f(0)+1,

ou seja, a função não para nunca. Só acontece para o valor 0, mas acontece.

se escrever f(x)=x==0?1:2*f(x/2)+1
Neste caso, estará tudo bem, pois o valor x=0 será atingido em algum momento, para todos os
valores de x.

Funções recursivas com operadores inadequados

Seja definir a função que verifica se todos os números são pares. Vamos observar algumas
implementações:

sendo que a função par(n) é dada por: par(n)=n%2==0?1:0 e a função nd(n) devolve o número
de dígitos de n.

todosPares1(n)=n<10?par(n)?1:0:todosPares1(n/10) + todosPares1(n%10)

todosPares2(n)=n<10?par(n):todosPares2(n/10) && todosPares2(n%10)

todosPares3(n)=n<10?par(n)?par(n/10) && todosPares3(n%10)

todosPares4(n)=n<10?par(n)?todosPares4(n/10) && par(n%10)

todosPares5(n)=n<10?par(n)?todosPares5(n/10) + todosPares5(n%10) == nd(n)

todosPares6(n)=n<10?par(n)?todosPares6(n/10) * todosPares6(n%10)

A 1a. implementação tem 2 problemas, sendo um grave e outro pode ser considerado como
um aviso. O problema menos grave é observar que a função par(n) já retorna 1 se n for par e 0
se não for. Ora a sub-expressão par(n)?1:0 é redundante pois como 1 é diferente de 0,
retornaria 1, que já é o seu valor, se n fosse par. Se por outro lado, se n nao fosse par, iria
retornar 0, e como 0 é igual a zero, iria retornar 0, que é ele mesmo. O seja: ao invés de
escrever par(n)?1:0, poderia ter escrito simplesmente par(n). Não está errado escrever como
foi escrito, mas não é conveniente. O problema grave resulta do operador utilizado para
escrever o caso genérico. Usou-se o '+'. Como a função par retorna 1 ou 0, pode-se fazer a
soma de números. O problema é que bastaria que tivesse 1 dígito par em um número com
mais de 1 dígito para a soma dar 1. Como 1 é diferente de zero, seria considerado verdadeiro,
entretanto, o número não teria todos os dígitos pares necessariamente (por exemplo, 12 tem
1 digito par, mas não tem todos os dígitos pares). Esta implementação estaria errada.

A 2a. implementação está correta pois utiliza-se do operador &&, ao invés do + da 1a.
implementação, que seria o operador adequado pois é uma operação envolvendo dados
booleanos (o fato de ser par aplicado a um digito - caso n<10 - garante que se trata de algo
verdadeiro ou falso, portanto booleano. Como no C padrão original, não existe o tipo
booleano, o mesmo é representado por uma convenção: se o retorno for algo diferente de
zero é verdadeiro, senão é falso. Adotamos: se for verdadeiro, retorna 1 e se for falso retorna
0, para ter um padrão.) e o operador binário deve ser o e-lógico pois obriga a que a condição
seja válida se ambos os operandos forem.

A 3a. implementação está incorreta pois a funçao par está se aplicando sobre o argumento
n/10, que representa um número e não um dígito. Então se o número tiver mais de 2 dígitos
pode dar um resultado inesperado. Por exemplo, todosPares3(126) resultaria em
todosPares3(126) = par(126/10) && todosPares3(126%10) = par(12) && todosPares3(6) = 1
&& par(6) = 1 && 1 = 1, ou seja verdadeiro, entretanto vemos que 126 não possui todos os
seus dígitos pares.

A 4a. implementação está correta pois a função par se aplica sobre o argumento (n%10), que é
um dígito. Neste caso, o seu comportamento é idêntico à 2a. implementação, que estava
correta. Usou-se a equivalência: todosPares4(n%10) =

par(n%10), pois n%10 satisfaz a (n%10)<10.

A 5a. implementação estaria correta, em tese, pois obriga-se a que a contagem dos pares
sobre cada parte do número seja igual ao número de dígitos. Além disto, tanto o caso terminal,
quanto o caso genérico retornariam um booleano, o que estaria conceitualmente correto. Mas
vamos verificar ativando o calculo de todosPares5(426):

todosPares5(426) = todosPares5(42) + todosPares5(6) == nd(426) = 3?

ora, todosPares5(42) = todosPares5(4) + todosPares5(2) == nd(42) = 2?

como todosPares5(4) = 1 e todosPares5(2) = 1, teríamos: 1+1 == 2, ou seja, 2 == 2,

que seria verdadeiro e que retornaria um número diferente de zero, digamos 1 (No C, não
temos garantia de que retorne 1. Mas suponhamos que seja, uma vez que verdadeiro poderia
estar associado a 1. Colocando este resultado de todosPares5(42) = 1 em todosPares5(426),
ficamos com: todosPares5(426) = 1 + todosPares5(6) == 3,

ou seja, 1+ 1 == 3, ou seja, 2==3, quer retornaria 0, falso. Ou seja, todosPares5(426) daria 0


como resultado, o que indicaria que o numero 426 não possui todos os seus dígitos pares, o
que seria incorreto, pois sabemos que isto ocorre. Portanto, deve-se ter muito cuidado em
implementações recursivas! Um indício de que algo estaria errado é que ao utilizar a função
todosPares5(n/10) e todosPares5(n%10), que sabemos que estariam retornando booleanos,
ainda assim estamos utilizando o operador '+'. Ora, sabemos que booleanos não podem ser
somados...

A 6a. implementação está correta pois o operador de multiplicação possui um comportamento


semelhante ao do e-logico. Basta 1 dígito não ser par para que a multiplicação retorne 0.

Ativação incorreta do ternário, operações com inteiros.

O ternário é um componente utilizado em expressões caracterizado por ter 3 operandos e 2


operadores. Os operadores '?' e ':' atuam em par e separam 3 campos nos quais podem ser
escritas outras expressões, como: [booleano]?[numero_v]:[numero_f] O 1o. campo [booleano]
é necessariamente um campo que deve retornar um booleano e portanto pode ser uma
expressão lógica ou uma expressão comparativa. Os dois outros campos [numero_v],
[numero_f] retornam números e portanto podem ser expressões aritméticas ou ternários.
Uma característica básica do ternário é que apenas um dos campos numéricos será ativado, e
não ambos. Se o booleano retornado pelo 1o. campo for verdadeiro, o campo [numero_v] é
ativado. Se o booleano retornado pelo 1o. campo for falso, o campo [numero_f] é ativado. No
C,

por conta da convenção de que um booleano estar associado ao fato de um número ser ou
não diferente de zero, poderia haver no cmapo que retorne um booleano, um número e no
campo reservado a números, um booleano. Entretanto, na hora de se utilizar um ternário,
deve-se preservar a visão correta. Pode-se abrir uma execeção de se utilizar booleanos nos
campos reservados a números, desde que ambos retornem booleanos.

Por exemplo:

1<2?1+2:5*2 ao ser ativada 1o. resolveria a expressão 1<2. Como esta é verdadeira, o 2o.
campo é ativado e retorna o resultado de 1+2 = 3.
3?0?4:5:6 ao ser ativada veria que como 3 é diferente de zero, portanto verdadeiro, então a
expressão a ser avaliada é 0?4:5. Por sua vez, como 0 é falso, o valor retornado é 5. Cabe
ressaltar que neste caso, nem o 6, nem o 4 são ativados. Poderia ser no lugar do 6 ou do 4
expressões complicadas que não são avaliadas!

O ternário é muito utilizado em funções recursivas, pois funções recursivas possuem pelo
menos 2 casos, o caso terminal e o caso genérico. Então o ternário é utilizado para separar os 2
casos.

No caso de expressões envolvendo números inteiros, uma das mais críticas é a divisão. Se
escrevemos 1/2, o resultado será 0, uma vez que 1 e 2 são inteiros, e que o resultado da
operação deve estar no mesmo universo dos números envolvidos e portanto deverá ser
inteiro. Esta é uma diferença fundamental do resultado da computação para a matemática, ou
para a calculadora, que quando dividimos 1 por 2 obtemos 0.5.

Erros gerais

Muitos erros são decorrentes de não saber usar funções, ou avaliar expressões que são
ingredientes básicos da matemática. Como as linguagens de programação procuram de certo
modo representar o que usamos na matemática, rever estes conceitos básicos é fundamental.

Alguns erros detectados são (inadmissíveis ...) do tipo:

3+5*f(3) simplificada para 8*f(2).

Se a expressão fosse:

(3+5)*f(3), isto levaria a 8*f(2).

A questão é que alguns operadores tem precedência sobre outros. Neste caso, o produto
(operador *) tem precedência sobre a soma (operador +).
A expressão 3+5*f(3) é vista como 3+(5*f(3)), onde a multiplicação deva ser feita antes da
soma e não como (3+5)*f(3), onde a soma teria a precedência. De qualquer modo, a
precedência no cálculo é sobre a função f(2), que se não sabemos o valor ficará sendo então a
expressão original.

Nos problemas, procuramos dividir um problema complexo em vários problemas mais simples
interligados. E cada um destes problemas é representado por funções que usam em essência,
expressões aritméticas, comparativas, lógicas ou ternários combinadas entre si.

Nas implementações, algumas funções são dadas, ou seja, não precisam ser implementadas,
bastando saber utilizá-las. Outras funções você deverá escrever, mas como estão interligadas,
vocẽ pode resolver algumas a partir de outras que você ainda não tenha resolvido. Ao final do
processo, você deve ter todas as funções resolvidas para verificar se a solução do problema
que você imaginou funciona de fato.

Outra dificuldade é a codificação de um raciocínio. Para isto geralmente passa-se por 2 etapas:
A 1a. etapa é a verbalização de um raciocínio, ou seja, a expressão do mesmo em palavras e
frases, porém contextualizados ou voltados para um ambiente de programação. A 2a. etapa é
a tradução da frase para o código da linguagem. A 1a. fase pede que você possa tentar resolver
vários problemas diferentes. Ao fazer isto, se percebe que muitos podem ser agrupados, por
serem semelhantes ou por usarem a mesma técnica ou abordagem para a sua solução. A 2a.
fase pede que você tenha contato com a máquina, que você coloque as funções no
computador e teste a sua solução. Nesta fase, geralmente é preciso bastante atenção em
detalhes pois a máquina possui uma compreensão restrita de regras e se você não codificar do
jeito esperado, a máquina irá rejeitar. Por isto é importante que vocẽ codifique o programa.
Infelizmente, nas 2 fases, para aprender é preciso dedicação e perseverança. Faça os exercícios
propostos, leia e estude os problemas resolvidos e codifique-os (embora todos já estejam
codificados...)

Você também pode gostar