Por que não consigo acessar um ponteiro para ponteiro para uma matriz de pilhas?

35

Por favor, dê uma olhada no código a seguir. Ele tenta passar uma matriz como uma char**para uma função:

#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;
}

O fato de que eu só posso obtê-lo para compilar lançando explicitamente &test2para char**já sugere que este código está errado.

Ainda assim, estou me perguntando o que exatamente está errado sobre isso. Posso passar um ponteiro para um ponteiro para uma matriz alocada dinamicamente, mas não posso passar um ponteiro para um ponteiro para uma matriz na pilha. Obviamente, posso solucionar o problema facilmente, atribuindo primeiro a matriz a uma variável temporária, da seguinte maneira:

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

Ainda assim, alguém pode me explicar por que ele não funciona para lançar char[256]a char**diretamente?

Andreas
fonte

Respostas:

29

Porque testnão é um ponteiro.

&testfornece um ponteiro para a matriz, do tipo char (*)[256]que não é compatível char**(porque uma matriz não é um ponteiro). Isso resulta em comportamento indefinido.

emlai
fonte
3
Mas por que o compilador C então permite passar algo do tipo char (*)[256]para char**?
ComFreek
@ComFreek Eu suspeito que, com avisos máximos e -Werror, isso não permite.
PiRocks
@ComFreek: Realmente não permite. Eu tenho que forçar o compilador a aceitá-lo, lançando-o explicitamente char**. Sem esse elenco, ele não é compilado.
Andreas
38

test é uma matriz, não um ponteiro e &test é um ponteiro para a matriz. Não é um ponteiro para um ponteiro.

Você pode ter sido informado de que uma matriz é um ponteiro, mas isso está incorreto. O nome de uma matriz é o nome de todo o objeto - todos os elementos. Não é um ponteiro para o primeiro elemento. Na maioria das expressões, uma matriz é automaticamente convertida em um ponteiro para seu primeiro elemento. Essa é uma conveniência que geralmente é útil. Mas há três exceções a esta regra:

  • A matriz é o operando de sizeof .
  • A matriz é o operando de & .
  • A matriz é uma string literal usada para inicializar uma matriz.

Em &test, a matriz é o operando de &, portanto, a conversão automática não ocorre. O resultado de &testé um ponteiro para uma matriz de 256 char, que tem o tipo char (*)[256], nãochar ** .

Para obter um ponteiro para um ponteiro charde test, você primeiro precisa fazer um ponteiro para char. Por exemplo:

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

Outra maneira de pensar sobre isso é perceber que os testnomes de todo o objeto - toda a matriz de 256 char. Ele não nomeia um ponteiro; portanto, em &test, não há ponteiro cujo endereço possa ser obtido, portanto, isso não pode produzir a char **. Para criar um char **, você deve primeiro ter um char *.

Eric Postpischil
fonte
11
Esta lista de três exceções é exaustiva?
Ruslan
8
@Ruslan: Sim, de acordo com o C 2018 6.3.2.1 3.
Eric Postpischil 01/02
Ah, e em C11 também havia o _Alignofoperador mencionado além de sizeofe &. Gostaria de saber por que eles removeram ...
Ruslan
@Ruslan: Isso foi removido porque foi um erro. _Alignofaceita apenas um nome de tipo como um operando e nunca aceitou uma matriz ou qualquer outro objeto como um operando. (Eu não sei o porquê; parece sintática e gramaticalmente que poderia ser sizeof, mas não é.)
Eric Postpischil
6

O tipo de test2é char *. Portanto, o tipo de &test2será char **compatível com o tipo de parâmetro xde printchar().
O tipo de testé char [256]. Então, o tipo de &testserá char (*)[256]qual é não compatível com o tipo de parâmetro xdeprintchar() .

Deixe-me mostrar a diferença em termos de endereços testetest2 .

#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;
}

Resultado:

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

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

Aponte para observação aqui:

A saída (endereço de memória) de test2e &test2[0]é numericamente igual e seu tipo também é o mesmo que é char *.
Mas os endereços test2e &test2são diferentes e seu tipo também é diferente.
O tipo de test2é char *.
O tipo de &test2é char **.

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

A saída (endereço de memória) de test, &teste &test[0]é numericamente mesmo , mas seu tipo é diferente .
O tipo de testé char [256].
O tipo de &testé char (*) [256].
O tipo de &test[0]é char *.

Como mostra a saída &testé o mesmo que &test[0].

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

Portanto, você está recebendo uma falha de segmentação.

HS
fonte
3

Você não pode acessar um ponteiro para um ponteiro porque &testnão é um ponteiro - é uma matriz.

Se você pegar o endereço de uma matriz, converter a matriz e o endereço da matriz em (void *)e compará-los, eles (exceto o pedantry de ponteiro possível) serão equivalentes.

O que você está realmente fazendo é semelhante a este (novamente, exceto com estrias alias):

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

o que é obviamente errado.

SS Anne
fonte
3

Seu código espera o argumento xdeprintchar que ponto a memória que contém uma(char *) .

Na primeira chamada, aponta para o armazenamento usado test2e, portanto, é realmente um valor que aponta para um(char *) , o último apontando para a memória alocada.

Na segunda chamada, no entanto, não existe um local onde esse (char *)valor possa ser armazenado e, portanto, é impossível apontar para essa memória. A transmissão (char **)adicionada adicionou um erro de compilação (sobre a conversão (char *)para (char **)), mas não faria com que o armazenamento aparecesse do nada para conter um(char *) inicializada para apontar para os primeiros caracteres do teste. A conversão de ponteiro em C não altera o valor real do ponteiro.

Para conseguir o que deseja, você deve fazer isso explicitamente:

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

Suponho que seu exemplo seja uma destilação de um pedaço de código muito maior; como exemplo, talvez você queira printcharincrementar o (char *)valor que o xvalor passado aponta para que, na próxima chamada, o próximo caractere seja impresso. Se não for esse o caso, por que você não passa um (char *)apontador para o personagem a ser impresso, ou apenas passa o próprio caractere?

Kevin Martin
fonte
Boa resposta; Concordo que a maneira mais fácil de manter isso claro é pensar se existe ou não um objeto C que contém o endereço da matriz, ou seja, um objeto ponteiro do qual você pode usar o endereço para obter um char **. Variáveis ​​/ objetos de matriz são simplesmente a matriz, com o endereço sendo implícito, não armazenado em nenhum lugar. Nenhum nível extra de indireção para acessá-los, ao contrário de uma variável de ponteiro que aponta para outro armazenamento.
Peter Cordes
0

Aparentemente, pegar o endereço de testé o mesmo que pegar o endereço 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;
}

Compile isso e execute:

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

Portanto, a causa final da falha de segmentação é que este programa tentará desreferenciar o endereço absoluto 0x42(também conhecido como'B' ), que seu programa não tem permissão para ler.

Embora com um compilador / computador diferente, os endereços sejam diferentes: Experimente online! , mas você ainda conseguirá isso, por algum motivo:

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

Mas o endereço que causa a falha de segmentação pode muito bem ser diferente:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
fonte
11
Tomar o endereço de testnão é o mesmo que tomar o endereço de test[0]. O primeiro tem tipo char (*)[256]e o último tem tipo char *. Eles não são compatíveis, e o padrão C permite que eles tenham representações diferentes.
Eric Postpischil 01/02
Ao formatar um ponteiro com %p, ele deve ser convertido em void *(novamente por razões de compatibilidade e representação).
Eric Postpischil 01/02
11
printchar(&test);pode travar para você, mas o comportamento não é definido pelo padrão C e as pessoas podem observar outros comportamentos em outras circunstâncias.
Eric Postpischil 01/02
Re “Portanto, a causa final da falha de segmentação é que este programa tentará desreferenciar o endereço absoluto 0x42 (também conhecido como 'B'), que provavelmente é ocupado pelo sistema operacional.”: Se houver uma falha no segmento tentando ler um local, significa que nada é mapeado lá, não que seja ocupado pelo sistema operacional. (Exceto que poderia haver algo mapeado lá como, digamos, somente execução sem permissões de leitura, mas isso é improvável.)
Eric Postpischil 01/02
11
&test == &test[0]viola as restrições em C 2018 6.5.9 2 porque os tipos não são compatíveis. O padrão C requer uma implementação para diagnosticar essa violação, e o comportamento resultante não é definido pelo padrão C. Isso significa que seu compilador pode produzir código avaliando-os como iguais, mas outro compilador pode não.
Eric Postpischil 01/02
-4

A representação de char [256]depende da implementação. Não deve ser o mesmo que char *.

Fundição &testde tipo char (*)[256]de char **rendimentos indefinido comportamento.

Em alguns compiladores, pode fazer o que você espera, e em outros não.

EDITAR:

Após testar com o gcc 9.2.1, parece que printchar((char**)&test)passa de fato test como valor convertido para char**. É como se a instrução fosse printchar((char**)test). Na printcharfunção, xé um ponteiro para o primeiro caractere do teste de matriz, não um ponteiro duplo para o primeiro caractere. Uma des-referência duplax resulta em uma falha de segmentação porque os 8 primeiros bytes da matriz não correspondem a um endereço válido.

Recebo exatamente o mesmo comportamento e resultado ao compilar o programa com o clang 9.0.0-2.

Isso pode ser considerado um bug do compilador ou o resultado de um comportamento indefinido cujo resultado pode ser específico do compilador.

Outro comportamento inesperado é que o código

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

A saída é

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

O comportamento estranho é isso xe*x tem o mesmo valor.

Isso é uma coisa do compilador. Duvido que isso seja definido pelo idioma.

chmike
fonte
11
Você quer dizer que a representação de char (*)[256]depende da implementação? A representação de char [256]não é relevante nesta questão - é apenas um monte de bits. Mas, mesmo que você queira dizer que a representação de um ponteiro para uma matriz é diferente da representação de um ponteiro para um ponteiro, isso também perde o sentido. Mesmo se eles tiverem as mesmas representações, o código do OP não funcionaria, porque o ponteiro para um ponteiro pode ser desreferenciado duas vezes, como é feito em printchar, mas o ponteiro para uma matriz não pode, independentemente da representação.
Eric Postpischil 01/02
@EricPostpischil a conversão de char (*)[256]para char **é aceita pelo compilador, mas não produz o resultado esperado porque a char [256]não é o mesmo que a char *. Eu assumi, a codificação é diferente, caso contrário, produziria o resultado esperado.
chmike 01/02
Não sei o que você quer dizer com "resultado esperado". A única especificação no padrão C de qual deve ser o resultado é que, se o alinhamento for inadequado char **, o comportamento será indefinido e que, caso contrário, se o resultado for convertido novamente em char (*)[256], ele será comparado ao ponteiro original. Por "resultado esperado", você pode querer dizer que, se (char **) &testfor posteriormente convertido em a char *, ele será comparado a &test[0]. Isso não é um resultado improvável em implementações que usam um espaço de endereço simples, mas não é puramente uma questão de representação.
Eric Postpischil 01/02
2
Além disso, "A conversão e teste do tipo char (*) [256] para char ** produz um comportamento indefinido." não está correto. C 2018 6.3.2.3 7 permite que um ponteiro para um tipo de objeto seja convertido em qualquer outro ponteiro em um tipo de objeto. Se o ponteiro não estiver alinhado corretamente para o tipo referenciado (o tipo referenciado char **é char *), o comportamento será indefinido. Caso contrário, a conversão será definida, embora o valor seja apenas parcialmente definido, conforme meu comentário acima.
Eric Postpischil 01/02
char (*x)[256]não é a mesma coisa que char **x. O motivo xe *ximprimir o mesmo valor do ponteiro é que ele xé simplesmente um ponteiro para a matriz. Você *x é a matriz , e usá-la em um contexto de ponteiro decai de volta para o endereço da matriz . Nenhum bug do compilador lá (ou no que (char **)&testfaz), apenas um pouco de ginástica mental necessária para descobrir o que está acontecendo com os tipos. (cdecl explica como "declare x como ponteiro para a matriz 256 de char"). Mesmo usando char*para acessar a representação de objeto de um char**não é UB; pode alias qualquer coisa.
Peter Cordes