Haciendo referencia a un personaje * que quedó fuera de scope

Recientemente comencé a progtwigr en C nuevamente después de haber progtwigdo en C ++ por un tiempo, y mi comprensión de los punteros está un poco oxidada.

Me gustaría preguntar por qué este código no está causando ningún error:

char* a = NULL; { char* b = "stackoverflow"; a = b; } puts(a); 

Pensé que debido a que b estaba fuera del scope, a debería hacer referencia a una ubicación de memoria no existente y, por lo tanto, sería un error de tiempo de ejecución al llamar a printf .

Ejecuté este código en MSVC aproximadamente 20 veces, y no se mostraron errores.

Dentro del ámbito donde se define b , se le asigna la dirección de un literal de cadena. Estos literales generalmente viven en una sección de memoria de solo lectura en lugar de la stack.

Cuando haces a=b , asignas el valor de b a a , es decir, ahora contiene la dirección de un literal de cadena. Esta dirección sigue siendo válida después de que b esté fuera de scope.

Si hubiera tomado la dirección de b y luego trató de anular la referencia a esa dirección, entonces invocaría un comportamiento indefinido .

Entonces, su código es válido y no invoca un comportamiento indefinido, pero lo siguiente hace:

 int *a = NULL; { int b = 6; a = &b; } printf("b=%d\n", *a); 

Otro ejemplo más sutil:

 char *a = NULL; { char b[] = "stackoverflow"; a = b; } printf(a); 

La diferencia entre este ejemplo y el tuyo es que b , que es una matriz, decae en un puntero al primer elemento cuando se asigna a. Entonces, en este caso, a contiene la dirección de una variable local que luego queda fuera del scope.

EDITAR:

Como nota al margen, es una mala práctica pasar una variable como el primer argumento de printf , ya que puede provocar una vulnerabilidad de cadena de formato . Es mejor usar una constante de cadena de la siguiente manera:

 printf("%s", a); 

O más simplemente:

 puts(a); 

Línea por línea, esto es lo que hace tu código:

 char* a = NULL; 

a es un puntero que no hace referencia a nada (establecido en NULL ).

 { char* b = "stackoverflow"; 

b es un puntero que hace referencia al literal de cadena constante, literal "stackoverflow" .

  a = b; 

a se establece para hacer referencia también al literal de cadena constante, literal "stackoverflow" .

 } 

b está fuera de scope. Pero como a no hace referencia a b , entonces eso no importa (solo hace referencia al mismo literal de cadena constante y estática al que hacía referencia b ).

 printf(a); 

Imprime el "stackoverflow" literal de cadena constante, referenciado por a .

Los literales de cadena están asignados estáticamente, por lo que el puntero es válido indefinidamente. Si hubiera dicho char b[] = "stackoverflow" , entonces estaría asignando una matriz char en la stack que se volvería inválida cuando el scope terminara. Esta diferencia también aparece para modificar cadenas: char s[] = "foo" stack asigna una cadena que puede modificar, mientras que char *s = "foo" solo le da un puntero a una cadena que se puede colocar en solo lectura Memoria, por lo que modificarla es un comportamiento indefinido.

Otras personas han explicado que este código es perfectamente válido. Esta respuesta es acerca de su expectativa de que, si el código hubiera sido inválido , habría habido un error de tiempo de ejecución al llamar a printf . No es necesariamente así.

Veamos esta variación en su código, que no es válido:

 #include  int main(void) { int *a; { int b = 42; a = &b; } printf("%d\n", *a); // undefined behavior return 0; } 

Este progtwig tiene un comportamiento indefinido, pero resulta bastante probable que, de hecho, imprimirá 42, por varias razones diferentes: muchos comstackdores dejarán la ranura de la stack para b asignada para todo el cuerpo del main , porque nada más necesita el espacio y minimizar el número de ajustes de stack simplifica la generación de código; incluso si el comstackdor desasignó formalmente la ranura de la stack, el número 42 probablemente permanece en la memoria hasta que otra cosa lo sobrescriba, y no hay nada entre a = &b y *a para hacer eso; las optimizaciones estándar (“propagación constante y de copia”) podrían eliminar ambas variables y escribir el último valor conocido para *a directamente en la statement printf (como si hubiera escrito printf("%d\n", 42) ).

Es absolutamente vital entender que “comportamiento indefinido” no significa “el progtwig se bloqueará de manera predecible”. Significa que “cualquier cosa puede pasar”, y cualquier cosa incluye parecer que funciona como lo pretendía el progtwigdor (en esta computadora, con este comstackdor, hoy).


Como nota final, ninguna de las herramientas de depuración agresivas a las que tengo acceso conveniente (Valgrind, ASan, UBSan) hace un seguimiento de la vida de las variables “auto” con suficiente detalle para detectar este error, pero GCC 6 produce esta advertencia divertida:

 $ gcc -std=c11 -O2 -W -Wall -pedantic test.c test.c: In function 'main': test.c:9:5: warning: 'b' is used uninitialized in this function printf("%d\n", *a); // undefined behavior ^~~~~~~~~~~~~~~~~~ 

Creo que lo que sucedió aquí fue la optimización que describí anteriormente, copiando el último valor conocido de b en *a y luego en el printf , pero su “último valor conocido” para b fue un “esta variable no está inicializada” centinela en lugar de 42. (Entonces genera un código equivalente a printf("%d\n", 0) .)

El código no genera ningún error porque simplemente está asignando el puntero de carácter b a otro puntero de carácter a y eso está perfectamente bien.

En C, puede asignar una referencia de puntero a otro puntero. aquí, en realidad, la cadena “stackoverflow” se usa como un literal y la ubicación de la dirección base de esa cadena se asignará a a variable.

Aunque está fuera del scope de la variable b la asignación se realizó con el puntero a. Así se imprimirá el resultado sin ningún error.

Los literales de cadena siempre se asignan de forma estática y el progtwig puede acceder en cualquier momento,

 char* a = NULL; { char* b = "stackoverflow"; a = b; } printf(a); 

Creo que, como prueba de respuestas anteriores, es bueno echar un vistazo a lo que realmente se encuentra dentro de su código. La gente ya mencionó que los literales de cuerdas están dentro de la sección .text. Entonces, (los literales) están simplemente, siempre, allí. Usted puede encontrar fácilmente esto para el código

 #include  int main() { char* a = 0; { char* b = "stackoverflow"; a = c; } printf("%s\n", a); } 

usando el siguiente comando

 > cc -S main.c 

Dentro de main.s encontrará, en la parte inferior

 ... ... ... .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "stackoverflow" L_.str.1: ## @.str.1 .asciz "%s\n" 

Puede leer más sobre las secciones de ensambladores (por ejemplo) aquí: https://docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html

Y aquí puede encontrar una cobertura muy bien preparada de los ejecutables de Mach-O: https://www.objc.io/issues/6-build-tools/mach-o-executables/