Почему я не могу получить доступ к указателю на указатель для массива стека?

35

Пожалуйста, взгляните на следующий код. Он пытается передать массив как char**функцию:

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

Тот факт, что я могу получить его только для компиляции, явно приведя &test2к char**уже намекам, что этот код неверен.

Тем не менее, мне интересно, что именно не так об этом. Я могу передать указатель на указатель на динамически размещенный массив, но не могу передать указатель на указатель для массива в стеке. Конечно, я легко могу обойти проблему, сначала назначив массив временной переменной, например так:

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

Тем не менее, кто - то может объяснить мне , почему он не работает в гипсе , char[256]чтобы char**напрямую?

Andreas
источник

Ответы:

29

Потому что testэто не указатель.

&testполучает указатель на массив типа char (*)[256], который не совместим с char**(потому что массив не является указателем). Это приводит к неопределенному поведению.

emlai
источник
3
Но почему компилятор Си затем разрешить прохождение что - то типа char (*)[256]в char**?
ComFreek
@ComFreek Я подозреваю, что с максимальным предупреждением и -Werror, это не позволяет.
PiRocks
@ComFreek: Это действительно не позволяет. Я должен заставить компилятор принять его, явно приведя его к char**. Без этого броска это не компилируется.
Андреас
38

testявляется массивом, а не указателем, и &testявляется указателем на массив. Это не указатель на указатель.

Возможно, вам сказали, что массив является указателем, но это неверно. Имя массива - это имя всего объекта - всех элементов. Это не указатель на первый элемент. В большинстве выражений массив автоматически преобразуется в указатель на его первый элемент. Это удобство, которое часто полезно. Но есть три исключения из этого правила:

  • Массив является операндом sizeof.
  • Массив является операндом &.
  • Массив - это строковый литерал, используемый для инициализации массива.

В &test, массив является операндом &, поэтому автоматического преобразования не происходит. Результатом &testявляется указатель на массив из 256 char, который имеет тип char (*)[256], а не char **.

Чтобы получить указатель на указатель на charfrom test, сначала нужно создать указатель на char. Например:

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

Еще один способ подумать об этом - понять, что testимена всего объекта - целого массива из 256 char. Он не называет указатель, поэтому в &testнем нет указателя, адрес которого можно взять, поэтому он не может создать char **. Для того чтобы создать char **, вы должны сначала иметь char *.

Эрик Постпищил
источник
1
Является ли этот список из трех исключений исчерпывающим?
Руслан
8
@Ruslan: Да, в соответствии с C 2018 6.3.2.1 3.
Эрик Постпишил
Да, и в C11 был также _Alignofупомянут оператор в дополнение к sizeofи &. Интересно, почему его убрали ...
Руслан
@Ruslan: Это было удалено, потому что это было ошибкой. _Alignofтолько принимает имя типа в качестве операнда и никогда не принимает массив или любой другой объект в качестве операнда. (Я не знаю почему; кажется, синтаксически и грамматически это может быть похоже sizeof, но это не так.)
Эрик Постпишил
6

Тип test2есть char *. Таким образом, тип &test2будет , char **который совместим с типом параметра xв printchar().
Тип testесть char [256]. Таким образом, тип &testбудет , char (*)[256]который не совместим с типом параметра xв printchar().

Позвольте мне показать вам разницу с точки зрения адресов testи 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;
}

Вывод:

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

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

Укажите, чтобы отметить здесь:

Выход (адрес памяти) из test2и &test2[0]является численно одинаковы и их тип и тот же , который char *.
Но есть test2и &test2разные адреса и их тип тоже разный.
Тип test2есть char *.
Тип &test2есть char **.

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

Выход (адрес памяти) из test, &testи &test[0]является числовым же , но их тип отличается .
Тип testесть char [256].
Тип &testесть char (*) [256].
Тип &test[0]есть char *.

Как показывает вывод, так &testже, как &test[0].

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

Следовательно, вы получаете ошибку сегментации.

HS
источник
3

Вы не можете получить доступ к указателю на указатель, потому что &testэто не указатель - это массив.

Если вы берете адрес массива, приводите массив и адрес массива к ним (void *)и сравниваете их, они (за исключением возможной педантизации указателей) будут эквивалентны.

То, что вы действительно делаете, похоже на это (опять же, за исключением строгого алиасинга):

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

что совершенно очевидно неправильно.

СС Энн
источник
3

Ваш код ожидает аргумент xо printcharк точке в память, содержащий (char *).

В первом вызове он указывает на хранилище, используемое для, test2и, таким образом, действительно является значением, указывающим на a (char *), последний указывает на выделенную память.

Во втором вызове, однако, нет места, где (char *)можно было бы хранить какое-либо такое значение, и поэтому невозможно указать на такую ​​память. Приведение к (char **)вам добавило бы устранение ошибки компиляции (о преобразовании (char *)в (char **)), но это не привело бы к тому, что хранилище казалось бы пустым, чтобы содержать (char *)инициализированный указатель на первые символы теста. Приведение указателя в C не меняет фактическое значение указателя.

Чтобы получить то, что вы хотите, вы должны сделать это явно:

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

Я предполагаю, что ваш пример - это фрагмент кода гораздо большего размера; Например, возможно, вы хотите printcharувеличить (char *)значение, на которое xуказывает переданное значение, чтобы при следующем вызове был напечатан следующий символ. Если это не так, почему бы вам просто не передать (char *)указание на символ, который будет напечатан, или даже просто передать сам символ?

Кевин Мартин
источник
Хороший ответ; Я согласен, что самый простой способ сохранить это - подумать о том, существует ли объект C, который содержит адрес массива, то есть объект-указатель, адрес которого вы можете получить, чтобы получить char **. Переменные / объекты массива - это просто массив с неявным адресом, который нигде не хранится. Нет дополнительного уровня косвенности для доступа к ним, в отличие от переменной-указателя, которая указывает на другое хранилище.
Питер Кордес
0

Очевидно, что взятие адреса testтакое же, как взятие адреса 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;
}

Скомпилируйте это и запустите:

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

Таким образом, основной причиной ошибки сегментации является то, что эта программа будет пытаться разыменовать абсолютный адрес 0x42(также известный как 'B'), который ваша программа не имеет разрешения на чтение.

Хотя с другим компилятором / машиной адреса будут другими: попробуйте онлайн! , но вы все равно получите это по какой-то причине:

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

Но адрес, который вызывает ошибку сегментации, может очень отличаться:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
источник
1
Получение адреса testне совпадает с получением адреса test[0]. Первый имеет тип char (*)[256], а второй имеет тип char *. Они не совместимы, и стандарт C позволяет им иметь разные представления.
Эрик Постпишил
При форматировании указателя с помощью %pон должен быть преобразован в void *(опять же из соображений совместимости и представления).
Эрик Постпишил
1
printchar(&test);может произойти сбой для вас, но поведение не определено стандартом C, и люди могут наблюдать другое поведение в других обстоятельствах.
Эрик Постпишил
Re «Таким образом, конечная причина ошибки сегментации заключается в том, что эта программа попытается разыменовать абсолютный адрес 0x42 (также известный как« B »), который, вероятно, занят операционной системой». Если имеется ошибка сегмента, пытающаяся прочитать местоположение, это означает, что там ничего не отображается, не то, что оно занято ОС. (За исключением того, что там может быть что-то отображенное как, скажем, только для выполнения без прав на чтение, но это маловероятно.)
Эрик Постпишил
1
&test == &test[0]нарушает ограничения в C 2018 6.5.9 2, потому что типы несовместимы. Стандарт C требует реализации для диагностики этого нарушения, и результирующее поведение не определяется стандартом C. Это означает, что ваш компилятор может выдавать код, оценивающий их как равные, но другой компилятор может этого не делать.
Эрик Постпишил
-4

Представление char [256]зависит от реализации. Это не должно быть так же, как char *.

Приведение &testтипа char (*)[256]к типу char **приводит к неопределенному поведению.

С некоторыми компиляторами он может делать то, что вы ожидаете, а с другими нет.

РЕДАКТИРОВАТЬ:

После тестирования с gcc 9.2.1 выясняется, что оно printchar((char**)&test)фактически передается test как приведенное значение char**. Это как если бы инструкция была printchar((char**)test). В printcharфункции xуказатель на первый символ теста массива, а не двойной указатель на первый символ. Двойная де-ссылка xприводит к ошибке сегментации, потому что 8 первых байтов массива не соответствуют действительному адресу.

Я получаю точно такое же поведение и результат при компиляции программы clang 9.0.0-2.

Это может рассматриваться как ошибка компилятора или как результат неопределенного поведения, результат которого может зависеть от компилятора.

Другое неожиданное поведение заключается в том, что код

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

Выход

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Странное поведение таково xи *xимеет одинаковое значение.

Это вещь компилятора. Я сомневаюсь, что это определяется языком.

chmike
источник
1
Вы имеете в виду, что представление char (*)[256]зависит от реализации? Представление char [256]не имеет отношения к этому вопросу - это просто куча битов. Но, даже если вы имеете в виду, что представление указателя на массив отличается от представления указателя на указатель, это также пропускает точку. Даже если они имеют одинаковые представления, код OP не будет работать, потому что указатель на указатель может быть разыменован дважды, как это делается в printchar, но указатель на массив не может, независимо от представления.
Эрик Постпишил
@EricPostpischil приведение от char (*)[256]до char **принимается компилятором, но не дает ожидаемого результата, потому что a char [256]не совпадает с a char *. Я предположил, что кодировка другая, иначе это дало бы ожидаемый результат.
Чмике
Я не знаю, что вы подразумеваете под «ожидаемым результатом». Единственная спецификация в стандарте C о том, каким должен быть результат, состоит в том, что, если выравнивание неадекватно char **, поведение не определено, и что, в противном случае, если результат преобразован обратно char (*)[256], он сравнивается равным исходному указателю. Под «ожидаемым результатом» вы можете подразумевать, что при (char **) &testдальнейшем преобразовании в a char *он сравнивается равным &test[0]. Это не является маловероятным результатом в реализациях, которые используют плоское адресное пространство, но это не просто вопрос представления.
Эрик Постпишил
2
Также «Приведение и проверка типа char (*) [256] к char ** приводит к неопределенному поведению». не является правильным. C 2018 6.3.2.3 7 позволяет преобразовать указатель на тип объекта в любой другой указатель на тип объекта. Если указатель неправильно выровнен для ссылочного типа (ссылочный тип char **is char *), то поведение не определено. В противном случае конверсия определяется, хотя значение определяется только частично, согласно моему комментарию выше.
Эрик Постпишил
char (*x)[256]это не то же самое, что char **x. Причина xи *xвывод одного и того же значения указателя в том, что xэто просто указатель на массив. Ваш *x - это массив , и использование его в контексте указателя возвращается к адресу массива . Никакой ошибки компилятора там (или в том, что (char **)&testделает), просто небольшая умственная гимнастика, необходимая, чтобы выяснить, что происходит с типами. (cdecl объясняет это как «объявить x как указатель на массив 256 char»). Даже использование char*для доступа к объектному представлению char**не является UB; это может псевдоним что угодно.
Питер Кордес