return
. Conceitos de retorno e devolução.
Caso dos procedimentos: não há devolução (tipo
de retorno void
).Perguntar quem é audiófilo, ou quem tem aparelhagem. Pedir para a descrever.
Uma aparelhagem é constituída por várias partes: colunas, amplificador, sintonizador, leitor de cassetes, leitora de CD.
Existem aparelhagens integradas, tipicamente mais baratas.
Quais as vantagens da divisão de uma aparelhagem em módulos?
Listar e discutir.
As vantagens são pelo menos:
Quais as vantagens da divisão em módulos para o produtor?
Listar e discutir.
As vantagens são pelo menos:
Listar e discutir:
Pelo menos:
A isso se chama capacidade de abstracção: para quê preocuparmo-nos com pormenores que são irrelevantes para a utilização? Quando conduzimos o automóvel preocupamo-nos com o mecanismo da embraiagem? Não!
O que se ganha com a abstracção? Acontece que o humano tem capacidades limitadas.
Pedir a um deles para dizer rapidamente quantos traços escrevi no ecrã. Começar por 1 e 2, passar a 5, saltar para 10 e 19. Explicar a nossa dificuldade em abarcar demasiadas coisas ao mesmo tempo. Mencionar que os indivíduos capazes de dizer rapidamente quantos fósforos há numa caixa de fósforos aberta poucos instantes antes são raros, casos patológicos, e normalmente vivem em hospitais psiquiátricos.
A abstracção permite-nos limitar a quantidade de informação com que temos de lidar. Isso leva a um melhor desempenho da tarefa que estamos a realizar, onde cometemos menos erros. Isto é verdade desde a condução de automóveis até à escrita de programas!
Os módulos têm uma função bem definida e uma interface que permite utilizá-los. Por exemplo, o amplificador tem, por trás, entradas para os sinais a amplificar e, à frente, o botão da alimentação, o controlo do volume, e pouco mais. O interior está acessível ao consumidor? Não!
E no caso de um relógio? O interior está acessível? Não! Só se vê o mostrador com os ponteiros! Mesmo nos relógios com caixa transparente o mecanismo está inacessível. Porquê?
Perguntar e discutir.
Porque:
O encapsulamento facilita a abstracção!
Em programação a divisão em módulos, a abstracção e o encapsulamento são conceitos muito importantes e úteis!
Hoje vamos falar de divisão em módulos e de abstracção. O encapsulamento ficará para mais tarde.
Um programador assume dois papeis distintos quando desenvolve um programa. É produtor dos módulos por um lado, quando desenvolve o seu mecanismo. É consumidor, quando os integra num sistema mais complexo.
Como produtor preocupa-se com o que o módulo faz, como o consumidor o vai usar, e como funciona o seu mecanismo interno. Produz o módulo, as suas especificações técnicas e diagramas da sua concretização prática, para além de preparar o manual de utilização.
Como consumidor abstrai-se do mecanismo interno, preocupando-se apenas com o que o módulo faz e como o deve utilizar: basta-lhe o manual do utilizador.
É importante perceber que é estabelecido um contrato informal entre produtor e consumidor: o produtor indica a forma correcta de utilização e garante (pelo menos dentro de um prazo) que se o utilizador fizer como está lá indicado, o módulo funcionará como pretendido.
Mas afinal, o que são os módulos em C++? Os módulos básicos são as rotinas, que podem ser funções ou procedimentos, e que se estudarão de seguida.
Nas aulas anteriores discutiu-se o que eram algoritmo e programa, qual o significado de tipo, variável e valor literal em C++, alguns dos operadores do C++. Mas, como se pega em tudo o que aprendemos e se resolve um problema?
Há muitas abordagens para a resolução de problemas. Porventura uma das mais clássicas em programação é chamada abordagem descendente, ou top down. Nesta abordagem o problema começa por ser analisado na globalidade, tentando-se identificar um conjunto pequeno de subproblemas em que possa ser decomposto. Feita esta decomposição, pode-se analisar cada subproblema da mesma forma, dividindo-o em subsubproblemas mais simples. E assim sucessivamente, até se chegar a problemas de resolução trivial.
Vamos estudar um exemplo. Suponhamos que pretendemos escrever um programa para somar duas fracções positivas fornecidas pelo utilizador e mostrar o resultado no ecrã na forma de uma fracção irredutível. Por exemplo, dadas as fracções:
o resultado deve ser6/9 + 7/3
Como resolver o problema? Pensemos no problema na globalidade. É fácil identificar três subproblemas:3/1
Agora podemos começar a abordar cada subproblema. Como ler as duas fracções?
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções....
//
Calcular fracção soma reduzida....
//
Escrever resultado....
}
Primeiro é necessário dizer ao utilizador do programa que deve introduzir as duas fracções:
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
Como as representar no programa? É necessário criar
quatro variáveis int
, pois cada fracção tem um numerador
e um denominador:
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
Depois é necessário ler o seu valor:
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
Ou seja:
Como escrever o resultado? Escrevendo as fracções de entrada seguidas da fracção de resultado. Vamos, para já, adiar o problema da escrita de cada fracção, admitindo que existe um procedimento chamado
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
//
Calcular fracção soma reduzida.
...
//
Escrever resultado....
}
escreveFracção()
para o
efeito. O que é um procedimento? É um módulo
constituído por um conjunto de instruções com uma
interface bem definida e que faz qualquer coisa. Ou seja:
Frisar reaproveitamento do código escrito três vezes!
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
//
Calcular fracção soma reduzida.
...
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(
?,
?);
cout << '.' << endl;
}
Mas escrever o quê para o resultado? O resultado da soma deve ser colocado em algum lado e já vimos que para isso são necessárias variáveis. É portanto necessário definir duas variáveis para o numerador e o denominador da soma:
Falta-nos pois calcular a soma. Como o fazer?
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
//
Calcular fracção soma reduzida:
int n;
int d;
...
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(n, d);
cout << '.' << endl;
}
Discutir soma de fracções. Denominador é mínimo múltiplo comum dos denominadores. Mas existe forma mais simples.
A forma mais simples de somar é reduzir ao mesmo denominador multiplicando ambos os termos da primeira fracção pelo denominador da segunda e vice versa.
Ou seja:
Mas chega? Que aconteceria com as entradas 6/9 e 7/3?
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
//
Calcular fracção soma reduzida:
int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
...
//
Escrever resultado:cout << "A soma de ";
escreveFracção(n1, d1);
cout << " com ";
escreveFracção(n2, d2);
cout << " é ";
escreveFracção(n, d);
cout << '.' << endl;
}
O resultado seria 81/27! Não está reduzida! Como reduzir?
Discutir.
Reduzir uma fracção é dividir numerador e denominador pelo máximo divisor comum dos dois! Então, podemos escrever:
Recordar
#include <iostream>
using namespace std;
int main()
{
//
Ler fracções:cout << "Introduza duas fracções (numerador denominador): ";
int n1, d1, n2, d2;
cin >> n1 >> d1 >> n2 >> d2;
//
Calcular fracção soma reduzida:
int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
int 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;
}
/=
!
Que significa mdc(n, d)
? Será que a linguagem C++ já
tem forma de calcular o máximo divisor comum? Infelizmente não. Temos
de ensinar o computador. O que fizemos foi, mais uma vez, adiar o problema.
Assumimos que existia uma função mdc()
para fazer o cálculo
pretendido.
Assinalar bem:
Procedimento faz qualquer coisa.
Função calcula qualquer coisa.
Já agora podemos também reduzir logo as fracções de entrada!
Mas cometemos um erro! O C++ não deixa definir duas variáveis com o mesmo nome na mesma rotina!
#include <iostream>
using namespace std;
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;
int k = mdc(n2, d2);
n2 /= k;
d2 /= k;
//
Calcular fracção soma reduzida:
int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
int 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;
}
k
.
#include <iostream>
using namespace std;
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;
int
k = mdc(n2, d2);
n2 /= k;
d2 /= k;
//
Calcular fracção soma reduzida:
int n = d2 * n1 + d1 * n2;
int d = d1 * d2;
int
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;
}
Falta-nos pois definir a função mdc()
e o procedimento
escreveFracção()
. No fundo o que estamos a fazer é
a artilhar o C++ com mais operações!
Mas o máximo divisor comum já vimos como se calcula! Para colocar em
r
o
máximo divisor comum de m
e n
(variáveis inteiras), basta fazer
Desenhar variáveis em UML.
int m;
int n;
Inicializadas automagicamente!
//
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
Aliás, como m
e n
nunca mudam de valor, podem ser
constantes. Que é isso?
As variáveis têm três características: nome, tipo e valor. A
terceira característica pode mudar. As outras são fixas. Se
quisermos que o valor seja fixo, então define-se uma constante. Uma
constante define-se como uma variável, mas tem a palavra chave const
após o tipo:
int const m;
int
const
n;
Inicializadas automagicamente!
//
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
Desenhar constantes em UML. Basta escrever {frozen} no cabeçalho.
Se necessário explicar porque, sendo constantes, podem assumir valores diferentes ao longo da execução do programa: duração das variáveis.
É necessário agora envolver este código numa caixa, encapsulá-lo, torná-lo num módulo:
Resta agora dizer que este módulo se chama
{
int const m;
int const n;
Inicializadas automagicamente!
//
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
}
mdc
:
Mas falta algo. Como inicializar
mdc
{
int const m;
int const n;
//
Inicializadas automagicamente!
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
}
m
e n
? Repare-se que se pretende
inicializar m
e n
primeiro com os valores de n1
e
d1
, depois com os valores
de n2
e d2
, e finalmente com os valores n
e
d
! É preciso dizer
que m
e n
são variáveis, aliás,
constantes especiais de entrada: os chamados
parâmetros.
mdc(int const m, int const n)
{
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
}
Finalmente falta dizer que o valor contido em r
no final da função
é o valor de saída da função. Para isso
usa-se uma instrução de retorno:
Mas temos de indicar ao compilador que esta função tem como valor de saída ou devolução um valor do tipomdc(int const m, int const n)
{
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
return r;
}
int
!
E temos a função definida! A definição compõe-se de um cabeçalho e um corpo. O cabeçalho diz como se usa a função: passa-se-lhe dois inteiros e ela devolve outro como resultado. O corpo diz como funciona. Mas não há nada que diga o que faz a função! Para isso usam-se comentários com três partes:
int mdc(int const m, int const n)
{
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
return r;
}
Explicar PC e CO. Explicar que normalmente é desejável que a pré-condição seja tão fraca (permissiva) quanto possível, acontecendo o contrário para a condição objectivo.
/**
Devolve o máximo divisor comum dos inteiros
positivos passados como argumento.
PC: 0 <
m
e 0 <n
.CO: o valor
r
devolvido é o mdc dem
en
.*/
int mdc(int const m, int const n)
{
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
return r;
}
Dizer que normalmente se usa @pre e @post no código real:
/**
Devolve o máximo divisor comum dos inteiros positivos passados como argumento.@pre 0 <
m
e 0 <n
.@post o valor
r
devolvido é o mdc dem
en
.*/
int mdc(int const m, int const n)
{
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
return r;
}
Finalmente, deve-se sempre explicitar a pré-condição e a condição objectivo no próprio código. Para isso usa-se as instruções de asserção.
Atenção: "Asserção" é o mesmo que "afirmação" ou "proposição que se apresenta como verdadeira".
Explicar instruções de asserção. Dizer o que acontece quando uma asserção é violada.
Com as instruções de asserção, o código fica:
/**
Devolve o máximo divisor comum dos inteiros positivos passados como argumento.@pre 0 <
m
e 0 <n
.@post o valor
r
devolvido é o mdc dem
en
.*/
int mdc(int const m, int const n)
{
assert(0 < m and 0 < n);
int r;
if(m < n)
r = m;
else
r = n;
while(m % r != 0 or n % r != 0)
--r;
assert(0 < r and m % r == 0 and n % r == 0);
return r;
}
Explicar brevemente problemas ao escrever a condição objectivo.
Note-se bem na diferença entre definição e invocação. Na invocação escreve-se o nome da função seguida de uma lista de expressões que servem para inicializar as variáveis ou constantes que são os parâmetros. Por isso eles não são inicializadas explicitamente na função! A estas expressões chama-se argumentos.
É importante perceber que os parâmetros são inicializados com os valores dos argumentos em cada chamada ou invocação da função!
Exemplificar fazendo pequeno traçado.
Explicar retorno. return
+ expressão
. A função
ao terminar em return
retorna (volta ao ponto em que foi chamada) e devolve
um valor. Distinguir bem retornar e devolver! Dizer que em inglês
"return" significa tanto retornar como devolver. O
português aqui é mais rico e tiramos partido disso.
Será que as duas variáveis n
, uma na função
main()
(sim! main()
é uma função!) e outra na função
mdc()
têm alguma coisa em comum? Não! Estão
em contextos diferentes! Numa aparelhagem também existem itens
distintos com nomes iguais em módulos distintos: por exemplo o botão
de ligar e desligar. São variáveis locais, que existem
apenas no contexto da função em que são definidas.
Falta o procedimento
escreveFracção()
. Um procedimento
não devolve nada. Limita-se a fazer qualquer coisa.
Isso indica-se dizendo que devolve void
(vazio)!
Qual o corpo do procedimento? Basta que escreva a fracção:
/**
Escreve no ecrã uma fracção, no formato usual, quelhe é passada na forma de dois argumentos inteiros positivos.
@pre nenhuma.
@post o ecrã contém
n/d
em quen
ed
são os valores den
ed
em base
decimal.
*/
void escreveFracção(int const n, int const d)
{
}
Um procedimento retorna quando se atinge a chaveta final ou quando ocorre um
/**
Escreve no ecrã uma fracção, no formato usual, quelhe é passada na forma de dois argumentos inteiros positivos.
@pre nenhuma.
@post o ecrã contém
n/d
em quen
ed
são os valores den
ed
em base decimal.*/
void escreveFracção(int const n, int const d)
{
cout << n << '/' << d;
}
return
sem expressão! Ou seja, podia-se ser redundante
dizendo:
/**
Escreve no ecrã uma fracção, no formato usual, quelhe é passada na forma de dois argumentos inteiros positivos.
@pre nenhuma.
@post o ecrã contém
n/d
em quen
ed
são os valores den
ed
em base decimal.*/
void escreveFracção(int const n, int const d)
{
cout << n << '/' << d;
return;
}
Dizer que mdc()
e escreveFracção()
devem ficar acima de
main()
!