Escrever o programa da aula anterior no quadro enquanto a aula não começa!
#include <iostream>
using namespace std;
/**
Devolve o máximo divisor comum dos inteiros positivos passados como
argumento.
@pre 0 <
m
e 0 <n
.@post
mdc
= mdc(m
,n
).*/
int mdc(int const m, int const n)
{
assert(0 < m and 0 < n);
int k;
if(m < n)
k = m;
else
k = n;
while(m % k != 0 or n % k != 0)
--k;
assert(0 < k and m % k == 0 and n % k == 0);
return k;
}
/**
Escreve no ecrã uma fracção, no formato usual, que lheé passada na forma de dois argumentos inteiros positivos.
@pre V.
@post O ecrã contém
n/d
(ou simplesmenten
, sed
= 1) em quen
ed
são os valores de
n
ed
em base decimal.*/
void escreveFracção(int const n, int const d)
{
cout << n;
if(d != 1)
cout << '/' << d;}
Deixar aqui espaço para o procedimento reduzFracção()
!
Perguntar se têm dúvidas acerca da aula passada. Explicar lentamente o programa.
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
int k = mdc(n1, d1);
n1 /= k;
d1 /= k;
k = mdc(n2, d2);
n2 /= k;
d2 /= k;
//
Calcular fracção soma reduzida:int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
k = mdc(n, d);
n /= k;
m /= k;
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(n, d);
cout << '.' << endl;
}
Na aula passada resolvemos o problema da soma de duas fracções. Hoje vamos melhorá-lo aproveitando para introduzir alguns conceitos adicionais. Se repararem bem no programa desenvolvido concluem que há instruções repetidas....
Perguntar quais. São as instruções para redução das fracções.
Ou seja, as instruções
deveriam ser colocadas dentro de um módulo. Esse módulo deve ser uma função ou um procedimento?
k = mdc(n, d);
n /= k;
d /= k;
Poderia ser uma função se devolvesse a fracção reduzida, mas uma função só pode devolver um valor! Então pode ser um procedimento que reduz a fracção. Vamos então colocar as instruções num módulo:
Notem que
/**
Reduz a fracção recebida como argumento.@pre 0 <
n
e 0 <d
.@post
mdc
(n
,d
) = 1 (en
/d
representa o mesmo racional que
originalmente).
*/
void reduzFracção(int n, int d)
{
assert(0 < n and 0 < d);
int const k = mdc(n, d);
n /= k;
d /= k;
assert(mdc(n, d) == 1);
}
n
e d
passaram a ser entradas do procedimento.
Então o programa passa a ser:
Vamos fazer um traçado deste programa.
#include <iostream>
using namespace std;
/**
Devolve o máximo divisor comum dos inteiros positivos passados como
argumento.
@pre 0 <
m
e 0 <n
.@post
mdc
= mdc(m
,n
).*/
int mdc(int const m, int const n)
{
assert(0 < m and 0 < n);
int k;
if(m < n)
k = m;
else
k = n;
while(m % k != 0 or n % k != 0)
--k;
assert(0 < k and m % k == 0 and n % k == 0);
return k;
}
/**
Escreve no ecrã uma fracção, no formato usual, que lheé passada na forma de dois argumentos inteiros positivos.
@pre V.
@post O ecrã contém
n/d
(ou simplesmenten
, sed
= 1) em quen
ed
são os valores de
n
ed
em base decimal.*/
void escreveFracção(int const n, int const d)
{
cout << n;
if(d != 1)
cout << '/' << d;}
/**
Reduz a fracção recebida como argumento.@pre 0 <
n
e 0 <d
.@post
mdc
(n
,d
) = 1 (en
/d
representa o mesmo racional que
originalmente).
*/
void reduzFracção(int n, int d)
{
assert(0 < n and 0 < d);
int const k = mdc(n, d);
n /= k;
d /= k;
assert(mdc(n, d) == 1);
}
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
reduzFracção(n1, d1);
reduzFracção(n2, d2);
//
Calcular fracção soma reduzida:int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
reduzFracção(n, d);
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(n, d);
cout << '.' << endl;
}
Fazer traçado dividindo claramente os contextos. main()
é um contexto e reduzFracção()
outro. Variáveis
são em notação UML! Usar 6/9 e 7/3. Dizer que os parâmetros
n
e d
são destruídos quando o procedimento
retorna!
Ou seja, o procedimento reduzFracção()
de facto reduz
uma fracção, mas é uma cópia da fracção
original que é reduzida! n1
e d1
em main()
ficam com os mesmos
valores! Não é o que se pretendia!
Este problema ocorreu porque em C++ os argumentos são passados por valor, ou seja, o valor de um argumento é usado para inicializar o parâmetro respectivo. I.e., os parâmetros são cópias dos argumentos!
Assim, os parâmetros representam entradas do módulo (função ou procedimento), e não
saídas! Mas os procedimentos, para serem úteis, têm de ter
alguma consequência! O procedimento escreveFracção()
,
por exemplo, tem uma consequência: o ecrã passa a conter mais uma fracção.
Falta pois descobrir como convencer o C++ de que os parâmetros também
podem ser saídas.
Como resolver o problema? O C++ fornece uma forma alternativa de passagem de argumentos: a passagem de argumentos por referência. Neste tipo de passagem os parâmetros tornam-se sinónimos dos argumentos!
A sintaxe da definição de parâmetros que são sinónimos é ligeiramente diferente da habitual:
O "'e' comercial" após o tipo do parâmetro significa que, quando o procedimento for invocado, o parâmetro funcionará como sinónimo da variável usada como argumento. Quando se usa um parâmetro que é uma referência o argumento respectivo tem de ser uma instância.
/**
Reduz a fracção recebida como argumento.@pre 0 <
n
e 0 <d
.@post
mdc
(n
,d
) = 1 (en
/d
representa o mesmo racional que
originalmente).
*/
void reduzFracção(int& n, int& d)
{
int k = mdc(n, d);
n /= k;
d /= k;
}
Explicar que instância é o nome genérico de variáveis ou constantes.
Fazer novo traçado, agora representar os parâmetros em UML, mas sem a caixa envolvente, pois não têm valor próprio: são sinónimos. Deles sai uma seta para a variável real. Usar sempre a notação das folhas teóricas!
Assim, os parâmetros que são referências podem servir tanto de entradas como de saídas!
Dizer que só se devem usar parâmetros referência no caso dos procedimentos, nunca no caso das funções (mais tarde relaxaremos a recomendação para permitir parâmetros de funções que são referências para constantes). Explicar que uma função que recebe argumentos por referência faz algo para além de calcular e devolver um valor, o que viola a regra elementar da modularização: cada módulo tem uma única função bem definida. Mencionar rotinas "dois em um".
Garantir que não sobrou qualquer dúvida. Repetir explicações se necessário. Frisar bem importância do conceito de passagem de argumentos por referência.
Agora vamos mudar de assunto. Já vos disse na aula passada que instâncias definidas dentro de uma rotina não têm nada a ver com instâncias com o mesmo nome definidas dentro de outra rotina. Porquê?
Começo por introduzir um conceito importante: o de bloco de instruções ou instrução composta. Quando se pretende, por alguma razão, agrupar um conjunto de instruções de modo a que funcionem como uma só, constrói-se um bloco de instruções colocando-as entre chavetas. Por exemplo:
Neste programa existem dois blocos de instruções: o corpo da função
int const j = 1;
int main()
{ //
Bloco 1.int i = 2;
if(0 < i)
{ //
Bloco 2.int j = i;
int const i = 3;
int k = 10;
cout << i << j << endl;
}
}
main()
e o bloco dentro de main()
.
Em C++ existem dois tipos de instâncias (para já): as locais e as globais, consoante o contexto em que são definidas. As instâncias locais são as que são definidas dentro de rotinas. As globais são definidas fora das rotinas. No exemplo temos uma instância global (constante) e quatro locais (três variáveis e uma constante).
As instâncias globais pertencem ao contexto global. As locais pertencem ao contexto do bloco de instruções em que foram definidas. Dentro do mesmo contexto não se podem definir duas instâncias com o mesmo nome! Ou seja:
Cada instância tem um âmbito e uma permanência. Por âmbito entende-se a zona do programa na qual o nome da instância é visível. Por permanência entende-se o período ou períodos de tempo durante o qual a instância existe.
int const j = 1;
erro!
int j = 11;//
int main()
{
int i = 2;
erro!
int i = 22;//
if(0 < i)
{
int j = i;
int const i = 3;
erro!
int const i = 33;//int k = 10;
cout << i << ' ' << j << endl;
}
}
Uma instância global é visível ao longo de todo o ficheiro desde o ponto de declaração até ao seu fim. Uma instância local é visível desde o ponto de definição até ao fim do bloco onde foi definida, incluindo os blocos embutidos.
Explicar blocos embutidos.
Assim, a constante global j
é visível.... Desenhar
zona no quadro
A variável local i
(a primeira) é visível....
Idem para todas.
Não notam nenhum problema? De facto: há aqui sobreposição de âmbitos! Como resolver?
Discutir.
De facto, o C++ especifica que, quando um nome definido num contexto colide com um nome definido num contexto mais exterior, o nome que é visível é o que está definido no contexto mais interior. Chama-se a este fenómeno ocultação. Ou seja:
Apagar linhas de visibilidade onde ocorre ocultação.
Perguntar por dúvidas.
Usar o programa das fracções como exemplo daqui para baixo!
Outra questão é: quando existem as instâncias?
Será que a constante k
existe sempre enquanto o programa está
a ser executado? Não!
As instâncias globais existem do princípio ao fim do programa: são estáticas. As instâncias locais existem desde o momento em que a instrução da sua definição é executada até ao fim do bloco onde são definidas: são automáticas. Diz-se que as instâncias são construídas e destruídas. As globais são construídas no início do programa e destruídas no fim. A locais são construídas quando a definição é executada e destruídas quando se atinge o final do seu bloco.
Fazer traçado do programa. As instâncias são construídas e destruídas, ou seja, os blocos UML são desenhados e apagados.
Explicar caso do programa completo no quadro! Dar ênfase a parâmetros como instâncias locais: criados e destruídos. No caso das referências é só o sinónimo que é destruído. Dizer que o âmbito das instâncias deve ser sempre o mais estreito possível.
Porque não definir n1
,
d1
, n2
, d2
, n
e d
logo no início?
Falei de instâncias globais, mas o nosso programa não as
usa. Será que deveria usar? E será indiferente usar
variáveis ou constantes globais? Vamos experimentar fazer n1
,
d1
, n2
, d2
globais.
Explicar porque é má ideia usar variáveis globais. Reescrever programa com variáveis globais:
Regra: Não usar variáveis globais!
Explicar porque constantes globais fazem todo o sentido. Exemplificar com o valor pi.
Temos, portanto, o nosso programa completo. Sim? Não...
Vamos fazer-lhe uma pequena alteração. Lembram-se da
forma como o programa foi desenvolvido de cima para baixo? Primeiro
pensou-se no problema globalmente, e escreveu-se main()
, depois pensou-se
nos subproblemas, e escreveu-se escreveFracção()
e reduzFracção()
,
e depois pensou-se no mdc()
. Mas a ordem das definições
não reflecte a ordem de desenvolvimento! O problema é
que, antes de se poder usar uma rotina, o
compilador tem de saber que ela existe.
Indicar ao C++ a existência de uma rotina é declará-la. É possível declarar uma
rotina e adiar a sua definição para mais tarde.
Uma declaração é idêntica a uma definição,
mas em que o corpo é substituído por um simples ponto-e-vírgula ;
. Se se declararem
as rotinas onde elas são necessárias,
podemos escrever as definições pela ordem que nos pareça
mais lógica. Ou seja:
#include <iostream>
using namespace std;
int main()
{
void escreveFracção(int n, int d);
void reduzFracção(int& n, int& d);
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
reduzFracção(n1, d1);
reduzFracção(n2, d2);
//
Calcular fracção soma reduzida:int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
reduzFracção(n, d);
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(n, d);
cout << '.' << endl;
}
/**
Escreve no ecrã uma fracção, no formato usual, que lheé passada na forma de dois argumentos inteiros positivos.
@pre V.
@post O ecrã contém
n/d
(ou simplesmenten
, sed
= 1) em quen
ed
são os valores de
n
ed
em base decimal.*/
void escreveFracção(int const n, int const d)
{
cout << n;
if(d != 1)
cout << '/' << d;}
/**
Reduz a fracção recebida como argumento.@pre 0 <
n
e 0 <d
.@post
mdc
(n
,d
) = 1 (en
/d
representa o mesmo racional que
originalmente).
*/
void reduzFracção(int& n, int& d)
{
assert(0 < n and 0 < d);
int mdc(int m, int n);
int const k = mdc(n, d);
n /= k;
d /= k;
assert(mdc(n, d) == 1);
}
/**
Devolve o máximo divisor comum dos inteiros positivos passados como
argumento.
@pre 0 <
m
e 0 <n
.@post
mdc
= mdc(m
,n
).*/
int mdc(int const m, int const n)
{
assert(0 < m and 0 < n);
int k;
if(m < n)
k = m;
else
k = n;
while(m % k != 0 or n % k != 0)
--k;
assert(0 < k and m % k == 0 and n % k == 0);
return k;
}
Uma declaração diz ao compilador tudo o que ele precisa de
saber para usar a rotina, i.e., a sua interface,
o seu cabeçalho. Em reduzFracção()
, por exemplo,
a declaração significa algo como: "compilador, existe uma
função (definida mais tarde) chamada mdc()
, com dois parâmetros
do tipo int
(passagem por valor), e que devolve um int
".
Se houver tempo, falar de nomes de instâncias, rotinas e do comprimento típico de rotinas.
Nomes:
Instâncias: Separar palavras com _
.
Rotinas: Não separar palavras. Iniciar todas as palavras excepto a primeira com maiúsculas.
Rotinas: