Vamos Programar? – Introdução à Programação #18
Declaração e inicialização de Apontadores
O mundo está constantemente a evoluir: desde o mais pequeno inseto à espécie Humana. Dentro das palavras que mais ouvimos atualmente, incluímos “evolução”, “mudança”, “futuro”.
A tecnologia tem revolucionado o mundo das mais diversas formas: do mais simples aparelho para medir o tempo ao mais complexo acelerador de partículas. Se quer entrar no mundo da tecnologia e deixar a sua marca, pode começar aqui.
Nesta semana, infelizmente, não haverá vídeo porém tentaremos fazer na próxima semana. Hoje iremos continuar o tema dos apontadores. Os apontadores, como já foi referido anteriormente, são um tipo de variáveis que armazenam endereços.
Declaração de apontadores
A declaração deste tipo de variáveis é bastante simples: para declarar um apontador basta colocar um * (asterisco) antes do nome da variável.
Sintaxe
Veja então a sintaxe da declaração de ponteiros.
1 | tipo *nome; |
Tal como nas restantes variáveis, temos que colocar o tipo de dados na declaração da variável devido ao facto do espaço ocupado por cada tipo de dados ser diferente.
Quando não é dado um valor a um apontador, geralmente este assume o valor da constante NULL, ou seja, não aponta para endereço nenhum.
A constante NULL está contida na biblioteca stdlib, ou seja, standard library e o seu valor é, na maioria das vezes, 0.
Inicialização de apontadores
Para inicializar um apontador, devemos igualar o seu valor ao endereço de uma outra variável do mesmo tipo.
Exemplo 1
Para exemplificarmos a relação entre uma variável comum e um ponteiro, iremos declarar o seguinte código:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <stdio.h> #include <stdlib.h> int main() { int numero = 5; int *ponteiro = № printf("%d e %d", numero, *ponteiro); return 0; } </stdlib.h></stdio.h> |
Dentro da função principal, declaramos duas variáveis: a primeira, chamada numero, é uma variável do tipo de números inteiros e conta com o número 5.
De seguida, declaramos um ponteiro cujo nome é, exatamente, ponteiro e este ponteiro aponta para o endereço da variável numero. Podemos então dizer que "ponteiro aponta para numero" ou "ponteiro é o endereço de numero".
Depois imprimimos o valor de numero e do endereço que para o qual aponta ponteiro. Isto irá imprimir "5 e 5", pois é o mesmo dizer numero e *ponteiro visto que o ponteiro ponteiro aponta para a variável numero.
Exemplo 2
O segundo exemplo é minimamente bizarro: efetuar a soma de duas variáveis utilizando apenas apontadores que apontem para estas mesmas variáveis. Ora veja:
Se está a ter problemas com a codificação dos caracteres no Windows, inclua a biblioteca "Windows" e altere a página de codificação da linha de comandos como está na linha 7.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <stdio.h> #include <stdlib.h> #include <windows.h> int main() { SetConsoleOutputCP(65001); int a, b, c; int *p, *q; a = 5; b = 4; p = &a; q = &b; c = *p + *q; printf("A soma de %d e %d é %d.", a, b, c); return 0; } </windows.h></stdlib.h></stdio.h> |
No exemplo anterior pode visualizar que são declaradas cinco variáveis: a a, b e c que são do tipo número inteiro e, de seguida, dois apontadores, p e q. Posteriormente é atribuído o valor 5 à variável a e o valor 4 à variável b.
Agora, para efetuar a soma utilizando os operadores, igualamos o apontador p ao endereço da variável a e o apontador q ao endereço da b. Finalmente, a soma (que estará contida na variável c) é igual à soma do conteúdo dos endereços que para os quais os apontadores p e q estão a apontar.
Exemplo 3
O exemplo anterior aparenta ser inútil, pois poderíamos fazer o mesmo com menos linhas de código. Mas, e se precisar de, por algum motivo, criar uma função que troque o valor de duas variáveis? Poderíamos, inicialmente, pensar no seguinte:
1 2 3 4 5 6 7 8 9 10 11 | int trocaDeValores( int x, int y) { /* Esta função está errada */ int temp; temp = x; x = y; y = temp; return 0; } |
Mas a função apresentada não vai realmente alterar os valores das variáveis, pois apenas recebe os seus valores e "coloca-os" numas míseras variáveis locais. A função correta teria que ser a seguinte:
1 2 3 4 5 6 7 8 9 10 | int trocaDeValores( int *p, int *q) { int temp; temp = *p; *p = *q; *q = temp; return 0; } |
Com esta função sim, poderíamos trocar os valores de duas variáveis, pois ao termos acesso aos seus endereços podemos facilmente efetuar as mudanças.
Exercícios
1 - Analise o seguinte trecho de código e descubra o porquê deste estar errado.
1 2 3 4 5 6 7 8 9 | int trocaDeValores( int *i, int *j) { int *temp; *temp = *i; *i = *j; *j = *temp; return 0; } |
2 - Crie uma função que receba três variáveis do tipo inteiro: minutosTotais e dois apontadores (um para a variável horas e outro para a variável dos minutos).
Dentro dessa função, os minutosTotais devem ser convertidos em horas e minutos e guardados nas respetivas variáveis cujos endereços foram passados para dentro da função.
Na função main, crie as variáveis que achar necessárias e peça o número de minutos que deseja converter em horas e minutos ao utilizador. De seguida utilize a função anteriormente criada e imprima o resultado dado por esta.
Não se esqueça que a função horasParaMinutos não deve retornar o número de horas e minutos, mas sim colocar esses valores nos endereços passados para a função.
Esta semana não iremos recomendar exercícios pois o conteúdo de hoje não é suficiente para novos exercícios. Porém, continue a praticar! Na próxima semana iremos continuar o tema dos apontadores.
Mais uma vez, gostávamos de saber a sua opinião. Caso tenha alguma dúvida, pode sempre utilizar os comentários para colocar a questão. Caso o faça, pedimos que utilize a keyword [DUVIDA] no início do seu comentários.
Esta saga |
Henrique Dias |
|
Mais episódios: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13][12] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23][24] |
Este artigo tem mais de um ano
Olá, esta rubrica é muito boa.
Podias explicar porque por vezes é melhor utilizar apontadores? Mais concretamente em termos de ocupação de memória ram.
Com apontadores estás a aceder à posição de memória onde a variável está armazenada, ou seja, estás a manipular directamente o armazenamento da variável.
Quando utilizas uma função para manipular variáveis, estás apenas a manipular um cópia dessas variáveis, mantendo o valor das variáveis originais inalterado, ou seja não estás a manipular a vairável armazenada na posição original, mas sim um cópia, armazenada noutra posição de memória.
A utilização de apontadores permite definir em que posição de memória queres alojar o valor x ou o valor y, dando-te total controlo na gestão da memória. Podes por exemplo utilizar a mesma posição para armazenar variáveis diferentes em pontos de execução do programa diferentes, isto possibilita a poupança de um byte, ou mais, no runtime do programa, uma vez que estás a utilizar sempre a mesma posição para armazenamento, em vez de utilizares várias posições.
Em C e C++ a gestão de memória é muito importante porque pode ser totalmente controlada pelo programador, ao invés do Java e do C#, onde há garbage collectors que tratam da limpeza das variáveis não utilizadas, automacticamente.
Uma utilização “responsável de apontadores” permite evitar erros durante o runtime, nomeadamente as falhas de segmentação.
Os apontadores são bastante úteis na programação de baixo nível, por exemplo no kernel des sistemas operativos ou em sistemas embebidos bare metal ou não, onde a gestão da memória é feita. Não é por acaso que praticamente todos os sistemas operativos são programados em C ou C++ no seu kernel.
porque os apontadores sao dinamicos, nao necessitas de alocar logo todo o espaço da memoria
Steve,
“Podias explicar porque por vezes é melhor utilizar apontadores? Mais concretamente em termos de ocupação de memória ram.”
A resposta é simples 😉
O C só tem passagem de valores por cópia.
Não sei se estudas-te assembler?irias perceber porquê 😉
Ao passares valores como parametros ás funções, são automaticamente copiados para o stack da função…
Se tiveres uma estructura gigante, não a vais passar como parametro para a função, porque nesse caso irias copiar toda a estructura para a função…isso era um horror, porque gastavas muita memoria, e além disso a tua função era lenta…e além disso, podia ocurrer o caso como em algumas archś de ficares sem memoria no stack 🙁
Então o que fazer?mandas o pointer para essa estructura, a função copia-o(o pointer para dentro da função), e manipulas o struct(ou não), de dentro da função 😉
Outro factor, acho que já foi referenciado acima…é que as variáveis só são visíveis no “scope” dessa função…ou seja dentro do stack da função!
O assembler iria ajudar-te a perceber porquê 😉 e podes parar o compiler tollchain a seguir ao compilador e ver o código 😉
Se dentro de uma função queres manipular uma variável que está fora…a única hipotse é passar o endereço de memória dessa variável para a função, dessa forma a função copia o endereço, e depois de dentro da função(no stack), manipulas a variável 😉
cmps
O exemplo “classico” na universidade era a utilização de ponteiros para criar uma lista telefónica. Se tivesses que definir um array estático no inicio do programa para, por exemplo, 1000 contactos, estarias a desperdiçar memória se o utilizador só fosse utilizar 200 contatctos. Se o utilizador ultrapassar os 1000 contactos o programa rebenta, pois já esgotou a memória disponivel. Assim, é melhor usar ponteiros e o programa vai alocando memoria para cada contato inserido, à medida das necessidades do utilizador. Os ponteiros são um pouco complicados, mas são uma arma poderosa. Além do facto de puderes utilizados para alocar memória há medida do necessário, podes usa-los para apontar para outras variáveis e utilizar esta vertente para a construção da lógica do teu programa
Obrigado pelas resposta 🙂
“Assim, é melhor usar ponteiros e o programa vai alocando memoria para cada contacto inserido, à medida das necessidades do utilizador.”
isto se alocares memoria manualmente 😉
Olá Steve.
Em primeiro lugar, obrigado. Em segundo, a maravilhosa comunidade de leitores já respondeu e MUITO bem à sua questão.
Finalmente, Desejo um Bom Ano Novo a todos vós 🙂
Obrigado Henrique,
Um Bom Ano pata ti e para os restantes colegas 😉
cmps
Nós agradecemos 🙂
Boas…
Um pormenor que é capaz de ajudar a complementar as explicações dadas.
Para além do que já foi dito, é recomendável relembrar que as variáveis declaradas dentro de uma função (variáveis locais), “perdem-se” assim que a função termina, por isso é que no exemplo 3, no primeiro código, as variáveis que queremos manipular, não são alteradas.
cUMPs
Psy
Exato 😉
Umas notas notas somente. Não altera nada a exposição, mas prepara as ideias para futuros desenvolvimentos.
O uso de apontadores, apesar de muito poderoso, cria imensas oportunidades de erro, alguns erros de difícil diagnóstico. Nunca subestimem a capacidade de fazer confusão quando se usam apontadores, nomeadamente apontadores que apontam para apontadores ou para estruturas que contêm apontadores.
Mesmo em C, desde o padrão 99, a alocação dinâmica de memória já não passa exclusivamente pelo uso de apontadores e os métodos alternativos devem ser usados para uma programação mais segura (e fácil). Isto não quer dizer que não se deva compreender os apontadores, mas que escrever código de fácil manutenção exige métodos alternativos sempre que possível e eficaz.
O C é uma linguagem de origem antiga e por isso tem certas limitações subjacentes ao era boa ideia décadas atrás.
Linguagens mais recentes como o C++, e algumas antigas mas não o C, permitem modificar variáveis passadas para funções sem o perigo extra de usar apontadores para essas variáveis. Na prática é como se se utilizar apontadores, mas não há o perigo de adicional que advém do uso de ponteiros.
Só uma + notinha. Desde a primeira versão padrão ISO/ANSI de C que o NULL vele o mesmo que 0 (zero), embora isso não seja garantido em versões anteriores. Por isso a minha recomendação foi colocar sempre um 0 no local de NULL. No entanto a compreensão do significado de NULL é importante porque existe muito código que o usa, mas eu não recomendo o seu uso.
+1,
Nesses casos que o José Simões reporta, podemos fazer uma macro para o preprocessador, definindo o termo NULL no inicio do ficheiro, e temos o problema resolvido(é assim que é feito no c89 uma macro em diversos ficheiros, no entanto o normal deveria ser em stddef.h, mas existem diversos locais como stdio.h, etc) 😉
Isto, para as ferramentas de compilação com preprocessador de texto, e que aceitem macros…caso contrario, somos obrigados a usar 0 é bem verdade.. 😉
É um bom comentário o seu 😉 , porque se levarmos o código para outro ambiente, a coisa pode não funcionar a primeira e garanti-mos desde logo a portabilidade… 😉