- As instruções de selecção
if
else
e as instruções condicionaisif
. Sintaxe. Transformação entre os dois tipos de instrução. Exemplos.- Asserções: conceitos. Dedução de asserções: directa e inversa. O caso da instrução de atribuição.
- As guardas de instruções de selecção.
- Metodologia de desenvolvimento de instruções de selecção. Importância de partir dos objectivos.
- Simplificação de instruções de selecção. A instrução
return
nas instruções controladas peloif
e consequente possibilidade de eliminar oelse
.- A instrução
?
:
.- Importância do cálculo curto-circuitado dos operadores lógicos.
Não há nenhum problema interessante cuja resolução não implique a utilização das palavras "se", "enquanto" ou "até". Os "ses" vão ser tratados hoje: são as chamadas instruções de selecção ou condicionais. Os "enquantos" e "atés" vão ser tratados nas próximas aulas: são as chamadas instruções iterativas. Nesta aula falar-se-á das instruções de selecção, ficando as instruções alternativas guardadas para as próximas aulas.
Referir aqui que estas instruções já foram introduzidas informalmente em aulas anteriores, sendo agora formalizadas.
Os traçados abaixo devem ser feitos rapidamente, pois eles já devem saber bem como isto tudo funciona na prática. O tom da abordagem também deverá ser alterado tendo esse facto em conta.
Existem duas instruções essenciais: a instrução de selecção e a instrução condicional.
A sintaxe da instrução de selecção é:
Quando é executada, o computador verifica se a condição é verdadeira ou não. Se for, a
if(condição)
instrução1
else
instrução2
instrução1
é executada. Se não, a instrução2
é
executada. Em qualquer dos casos a execução continua
após a instrução de selecção.
Nunca são executadas as duas instruções. Executa-se
sempre uma.
Fazer traçado do programa:
A sintaxe da instrução condicional é
int x;
cin >> x;
if(x < 0)
x = 0;
else
x = 1;
cout << x << endl;
Quando é executada, o computador verifica se a condição é verdadeira ou não. Se for, a
if(condição)
instrução1
instrução1
é executada. Senão nada acontece. Em qualquer
dos casos a execução continua após a instrução
condicional.
Fazer traçado do programa:
Assim sendo, uma instrução condicional pode sempre ser transformada na correspondente instrução de selecção.
int x;
cin >> x;
if(x < 0)
x = 0;
cout << x << endl;
é o mesmo que
if(condição)
instrução1
É extremamente importante fazer o desenvolvimento disciplinado do código, o que implica naturalmente começar por o especificar completamente. Isso vai ficar claro na aula prática.
if(condição)
instrução1
else
; //
Instrução nula! Não faz nada!
Na aula prática ser-vos-á pedido para resolverem um problema. A primeira solução que proporão estará errada. Os docentes das práticas dir-vos-ão porquê. Vocês corrigirão e... a solução estará errada na mesma. Eles dirão de novo porquê e vocês corrigirão. Só à terceira vai ficar bem... Depois propor-vos-ão que simplifiquem a solução. O mais provável é que introduzam um erro na resolução! Lembrem-se disto para tentarem não cometer nenhum erro! Quem já leu as folhas teóricas (e sei que foram poucos) não pode participar neste jogo, pois já sabe a resposta...
Vamos começar com um problema simples. Pretende-se escrever uma função que devolva o valor absoluto do dum inteiro passado como argumento.
A primeira coisa a fazer é escrever a estrutura da função, incluindo um comentário documentando a função:
É importante perceber que partes dizem "como se utiliza", "o que faz" e "como funciona" a função. Alguém sabe dizer-me?
/**
Devolve o valor absoluto do argumento.@pre ?.
@post ?.
*/
int absolutoDe(int const valor)
{
...
}
Discutir respostas. Anotar código com cores (cabeçalho: como se usa, comentário de documentação: o que faz, corpo: como funciona).
Qual será a pré-condição?
Discutir.
A pré-condição é uma expressão com
valor lógico que é verdadeiro quando os parâmetros
tiverem valores para os quais a função "funciona".
Neste caso há restrições a impor a valor
?
Discutir
o assunto. Na realidade deve-se impor a limitação de que valor
<> numeric_limits<int>::min()
! Não se fará para
simplificar, pois não é oportuno falar de numeric_limits
aqui.
Logo, a expressão deve ser verdadeira
independentemente do valor de valor
. Como não há
mais nenhum parâmetro, a expressão tem der sempre verdadeira!
Isto deve ser bem discutido, fazendo analogias com os conjuntos.
/**
Devolve o valor absoluto do argumento.@pre V.
@post ?.
*/
int absolutoDe(int const valor)
{
...
}
E a condição objectivo? O que é o valor absoluto
de um número valor
?
Discutir.
É claro que o valor devolvido deve ser >= 0! Mas chega?
A condição objectivo tal como está não impõe qualquer relação entre o valor devolvido e
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
.*/
int absolutoDe(int const valor)
{
...
}
valor
!
Senão, repare-se:
Esta função está correcta? Não! Mas calma, devolvendo 5 verifica-se sempre ou não verifica a condição objectivo?
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
.*/
int absolutoDe(int const valor)
{
return 5;
}
Então que mais devemos afirmar acerca do valor devolvido?
Discutir. Concluir que deve ser:
Perfeito. Agora, para simplificar, vamos definir uma variável para conter o valor a devolver pela função quando esta retornar:
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
...
}
Discutir nome parecido com o da função!
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
int absoluto;
...
return absoluto;
}
Podemos por isso repetir as condições que se admite ou se sabe serem verdadeiras em cada instante. No início da função assumimos a pré-condição como verdadeira. No seu final, antes do retorno, escrevemos a condição objectivo:
A estes comentários com afirmações acerca do estado do programa, i.e., acerca dos valores das instâncias, em cada intervalo entre instruções chama-se asserções, como vocês já sabem.
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
//
V.
int absoluto;
...
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).return absoluto;
}
É boa ideia colocar logo as respectivas instruções asserção:
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
assert(true);//
V.
int absoluto;
...
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).assert(0 <= absoluto and (absoluto == valor or absoluto == -valor));
return absoluto;
}
Dizer que assert(true);
não é muito útil, pelo que se
eliminará.
De regresso ao problema que temos de resolver, digam-me: que instruções posso eu usar para resolver o problema, se não sempre, pelo menos em algumas circunstâncias? Pensem em simples instruções de atribuição. Inspirem-se na condição objectivo!
Discutir. Fazê-los chegar a:
absoluto
= valor;
absoluto
= -valor;
Perfeito! É claro que nem sempre cada uma destas instruções resolve o problema. Em que circunstâncias o farão? É claro que aqui podíamos recorrer apenas à intuição: este é um problema tão simples! Em vez disso vamos formalizar um pouco, o que nos vai permitir perceber melhor o problema, as instruções de selecção, e vai também preparar-nos para resolver problemas mais complicados.
Comecemos por um parênteses.
Vamos supor uma dada instrução de atribuição. Por exemplo:
Em que circunstâncias se garante que, depois desta instrução, se tem 2 <
double x, y;
...
y = x * x;
y
?
Antes de resolver o problema vamos transformar o nosso requisito acerca
de y
numa asserção que deve ser verdadeira depois
da instrução:
Então o nosso problema é achar uma asserção que, se for verdadeira antes da instrução, leve forçosamente ao resultado desejado:
y = x * x;
//
2 <y
.
Podem-se colocar asserções entre todas as instruções de um programa. Cada asserção funciona com pré-condição para a instrução que se lhe segue e como condição objectivo para a instrução que a precede. Logo:
//
?.y = x * x;
//
2 <y
.
O problema então é determinar uma PC tal que, depois da instrução, se verifique a CO.
//
PC: ?.y = x * x;
//
CO: 2 <y
.
Uma solução possível é
É verdade, mas não será uma pré-condição demasiado forte? É!
//
PC: 100 <x
.y = x * x;
//
CO: 2 <y
.
Discutir que uma asserção é mais forte se for mais restritiva, ou seja, se o conjunto dos valores que a verificam for "mais pequeno".
Vamos reformular o nosso problema: qual será a pré-condição mais fraca que garante que, depois da instrução, a condição objectivo se verifica?
Discutir que uma asserção é mais fraca se for menos restritiva, ou seja, se o conjunto dos valores que a verificam for "maior". I.e., condições fortes correspondem a conjuntos pequenos e condições fracas correspondem a conjuntos grandes.
Repare-se que se pôs o problema ao contrário do que é usual: partimos da condição objectivo, e não ao contrário. Fizemo-lo por duas razões:
onde CO é a condição objectivo, que se pretende que seja verdadeira depois da atribuição. Então a pré-condição mais fraca que é necessário impor antes da instrução pode ser obtida substituindo na condição objectivo todas as ocorrências da variável pela expressão.
variável = expressão;
//
CO
Como assim? Regresse-se ao nosso exemplo:
//
PC: ?.y = x * x;
//
CO: 2 <y
.
Aplique-se a receita:
Perfeito!//
PC: 2 <x
2, ou seja,x
< -2½ ou 2½ <x
.y = x * x;
//
CO: 2 <y
.
Funciona sempre? Sempre. Não falha.
Outro exemplo:
Onde está a atribuição? Bom, isto é equivalente a:
//
PC: ?.++x;
//
CO:x
<= 0.
Logo
//
PC: ?.x = x + 1;
//
CO:x
<= 0.
E se for:
//
PC:x
+ 1 <= 0, ou seja,x
<= -1.x = x + 1;
//
CO:x
<= 0.
O mesmo:
//
PC: ?.x = x * x - 2;
//
CO: 0 <=x
.
Ver se perceberam bem! Discutir.//
PC: 0 <=x
2 - 2, ou seja, 2 <=x
2, ou seja,x
<= -2½ ou 2½ <=x
x = x * x - 2;
//
CO: 0 <=x
.
Fim de parênteses!
Regressados ao nosso problema original, podemos agora perguntar-nos: quais as pré-condições mais fracas que permitem atingir os objectivos depois das duas instruções?
e
//
PC1: ?.absoluto
= -valor;
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).
Simples: são atribuições, aplique-se a receita.//
PC2: ?.absoluto
= valor;
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).
e
//
PC1: 0 <= -valor
e (-valor
=valor
ou -valor
= -valor
), ou seja,//
PC1:valor
<= 0 e (-valor
=valor
ou V), ou seja,//
PC1:valor
<= 0 e V, ou seja,//
PC1:valor
<= 0.absoluto
= -valor;
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).
Conclusão de La Palisse: a instrução
//
PC2: 0 <=valor
e (valor
=valor
ouvalor
= -valor
), ou seja,//
PC2: 0 <=valor
e (V ouvalor
= -valor
), ou seja,//
PC2: 0 <=valor
e V, ou seja,//
PC2: 0 <=valor
.absoluto
= valor;
//
0 <=absoluto
e (absoluto
=valor
ouabsoluto
= -valor
).
só leva ao resultado pretendido se
absoluto
= -valor;
valor
<= 0, e a instrução
só leva ao resultado pretendido se 0 <=
absoluto
= valor;
valor
...
Bom, isto significa que precisamos mesmo de uma instrução de selecção, para seleccionar qual das duas instruções usar em cada momento:
Aqui há um ponto importante: a instrução de selecção tem duas instruções alternativas. A primeira é executada quando a condição for verdadeira. Neste caso quando
if(valor <= 0)
absoluto = -valor;
else
absoluto = valor;
valor
<= 0. A segunda instrução alternativa é
executada quando a condição for falsa. Neste caso quando 0
< valor
.
Mas 0 < valor
não foi a condição encontrada para a
instrução
Será isso problemático? Não, porque 0 <
absoluto
= valor;
valor
implica
0 <= valor
. Na nossa notação isso escreve-se colocando
uma asserção após a outra:
É importante perceber esta implicação: se um valor for maior que zero, então é forçosamente maior ou igual a zero.
//
0 <valor
.//
0 <=valor
.
Pode-se pensar em termos de teoria dos conjuntos: o conjunto dos valores maiores do que zero está contido no conjunto dos valores maiores ou iguais a zero.
Fixem esta relação: uma implicação corresponde nos conjuntos à relação "está contido".
Dizer também que se uma asserção implica outra, então a primeira é mais forte que a segunda.
Voltando ao problema, o que se passa neste caso é que a instrução
de selecção nos força a decidir qual das instruções
lida com o caso valor
= 0. Qualquer uma pode tratar desse caso, pelo
que se escolheu que fosse a primeira. Se se quisesse que fosse a
segunda que alteração teríamos de fazer?
Discutir. Escrever versão completa da função.
Para simplificar pode-se usar um operador especial:
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
int absoluto;
if(valor <= 0)
absoluto = -valor;
else
absoluto = valor;
assert( 0 <= absoluto and (absoluto == valor or absoluto = = -valor));
return absoluto;
}
?
:
.
que se pode simplificar para:
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
int absoluto = valor <= 0 ? -valor : valor;
assert( 0 <= absoluto and (absoluto == valor or absoluto = = -valor));
return absoluto;
}
/**
Devolve o valor absoluto do argumento.@pre V.
@post 0 <=
absolutoDe
e (absolutoDe
=valor
ouabsolutoDe
= -valor
).*/
int absolutoDe(int const valor)
{
return valor <= 0 ? -valor : valor;
}
Neste caso não é possível escrever a instrução de asserção para a condição objectivo, o que é uma desvantagem.
Um outro exemplo. O problema agora é escrever uma função:
Dizer que
double valorMaisPróximoDentroDe(double const v, double const mín, double const máx)
{
double r;
...
return r;
}
r
, v
, mín
e máx
e não são os melhores nomes para as variáveis, mas sim um compromisso
para escrever expressões curtas no quadro.
A ideia é devolver o valor do intervalo fechado [mín
, máx
] que esteja
mais próximo de v
.
A primeira coisa a fazer é escrever a pré-condição e a condição objectivo. Qual é a pré-condição?
Discutir.
É claro que isto só faz sentido quando mín
<= máx
!
E a condição objectivo?
Discutir.
A condição objectivo deve dizer que, das três uma,
v
é menor que mín
e o valor devolvido é igual a
mín
;v
é maior que máx
e o valor devolvido é igual a
máx
;v
pertence ao intervalo e o valor devolvido é igual a v
!Olhando para a condição objectivo vemos uma disjunção: há três termos no ou. É provável, portanto, que o problema seja resolúvel com uma instrução de selecção.
/**
Devolve o valor dev
limitado ao intervalo dado.@pre
mín
<=máx
.@post (
v
<mín
evalorMaisPróximoDentroDe
=mín
) ou(
máx
<v
evalorMaisPróximoDentroDe
=máx
) ou(
mín
<=v
ev
<=máx
evalorMaisPróximoDentroDe
=v
).*/
double valorMaisPróximoDentroDe(double const v, double const mín, double const máx)
{
assert(mín <= máx);
//
PC:mín
<=máx
.
double r;
...
//
CO:(
v
<mín
er
=mín
) ou (máx
<v
er
=máx
) ou(
mín
<=v
ev
<=máx
er
=v
).
assert((xv < mín and r == mín) or (máx < v and r == máx) or
(mín <= v and v <= máx and r == v));
return r;
}
Chamar a atenção para isto!
Que instruções resolvem o problema, pelo menos em algumas circunstâncias? Fácil! Basta olhar para a condição objectivo:
Vamos ver quais as respectivas pré-condições mais fracas.
r = mín;
r = máx;
r = v;
Para r = mín;
temos:
Pode-se simplificar esta pré-condição:
//
PC1: (v
<mín
emín
=mín
) ou (máx
<v
emín
=máx
) ou//
(mín
<=v
ev
<=máx
emín
=v
).
r = mín;//
(v
<mín
er
=mín
) ou (máx
<v
er
=máx
) ou//
(mín
<=v
ev
<=máx
er
=v
).
Para
//
PC1:v
<mín
ou (máx
<v
emín
=máx
) ou (v
<=máx
emín
=v
).
r = máx;
temos:
Pode-se simplificar a pré-condição para:
//
PC2: (v
<mín
emáx
=mín
) ou (máx
<v
emáx
=máx
) ou//
(mín
<=v
ev
<=máx
emáx
=v
).
r = máx;//
(v
<mín
er
=mín
) ou (máx
<v
er
=máx
) ou//
(mín
<=v
ev
<=máx
er
=v
).
Para
//
PC2: (v
<mín
emáx
=mín
) oumáx
<v
ou (mín
<=v
emáx
=v
).
r = v;
temos:
Pode-se simplificar esta pré-condição para:
//
PC3: (v
<mín
ev
=mín
) ou (máx
<v
ev
=máx
) ou//
(mín
<=v
ev
<=máx
ev
=v
).
r = v;//
(v
<mín
er
=mín
) ou (máx
<v
er
=máx
) ou//
(mín
<=v
ev
<=máx
er
=v
).
ou seja
//
PC3: F ou F ou (mín
<=v
ev
<=máx
).
//
PC3:mín
<=v
ev
<=máx
.
Temos de escrever uma instrução de selecção com três instruções controladas. Como cada instrução de selecção controla exactamente duas instruções, temos de encadear instruções de selecção:
Sabendo que//
PC:mín
<=máx
.if(
C1)
PC1:
//v
<mín
ou (máx
<v
emín
=máx
) ou (v
<=máx
emín
=v
).
r = mín;
else if(
C2)
//
PC2: (v
<mín
emáx
=mín
) oumáx
<v
ou (mín
<=v
emáx
=v
).r = máx;
else
//
PC3:mín
<=v
ev
<=máx
.r = v;
mín
<= máx
, as pré-condições podem
ser simplificadas:
Claramente há uma sobreposição das pré-condições. Imagine-se que
//
PC:mín
<=máx
.if(
C1)
PC1:
//v
<mín
ou (máx
<v
emín
=máx
) oumín
=v
, ou seja,
// v
<=mín
ou (máx
<v
emín
=máx
).
r = mín;
else if(
C2)
//
PC2: (v
<mín
emáx
=mín
) oumáx
<v
oumáx
=v
, ou seja,//
(v
<mín
emáx
=mín
) oumáx
<=v
.r = máx;
else
//
PC3:mín
<=v
ev
<=máx
.r = v;
mín
= máx
e v
> máx
. Esse caso é previsto tanto pela primeira
instrução como pela segunda! Pode-se eliminar da primeira
pré-condição! Imagine-se que mín
= máx
e v
<
mín
. Esse caso é previsto tanto pela primeira instrução
como pela segunda! Pode-se eliminar da segunda pré-condição!
Ainda sobram duas sobreposições: a primeira e a terceira instruções lidam bem com o caso
//
PC:mín
<=máx
.if(
C1)
PC1:
//v
<=mín
.
r = mín;
else if(
C2)
//
PC2:máx
<=v
.r = máx;
else
//
PC3:mín
<=v
ev
<=máx
.r = v;
v
= mín
e a segunda e a terceira
instruções lidam bem com o caso v
= máx
. Pode-se fazer
com que seja a terceira instrução a lidar com esses casos:
Então que condições escolher para os
//
PC:mín
<=máx
.if(
C1)
PC1:
//v
<mín
.
r = mín;
else if(
C2)
//
PC2:máx
<v
.r = máx;
else
//
PC3:mín
<=v
ev
<=máx
.r = v;
if
?
A primeira é fácil: v
< mín
!
E a segunda? v
> máx
. Logo:
Em que circunstâncias é executada a terceira instrução? Quando
//
PC:mín
<=máx
.if(xv < mín)
PC1:
//v
<mín
.
r = mín;
else if(máx < v)
//
PC2:máx
<v
.r = máx;
else
//
PC3:mín
<=v
ev
<=máx
.r = v;
mín
<= v
e v
<= máx
, como se pretendia!
Concluindo:
O problema pode ser resolvido com instruções de retorno imediato (embora nessa caso a instrução de asserção para a condição objectivo não possa ser mantida):
/**
Devolve o valor dev
limitado ao intervalo dado.@pre
mín
<=máx
.@post (
v
<mín
evalorMaisPróximoDentroDe
=mín
) ou(
máx
<v
evalorMaisPróximoDentroDe
=máx
) ou(
mín
<=v
ev
<=máx
evalorMaisPróximoDentroDe
=v
).*/
double valorMaisPróximoDentroDe(double const v, double const mín, double const máx)
{
assert(mín <= máx);
double r;
if(xv < mín)
r = mín;
else if(máx < v)r = máx;
else
r = v;
assert( (v < mín and r == mín) or (máx < v and r == máx) or
(mín <= v and v <= máx and r == v));
return r;
}
Mas nesse caso os
/**
Devolve o valor dev
limitado ao intervalo dado.@pre
mín
<=máx
.@post (
v
<mín
evalorMaisPróximoDentroDe
=mín
) ou(
máx
<v
evalorMaisPróximoDentroDe
=máx
) ou(
mín
<=v
ev
<=máx
evalorMaisPróximoDentroDe
=v
).*/
double valorMaisPróximoDentroDe(double const v, double const mín, double const máx)
{
assert(mín <= máx);
if(xv < mín)
return mín;
else if(máx < v)return máx;
else
return v;
}
else
são redundantes, pelo que é
comum escrever-se:
/**
Devolve o valor dev
limitado ao intervalo dado.@pre
mín
<=máx
.@post (
v
<mín
evalorMaisPróximoDentroDe
=mín
) ou(
máx
<v
evalorMaisPróximoDentroDe
=máx
) ou(
mín
<=v
ev
<=máx
evalorPróximoDentro
=v
).*/
double valorMaisPróximoDentroDe(double const v, double const mín, double const máx)
{
assert(mín <= máx);
if(xv < mín)
return mín;
if(máx < v)return máx;
return v;
}
Discutir porque é assim! Mas dizer que o mais claro é mesmo
manter os else
!
Seguimos uma metodologia no desenvolvimento desta instrução de selecção. Essa metodologia diz o seguinte: