Déclaration de tableau : *tab = {1, 2}

Bonjour à tous,

Pourquoi ça segfault quand on fait :
int main ()
{
    int *a = {1, 2};

    for(int i = 0; i < 2; i++)
    {
        printf("%d\n", *(a + i));
    }
  
  return 0;
}


Et pourquoi ça segfault pas quand on fait :
int main ()
{
    int a[] = {1, 2};

    for(int i = 0; i < 2; i++)
    {
        printf("%d\n", *(a + i));
    }
  
  return 0;
}


Merci

2 réponses

  1. Modérateur
    Bonjour

    Explication du problème

    En fait il y a une différence entre déclarer ton tableau comme étant de type
    int[]
    ou
    int *


    Avec
    int[] t1 = {1, 2}
    , tout se passe "comme tu le crois". Le bloc
    {1,2}
    est créé en mémoire, et on prend l'adresse qui correspond au début de ce bloc pour l'assigner à
    t1
    . On n'a donc pas initialisé
    t1
    avec le bloc
    {1, 2}
    mais avec son adresse. Par la suite,
    t1
    peut être casté en
    int *
    car c'est une adresse, et donc on ne fait que changer le type du pointeur de
    int[]</code c> vers <code>int *
    .

    Avec
    int * t2 = {3, 4}
    , il n'en est rien. On initialise un pointeur au sens strict, donc il faut qu'à droite on ait un pointeur. Quand le type assigné à un pointeur n'est pas un pointeur, alors seuls les 32 premiers bits sont pris en compte. Tout se passe donc comme si
    {3, 4}
    était casté en
    int32
    . Comme le type de {3, 4} n'a pas été déclaré (tel que
    t1
    ), le compilateur lui donne par défaut le type
    int32[]
    . Donc ici,
    t2
    sera initialisé avec la première "case", donc à
    3
    .

    Et c'est ce que montre ce petit programme :

    #include <stdio.h>
    
    int main() {
        printf("sizeof(int) = %zu sizeof(int *) = %zu\n", sizeof(int), sizeof(int *));
        int t1[] = {1, 2};
        printf("t1 = %x\n", t1);
        int * t2 = {3, 4};
        printf("t2 = %x\n", t2);
        return 0;
    }


    ... pour lequel la compilation renvoie :

    toto.c: In function ‘main’:
    toto.c:7:17: warning: initialization of ‘int *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
    int * t2 = {3, 4};
    ^
    toto.c:7:17: note: (near initialization for ‘t2’)
    toto.c:7:20: warning: excess elements in scalar initializer
    int * t2 = {3, 4};
    ^
    toto.c:7:20: note: (near initialization for ‘t2’)


    ... et dont le résultat est :

    sizeof(int) = 4 sizeof(int *) = 8
    t1 = 4df4baf0
    t2 = 3


    Ici on voit en particulier que
    t2
    est initialisé avec la copie bit à bit de
    {3, 4}
    qui sera vu comme un
    int
    , puisque
    {3, 4}
    n'est pas un pointeur. Comme c'est assez cavalier,
    gcc
    a eu le bon goût de nous prévenir avec le message warning: initialization of ‘int *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]

    On pourrait se dire que comme t2 peut emmagasiner 8 octets et qu'un int ne fait que 4 octets, l'intégralité de
    {3, 4}
    peut être intégralement recopiée bit à bit dans
    t2
    . On initialiserait donc
    t2
    à
    3 * 8^4 + 4
    . Il n'en est rien seul la première case,
    3
    a été copiée. Ceci confirme que le bloc à droite de
    =
    a été casté en un scalaire (une valeur numérique si tu préfères) de type
    int32
    . On comprend alors ce que gcc voulait dire par warning: excess elements in scalar initializer.

    Conclusion
    • Quand tu alloues un tableau d'entiers avec
      malloc
      utilises
      int *
      .
    • Quand tu initialises un tableau d'entiers avec un bloc de données constant (e.g.
      {1, 2}
      ) utilises
      int[]
      .
    • Par la suite un pointeur obtenu par
      int[]
      et
      int *
      ont sémantiquement le même comportement, mais restent de type différents. Tu auras donc besoin de caster un
      int[]
      en
      int *
      si tu souhaites utiliser une fonction qui prend un
      int *
      en paramètre.


    Remarques complémentaires sur les tableaux et le pointeurs
    • Si tu es sensible à ce qui se passe en mémoire, évite le type
      int
      car sa taille effective dépend de l'architecture du système. Tu peux par exemple utiliser
      int32 *
      après avoir inclu
      #include<stdint.h>
      ). Typiquement dans une application réseau, où les tailles dans un paquet sont normalisés, tu dois savoir ce qui se passe au bit près.
    • Comme ni
      int[]
      ni
      int *
      ne portent la taille du tableau (ni dans le type, ni en mémoire), une fonction qui prend un tel pointeur en paramètre aura besoin du nombre de cases dans ton tableau.
    • On ne passe en général jamais un
      int[N]
      avec N une valeur entière en paramètre d'une fonction : cela recopie les N entiers en pile. C'est donc "coûteux" car ça engendre des recopies inutiles. De plus, on ne passe plus le tableau par pointeur mais par recopie. Donc si la fonction tente de modifier le tableau, ce changement n'aurait lieu que sur la recopie.
    • Afin d'éviter de devoir passer systématiquement la taille des chaînes aux fonctions de
      <string.h>
      la convention choisie en C a consisté à définir un caractère d'arrêt
      '\0'
      . Mais comme
      '\0'
      vaut
      0
      (voir table ascii), ce genre d'astuce ne peut pas être utilisé pour un tableau de valeur numérique.
    • Pendant qu'on est dans les chaînes, la syntaxe
      "toto"
      n'est pas équivalente
      {'t', 'o', 't', 'o', '\0'}
      . Dans la première écriture, chaque lettre correspond bien à un
      char
      soit 1 octet. Dans la seconde écriture,
      {'t', 'o', 't', 'o', '\0'}
      sera vu comme un
      int32[]
      par le compilateur. Si tu initialises une chaîne avec ça, tu auras donc le même problème qu'avec
      t2
      , qui sera alors initialisé à
      (int) 't'
      soit
      74
      (voir tableau ascii).
    • On peut tout à fait utiliser la syntaxe avec les guillemets pour initialiser un pointeur "étoilé" comme un
      char *
      (exemple :
      const char * s = "bonjour";
      ).


    Bonne chance
    1
    1. Bonjour,

      La question devrait plutôt être : pourquoi la ligne suivante compile ?
          int *a = {1, 2};

      Cette ligne n'a aucun sens. La syntaxe {1,2} n'a sens que pour initialiser un tableau ou une structure. Mais ici il n'y a ni tableau ni structure!
      Le compilateur doit émettre un gros warning et la traiter comme s'il avait vu :
          int *a = 2;
      0
      1. Bonjour,

        Mon but à la base était de parcourir un tableau de int* comme un tableau de char* :

        #include <stdio.h>

        int main ()
        {
        char a[] = {'a', 'b'};

        for(int i = 0; i < 2; i++)
        {
        printf("%c\n", *(a + i));
        }

        return 0;
        }



        Et ici, pareil, quand je faisais :

        char *a = {'a', 'b'};

        Il me crachait un segfault. Il n'y a pas une histoire comme quoi l'un est en lecture seule et l'autre expression est en lecture écriture ?
        0
      2. Il n'y a pas de différence d'utilisation pour
        char 
        et
        int
        . Sauf qu'il existe une notation pratique pour un table de char souvent appelée "chaîne de caractères'.
        Dans tous les cas,
        char*a
        ne crée pas un tableau, il peut pointer dessus. Pour
        int*a
        , même réponse.
        char a[] = {'a','b','c','\0'};// ok, a est un tableau
        char b[] = "abc";             // ok, notation chaîne pour un tableau de char
        char *c = "abc";              // toléré car "abc" est un tableau de 4 char constants et c pointe dessus
        const char *d = "abc";        // Ok, d pointe sur un tableau constant, notation possible
        char *e = {'a','b','c','\0'}; // aucun sens
        char *f = a;                  // ok, pointe sur un tableau
        char g[] = a;                 // interdit, copie de tableaux
        
        int h[] = {1,2,3,4};     // ok, h est un tableau
        int i[] = "abc";         // aucun sens (tableau d'int et tableau de const char)
        int *j = {1,2,3,4};      // aucun sens
        int *k = h;              // ok, pointe sur un tableau
        int l[] = h;             // interdit, copie de tableaux
        0
      3. merci pour cette info.
        mais concernant les écritures :

        char *a = {'a','b','c','\0'};

        et

        char *a = "abc";

        Cela veut dire que déclarer un tableau avec les { } et les " " ne sont pas équivalentes ?
        Qu'est ce qui diffère ?
        0
      4. Les accolades sont des accolades et les guillemets sont des guillemets, donc ça n'est pas équivalent.

        Et comme je l'ai écris dans tes 2 expressions a est un pointeur car l'étoile est une étoile. On la voit bien les 2 fois.
        Ce qu'il faut savoir c'est que "abc" est un tableau de 4 caractères constants.
        La seule exception comme écrit avant est que la ligne suivante est acceptée. C'est elle l'exception qui n'est pas logique.
        char b[] = "abcde";           // ok, notation chaîne pour un tableau de char

        On n'a normalement pas le droit d'écrire tableau=tableau, mais il y a exception ici. C'est compris comme : recopie le tableau "abcde" pour initialiser le tableau b (taille par défaut = 5+1).
        char a[4] = "abcde";    // erreur
        char b[5] = "abcde";    // ok (mais erreur en C++)
        char c[6] = "abcde";    // ok vaut "abcde\0"
        char d[9] = "abcde";    // ok vaut "abcde\0\0\0\0"
        char e[] = "abcde";     // ok vaut "abcde\0"
        const char* f = "abcde";// f est un POINTEUR
        
        printf("%d", sizeof(e));      // => 6
        printf("%d", sizeof("abcde"));// => 6
        printf("%d", sizeof(d));      // => 9
        printf("%d", sizeof(f));      // => 4 ou 8 (c'est un pointeur)
        0