Aula 14 - Apontadores - Parte II
Conceitos-Chave:
- variável alocada dinamicamente, variável que está alocada na memória dinâmica, cujo tamanho pode ser alterada
- new, cria uma variável na memória dinâmica e retorna um apontador para a mesma
- delete, desaloja da memória dinâmica a variável dinâmica apontada
- operações com endereço, é possível aceder à variável seguinte, muito útil para arrays
Memória dinâmica
Até agora temos alojado as variáveis e arrays estaticamente. Isto significa que quando inicializámos uma variável, esta irá possuir sempre o mesmo tamanho ao longo do programa.
Numa variável normal, não há qualquer problema, mas num array isto traz limitações. A primeira é que temos de conhecer o tamanho do array antes de começar o programa. Outra é que não podemos alterar o tamanho do array, consoante as nossas necessidades.
Uma solução para o primeiro problema é especificar um valor máximo para o array. No entanto, estaremos a gastar espaço na memória desnecessariamente e nada nos dá garantia que o espaço será suficiente (se tal não for, preparem-se para erros constantes durante a execução).
A única alternativa viável é alojá-los dinamicamente. Uma variável dinâmica só pode ser acedida através de um apontador, ou seja, ao criarmos uma variável dinâmica, não podemos ter acesso directo à mesma, apenas através de um apontador. Para alojarmos uma variável dinamicamente, teremos que usar o operador new:
int *apont = new int;
Para perceberem o que está a acontecer no exemplo acima, vejam o seguinte esquema:
Esquema:
1 - Declaramos um apontador para um inteiro (int *apont)
2 - Criamos um inteiro na memória dinâmica (através do operador new). Notem que o inteiro que criamos não tem um "nome" (identificador é o termo mais correcto). Para podermos aceder ao inteiro criado na memória dinâmica temos, então, que utilizar um apontador (neste caso é o apontador "apont").
Poderemos depois alterar o valor do inteiro criado como num apontador normal:
*apont = 20;
Não se esqueçam que estamos a afectar o valor apontado pelo apontador (uma boa maneira de ler é "valor apontado por apont será igual a 20")
Poderemos também definir um valor ao criarmos uma variável na memória dinâmica:
int *apont = new int(20);
Com a criação de memória dinâmica temos também uma enorme responsabilidade: de libertá-la.
Libertar a memória dinâmica
Enquanto que a memória estática é libertada automaticamente pelo programa (não precisamos de nos preocupar com ela), a memória dinâmica tem de ser libertada pelo programador. Se nos esquecermos de a libertar esta irá persistir sempre. Irá então utilizar recursos do sistema ou mesmo esgotar toda a memória do sistema. Moral da história: não se esqueçam nunca de libertar a memória após criarem (quando não precisarem mais dela, claro).
Para libertar a memória criada utiliza-se o operador delete.
int *apont = new int(20);
//fazer alguma coisa....
delete apont;
Simples, mas o seu esquecimento é a causa de muitos erros graves de programas.....
Nota:
Ao fazermos delete apont; estaremos a libertar a variável criada em new int(20); e não o apontador apont. Não nos temos de preocupar com ele, uma vez que é um apontador criado estaticamente e será eliminado estaticamente.
Dica:
É bastante aconselhável que façam apont = NULL; após o delete apont;.
Como expliquei nos exemplos acima, fazer delete apont; apenas limpa a variável da memória dinâmica. O apontador apont vai continuar a apontar para o mesmo endereço de anteriormente. Para evitar que o programador o utilize por engano (após o termos eliminado), ao fazer apont = NULL; estamos a fazer com que o apontador não aponte para nada, e podemos utilizar uma condição para saber se o apontador existe:
if (!apont) //o mesmo que if (apont == NULL)
//...
Alojar e libertar arrays dinamicamente
Alojar um array dinamicamente é tão simples como alojar uma variável normal:
int *array = new int[100]; // aloja um array com 100 ints
e para libertar a memória:
delete [] array;
Operações com o endereço
Para ilustrar uma simples operação com o enderenço de variáveis vejam o exemplo abaixo:
#include <iostream>
using namespace std;
int x = 20;
int y = 10;
int *pointer; //apontador para uma variável do tipo int
void printthis();
int main()
{
pointer = &x; //o apontador assume o valor do endereço da variável
printthis();
cout << "Vai ser incrementado o endereco por um valor, prima ENTER." << endl;
cin.get();
pointer++;
printthis();
cin.get();
return 0;
}
void printthis()
{
cout << "Variavel x:" << endl;
cout << "\nValor de x: " << x << endl;
cout << "Endereco de x: " << &x << endl;
cout << "\n\nVariavel y:" << endl;
cout << "\nValor de y: " << y << endl;
cout << "Endereco de y: " << &y << endl;
cout << "\n\nVariavel pointer:" << endl;
cout << "\nValor de pointer: " << pointer << endl;
cout << "Endereco de pointer: " << &pointer << endl;
cout << "Valor da variavel apontada por pointer (x): " << *pointer << "\n------------\n" << endl;
}
Que me dá o seguinte output
Variavel x:
Valor de x: 20
Endereco de x: 0x434000
Variavel y:
Valor de y: 10
Endereco de y: 0x434004
Variavel pointer:
Valor de pointer: 0x434000
Endereco de pointer: 0x437010
Valor da variavel apontada por pointer (x): 20
------------
Vai ser incrementado o endereco por um valor, prima ENTER.
Variavel x:
Valor de x: 20
Endereco de x: 0x434000
Variavel y:
Valor de y: 10
Endereco de y: 0x434004
Variavel pointer:
Valor de pointer: 0x434004
Endereco de pointer: 0x437010
Valor da variavel apontada por pointer (x): 10
Como podem ver ao incrementarmos pointer estamos na verdade a fazer com que pointer tome o endereço da variável seguinte (4 bytes depois), podendo então interferir no valor desta.
Nota:
Este exemplo serviu apenas para explicar uma operação simples com o endereço. O próprio exemplo não é muito correcto, pois em situações reais, nada nos garante que a variável que está posicionada depois na memória é a variável que pretendemos usar.
Apontadores e arrays
Um apontador é equivalente ao endereço do primeiro elemento que aponta. Assim:
#include <iostream>
using namespace std;
int array[10];
int *pointer;
int main()
{
pointer = array;
array[0] = 50;
cout << "array = " << array[0] << endl;
cout << "pointer = " << *pointer << endl;
cin.get();
return 0;
}
Daria o mesmo resultado. Poderemos então aproveitar isso para aceder aos outros elementos do array. Por exemplo, se quisermos aceder ao elemento nº 7:
#include <iostream>
using namespace std;
int array[10];
int *pointer;
int main()
{
pointer = array;
array[7] = 50;
cout << "array = " << array[7] << endl;
cout << "pointer = " << *(pointer+7) << endl;
cin.get();
return 0;
}
Na verdade um array é uma espécie de apontador. Assim:
#include <iostream>
using namespace std;
int array[10];
int *pointer;
int main()
{
pointer = array;
array[0] = 50;
cout << "array = " << *array << endl;
cout << "pointer = " << *pointer << endl;
cin.get();
return 0;
}
E também:
#include <iostream>
using namespace std;
int array[10];
int *pointer;
int main()
{
pointer = array;
array[7] = 50;
cout << "array = " << *(array+7) << endl;
cout << "pointer = " << *(pointer+7) << endl;
cin.get();
return 0;
}
Na verdade, ao utilizarmos os parenteses rectos ( [] ), estamos a utilizar um operador de desreferência, chamado offset operator ou em português operador de indexação. Assim podemos fazer:
#include <iostream>
using namespace std;
int main()
{
int array[2];
int *pointer;
pointer = array;
array[0] = 0;
pointer[1] = 1;
for (int i = 0; i < 2; i++)
{
cout << "Array[" << i << "] = " << array[i] << endl;
cout << "Apontador[" << i << "] = " << pointer[i] << endl;
}
cin.get();
return 0;
}
Ou seja é equivalente fazer:
array[10] = 15;
ou fazer :
*(array+10) = 15;
Isto torna-se especialmente útil para funções. Até agora nunca utilizamos arrays como argumentos de funções, vejam como no exemplo seguinte:
#include <iostream>
using namespace std;
int limpar_array(int *array, int tamanho);
int main()
{
const int TAMANHO = 10; //variável constante
int lista[TAMANHO];
lista[0] = 4; //só para testarmos se o array foi limpo
cout << lista[0] << endl;
limpar_array(lista, TAMANHO);
cout << lista[0] << endl;
cin.get();
return 0;
}
int limpar_array(int *array, int tamanho)
{
for (int i = 0; i < tamanho; i++)
{
array[i] = 0;
}
}
Então qual é a diferença entre um apontador e um array? A diferença principal é que o tamanho de um array tem de ser constante, enquanto o tamanho de um apontador (utilizado para memória dinâmica) pode variar. Vejam o exemplo seguinte (uma versão aperfeiçoada do exemplo acima:
#include <iostream>
using namespace std;
int limpar_array(int *array, int tamanho);
int main()
{
int tamanho;
cout << "Insira um tamanho para o array (menor que 1000, e apenas uma sugestao)" << endl;
cin >> tamanho;
int *lista = new int [tamanho];
lista[0] = 4; //só para testarmos se o array foi limpo
cout << lista[0] << endl;
limpar_array(lista, tamanho);
cout << lista[0] << endl;
cin.get();
cin.get();
return 0;
}
int limpar_array(int *array, int tamanho)
{
for (int i = 0; i < tamanho; i++)
{
array[i] = 0;
}
}
Como podem ver, uma alteração muito pequena trouxe uma grande característica a este programa (embora continue a servir para praticamente nada :)
Próxima aula -> C++ 15 - Classes - Parte II
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