En C, on peut utiliser un littéral de chaîne dans une déclaration comme celle-ci:
char s[] = "hello";
ou comme ceci:
char *s = "hello";
Alors, quelle est la difference? Je veux savoir ce qui se passe réellement en termes de durée de stockage, à la fois lors de la compilation et de l'exécution.
La différence ici est que
char *s = "Hello world";
placera "Hello world"
dans = parties en lecture seule de la mémoire, et rendant s
un pointeur sur qui rend illégale toute opération d’écriture sur cette mémoire.
Tout en faisant:
char s[] = "Hello world";
place la chaîne littérale dans la mémoire en lecture seule et la copie dans la mémoire nouvellement allouée de la pile. Faisant ainsi
s[0] = 'J';
légal.
Tout d'abord, dans les arguments de fonction, ils sont exactement équivalents:
void foo(char *x);
void foo(char x[]); // exactly the same in all respects
Dans d'autres contextes, char *
alloue un pointeur, tandis que char []
alloue un tableau. Où est la ficelle dans le premier cas, vous demandez? Le compilateur alloue en secret un tableau anonyme anonyme pour contenir le littéral de chaîne. Alors:
char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;
Notez que vous ne devez jamais essayer de modifier le contenu de ce tableau anonyme via ce pointeur. les effets sont indéfinis (ce qui signifie souvent un crash):
x[1] = 'O'; // BAD. DON'T DO THIS.
L'utilisation de la syntaxe de tableau l'alloue directement dans une nouvelle mémoire. Ainsi, la modification est sûre:
char x[] = "Foo";
x[1] = 'O'; // No problem.
Cependant, le tableau ne vit que tant que sa portée continue, donc si vous faites cela dans une fonction, ne renvoyez ni ne perdez pas de pointeur sur ce tableau - faites une copie à la place avec strdup()
ou similaire. Si le tableau est alloué dans la portée globale, bien sûr, pas de problème.
Cette déclaration:
char s[] = "hello";
Crée un objet - un tableau char
de taille 6, appelé s
, initialisé avec les valeurs 'h', 'e', 'l', 'l', 'o', '\0'
. L'emplacement de ce tableau en mémoire et la durée de vie de ce tableau dépendent de l'emplacement de la déclaration. Si la déclaration est dans une fonction, elle vivra jusqu'à la fin du bloc dans lequel elle est déclarée et sera presque certainement allouée sur la pile; si elle se trouve en dehors d'une fonction, elle sera probablement stockée dans un "segment de données initialisé" chargé à partir du fichier exécutable dans la mémoire inscriptible lorsque le programme est en cours. courir.
D'autre part, cette déclaration:
char *s ="hello";
Crée deux objets:
char
s contenant les valeurs 'h', 'e', 'l', 'l', 'o', '\0'
, qui n'a pas de nom et a durée de stockage statique (ce qui signifie qu'il dure toute la vie du programme); ets
, qui est initialisée avec l'emplacement du premier caractère de ce tableau non nommé en lecture seule.Le tableau en lecture seule non nommé est généralement situé dans le segment "texte" du programme, ce qui signifie qu'il est chargé du disque dans la mémoire en lecture seule, avec le code lui-même. L'emplacement de la variable de pointeur s
en mémoire dépend de l'emplacement où la déclaration apparaît (comme dans le premier exemple).
Compte tenu des déclarations
char *s0 = "hello world";
char s1[] = "hello world";
supposons la carte mémoire hypothétique suivante:
0x01 0x02 0x03 0x04 0x00008000: 'h' 'e' 'l' 'l' 0x00008004: 'o' '' 'w' '' ''. '. ] 0x00008008: 'r' 'l' 'd' 0x00 ... S0: 0x00010000: 0x00 0x00 0x80 [0x] S1: 0x00010004: 'h' 'e' ' l '' l ' 0x00010008:' o '' '' w '' o ' 0x0001000C:' r '' l '' d '0x00
Le littéral de chaîne "hello world"
est un tableau à 12 éléments de char
(const char
en C++) avec une durée de stockage statique, ce qui signifie que la mémoire correspondante est allouée lorsque le programme démarre et reste alloué. jusqu'à ce que le programme se termine. Tenter de modifier le contenu d'un littéral de chaîne appelle un comportement indéfini.
La ligne
char *s0 = "hello world";
définit s0
comme un pointeur sur char
avec une durée de stockage automatique (ce qui signifie que la variable s0
n'existe que pour l'étendue dans laquelle elle est déclarée) et copie le adresse de la chaîne littérale (0x00008000
dans cet exemple). Notez que, puisque s0
pointe sur un littéral de chaîne, il ne devrait pas être utilisé comme argument d'une fonction qui tenterait de le modifier (par exemple, strtok()
, strcat()
, strcpy()
, etc.).
La ligne
char s1[] = "hello world";
définit s1
comme un tableau à 12 éléments de char
(la longueur est tirée du littéral de chaîne) avec une durée de stockage automatique et copie le conten du littéral dans le tableau. Comme vous pouvez le constater sur la carte mémoire, nous avons deux copies de la chaîne "hello world"
; la différence est que vous pouvez modifier la chaîne contenue dans s1
.
s0
et s1
sont interchangeables dans la plupart des contextes; voici les exceptions:
sizeof s0 == sizeof (char*)
sizeof s1 == 12
type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char
Vous pouvez réaffecter la variable s0
pour qu'elle pointe vers un autre littéral ou une autre variable. Vous ne pouvez pas réaffecter la variable s1
pour qu'elle pointe vers un autre tableau.
C99 N1256 draft
Il existe deux utilisations différentes des littéraux de chaîne de caractères:
Initialiser char[]
:
char c[] = "abc";
C'est "plus de magie", et décrit à 6.7.8/14 "Initialisation":
Un tableau de type caractère peut être initialisé par un littéral de chaîne de caractères, éventuellement entre accolades. Les caractères successifs du littéral de chaîne de caractères (y compris le caractère nul final s'il y a de la place ou si le tableau est de taille inconnue) initialisent les éléments du tableau.
Donc, ceci est juste un raccourci pour:
char c[] = {'a', 'b', 'c', '\0'};
Comme tout autre tableau régulier, c
peut être modifié.
Partout ailleurs: il génère un:
Alors quand tu écris:
char *c = "abc";
Ceci est similaire à:
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
Notez la conversion implicite de char[]
en char *
, qui est toujours légale.
Ensuite, si vous modifiez c[0]
, vous modifiez également __unnamed
, qui est UB.
Ceci est documenté à la section 6.4.5 "Littéraux de chaîne":
5 Dans la phase de traduction 7, un octet ou un code de valeur zéro est ajouté à chaque séquence de caractères multi-octets résultant d'un littéral de chaîne ou de littéraux. La séquence de caractères multi-octets est ensuite utilisée pour initialiser un tableau de durée et de longueur de stockage statique juste suffisante pour contenir la séquence. Pour les littéraux de chaîne de caractères, les éléments du tableau ont le type char et sont initialisés avec les octets individuels de la séquence de caractères multi-octets [...]
6 Il n'est pas précisé si ces tableaux sont distincts à condition que leurs éléments aient les valeurs appropriées. Si le programme tente de modifier un tel tableau, le comportement n'est pas défini.
6.7.8/32 "Initialisation" donne un exemple direct:
EXEMPLE 8: La déclaration
char s[] = "abc", t[3] = "abc";
définit les objets de tableau de caractères "simples"
s
ett
dont les éléments sont initialisés avec des littéraux de chaîne de caractères.Cette déclaration est identique à
char s[] = { 'a', 'b', 'c', '\0' }, t[] = { 'a', 'b', 'c' };
Le contenu des tableaux est modifiable. D'autre part, la déclaration
char *p = "abc";
définit
p
avec le type "pointeur sur caractère" et l'initialise pour pointer sur un objet de type "tableau de caractère" de longueur 4 dont les éléments sont initialisés avec un littéral de chaîne de caractères. Si vous tentez d'utiliserp
pour modifier le contenu du tableau, le comportement n'est pas défini.
Implémentation de GCC 4.8 x86-64 ELF
Programme:
#include <stdio.h>
int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}
Compiler et décompiler:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
La sortie contient:
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
Conclusion: GCC stocke char*
le dans la section .rodata
, pas dans .text
.
Si nous faisons la même chose pour char[]
:
char s[] = "abc";
on obtient:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
il est donc stocké dans la pile (par rapport à %rbp
).
Notez cependant que le script de l'éditeur de liens par défaut place .rodata
et .text
dans le même segment, qui dispose de l'autorisation d'exécution, mais non de celle-ci. Ceci peut être observé avec:
readelf -l a.out
qui contient:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
char s[] = "hello";
déclare que s
est un tableau de char
suffisamment long pour contenir l'initialiseur (5 + 1 char
s) et initialise le tableau en copiant les membres du littéral de chaîne donné dans le tableau .
char *s = "hello";
déclare que s
est un pointeur sur un ou plusieurs (dans ce cas, plusieurs) char
s et le pointe directement sur un emplacement fixe (en lecture seule) contenant le littéral "hello"
.
char s[] = "Hello world";
Ici, s
est un tableau de caractères qui peut être écrasé si on le souhaite.
char *s = "hello";
Un littéral de chaîne est utilisé pour créer ces blocs de caractères quelque part dans la mémoire sur laquelle pointe ce pointeur s
. Nous pouvons ici réaffecter l'objet sur lequel il pointe en le modifiant, mais tant qu'il pointe vers un littéral de chaîne, le bloc de caractères sur lequel il pointe ne peut pas être modifié.
De plus, considérez que, comme en lecture seule, l’utilisation des deux est identique, vous pouvez accéder à un caractère en indexant avec le format []
ou *(<var> + <index>)
:
printf("%c", x[1]); //Prints r
Et:
printf("%c", *(x + 1)); //Prints r
Évidemment, si vous essayez de faire
*(x + 1) = 'a';
Vous obtiendrez probablement une erreur de segmentation, car vous essayez d'accéder à la mémoire en lecture seule.
Juste pour ajouter: vous obtenez également des valeurs différentes pour leurs tailles.
printf("sizeof s[] = %zu\n", sizeof(s)); //6
printf("sizeof *s = %zu\n", sizeof(s)); //4 or 8
Comme mentionné ci-dessus, pour un tableau, '\0'
sera alloué comme élément final.
char *str = "Hello";
Les commandes ci-dessus définissent str pour pointer sur la valeur littérale "Hello" qui est codée en dur dans l'image binaire du programme, marquée comme étant en lecture seule dans la mémoire, signifie que toute modification de ce littéral String est illégale et que des erreurs de segmentation sont générées.
char str[] = "Hello";
copie la chaîne dans la mémoire allouée récemment sur la pile. Ainsi, toute modification est autorisée et légale.
means str[0] = 'M';
changera la str en "Mello".
Pour plus de détails, merci de répondre à la même question:
char *s1 = "Hello world"; // Points to fixed character string which is not allowed to modify
char s2[] = "Hello world"; // As good as fixed array of characters in string so allowed to modify
// s1[0] = 'J'; // Illegal
s2[0] = 'J'; // Legal
Dans le cas de:
char *x = "fred";
x est un lvalue - il peut être assigné à. Mais dans le cas de:
char x[] = "fred";
x n'est pas une valeur, c'est une valeur - vous ne pouvez pas l'affecter.
À la lumière des commentaires, il devrait être évident que: char * s = "hello"; Est une mauvaise idée, et devrait être utilisé dans une portée très étroite.
Cela pourrait être une bonne occasion de souligner que la "const rectness" est une "bonne chose". Chaque fois que vous le pouvez, utilisez le mot-clé "const" pour protéger votre code, contre les appelants ou les programmeurs "détendus", qui sont généralement les plus "détendus" lorsque des pointeurs entrent en jeu.
Assez mélodrame, voici ce que l’on peut réaliser en ornant des pointeurs avec "const". (Remarque: il faut lire les déclarations de pointeur de droite à gauche.) Voici les 3 façons différentes de se protéger lorsque vous jouez avec des pointeurs:
const DBJ* p means "p points to a DBJ that is const"
- En d’autres termes, l’objet DBJ ne peut pas être modifié via p.
DBJ* const p means "p is a const pointer to a DBJ"
En d'autres termes, vous pouvez modifier l'objet DBJ via p, mais vous ne pouvez pas modifier le pointeur p lui-même.
const DBJ* const p means "p is a const pointer to a const DBJ"
- c’est-à-dire que vous ne pouvez pas modifier le pointeur p lui-même, ni modifier l’objet DBJ via p.
Les erreurs liées aux tentatives de mutations const-ant sont interceptées au moment de la compilation. Il n'y a pas de pénalité d'espace ou de vitesse d'exécution pour const.
(On suppose que vous utilisez le compilateur C++, bien sûr?)
--DBJ