web-dev-qa-db-fra.com

Que fait ** en langage C

Je connais C avec une bonne expérience de Java) et j'essaie de comprendre les pointeurs et les tableaux.

Je sais que cet indice operator[] fait partie d'une définition de tableau, donc:

int numbers[] = {1,3,4,5};

créerait un tableau entier, qui serait représenté en mémoire par 16 octets, 4 lots de 4 octets:

numbers[0] = 1, address 0061FF1C
numbers[1] = 3, address 0061FF20
numbers[2] = 4, address 0061FF24
numbers[3] = 5, address 0061FF28

Cependant, en ce qui concerne les pointeurs, mes connaissances commencent à se dégrader. Par conséquent, si je devais créer un pointeur sur les nombres de tableaux, je procéderais comme suit:

int *pNumbers = &numbers[0];

qui ressemblerait à ceci:

pointer to numbers

Et je suppose que ce serait de taille 4 octets?

Cependant, le ** Je lis comme "pointeur sur un pointeur" ce qui n'a aucun sens pour moi, pourquoi quelqu'un voudrait-il avoir un pointeur sur un pointeur, sûrement si a-> b-> c alors a-> c suffirait? Je sais que quelque chose me manque, et cela doit avoir quelque chose à voir avec les tableaux car argv peut être de type char[ ] ou char ** comme vu ci-dessous:

int main(int argc, char **argv){}

Alors:

  • qu'est-ce que c'est (**)?
  • à quoi sert-il?
  • comment est-il représenté en mémoire?
36
James

En C, les arguments sont passés par valeurs. Par exemple, si vous avez un entier visible dans main

int main( void )
{
    int x = 10;
    //...

et la fonction suivante

void f( int x )
{
    x = 20;
    printf( "x = %d\n", x );
} 

alors si vous appelez la fonction principale comme celle-ci

f( x );

alors le paramètre obtient la valeur de la variable x dans main. Cependant, le paramètre lui-même occupe en mémoire une étendue différente de celle de l'argument. Ainsi, toute modification du paramètre dans la fonction n'influence pas la variable d'origine dans principale, car ces modifications se produisent dans une étendue de mémoire différente.

Alors, comment changer la varible en main dans la fonction?

Vous devez transmettre une référence à la variable à l'aide de pointeurs.

Dans ce cas, la déclaration de fonction ressemblera à

void f( int *px );

et la définition de la fonction sera

void f( int *px )
{
    *px = 20;
    printf( "*px = %d\n", *px );
} 

Dans ce cas, c'est l'étendue de la mémoire occupée par la variable d'origine x qui change car, dans la fonction, nous avons accès à cette étendue à l'aide du pointeur.

    *px = 20;

Naturellement, la fonction doit être appelée dans main comme

f( &x );

Tenez compte du fait que le paramètre lui-même qui est le pointeur px est comme d'habitude une variable locale de la fonction. C'est-à-dire que la fonction crée cette variable et l'initialise avec l'adresse de la variable x.

Supposons maintenant que vous avez déclaré un pointeur par exemple de la manière suivante

int main( void )
{
   int *px = malloc( sizeof( int ) );
   //..

Et la fonction définie comme

void f( int *px )
{
    px = malloc( sizeof( int ) );

    printf( "px = %p\n", px );
}

Comme le paramètre px est une variable locale qui lui attribue une valeur n’influence pas le pointeur original. La fonction change une étendue de mémoire différente de celle occupée par le pointeur d'origine px dans main.

Comment changer le pointeur d'origine dans la fonction? Il suffit de passer par référence!

Par exemple

f( &px );
//...

void f( int **px )
{
    *px = malloc( sizeof( int ) );

    printf( "*px = %p\n", *px );
}

Dans ce cas, la valeur stockée dans le pointeur d'origine sera modifiée dans la fonction, car la fonction utilisant le déréférencement accède à la même étendue mémoire que celle où le pointeur d'origine a été défini.

32
Vlad from Moscow

Q: qu'est-ce que c'est (**)?

A: Oui, c'est exactement ça. Un pointeur sur un pointeur.

Q: quel usage en a-t-il?

A: Il a un certain nombre d'utilisations. En particulier pour représenter des données en 2 dimensions (images, etc.). Dans le cas de votre exemple char** argv peut être considéré comme un tableau d’un tableau de chars. Dans ce cas, chaque char* pointe sur le début d'une chaîne. Vous pouvez en fait déclarer ces données vous-même explicitement comme tel.

char* myStrings[] = {
    "Hello",
    "World"
};

char** argv = myStrings;

// argv[0] -> "Hello"
// argv[1] -> "World"

Lorsque vous accédez à un pointeur comme un tableau, le nombre avec lequel vous l'indexez et la taille de l'élément lui-même sont utilisés pour compenser l'adresse du prochain élément du tableau. Vous pouvez également accéder à tous vos numéros comme suit, et c’est en fait ce que C est en train de faire. Gardez à l'esprit que le compilateur sait combien d'octets un type comme int utilise lors de la compilation. Donc, il sait à quel point chaque étape doit être grande.

*(numbers + 0) = 1, address 0x0061FF1C
*(numbers + 1) = 3, address 0x0061FF20
*(numbers + 2) = 4, address 0x0061FF24
*(numbers + 3) = 5, address 0x0061FF28

Le * opérateur est appelé opérateur de déréférence. Il est utilisé pour récupérer la valeur de la mémoire pointée par un pointeur. numbers est littéralement juste un pointeur sur le premier élément de votre tableau.

Dans le cas de mon exemple, myStrings pourrait ressembler à ceci, à supposer qu'un pointeur/adresse ait 4 octets, ce qui signifie que nous sommes sur une machine 32 bits.

myStrings = 0x0061FF14

// these are just 4 byte addresses
(myStrings + 0) -> 0x0061FF14 // 0 bytes from beginning of myStrings
(myStrings + 1) -> 0x0061FF18 // 4 bytes from beginning of myStrings

myStrings[0] -> 0x0061FF1C // de-references myStrings @ 0 returning the address that points to the beginning of 'Hello'
myStrings[1] -> 0x0061FF21 // de-references myStrings @ 1 returning the address that points to the beginning of 'World'

// The address of each letter is 1 char, or 1 byte apart
myStrings[0] + 0 -> 0x0061FF1C  which means... *(myStrings[0] + 0) = 'H'
myStrings[0] + 1 -> 0x0061FF1D  which means... *(myStrings[0] + 1) = 'e'
myStrings[0] + 2 -> 0x0061FF1E  which means... *(myStrings[0] + 2) = 'l'
myStrings[0] + 3 -> 0x0061FF1F  which means... *(myStrings[0] + 3) = 'l'
myStrings[0] + 4 -> 0x0061FF20  which means... *(myStrings[0] + 4) = 'o'
11
kirk roerig

La manière traditionnelle d'écrire l'argument argv est char *argv[], Qui donne plus d'informations sur ce que c'est, un tableau de pointeurs sur des caractères (c'est-à-dire un tableau de chaînes).

Cependant, lorsque vous passez un tableau à une fonction, il se décompose en un pointeur, vous laissant avec un pointeur sur lequel pointer vers char ou char **.


Bien sûr, les doubles astérisques peuvent également être utilisés lors du déréférencement d'un pointeur sur un pointeur. Ainsi, sans le contexte ajouté à la fin de la question, il existe deux réponses à la question que signifie ** En C, en fonction du contexte.

Pour continuer avec l'exemple argv, une façon d'obtenir le premier caractère du premier élément de argv serait de faire argv[0][0], ou vous pouvez utiliser l'opérateur de déréférence deux fois, comme dans **argv.

L’indexation et le déréférencement de tableaux sont interchangeables dans la plupart des endroits car, pour tout pointeur ou array p et index i l’expression p[i] Est équivalent à *(p + i). Et si i est 0, Nous avons *(p + 0) qui peut être abrégé en *(p), ce qui revient au même que *p.

Comme curiosité, parce que p[i] Équivaut à *(p + i) et à la propriété commutative de l'addition, l'expression *(p + i) est égale à *(i + p) qui conduit à p[i] égal à i[p].


Enfin, pour vous avertir de l’utilisation excessive de pointeurs, vous pourriez parfois entendre la phrase programmeur à trois étoiles , ce qui est le cas lorsque vous utilisez trois astérisques comme dans *** (comme dans un pointeur vers un pointeur vers un pointeur). Mais pour citer le lien

Juste pour être clair: être appelé un ThreeStarProgrammer n'est généralement pas un compliment

Et un autre avertissement: n tableau de tableaux est pas identique à un pointeur sur un pointeur (Lien vers une ancienne réponse de mine, qui montre également la disposition en mémoire d'un pointeur sur un pointeur au lieu d'un tableau de tableaux.)

8

** dans la déclaration représente un pointeur à l'autre. Le pointeur est lui-même un type de données et, comme d’autres types de données, il peut avoir un pointeur.

int i = 5, j = 6; k = 7;
int *ip1 = &i, *ip2 = &j; 
int **ipp = &ip1;  

enter image description here

Pointeur à pointeur sont utiles en cas d’allocation d’un tableau 2D dynamique. Pour allouer un tableau 2D 10x10 (peut ne pas être contigu)

int **m = malloc(sizeof(int *)*10;  
for(int i = 0; i < 10; i++)
    m[i] = malloc(sizeof(int)*10  

Il est également utilisé lorsque vous souhaitez modifier la valeur d'un pointeur via une fonction.

void func (int **p, int n)  
{
    *p = malloc(sizeof(int)*n); // Allocate an array of 10 elements 
}

int main(void)
{
    int *ptr = NULL;
    int n = 10;
    func(&ptr, n);
    if(ptr)
    {
        for(int i = 0; i < n; i++)
        {  
             ptr[i] = ++i;
        }  
    }

    free(ptr);
}

Lectures supplémentaires: Pointeur à Pointeur .

5
haccks

Considérez si vous avez une table de pointeurs - telle qu'une table de chaînes (étant donné que les chaînes en "C" sont gérées simplement comme des pointeurs vers le premier caractère de la chaîne).

Ensuite, vous avez besoin d'un pointeur sur le premier pointeur de la table. D'où le "char **".

Si vous avez une table en ligne avec toutes les valeurs, comme une table d’entiers à deux dimensions, il est tout à fait possible de s’échapper avec un seul niveau d’indirection (c’est-à-dire un simple pointeur, comme "int *"). Mais lorsqu'il existe un pointeur au milieu qui doit être déréférencé pour atteindre le résultat final, cela crée un deuxième niveau d'indirection, puis le pointeur à pointeur est essentiel.

Une autre clarification ici. Dans "C", le déréférencement via la notation de pointeur (par exemple "* ptr") par rapport à la notation d'indice de tableau (par exemple ptr [0]) présente peu de différence, hormis la valeur d'index évidente dans la notation de tableau. Le seul moment où l'astérisque par rapport aux crochets compte vraiment est lors de l'allocation d'une variable (par exemple, int * x; est très différent de int x [1]).

4
ash

Tout d’abord, rappelez-vous que C traite les tableaux très différemment de Java. Une déclaration comme

char foo[10];

alloue suffisamment de mémoire pour 10 char valeurs et rien d'autre (modulo de tout espace supplémentaire pour satisfaire les exigences d'alignement); aucun stockage supplémentaire n'est mis de côté pour un pointeur sur le premier élément ou tout autre type de métadonnées telles que la taille du tableau ou le type de classe d'élément. Il n'y a pas d'objet foo en dehors des éléments du tableau eux-mêmes1. Au lieu de cela, il existe une règle dans le langage selon laquelle le compilateur voit à chaque fois un tableau expression qui n'est pas l'opérande de sizeof ou unary Opérateur & (Ou un littéral utilisé pour initialiser un autre tableau dans une déclaration), il convertit implicitement cette expression à partir du type "N- element array of T "to" pointeur sur T ", et la valeur de l'expression est l'adresse du premier élément du tableau.

Cela a plusieurs implications. Tout d'abord, lorsque vous passez une expression de tableau en tant qu'argument à une fonction, la fonction reçoit en réalité une valeur de pointeur:

char foo[10];
do_something_with( foo );
...
void do_something_with( char *p )
{
  ...
}

Le paramètre formel p correspondant au paramètre actuel foo est un pointeur sur char, pas un tableau de char. Pour rendre les choses confuses, C permet à do_something_with D’être déclaré comme

void do_something_with( char p[] )

ou même

void do_something_with( char p[10] )

mais dans le cas des déclarations de paramètres de fonction, T p[] et T p[N] sont identiques à T *p et les trois déclarent p comme un pointeur et non un tableau.2. Notez que cela n’est vrai que pour les déclarations de paramètres de fonction.

La deuxième implication est que l’opérateur d’indice [] Peut être utilisé sur des opérandes de pointeur ainsi que sur des opérandes de tableau, tels que

char foo[10];
char *p = foo;
...
p[i] = 'A'; // equivalent to foo[i] = 'A';

La dernière implication conduit à un cas de gestion des pointeurs à pointeurs - supposons que vous ayez un tableau de pointeurs comme

const char *strs[] = { "foo", "bar", "bletch", "blurga", NULL };

strs est un tableau à 5 éléments de const char *3; cependant, si vous le transmettez à une fonction comme

do_something_with( strs );

alors ce que la fonction reçoit est en réalité un pointeur sur un pointeur, pas un tableau de pointeurs:

void do_something_with( const char **strs ) { ... }

Les pointeurs vers les pointeurs (et les niveaux plus élevés d'indirection) apparaissent également dans les situations suivantes:

  • Écriture dans un paramètre de type pointeur: Rappelez-vous que C transmet tous les paramètres par valeur; le paramètre formel dans la définition de fonction est un objet en mémoire différent du paramètre réel dans l'appel de fonction; si vous souhaitez que la fonction mette à jour la valeur du paramètre actuel, vous devez passer un pointeur sur ce paramètre:

    void foo( T *param ) // for any type T
    {
      *param = new_value(); // update the object param *points to*
    }
    
    void bar( void )
    {
      T x;
      foo( &x );   // update the value in x
    }
    
    Supposons maintenant que nous remplacions le type T par le type de pointeur R *. Notre extrait de code ressemble à ceci:

    void foo( R **param ) // for any type R *
    {
      ...
      *param = new_value(); // update the object param *points to*
      ...
    } 
    
    void bar( void )
    {
      R *x;
      foo( &x );   // update the value in x
    }
    
    Même sémantique - nous mettons à jour la valeur contenue dans x. C'est juste que dans ce cas, x a déjà un type de pointeur, nous devons donc passer un pointeur au pointeur. Cela peut être étendu à des niveaux de direction plus élevés:

    void foo( Q ****param ) // for any type Q ***
    {
      ...
      *param = new_value(); // update the object param *points to*
      ...
    } 
    
    void bar( void )
    {
      Q ***x;
      foo( &x );   // update the value in x
    }
    
  • Tableaux multidimensionnels alloués dynamiquement: Une technique courante pour attribuer des tableaux multidimensionnels en C consiste à allouer un tableau de pointeurs, et pour chaque élément de ce tableau, allouez un tampon auquel le pointeur se réfère:

    T **arr;
    arr = malloc( rows * sizeof *arr );  // arr has type T **, *arr has type T *
    if ( arr )
    {
      for ( size_t i = 0; i < rows; i++ )
      {
        arr[i] = malloc( cols * sizeof *arr[i] ); // arr[i] has type T *
        if ( arr[i] )
        {
          for ( size_t j = 0; j < cols; j++ )
          {
            arr[i][j] = some_initial_value();
          }
        }
      }
    }
    
    Ceci peut être étendu à des niveaux plus élevés d'indirection, vous avez donc des types comme T *** Et T ****, Etc.

1. Cela explique en partie pourquoi les expressions de tableau peuvent ne pas être la cible d'une affectation; il n'y a rien à assigner à .

  1. Il s'agit d'une conservation du langage de programmation B à partir duquel C a été dérivé. dans B, un pointeur est déclaré comme auto p[].

  2. Chaque littéral de chaîne est un tableau de char, mais comme nous ne les utilisons pas pour initialiser des tableaux individuels de char, les expressions sont converties en valeurs de pointeur.

4
John Bode

De votre exemple int * Vous dites

Et je suppose que ce serait de taille 4 octets?

Contrairement à Java, C ne spécifie pas les tailles exactes de ses types de données. Différentes implémentations peuvent utiliser et utilisent différentes tailles (mais chaque implémentation doit être cohérente). Les ints à 4 octets sont courants, mais ints peut être aussi petit que deux octets, et rien ne les limite intrinsèquement à quatre. La taille des pointeurs est encore moins spécifiée, mais cela dépend généralement de l'architecture matérielle à laquelle l'implémentation C est destinée. Les tailles de pointeur les plus courantes sont quatre octets (typique pour les architectures 32 bits) et huit octets (commun pour les architectures 64 bits).

qu'est-ce que c'est (**)?

Dans le contexte que vous présentez, cela fait partie de l'indicatif de type char **, Qui décrit un pointeur sur un pointeur vers char, comme vous le pensiez.

à quoi sert-il?

Plus ou moins les mêmes utilisations en tant que pointeur sur tout autre type de données. Parfois, vous souhaitez ou devez accéder indirectement à une valeur de pointeur, tout comme vous pouvez ou souhaitez accéder indirectement à une valeur de tout autre type. En outre, il est utile pour pointer vers (le premier élément de) un tableau de pointeurs, ce qui explique comment il est utilisé dans le deuxième paramètre pour une fonction C main().

Dans ce cas particulier, chaque char * Dans le tableau pointé vers lui-même pointe vers l'un des arguments de ligne de commande du programme.

comment est-il représenté en mémoire?

C ne spécifie pas, mais généralement les pointeurs sur les pointeurs ont la même représentation que les pointeurs sur tout autre type de valeur. La valeur qu'il pointe est simplement une valeur de pointeur.

4
John Bollinger

** signifie pointeur à pointeur puisque vous connaissez le nom. Je vais expliquer chacune de vos questions:

qu'est-ce que c'est (**)?

Pointeur à pointeur. Parfois, les gens appellent double pointeur. Par exemple:

int a = 3;
int* b = &a; // b is pointer. stored address of a
int**b = &b;  // c is pointer to pointer. stored address of b
int***d = &c; // d is pointer to pointer to pointer. stored address of d. You get it. 

comment est-il représenté en mémoire?

c dans l'exemple ci-dessus est juste une variable normale et a la même représentation que d'autres variables (pointeur, int ...). La taille de la mémoire de la variable c est la même que b et elle dépend de la plate-forme. Par exemple, pour un ordinateur 32 bits, chaque adresse de variable inclut 32 bits, de sorte que la taille sera de 4 octets (8x4 = 32 bits). Sur un ordinateur 64 bits, chaque adresse de variable sera de 64 bits, de sorte que la taille sera de 8 octets (8x8 = 64 bits).

à quoi sert-il?

Il y a beaucoup d'usages pour pointeur à pointeur, cela dépend de votre situation. Par exemple, voici un exemple que j'ai appris dans ma classe d'algorithme. Vous avez une liste chaînée. Maintenant, vous voulez écrire une méthode pour changer cette liste chaînée, et votre méthode peut avoir changé de tête de liste chaînée. (Exemple: supprimer un élément avec une valeur égale à 5, supprimer un élément principal, permuter, ...). Donc vous avez deux cas:

1. Si vous venez de passer un pointeur d’élément head . Peut-être que cet élément de tête sera supprimé et que ce pointeur ne sera plus valide.

2. Si vous passez le pointeur du pointeur de l’élément head. Si votre élément head est supprimé, vous ne rencontrez aucun problème car le pointeur du pointeur est toujours présent. Cela ne fait que changer les valeurs d'un autre noeud principal.

Vous pouvez faire référence ici à l'exemple ci-dessus: pointeur à pointeur dans la liste liée

Une autre utilisation consiste à utiliser un tableau à deux dimensions. C est différent de Java. Tableau bidimensionnel en C, en fait juste un bloc de mémoire continu. Tableau à deux dimensions dans Java est un bloc multi-mémoire (dépend de votre rangée de matrice)

J'espère que cette aide :)

4
hqt

** représente un pointeur sur un pointeur. Si vous voulez passer un paramètre par référence, vous utiliseriez *, mais si vous voulez passer le pointeur lui-même par référence, vous avez besoin d’un pointeur sur le pointeur, d’où **.

3
John Sensebe

Je pense que je vais ajouter ma propre réponse ici et que tout le monde a fait un travail remarquable, mais j'étais vraiment confus quant à la signification d'un pointeur sur un pointeur. La raison pour laquelle j'ai proposé cela est parce que j'avais l'impression que toutes les valeurs sauf les pointeurs étaient passées par valeur et que les pointeurs étaient passés par référence. Voir ce qui suit:

void f(int *x){
    printf("x: %d\n", *x);
    (*x)++;
}

void main(){
   int x = 5;
   int *px = &x;
   f(px);
   printf("x: %d",x);
}

produirait:

x: 5
x: 6

Cela m'a fait penser (pour quelque raison que ce soit) que les pointeurs ont été passés par référence lorsque nous passons dans le pointeur, le manipulant, puis en sortant et en imprimant la nouvelle valeur. Si vous pouvez manipuler un pointeur dans une fonction ... pourquoi avoir un pointeur sur un pointeur afin de manipuler le pointeur pour commencer!

Cela me semblait faux, et à juste titre, car il serait idiot de disposer d’un pointeur pour manipuler un pointeur alors que vous pouvez déjà manipuler un pointeur dans une fonction. La chose avec C cependant; is tout est passé par valeur, même les pointeurs. Permettez-moi de vous expliquer davantage en utilisant des pseudo-valeurs au lieu des adresses.

//this generates a new pointer to point to the address so lets give the
//new pointer the address 0061FF28, which has the value 0061FF1C.
void f(int 0061FF1C){
    // this prints out the value stored at 0061FF1C which is 5
    printf("x: %d\n", 5);
    // this FIRST gets the value stored at 0061FF1C which is 5
    // then increments it so thus 6 is now stored at 0061FF1C
    (5)++;
}

void main(){
   int x = 5;

   // this is an assumed address for x
   int *px = 0061FF1C;

   /*so far px is a pointer with the address lets say 0061FF24 which holds
    *the value 0061FF1C, when passing px to f we are passing by value...
    *thus 0061FF1C is passed in (NOT THE POINTER BUT THE VALUE IT HOLDS!)
    */

   f(px);

   /*this prints out the value stored at the address of x (0061FF1C) 
    *which is now 6
    */
   printf("x: %d",6);
}

Mon principal malentendu des pointeurs à pointeurs est le passage par valeur vs passage par référence. Le pointeur d'origine n'a pas du tout été transmis à la fonction. Nous ne pouvons donc pas changer l'adresse indiquée, mais uniquement l'adresse du nouveau pointeur (qui a l'illusion d'être l'ancien pointeur car il pointe vers l'adresse où l'ancien pointeur était pointant vers!).

3
James

C'est un pointeur sur un pointeur. Si vous vous demandez pourquoi vous souhaitez utiliser un pointeur vers un pointeur, voici un fil de discussion similaire qui répond à cela de différentes manières.

Pourquoi utiliser le double pointeur? Ou Pourquoi utiliser les pointeurs à pointeurs?

3
tramstheman

Je comprendrais char **argv comme char** argv. À présent, char* est fondamentalement un tableau de char, donc (char*)* est un tableau de tableaux de char.

En d'autres mots (en vrac), argv est un tableau de chaînes. Dans cet exemple particulier: l'appel

myExe dummyArg1 dummyArg2

dans la console ferait argv comme

argv[0] = "myExe"
argv[1] = "dummyArg1"
argv[2] = "dummyArg2"
1
Quang Hoang

Par exemple, ** est un pointeur sur un pointeur. char **argv est le même que char *argv[] et c'est pareil avec char argv[][]. C'est une matrice.

Vous pouvez déclarer une matrice de 4 lignes, par exemple, mais avec un nombre de colonnes différent, comme JaggedArrays.

Il est représenté sous forme de matrice.

Ici vous avez une représentation en mémoire.

1
Simply Me