¿Por qué no puedo acceder a un puntero a puntero para una matriz de pila?

35

Por favor, eche un vistazo al siguiente código. Intenta pasar una matriz como a char**a una función:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

El hecho de que sólo puedo que se compile por colada de manera explícita &test2a char**que ya da a entender que este código es incorrecto.

Aún así, me pregunto qué tiene de malo exactamente. Puedo pasar un puntero a un puntero a una matriz asignada dinámicamente, pero no puedo pasar un puntero a un puntero para una matriz en la pila. Por supuesto, puedo solucionar fácilmente el problema asignando primero la matriz a una variable temporal, así:

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

Aún así, puede que alguien me explique por qué no funciona para echar char[256]a char**directamente?

Andreas
fuente

Respuestas:

29

Porque testno es un puntero.

&testle proporciona un puntero a la matriz, de tipo char (*)[256], que no es compatible con char**(porque una matriz no es un puntero). Esto da como resultado un comportamiento indefinido.

emlai
fuente
3
Pero, ¿por qué el compilador de C permite pasar algo de tipo char (*)[256]a char**?
ComFreek
@ComFreek Sospecho que con advertencias máximas y -Werror, no permite eso.
PiRocks
@ComFreek: Realmente no lo permite. Tengo que forzar al compilador a aceptarlo explícitamente char**. Sin ese elenco, no se compila.
Andreas
38

testes una matriz, no un puntero, y &testes un puntero a la matriz. No es un puntero a un puntero.

Es posible que le hayan dicho que una matriz es un puntero, pero esto es incorrecto. El nombre de una matriz es un nombre de todo el objeto, todos los elementos. No es un puntero al primer elemento. En la mayoría de las expresiones, una matriz se convierte automáticamente en un puntero a su primer elemento. Esa es una conveniencia que a menudo es útil. Pero hay tres excepciones a esta regla:

  • La matriz es el operando de sizeof.
  • La matriz es el operando de &.
  • La matriz es un literal de cadena utilizado para inicializar una matriz.

En &test, la matriz es el operando de &, por lo que no se produce la conversión automática. El resultado de &testes un puntero a una matriz de 256 char, que tiene tipo char (*)[256], no char **.

Para obtener un puntero a un puntero chardesde test, primero debe hacer un puntero a char. Por ejemplo:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

Otra forma de pensar en esto es darse cuenta de que testnombra todo el objeto, la matriz completa de 256 char. No nombra un puntero, por lo tanto, &testno hay ningún puntero cuya dirección se pueda tomar, por lo que esto no puede producir un char **. Para crear un char **, primero debe tener un char *.

Eric Postpischil
fuente
1
¿Es exhaustiva esta lista de tres excepciones?
Ruslan
8
@Ruslan: Sí, según C 2018 6.3.2.1 3.
Eric Postpischil
Ah, y en C11 también se _Alignofmencionó el operador además de sizeofy &. Me pregunto por qué lo quitaron ...
Ruslan
@Ruslan: Eso fue eliminado porque fue un error. _Alignofsolo acepta un nombre de tipo como operando y nunca aceptó una matriz o cualquier otro objeto como operando. (No sé por qué; parece sintáctica y gramaticalmente que podría ser sizeof, pero no lo es.)
Eric Postpischil
6

El tipo de test2es char *. Entonces, el tipo de &test2será char **compatible con el tipo de parámetro xde printchar().
El tipo de testes char [256]. Entonces, el tipo de &testserá el char (*)[256]que no sea compatible con el tipo de parámetro xde printchar().

Déjame mostrarte la diferencia en términos de direcciones de testy test2.

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Salida:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

Punto a tener en cuenta aquí:

La salida (dirección de memoria) de test2y &test2[0]es numéricamente igual y su tipo también es el mismo que es char *.
Pero las direcciones test2y &test2son diferentes y su tipo también es diferente.
El tipo de test2es char *.
El tipo de &test2es char **.

x = &test2
*x = test2
(*x)[0] = test2[0] 

La salida (dirección de memoria) de test, &testy &test[0]es numéricamente igual pero su tipo es diferente .
El tipo de testes char [256].
El tipo de &testes char (*) [256].
El tipo de &test[0]es char *.

Como muestra la salida &testes igual a &test[0].

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

Por lo tanto, obtiene un fallo de segmentación.

HS
fuente
3

No puede acceder a un puntero a un puntero porque &testno es un puntero, es una matriz.

Si toma la dirección de una matriz, eche la matriz y la dirección de la matriz (void *), y compárelos, serán equivalentes (salvo posible pedantería de puntero).

Lo que realmente está haciendo es similar a esto (de nuevo, salvo alias estricto):

putchar(**(char **)test);

lo cual obviamente está mal.

SS Anne
fuente
3

Su código espera que el argumento xde printcharque el punto de memoria que contiene una (char *).

En la primera llamada, apunta al almacenamiento utilizado test2y, por lo tanto, es un valor que apunta a a (char *), este último apunta a la memoria asignada.

En la segunda llamada, sin embargo, no hay lugar donde se (char *)pueda almacenar dicho valor y, por lo tanto, es imposible señalar dicha memoria. El reparto (char **)que agregó habría eliminado un error de compilación (sobre la conversión (char *)a (char **)), pero no haría que el almacenamiento apareciera de la nada para contener un (char *)inicializado para señalar los primeros caracteres de la prueba. La conversión del puntero en C no cambia el valor real del puntero.

Para obtener lo que desea, debe hacerlo explícitamente:

char *tempptr = &temp;
printchar(&tempptr);

Supongo que su ejemplo es una destilación de un fragmento de código mucho más grande; Como ejemplo, tal vez desee printcharincrementar el (char *)valor al que xapunta el valor pasado para que en la próxima llamada se imprima el siguiente carácter. Si ese no es el caso, ¿por qué no pasa simplemente una (char *)señal al personaje que se va a imprimir, o incluso simplemente pasa el personaje en sí?

Kevin Martin
fuente
Buena respuesta; Estoy de acuerdo en que la forma más fácil de mantener esto en claro es pensar si hay un objeto C que contenga la dirección de la matriz, es decir, un objeto puntero del que puede tomar la dirección para obtener un char **. Las variables / objetos de matriz simplemente son la matriz, con la dirección implícita, no almacenada en ningún lugar. No hay un nivel adicional de indirección para acceder a ellos, a diferencia de una variable de puntero que apunta a otro almacenamiento.
Peter Cordes
0

Aparentemente, tomar la dirección de testes lo mismo que tomar la dirección de test[0]:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Compila eso y ejecuta:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

Entonces, la causa principal de la falla de segmentación es que este programa intentará desreferenciar la dirección absoluta 0x42(también conocida como 'B'), que su programa no tiene permiso para leer.

Aunque con un compilador / máquina diferente, las direcciones serán diferentes: ¡ Pruébelo en línea! , pero aún así obtendrá esto, por alguna razón:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

Pero la dirección que causa la falla de segmentación puede ser muy diferente:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
fuente
1
Tomar la dirección de testno es lo mismo que tomar la dirección de test[0]. El primero tiene tipo char (*)[256], y el segundo tiene tipo char *. No son compatibles, y el estándar C les permite tener representaciones diferentes.
Eric Postpischil
Al formatear un puntero con %p, debe convertirse a void *(nuevamente por razones de compatibilidad y representación).
Eric Postpischil
1
printchar(&test);puede fallar para usted, pero el comportamiento no está definido por el estándar C y las personas pueden observar otros comportamientos en otras circunstancias.
Eric Postpischil
Re "Entonces, la causa principal de la falla de segmentación es que este programa intentará desreferenciar la dirección absoluta 0x42 (también conocida como 'B'), que probablemente esté ocupada por el sistema operativo".: Si hay una falla de segmento que intenta leer una ubicación, significa que no hay nada asignado allí, no es que esté ocupado por el sistema operativo. (Excepto que podría haber algo mapeado allí como, por ejemplo, ejecutar solo sin permisos de lectura, pero eso es poco probable)
Eric Postpischil
1
&test == &test[0]viola las restricciones en C 2018 6.5.9 2 porque los tipos no son compatibles. El estándar C requiere una implementación para diagnosticar esta violación, y el comportamiento resultante no está definido por el estándar C. Eso significa que su compilador podría producir código evaluándolos para que sean iguales, pero otro compilador podría no serlo.
Eric Postpischil
-4

La representación de char [256]depende de la implementación. No debe ser lo mismo que char *.

Casting &testde tipo char (*)[256]para char **producir un comportamiento indefinido.

Con algunos compiladores, puede hacer lo que espera, y en otros no.

EDITAR:

Después de probar con gcc 9.2.1, parece que printchar((char**)&test) de hecho pasa test como valor emitido a char**. Es como si la instrucción fuera printchar((char**)test). En la printcharfunción, xes un puntero al primer carácter de la prueba de matriz, no un puntero doble al primer carácter. Un resultado de doble xdesreferencia en una falla de segmentación porque los 8 primeros bytes de la matriz no corresponden a una dirección válida.

Obtengo exactamente el mismo comportamiento y resultado al compilar el programa con clang 9.0.0-2.

Esto puede considerarse como un error del compilador o el resultado de un comportamiento indefinido cuyo resultado podría ser específico del compilador.

Otro comportamiento inesperado es que el código

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

La salida es

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

El comportamiento extraño es que x y *xtiene el mismo valor.

Esto es una cosa del compilador. Dudo que esto esté definido por el lenguaje.

chmike
fuente
1
¿Quiere decir que la representación de char (*)[256]depende de la implementación? La representación de char [256]no es relevante en esta pregunta, es solo un montón de bits. Pero, incluso si quiere decir que la representación de un puntero a una matriz es diferente de la representación de un puntero a un puntero, eso también pierde el punto. Incluso si tienen las mismas representaciones, el código del OP no funcionaría, porque el puntero a un puntero se puede desreferenciar dos veces, como se hace en printchar, pero el puntero a una matriz no puede, independientemente de la representación.
Eric Postpischil
@EricPostpischil el compilador acepta la conversión de char (*)[256]a char **, pero no produce el resultado esperado porque a char [256]no es lo mismo que a char *. Asumí, la codificación es diferente, de lo contrario produciría el resultado esperado.
Chmike
No sé qué quieres decir con "resultado esperado". La única especificación en el estándar C de cuál debería ser el resultado es que, si la alineación es inadecuada char **, el comportamiento no está definido, y que, de lo contrario, si el resultado se convierte de nuevo char (*)[256], se compara igual al puntero original. Por "resultado esperado", puede querer decir que, si (char **) &testse convierte más a a char *, se compara igual a &test[0]. Ese no es un resultado improbable en implementaciones que usan un espacio de direcciones plano, pero no es puramente una cuestión de representación.
Eric Postpischil
2
Además, "Casting y prueba de tipo char (*) [256] a char ** produce un comportamiento indefinido". no es correcto. C 2018 6.3.2.3 7 permite que un puntero a un tipo de objeto se convierta a cualquier otro puntero a un tipo de objeto. Si el puntero no está alineado correctamente para el tipo referenciado (el tipo referenciado char **es char *), entonces el comportamiento es indefinido. De lo contrario, la conversión está definida, aunque el valor solo está parcialmente definido, según mi comentario anterior.
Eric Postpischil
char (*x)[256]No es lo mismo que char **x. La razón xe *ximprimir el mismo valor de puntero es que xes simplemente un puntero a la matriz. Tu *x es la matriz , y usarla en un contexto de puntero se desintegra a la dirección de la matriz . No hay errores de compilación allí (o en qué lo (char **)&testhace), solo se requiere un poco de gimnasia mental para darse cuenta de que está sucediendo con los tipos. (cdecl lo explica como "declarar x como puntero a la matriz 256 de char"). Incluso usar char*para acceder a la representación de objetos de a char**no es UB; puede alias cualquier cosa.
Peter Cordes