Baixe o app para aproveitar ainda mais
Prévia do material em texto
1ºAula Ponteiros – parte I Objetivos de aprendizagem Ao término desta aula, vocês serão capazes de: • compreender como funcionam os ponteiros; • entender como usar os ponteiros. Olá, Bem-vindos(as) a disciplina Estruturas de Dados I. Para começar os nossos estudos, vamos aprender alguns recursos que serão necessários para entender as estruturas que aprenderemos mais adiante. O primeiro recurso que vamos estudar são os ponteiros. Trata-se de um recurso poderoso o qual permite que você crie variáveis que apontem para outras, permitindo que você altere os seus valores. Como esta matéria envolve muita prática, reproduzimos muitos trechos de códigos para que você entenda melhor como funcionam os ponteiros. Para não ficar muito concentrado, este tema será dividido em duas aulas. Nesta primeira aula, você vai entender os conceitos básicos que envolvem o uso de ponteiros. Bons estudos! Estrutura de Dados I 6 1 - Compreendendo como funcionam os ponteiros 2 - Usando Ponteiros em Funções 1 - Compreendendo como funcionam os ponteiros Antes de entendermos o que são e como funcionam os ponteiros, vamos ver como a memória funciona nos computadores. A memória é organizada em espaços predeterminados, onde em cada espaço abriga um dado. Além disso, cada espaço possui um endereço que funciona para localizar o dado. Podemos fazer uma analogia desse conceito a casas, onde possuem informações que permitem a sua localização, como, a rua, o bairro, o número e o CEP. Outra analogia que podemos fazer são com os guarda- volumes presentes em bancos e em supermercados. Esses guarda-volumes, nada mais são do que armários -, abrigam um item que a pessoa deseja guardar. Para localizar e acessar o armário, nota-se que ele possui um número para a sua identificação. Figura 1 – Guarda-volumes no Aeroporto de Congonhas Fonte: <https://commons.wikimedia.org/wiki/File:Locker_in_congonhas_ airport,_s%C3%A3o_paulo..jpg>. Acesso em: 20 mar. 2018. Quando criamos uma variável normal, reservamos um bloco (ou mais de um, dependendo do tipo do dado) para abrigar o dado. Nesse caso, caberá ao computador reservar um espaço da memória e gerenciá-lo, de acordo com as operações que nós especificamos nesse bloco. Não sabemos em qual posição o bloco ficará. Figura 2 – Figura ilustrativa que mostra a alocação de uma variável i na memória. Fonte: MARTINEZ, 2009, p. 325. Seções de estudo Porém, em algumas linguagens de programação (como a C++), podemos criar variáveis que abrigam endereços de memória, que apontam para outros blocos de memória. Isso se chama princípio da indireção e é a base do conceito de ponteiros. Mas, como extraímos a posição de memória de uma variável? Na linguagem C++, podemos saber em qual bloco de memória fica a variável. Podemos usar o operador & (E comercial) precedido do nome da variável para sabermos a posição do bloco de memória. Vejam esse exemplo: Exemplo 1.1 - Testando o operador & Vamos fazer uma pequena recapitulação: declaramos uma variável chamada teste e colocamos valor zero nessa variável (podíamos ter colocado qualquer outro valor). Em seguida, usamos o operador cout para exibir a posição atual da variável teste, com a ajuda do operador &. Ao executar o programa, um código hexadecimal vai aparecer na sua tela. Não estranhe se for diferente, pois esse número varia a cada execução, e depende de quais blocos de memória estão disponíveis para uso no momento da execução. Esse código hexadecimal é a posição (também chamada de endereço de memória) que está localizada a variável teste, no instante que o programa está sendo executado. Como já mencionamos, essa posição muda a cada vez que o programa é novamente executado. Você pode provar isso executando várias vezes esse mesmo programa. Outra ideia que podemos fazer é testar isso com várias variáveis diferentes. No exemplo a seguir, mostramos a declaração de três variáveis e exibimos a posição de memória dessas três variáveis. 7 Exemplo 1.2 - Testando o operador & com várias variáveis Agora que você já sabe como extrair a posição de memória de uma variável, vamos saber o que são ponteiros. Um ponteiro é uma variável que armazena uma posição de memória do sistema. Através dessa variável, podemos acessar a variável localizada no endereço armazenado através desse ponteiro. Figura 3 – Figura ilustrativa que mostra um ponteiro p, que aponta para a variável i. Fonte: MARTINEZ, 2009, p. 325. Uma analogia que podemos fazer é com os atalhos para programas e arquivos, os quais fazemos nos computadores - principalmente em sistemas operacionais Windows™. Esses atalhos são arquivos com referências a arquivos ou a programas que estão em outra pasta no computador. Assim, esses atalhos apontam para esses arquivos. A implementação de ponteiros é um recurso que varia de linguagem para linguagem. A linguagem C++ é uma das linguagens que permitem esse recurso. Podemos declarar ponteiros nessa linguagem de diversas formas. A primeira é a forma clássica: Neste primeiro caso, temos uma forma similar à declaração de variáveis normais. A única diferença é a presença do asterisco (*), chamado nessa linguagem pelo nome de operador de indireção, pois por meio dele declaramos que essa variável é um ponteiro e podemos acessar o conteúdo presente no endereço de memória apontado por esse ponteiro. Inicialmente, quando declaramos o ponteiro, ele terá o valor NULL, pois não estará apontando para absolutamente nada. Podemos comprovar isso escrevendo o seguinte programa: Exemplo 1.3 - Testando o ponteiro, sem atribuir nenhum endereço de memória Assim, para que possamos utilizar efetivamente o ponteiro, devemos indicar a ele qual endereço de memória essa variável especial apontará. Uma atribuição simples resolve, mas com uma pequena modificação: como a variável armazena endereços de memória, devemos indicar a posição de memória que será apontada, com o operador &: Vale lembrar que não estamos usando o asterisco na variável apt, pois estamos armazenando o valor que essa Estrutura de Dados I 8 variável conterá - que é o endereço de memória. Se a variável valor estiver na posição 3625, a variável apt passará a ter o valor real de 3625, podendo a partir desse fato, apontar para a variável valor. Assim, para que possamos exemplificar isso, vamos escrever um programa que: 1. declare um ponteiro para um valor inteiro, e uma variável do tipo inteiro; 2. atribua um valor para essa variável; 3. atribua o endereço de memória da variável para o ponteiro. 4. mostre na tela o valor da variável e do valor que está sendo apontado pelo ponteiro. Como o ponteiro está apontando para a variável, esperamos que os valores mostrados ao fim da execução sejam os mesmos. Vamos ao código: Exemplo 1.4 - Testando a atribuição de um endereço de memória a um ponteiro Execute o programa e veja que o comportamento esperado aconteceu: os dois valores apresentados são iguais. Nesse programa apresentado, você viu na prática o principal uso do operador asterisco, que serve para recuperar o valor contido no endereço de memória que está armazenado pelo ponteiro. Através do ponteiro, você pode alterar os dados que estão armazenados na posição, sem a necessidade de manipular a variável real. O próximo exemplo, que apresentaremos a seguir, mostra três alterações realizadas por intermédio do ponteiro. A cada alteração é feita a comparação com o valor da variável real. Vamos ao exemplo: Exemplo 1.5 - Testando a alteração de dados usando um ponteiro Com isso, mostramos que é possível alterar dados presentes em uma posição de memória usando ponteiros. Mas, sempre devemos ter cuidado em usar o asterisco antes do ponteiro, para indicar que estamos usando a posição de 9 memória referenciada por esse ponteiro. Recapitulando • para declararmos um ponteiro: int *apt; • para atribuirmos uma posição de memória a esse ponteiro: apt = &var; • para usar o valor contido na posiçãode memória apontado por esse ponteiro: *apt; • para descobrir qual a posição de memória armazenada por esse ponteiro: apt; • para descobrir qual é a posição de memória que está localizado o ponteiro - ou seja, a posição dessa variável: &apt; Lembre-se: quando você fazer uma atribuição com valores usando ponteiros como intermediários, não se esqueça de colocar o asterisco, senão você estará alterando a posição de memória que vai ser apontado por esse ponteiro, o que pode causar problemas no seu programa. Como já falamos, essa é a forma clássica de se lidar com ponteiros. A linguagem C++ permite outra forma, que realiza a criação do ponteiro e a alocação de um espaço de memória e a atribuição de um endereço de memória de uma vez só. Essa técnica se chama alocação dinâmica de memória. A sintaxe é esta: Para testar essa variante, vamos a mais um código: Exemplo 1.6 - Testando a segunda variante de alocação de ponteiros Esse código realiza várias operações, a saber: 1. cria o ponteiro e aloca um endereço de memória para o tipo inteiro; 2. verifica se o ponteiro efetivamente está apontando para alguma coisa, verificando se o seu endereço está com o valor NULL. Caso seja, o programa exibe uma mensagem e interrompe a sua execução com o comando return 0; 3. se o ponteiro estiver apontando para alguma coisa, vamos testar uma atribuição com ele; 4. em seguida, verificamos a posição de memória armazenada pelo ponteiro e o valor contido na posição de memória, para verificar se atribuição foi feita de maneira correta. Executamos esse programa e verificamos as saídas. Veja que uma posição de memória efetivamente foi alocada (sem precisar que o programador crie outra variável para isso) e seu endereço foi armazenado no ponteiro. A atribuição do valor 36 por intermédio do ponteiro foi feita com êxito. Alocando e desalocando memória Quando criamos uma nova variável, estamos alocando memória (ou seja, reservando um espaço de memória) para armazenar os dados dessa variável. Na forma clássica de ponteiros, fazemos duas alocações de memória distintas, uma para criar o ponteiro e outra para criar a variável que será apontada pelo ponteiro. Nessa forma moderna, podemos fazer essas duas alocações em uma linha só. Criamos o espaço para o ponteiro e o espaço de memória que servirá para armazenar os dados, sem precisar efetivamente declarar outra variável para isso. A linguagem C++, ao contrário de outras linguagens, não possui um mecanismo que libera memória automaticamente (como o famoso coletor de lixo da linguagem Java). A memória pode ser liberada de duas formas: encerrando o programa, o que obviamente, desalocaria toda a memória que o programa ocupa ou usando o operador delete, que desaloca o espaço de memória apontado por um ponteiro, desde que tenha sido criado com o operador new. A sintaxe é esta: delete <variável>; delete apt; Assim, terminamos o nosso entendimento inicial sobre ponteiros. Agora, vamos ver como os ponteiros funcionam em certas situações específicas: vamos abordar seu uso com funções. Estrutura de Dados I 10 2 - Usando ponteiros em funções Quando você iniciou seus estudos em algoritmos e na programação de computadores, deve ter aprendido como funciona a passagem de parâmetros por valor e por referência. Vamos retomar brevemente: A passagem por valor apenas transfere uma cópia do valor da variável para a função, não podendo alterar o valor original da função. Enquanto que a passagem por referência passa o endereço de memória para a função, podendo assim alterar o valor diretamente na variável original. Podemos especificar ponteiros como parâmetros de funções. Quando isso acontece, devemos declarar na chamada da função o endereço de memória da variável que será passada como parâmetro. O funcionamento será igual se declararmos uma variável com passagem de valor por referência. Para comprovar isso, vamos criar um programa com duas funções. A primeira vai trocar dois números usando a clássica passagem de parâmetros por referência. Já a segunda função vai trocar dois números por meio de ponteiros. A função principal executará as duas funções, usando duas variáveis com o mesmo valor. Vamos ao programa: Exemplo 1.7 - Testando o uso de ponteiros em funções Execute o programa e veja que as duas funções fazem a mesma coisa. Essas funções trocam valores de duas variáveis originais, por meio de formas diferentes. E com isso, finalizamos a aula. Na próxima aula, vamos ver como funcionam os ponteiros para vetores e matrizes, ponteiros para ponteiros e ponteiros para registros. Retomando a aula Chegamos ao final da nossa primeira aula. Vamos relembrar os conceitos iniciais? 1 - Compreendendo como funcionam os ponteiros Nessa primeira seção, vimos que os ponteiros são variáveis especiais que armazenam endereços de memória ao invés de dados. Essas variáveis servem para apontar para endereços de memória atribuídos a uma ou mais (ou a nenhuma) variáveis. Vimos o uso de dois operadores unários: O “E” comercial (&), que serve para extrair o endereço de memória de uma variável e o asterisco, que serve para declarar um ponteiro e para acessar um valor que está armazenado na posição de memória armazenada pelo ponteiro. Vimos também os seguintes comandos: • para declararmos um ponteiro: int *apt; • para atribuirmos uma posição de memória a esse ponteiro: apt = &var; 11 • para usar o valor contido na posição de memória apontado por esse ponteiro: *apt; • para descobrir qual a posição de memória armazenada por esse ponteiro: apt; • para descobrir qual é a posição de memória que está localizado o ponteiro - ou seja, a posição dessa variável: &apt; 2 - Usando Ponteiros em Funções Nessa seção, vimos que podemos usar ponteiros como parâmetros de funções. O funcionamento nestes casos é semelhante a passagem de valores por referência, onde a função pode alterar o valor da variável usada na chamada. Nessas situações, devemos ter o cuidado de passar na chamada o endereço de memória da variável, usando o operador &. HOLZNER, Steven; ANTUNES, Alvaro Rodrigues. C++ : black book. São Paulo: Makron Books do Brasil, 2001. JAMSA, Kris; KLANDERM LARS; SANTOS, Jeremias René D. Pereira dos. Programando em C/C++ : a bíblia. São Paulo: Makron Books do Brasil, 1999. KENT, Jeff. C++ desmistificado. Rio de Janeiro: Alta Books, 2004. MARTINEZ, Fábio Henrique Viduani. Programação de Computadores I. UFMS, 2009. Disponível em: <http://www. facom.ufms.br/~montera/progiv1.pdf>. Acesso em: 19 mai. 2018. TENENBAUM, Aaron M.; AUGENSTEIN, Moshe J.; LANGSAN, Yedidyah. et al. Estruturas de dados usando C. São Paulo: Pearson Makron Books; São Paulo: Makron Books do Brasil; São Paulo: McGraw-Hill, 2013. Vale a pena ler FEOFILOFF, Paulo. Endereços e ponteiros. 2018. Disponível em: <https://www.ime.usp.br/~pf/ algoritmos/aulas/pont.html>. Acesso em: 19 mai. 2018. SANCHES, Bruno Crivelari. Matrizes Dinâmicas. Ponto V, 2009. Disponível em: <http://www.pontov.com.br/site/ cpp/46-conceitos-basicos/57-matrizes-dinamicas>. Acesso em: 19 mai. 2018. Vale a pena acessar Vale a pena Minhas anotações
Compartilhar