Uno suele asociar ‘no modificable’ con el término literal
char* str = "Hello World!";
*str="B"; // Bus Error!
Sin embargo, al usar literales compuestos, descubrí rápidamente que son completamente modificables (y al observar el código de máquina generado, se ve que se colocan en la pila):
char* str = (char[]){"Hello World"};
*str="B"; // A-Okay!
estoy compilando con clang-703.0.29
. ¿No deberían esos dos ejemplos generar exactamente el mismo código de máquina? ¿Es un literal compuesto realmente un literal, si es modificable?
EDITAR: un ejemplo aún más corto sería:
"Hello World"[0] = 'B'; // Bus Error!
(char[]){"Hello World"}[0] = 'B'; // Okay!
Un literal compuesto es un lvalue y los valores de sus elementos son modificables. En caso de
char* str = (char[]){"Hello World"};
*str="B"; // A-Okay!
está modificando un literal compuesto que es legal.
C11-§6.5.2.5/4:
Si el nombre del tipo especifica una matriz de tamaño desconocido, el tamaño está determinado por la lista de inicializadores como se especifica en 6.7.9, y el tipo del literal compuesto es el del tipo de matriz completa. De lo contrario (cuando el nombre de tipo especifica un tipo de objeto), el tipo del literal compuesto es el especificado por el nombre de tipo. En cualquier caso, el resultado es un lvalue.
Como puede verse, el tipo de literal compuesto es un tipo de matriz completa y es lvalue, por lo tanto, es modificable a diferencia de literales de cadena
Standard también menciona que
§6.5.2.5/7:
Los literales de cadena y los literales compuestos con tipos calificados const no necesitan designar objetos distintos.101
Además dice:
11 EJEMPLO 4 Un literal compuesto de solo lectura se puede especificar a través de construcciones como:
(const float []){1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6}
12 EJEMPLO 5 Las siguientes tres expresiones tienen diferentes significados:
"/tmp/fileXXXXXX"
(char []){"/tmp/fileXXXXXX"}
(const char []){"/tmp/fileXXXXXX"}
El primero siempre tiene una duración de almacenamiento estática y tiene un tipo de matriz de char
pero no necesita ser modificable; los dos últimos tienen duración de almacenamiento automático cuando ocurren dentro del cuerpo de una función, y el el primero de estos dos es modificable.
13 EJEMPLO 6 Al igual que los literales de cadena, los literales compuestos calificados const se pueden colocar en la memoria de solo lectura e incluso se pueden compartir. Por ejemplo,
(const char []){"abc"} == "abc"
podría producir 1 si se comparte el almacenamiento de los literales.
La sintaxis literal compuesta es una expresión abreviada equivalente a una declaración local con un inicializador seguido de una referencia al objeto sin nombre así declarado:
char *str = (char[]){ "Hello World" };
es equivalente a:
char __unnamed__[] = { "Hello world" };
char *str = __unnamed__;
los __unnamed__
tiene almacenamiento automático y se define como modificable, se puede modificar mediante el puntero str
inicializado para señalarlo.
En el caso de char *str = "Hello World!";
el objeto señalado por str
no se supone que se modifique. De hecho, intentar modificarlo tiene un comportamiento indefinido.
El estándar C podría haber definido estos literales de cadena como si tuvieran tipo const char[]
en lugar de char[]
pero esto generaría muchas advertencias y errores en el código heredado.
Sin embargo, es aconsejable pasar una bandera al compilador para hacer implícitamente tales cadenas literales. const
y hacer todo el proyecto const
correcto, es decir: definir todos los argumentos de puntero que no se utilizan para modificar su objeto como const
. Para gcc
y clang
la opción de línea de comando es -Wwrite-strings
. También recomiendo encarecidamente habilitar muchas más advertencias y hacerlas fatales con -Wall -W -Werror
.
Ni siquiera estoy seguro de que sea UB, nunca he mirado el oficial estándar de idiomapero dice en 6.5.2.5 en el punto 12, dice que
(char[]){"abc"}
está diseñado para ser modificable.– hgiesel
17 de abril de 2016 a las 12:09
Tenga en cuenta que el ejemplo anterior en realidad no muestra el comportamiento de lvalue-literal (un ejemplo más claro sería algo así como
(int){1} = 2;
) – la principal diferencia entre sus dos fragmentos es que en el primero tiene un literal de cadena verdadero, mientras que en el segundo inicializar una matriz local con un literal de cadena: tendría el mismo comportamiento si solo modificara el primero para leerchar str[] = ...
.– Leushenko
17 de abril de 2016 a las 12:16
@Leushenko ¿Yo? En el caso literal compuesto, inicializo
str
con un puntero a su primer carácter. Cuando yo digochar str[] = …
inicializo un literal de caracteres no modificable y copio su contenido en la matrizstr
en la pila– hgiesel
17 de abril de 2016 a las 12:21
@hgiesel: Tienes razón, de hecho es un “objeto anónimo”. Puedes usar el
const
calificador para decirle explícitamente al compilador su intención. Tenga en cuenta que en C es responsabilidad del programador no romper este contrato. Incluso para literales de cadenano hay garantía de que se escriba no work (y C lo permite explícitamente como una extensión de implementación). Por lo tanto, no obtener un error no significa que sea un comportamiento definido. En caso de duda, lea la norma.– demasiado honesto para este sitio
17 de abril de 2016 a las 12:21
@hgiesel: No. 1) El lenguaje C ni siquiera exige el uso de una pila (ni un montón, por cierto) y hay implementaciones que no lo hacen. 2) Hay un objeto asignado en alguna parte, más el literal de cadena para inicializarlo. Pero, ¿debe haber el mismo objeto asignado dos veces? Su ejemplo que usa una cadena no es bueno, ya que puede usar el literal directamente, pero sin embargo es válido.
– demasiado honesto para este sitio
17 de abril de 2016 a las 12:27