Como ponteiro para ponteiros funcionam em C?


171

Como ponteiros para ponteiros funcionam em C? Quando você os usaria?


43
Não, não dever de casa .... só queria saber ... porque eu vejo muito quando leio o código C.

1
Um ponteiro para ponteiro não é um caso especial de algo, então eu não entendo o que você não entende sobre void **.
Akappa

para matrizes 2D, o melhor exemplo é a linha de comando args "prog arg1 arg2" é armazenada char ** argv. E se o chamador doesnt deseja alocar a memória (a função chamada irá alocar a memória)
resultsway

1
Você tem um exemplo agradável de "ponteiro para ponteiro" uso em Git 2.0: ver minha resposta abaixo
VonC

Respostas:


359

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 ccomo um ponteiro para a string (somente leitura) "olá" e, portanto, contém o valor 63. cdeve 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 cpaponta para c, isto é, contém o endereço de c(que é 58). Podemos ir ainda mais longe. Considerar:

const char ***cpp = &cp;

Agora cpparmazena 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:

  • O nome de uma matriz geralmente gera o endereço do seu primeiro elemento. Portanto, se a matriz contém elementos do tipo 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.
  • Embora uma matriz de strings pareça unidimensional, ela é de fato bidimensional, pois strings são matrizes de caracteres. Assim: char **.
  • Uma função fprecisará aceitar um argumento do tipo t **para alterar uma variável do tipo t *.
  • Muitos outros motivos numerosos demais para serem listados aqui.

7
sim bons example..i entender o que eles are..but como e quando usá-los é mais important..now ..

2
Stephan fez um bom trabalho reproduzindo, basicamente, o diagrama da linguagem de programação C da Kernighan & Richie. Se você está programando C, e não possui este livro e é legal com a documentação em papel, eu sugiro que você o compre, a despesa (razoavelmente) modesta se pagará muito rapidamente em produtividade. Tende a ser muito claro em seus exemplos.
J. Polfer

4
char * c = "olá" deve ser const char * c = "olá". Também é muito enganador dizer que "uma matriz é armazenada como o endereço do primeiro elemento". Uma matriz é armazenada como ... uma matriz. Freqüentemente, seu nome produz um ponteiro para o primeiro elemento, mas nem sempre. Sobre ponteiros para ponteiros, eu diria simplesmente que eles são úteis quando uma função precisa modificar um ponteiro passado como parâmetro (então você passa um ponteiro para o ponteiro).
Bastien Léonard

4
A menos que eu esteja interpretando mal essa resposta, ela parece errada. c é armazenado em 58 e aponta para 63, cp é armazenado em 55 e aponta para 58 e cpp não é representado no diagrama.
219 Thanatos

1
Parece bom. A questão menor foi tudo o que me impediu de dizer: Ótimo post. A explicação em si foi excelente. Mudando para uma votação positiva. (Talvez stackoverflow precisa ponteiros revisão?)
Thanatos

46

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.


32

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

http://i.stack.imgur.com/bpfxT.gif

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 ppnão está NULLno 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 --)!



@kumar boa referência. Eu o incluí na resposta para obter mais visibilidade.
VonC

Este vídeo foi essencial para eu entender o seu exemplo. Senti-me particularmente confuso (e beligerante) até desenhar um diagrama de memória e rastrear o progresso do programa. Dito isto, ainda me parece um pouco misterioso.
11287 Chris

@ Chris Ótimo vídeo, obrigado por mencioná-lo! Incluímos seu comentário na resposta para obter mais visibilidade.
VonC 11/07

14

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.”


1
Eu tive que ler essa passagem algumas vezes ... +1 por me fazer pensar!
Ruben Steins

É por isso que Lewis Carroll não é um escritor comum.
metarose

1
Então ... seria assim? nome -> 'O Homem Idoso Idoso' -> chamada -> 'da Haddock Eyes' -> música -> 'A-sentado em um Gate'
tisaconundrum


7

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.


Bem explicado para a parte 'Why'
Rana Deep

7

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.

computer.howstuffworks.com Link >>

www.flippinbits.com Link >>


7

Considere a figura e o programa abaixo para entender melhor esse conceito .

Diagrama de ponteiro duplo

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 = &num;

   // 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

5

é 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;
}

desculpe, eu sei que foi muito ruim. Tente ler, erm, este: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer

5

Você tem uma variável que contém um endereço de algo. Isso é um ponteiro.

Então você tem outra variável que contém o endereço da primeira variável. Isso é um ponteiro para o ponteiro.


3

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]).


2
ponteiros para ponteiros não devem ser confundidos com matrizes de rank2, por exemplo, int x [10] [10] onde você escreve x [5] [6], acessa o valor na matriz.
Pete Kirkham

Este é apenas um exemplo em que um vazio ** é apropriado. Um ponteiro para ponteiro é apenas um ponteiro que aponta para, bem, um ponteiro.
Akappa

1

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;
}


0

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 :)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.