Como ponteiros para ponteiros funcionam em C? Quando você os usaria?
Como ponteiros para ponteiros funcionam em C? Quando você os usaria?
Respostas:
Vamos assumir um computador de 8 bits com endereços de 8 bits (e, portanto, apenas 256 bytes de memória). Isso faz parte dessa memória (os números na parte superior são os endereços):
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| | 58 | | | 63 | | 55 | | | h | e | l | l | o | \0 | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
O que você pode ver aqui é que, no endereço 63, a string "olá" é iniciada. Portanto, neste caso, se esta é a única ocorrência de "olá" na memória,
const char *c = "hello";
... define c
como um ponteiro para a string (somente leitura) "olá" e, portanto, contém o valor 63. c
deve ser armazenado em algum lugar: no exemplo acima, no local 58. É claro que não podemos apenas apontar para caracteres , mas também para outros ponteiros. Por exemplo:
const char **cp = &c;
Agora cp
aponta para c
, isto é, contém o endereço de c
(que é 58). Podemos ir ainda mais longe. Considerar:
const char ***cpp = &cp;
Agora cpp
armazena o endereço de cp
. Portanto, ele tem o valor 55 (com base no exemplo acima) e você adivinhou: ele próprio é armazenado no endereço 60.
Por que alguém usa ponteiros para ponteiros:
t
, uma referência para a matriz tem tipo t *
. Agora considere uma matriz de matrizes do tipo t
: naturalmente, uma referência a essa matriz 2D terá type (t *)*
= t **
e, portanto, é um ponteiro para um ponteiro.char **
.f
precisará aceitar um argumento do tipo t **
para alterar uma variável do tipo t *
.Como ponteiros para ponteiros funcionam em C?
Primeiro, um ponteiro é uma variável, como qualquer outra variável, mas que contém o endereço de uma variável.
Um ponteiro para um ponteiro é uma variável, como qualquer outra variável, mas que contém o endereço de uma variável. Essa variável passa a ser um ponteiro.
Quando você os usaria?
Você pode usá-los quando precisar retornar um ponteiro para alguma memória na pilha, mas não usando o valor de retorno.
Exemplo:
int getValueOf5(int *p)
{
*p = 5;
return 1;//success
}
int get1024HeapMemory(int **p)
{
*p = malloc(1024);
if(*p == 0)
return -1;//error
else
return 0;//success
}
E você chama assim:
int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5
int *p;
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap
Existem outros usos também, como o argumento main () de todo programa C tem um ponteiro para um ponteiro para argv, onde cada elemento contém uma matriz de caracteres que são as opções da linha de comando. Você deve ter cuidado, porém, ao usar ponteiros de ponteiros para apontar para matrizes bidimensionais, é melhor usar um ponteiro para uma matriz bidimensional.
Por que é perigoso?
void test()
{
double **a;
int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)
double matrix[ROWS][COLUMNS];
int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}
Aqui está um exemplo de um ponteiro para uma matriz bidimensional feita corretamente:
int (*myPointerTo2DimArray)[ROWS][COLUMNS]
Você não pode usar um ponteiro para uma matriz bidimensional, se desejar oferecer suporte a um número variável de elementos para as ROWS e COLUMNS. Mas quando você souber de antemão, usaria uma matriz bidimensional.
Eu gosto deste exemplo de código "mundo real" de ponteiro para uso de ponteiro, no Git 2.0, confirme 7b1004b :
Linus disse uma vez:
Na verdade, gostaria que mais pessoas entendessem o tipo de código de baixo nível realmente essencial. Coisas não grandes e complexas, como a pesquisa de nome sem bloqueio, mas simplesmente o bom uso de ponteiros para ponteiros etc.
Por exemplo, eu já vi muitas pessoas que excluem uma entrada de lista vinculada unicamente mantendo o controle da entrada "anterior" e, em seguida, para excluir a entrada, fazendo algo como
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
e sempre que vejo código assim, simplesmente digo "Essa pessoa não entende ponteiros". E, infelizmente, é bastante comum.
As pessoas que entendem ponteiros simplesmente usam um " ponteiro para o ponteiro de entrada " e inicializam isso com o endereço do list_head. E então, ao percorrer a lista, eles podem remover a entrada sem usar condicionais, apenas fazendo um
*pp = entry->next
A aplicação dessa simplificação permite perder 7 linhas desta função, mesmo adicionando 2 linhas de comentário.
- struct combine_diff_path *p, *pprev, *ptmp;
+ struct combine_diff_path *p, **tail = &curr;
Chris destaca nos comentários o vídeo de 2016 " Problema de ponteiro duplo de Linus Torvalds ", de Philip Buuck .
kumar aponta nos comentários a postagem do blog " Linus on Understanding Pointers ", onde Grisha Trubetskoy explica:
Imagine que você tem uma lista vinculada definida como:
typedef struct list_entry {
int val;
struct list_entry *next;
} list_entry;
Você precisa iterá-lo do começo ao fim e remover um elemento específico cujo valor seja igual ao valor de to_remove.
A maneira mais óbvia de fazer isso seria:
list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;
while (entry) { /* line 4 */
if (entry->val == to_remove) /* this is the one to remove ; line 5 */
if (prev)
prev->next = entry->next; /* remove the entry ; line 7 */
else
head = entry->next; /* special case - first entry ; line 9 */
/* move on to the next entry */
prev = entry;
entry = entry->next;
}
O que estamos fazendo acima é:
- iterando sobre a lista até a entrada ser
NULL
, o que significa que chegamos ao final da lista (linha 4).- Quando encontramos uma entrada que queremos remover (linha 5),
- atribuímos o valor do próximo ponteiro atual ao anterior,
- eliminando assim o elemento atual (linha 7).
Há um caso especial acima - no início da iteração não há entrada anterior (
prev
éNULL
) e, para remover a primeira entrada na lista, você deve modificar a cabeça (linha 9).O que Linus estava dizendo é que o código acima poderia ser simplificado, tornando o elemento anterior um ponteiro para um ponteiro, e não apenas um ponteiro .
O código fica assim:
list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;
while (entry) {
if (entry->val == to_remove)
*pp = entry->next;
pp = &entry->next;
entry = entry->next;
}
O código acima é muito semelhante à variante anterior, mas observe como não precisamos mais observar o caso especial do primeiro elemento da lista, pois
pp
não estáNULL
no início. Simples e inteligente.Além disso, alguém nesse segmento comentou que o motivo é melhor porque
*pp = entry->next
é atômico. Certamente NÃO é atômico .
A expressão acima contém dois operadores de desreferência (*
e->
) e uma atribuição, e nenhuma dessas três coisas é atômica.
Este é um equívoco comum, mas quase nada em C deve ser considerado atômico (incluindo os operadores++
e--
)!
Ao abordar sugestões de um curso de programação na universidade, recebemos duas dicas de como começar a aprender sobre elas. O primeiro foi visualizar o Pointer Fun With Binky . O segundo era pensar na passagem dos Haddocks 'Eyes, de Through the Looking-Glass, de Lewis Carroll
"Você está triste", disse o Cavaleiro em tom ansioso: "Deixe-me cantar uma música para confortá-lo."
"É muito longo?" Alice perguntou, pois ouvira muita poesia naquele dia.
“É longo”, disse o Cavaleiro, “mas é muito, muito bonito. Todo mundo que me ouve cantar - ou traz lágrimas aos olhos, ou então -
"Ou então o que?" disse Alice, pois o Cavaleiro fez uma pausa repentina.
“Ou então não, você sabe. O nome da música é chamado 'Haddocks' Eyes '. ”
“Oh, esse é o nome da música, não é?” Alice disse, tentando se sentir interessada.
"Não, você não entende", disse o Cavaleiro, parecendo um pouco irritado. “É assim que o nome é chamado. O nome realmente é 'The Aged Aged Man'. ”
"Então eu deveria ter dito 'é assim que a música se chama'?" Alice se corrigiu.
“Não, você não deveria: isso é outra coisa! A música se chama 'Ways And Means': mas é assim que se chama, você sabe! ”
"Bem, qual é a música, então?" Alice disse, que já estava completamente desnorteada.
"Eu estava chegando a isso", disse o cavaleiro. “A música é realmente 'A-sitting On A Gate': e a música é minha própria invenção.”
Você pode ler isto: Ponteiros para Ponteiros
Espero que isso ajude a esclarecer algumas dúvidas básicas.
Quando é necessária uma referência a um ponteiro. Por exemplo, quando você deseja modificar o valor (endereço apontado) de uma variável de ponteiro declarada no escopo de uma função de chamada dentro de uma função chamada.
Se você passar um único ponteiro como argumento, modificará cópias locais do ponteiro, não o ponteiro original no escopo da chamada. Com um ponteiro para um ponteiro, você modifica o último.
Um ponteiro para um ponteiro também é chamado de identificador . Um uso geralmente ocorre quando um objeto pode ser movido na memória ou removido. Muitas vezes é responsável bloquear e desbloquear o uso do objeto, para que ele não seja movido ao acessá-lo.
É frequentemente usado em ambiente restrito à memória, como o Palm OS.
Considere a figura e o programa abaixo para entender melhor esse conceito .
Conforme a figura, ptr1 é um ponteiro único com endereço da variável num .
ptr1 = #
Da mesma forma, ptr2 é um ponteiro para ponteiro (ponteiro duplo) que está tendo o endereço do ponteiro ptr1 .
ptr2 = &ptr1;
Um ponteiro que aponta para outro ponteiro é conhecido como ponteiro duplo. Neste exemplo, ptr2 é um ponteiro duplo.
Valores acima do diagrama:
Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000
Exemplo:
#include <stdio.h>
int main ()
{
int num = 10;
int *ptr1;
int **ptr2;
// Take the address of var
ptr1 = #
// Take the address of ptr1 using address of operator &
ptr2 = &ptr1;
// Print the value
printf("Value of num = %d\n", num );
printf("Value available at *ptr1 = %d\n", *ptr1 );
printf("Value available at **ptr2 = %d\n", **ptr2);
}
Resultado:
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
é um ponteiro para o valor do endereço do ponteiro. (isso é terrível, eu sei)
basicamente, permite passar um ponteiro para o valor do endereço de outro ponteiro, para que você possa modificar para onde outro ponteiro está apontando de uma sub-função, como:
void changeptr(int** pp)
{
*pp=&someval;
}
Um ponteiro para ponteiro é, bem, um ponteiro para ponteiro.
Um exemplo significativo de someType ** é uma matriz bidimensional: você tem uma matriz, preenchida com ponteiros para outras matrizes, portanto, ao escrever
apontador [5] [6]
você acessa na matriz que contém ponteiros para outras matrizes em sua 5ª posição, obtém o ponteiro (deixe o nome do fpointer) e, em seguida, acessa o sexto elemento da matriz referenciada a essa matriz (fpointer [6]).
Como funciona: é uma variável que pode armazenar outro ponteiro.
Quando você os usaria: Muitos usa um deles se sua função deseja construir um array e devolvê-lo ao chamador.
//returns the array of roll nos {11, 12} through paramater
// return value is total number of students
int fun( int **i )
{
int *j;
*i = (int*)malloc ( 2*sizeof(int) );
**i = 11; // e.g., newly allocated memory 0x2000 store 11
j = *i;
j++;
*j = 12; ; // e.g., newly allocated memory 0x2004 store 12
return 2;
}
int main()
{
int *i;
int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
for ( int j=0; j<n; j++ )
printf( "roll no = %d \n", i[j] );
return 0;
}
Eu criei um vídeo de 5 minutos que explica como os ponteiros funcionam:
Existem muitas explicações úteis, mas eu não encontrei apenas uma descrição curta, então ..
Basicamente, o ponteiro é o endereço da variável. Código de resumo curto:
int a, *p_a;//declaration of normal variable and int pointer variable
a = 56; //simply assign value
p_a = &a; //save address of "a" to pointer variable
*p_a = 15; //override the value of the variable
//print 0xfoo and 15
//- first is address, 2nd is value stored at this address (that is called dereference)
printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a);
Informações também úteis podem ser encontradas no tópico O que significa referência e desreferência
E não tenho tanta certeza de quando é que os ponteiros podem ser úteis, mas, em comum, é necessário usá-los quando você estiver fazendo alguma alocação manual / dinâmica de memória - malloc, calloc, etc.
Espero que também ajude a esclarecer a problemática :)