Aula 17 - Técnicas Avançadas de C++ - Parte I
Conceitos-Chave:
- overload de funções, funções com o mesmo identificador, mas com argumentos/tipo de retorno diferentes.
- parâmetros predefinidos, argumentos que assumem um determinado valor, quando o utilizador não o especifica
- constantes, variáveis que só assumem um valor (não podem ser alteradas), aquando a declaração da mesma.
- membros estáticos, membros de uma classe com o mesmo tempo de vida de uma variável global, mas com o alcance de uma variável local. São comuns a todos os objectos de uma classe
- métodos estáticos, métodos de uma classe que podem ser chamados sem criar um objecto.
- apontadores para funções, apontadores que apontam para uma função. Têm a particularidade de se poder alterar à vontade (pode apontar para qualquer função) em runtime.
Técnicas avançadas de C++
Na verdade o objectivo das próximas aulas é de ensinar algumas técnicas bastante úteis, que podem passar por algoritmos, criação de classes, instruções novas e bibliotecas novas (STL, por exemplo). Não significam, por isso, que são complicadas, destinam-se apenas a ajudar a resolver alguns problemas, ou encontrar melhor soluções para os vossos jogos.
Overload de funções
Em C++ podemos ter várias funções com o mesmo identificador, mas com diferentes argumentos/código. Por exemplo, imaginem uma função que calcula a área de um rectângulo:
#include <iostream>
using namespace std;
void area (int c, int l);
void area (double c, double l); //é uma função com o mesmo identificador, mas na
//realidade até são bastante diferentes
int main()
{
area (2, 4); //será chamada a função int area
area (5.2, 1.3); //será chamada a função double area
cin.get();
return 0;
}
void area (int c, int l)
{
cout << "Esta a ser usada a funcao int area" << endl;
cout << "Area = " << (c * l) << endl;
}
void area (double c, double l)
{
cout << "Esta a ser usada a funcao double area" << endl;
cout << "Area = " << (c * l) << endl;
}
Como vêm, a diferença entre as duas, que provoca o overload (sobrecarregamento) das funções é o tipo de argumentos. Também o número de argumentos de uma função pode provocar o seu overload, por exemplo:
int soma (int x, int y);
int soma (int x, int y, int z);
Isto torna-se bastante útil na interface entre uma classe e o programador, por exemplo:
int area (Rectangulo rect);
int area (int comprimento, int largura);
Dica:
Normalmente o código entre funções que estão "sobrecarregadas" não difere muito, sendo que apenas difere no tratamento dos dados (muda o tipo de variáveis, ou acrescenta mais uma variável, por exemplo), de modo a evitar confusões no código.
Parâmetros predefinidos
Uma função pode apresentar parâmetros opcionais, em que o programador pode ou não, consoante as suas necessidades, atribuir valores. Caso não atribua um valor ao parâmetro, a este será dado um valor predefinido, que foi escolhido anteriormente, na definição da função. Imaginem a seguinte função soma:
#include <iostream>
using namespace std;
int soma (int a, int b, int c = 0); // c é igual a 0, de modo a não interferir
// na operação soma
int main()
{
int x = soma (4, 3);
int y = soma (3, 7, 6);
cout << "X: " << x << "\nY: " << y << endl;
cin.get();
return 0;
}
int soma (int a, int b, int c) // notem que na definição da função não é preciso
{ // colocar c = 0, só na declaração (acima)
return (a + b + c);
}
Como vêm é muito útil e podemos dar uma escolha quanto às variáveis a usar. Isto pode resultar bem ao criar um motor de jogo, pois podemos dar-lhes a escolha de utilizar ou não argumentos na função (se escolher não utilizar, o seu valor ficará a cargo dos programadores da função).
Notem também que só podem colocar os valores predefinidos ou na declaração ou na definição da função, nunca nos ambos. Normalmente coloca-se na declaração, porque é a que aparece nos ficheiros header.
Resta dizer que, por razões óbvias, só podem utilizar valores predefinidos nos últimos parâmetros da função. Por exemplo:
f(int x = 0, y, z = 0);
Não é correcto.
Arrays como argumentos de funções
Como já tinhamos visto, passar um array a uma função é bastante fácil:
#include <iostream>
using namespace std;
int soma (int array[], int size);
int main()
{
int array[5] = {2, 4, 5, 2, 1}; //cria um array de 5 inteiros
cout << "Soma: " << soma(array, 5) << endl;
cin.get();
return 0;
}
int soma (int array[], int size)
{
int valor = 0;
for (int i = 0; i < size; i++) //percorre o array, de 0 a 4
{
valor += array[i]; //vai somando....
}
return valor;
}
Quando um array é passado como argumento a uma função esta não irá criar uma cópia do mesmo na memória (como faz normalmente com qualquer variável), mas sim criar um apontador para o seu primeiro elemento. Assim, ao alterarmos um array dentro de uma função estaremos realmente a alterar esse array fora da função (lembrem-se de referências e apontadores).
Resumindo, se passarmos um array a uma função e alterarmos o array dentro da função, depois deste processamento o array continuará alterado (ao contrário das variáveis, que só podem ser alteradas através de referências e apontadores).
#include <iostream>
using namespace std;
void duplicar (int array[], int size);
int main()
{
int array[5] = {2, 4, 5, 2, 1}; //cria um array de 5 inteiros
cout << "Array normal: " << endl;
for (int i = 0; i < 5; i++)
{
cout << "[" << i << "] - " << array[i] << endl;
}
cout << "\nArray apos duplicacao: " << endl;
duplicar(array, 5);
for (int i = 0; i < 5; i++)
{
cout << "[" << i << "] - " << array[i] << endl;
}
cin.get();
return 0;
}
void duplicar (int array[], int size)
{
for (int i = 0; i < size; i++) //percorre o array, de 0 a 4
{
array[i] *= 2; //multiplica cada numero por 2
}
}
Como vêm não precisamos de recorrer a apontadores/referências para alterar o valor através da função. Aliás, como tinha explicado, um array e um apontador são muito semelhantes. Assim, o exemplo abaixo é praticamente igual ao de cima:
void duplicar (int *array, int size)
{
for (int i = 0; i < size; i++) //percorre o array, de 0 a 4
{
*array *= 2; //multiplica cada numero por 2
array++; //move o endereço do apontador/array, para captar o valor seguinte
}
}
Constantes
C++ aceita variáveis constantes, ou seja, variáveis que têm um valor fixo. Se tentarmos mudar o valor da variável o compilador irá considerar como um erro e não deixará compilar. Há várias vantagens em considerar variáveis como constantes, principalmente com apontadores e em métodos de classes.
Uma variável constante define-se assim:
const tipo_de_variavel nome_da_variavel = valor_constante;
Por exemplo:
const int x = 9;
const float pi = 3.14;
Não se esqueçam que têm sempre que atribuir um valor quando definem a variável. O exemplo seguinte daria erro:
const float pi;
pi = 3.14;
Uma vantagem de criar um apontador constante para uma variável é de fazer com que a variável apontada não seja modificada (estaremos a criar uma variável read-only, só para leitura):
#include <iostream>
using namespace std;
int main()
{
int x = 30;
const int *pointer = &x;
// *pointer = 2; <- Daria erro
cout << *pointer << endl;
cin.get();
return 0;
}
Poderemos também criar métodos constantes dentro de classes. Estes têm a vantagem de não poderem modificar nenhuma variável no objecto, o que previne alguns erros de programação.
class Monstro {
public:
void setID(int id) { this->id = id; }
const int getID()
{
// id = 2; <- isto daria erro, uma vez que não podemos alterar o objecto
return id;
}
private:
int id;
}
Resumindo, criar variáveis/métodos constantes serve, principalmente, para evitar que o programador cometa erros por distração. É, então, útil caso o código seja distribuido por vários programadores, de modo a que eles saibam quais as variáveis que não podem alterar.
Membros estáticos
Uma variável estática é uma variável única, para todos os objectos de uma classe. Esta variável tem o mesmo tempo de vida de uma variável global, mas possui o mesmo alcance do que um membro de um objecto.
Vejam abaixo um uso comum de membros estáticos, para contagem do número de objectos de uma classe:
#include <iostream>
using namespace std;
class Monstro {
public:
static int number; // o número de monstros em todo o programa
Monstro(char *name) // construtor
{
this->name = name;
number++; //a variável estática vai ser incrementada
}
private:
char *name;
};
int Monstro::number = 0; //temos que iniciar a variável globalmente
int main()
{
Monstro orc("Orc");
Monstro spider("Spider");
Monstro goblin("Goblin");
cout << "Numero de Monstros: " << Monstro::number << endl;
cin.get();
return 0;
}
Como podem ver, são muito úteis, uma vez que existem independentemente do objecto, mas continuam a fazer parte da classe. Reparem só na seguinte linha de código:
int Monstro::number = 0;
Para podermos aceder a um membro estático teremos sempre que inicializá-lo como uma variável global.Métodos estáticos
Também podemos criar métodos estáticos. Estes têm a particularidade de poderem ser chamados sem termos criado um objecto da classe. Por exemplo, imaginem a seguinte classe Math:
#include <iostream>
using namespace std;
class Math {
public:
static int somar(int a, int b) { return a+b; }
static int subtrair(int a, int b) { return a-b; }
static int multiplicar(int a, int b) { return a*b; }
static int dividir(int a, int b) { return a/b; }
};
int main()
{
int x = Math::somar(4, 5);
int y = Math::dividir(20, 4);
int z;
Math m;
z = m.subtrair(6, 2); // isto foi só para mostrar que podemos aceder normalmente
// através do objecto a métodos estáticos. O mesmo
// acontece para membros estáticos
cout << "X: " << x << ", Y: " << y << endl;
cin.get();
return 0;
}
Function Pointers (Apontadores para funções)
Function Pointers (acho que o termo correcto em português é apontadores para funções :) são muito úteis na programação. Aqui só vou mostrar um exemplo fácil. Por exemplo, imaginem que queríamos criar uma calculadora completa, que oferecesse ao programador uma simples função que, sem recorrer a operadores condicionais (if, else, switch) permitisse fazer as 4 operações básicas da matemática (somar, subtrair, dividir e multiplicar). Os apontadores para funções permitiriam isso.
Uma função, tal como uma variável, possui um endereço na memória. Podemos então colocar um apontador para a mesma entre os argumentos de uma função e chamá-la. Por exemplo, poderíamos fazer:
int
Vejam o exemplo abaixo, da tal calculadora:
#include <iostream>
using namespace std;
int somar(int a, int b) { return a+b; }
int subtrair(int a, int b) { return a-b; }
int multiplicar(int a, int b) { return a*b; }
int dividir(int a, int b) { return a/b; }
int main()
{
int (*calculadora)(int a, int b); //criamos um apontador para uma função
calculadora = somar; //calculadora passa a apontar para a função somar
int x = (*calculadora)(4, 6);
calculadora = multiplicar; //calculadora passa a apontar para a função somar
int y = (*calculadora)(4, 2);
cout << "X: " << x << ", Y: " << y << endl;
cin.get();
return 0;
}
Admito que possa parecer um pouco confuso, mas não é nada de outro mundo. Senão vejamos:
int (*calculadora)(int a, int b); - estamos a criar um apontador para funções. Este apontador em específico consegue apontar apenas para funções que apresentem dois argumentos de tipo int (o nome não precisa de ser a ou b, como vamos ver à frente) e que retornem variáveis do tipo int.
calculadora = somar; esta nem precisa de explicações. Estamos a fazer com que o apontador calculadora aponte para a função somar.
int x = (*calculadora)(4, 6); - para podermos aceder à função apontada pelo apontador calculadora temos de usar o operador desreferência (*). E, claro, introduzir os argumentos.
Dica:
Nas declarações de uma função não somos obrigados a especificar o nome dos argumentos (só o tipo de variável), mas somos obrigados a especificar o nome na definição. Assim poderíamos ter feito: int (*calculadora)(int, int);
Isto não se aplica só a apontadores para funções, mas a todo o tipo de funções, desde que seja na sua definição: int f(int, int); por exemplo
Não foi assim tão difícil, pois não? Agora vejamos um exemplo um pouco mais complicado, utilizando uma função intermediária.
#include <iostream>
using namespace std;
int somar(int a, int b) { return a+b; }
int subtrair(int a, int b) { return a-b; }
int multiplicar(int a, int b) { return a*b; }
int dividir(int a, int b) { return a/b; }
int calculadora(int a, int b, int (*operacao)(int, int));
int main()
{
int x = calculadora(4, 6, somar);
int y = calculadora(4, 2, multiplicar);
cout << "X: " << x << ", Y: " << y << endl;
cin.get();
return 0;
}
int calculadora(int a, int b, int (*operacao)(int, int))
{
int resultado = operacao(a, b);
return resultado;
}
int calculadora(int a, int b, int (*operacao)(int, int));
Esta declaração de função é totalmente normal... até chegarmos ao último parâmetro. Se repararem, sem o '*' este parâmetro seria exactamente uma função: int operacao (int, int).
Com esta instrução estamos a dizer ao programa para aceitar as funções que possuam dois argumentos inteiros e que retornem um valor inteiro.
int x = calculadora(4, 6, somar);
Como podem vêr estamos a passar a função como se fosse uma variável (é um apontador, na verdade).
Próxima aula -> C++ 18 - Técnicas Avançadas de C++ - Parte 2
Fazer o download do código fonte dos exemplos da aula
Fazer o download da aula em PDF (Brevemente)
Ir para o topo da página