Чому я не можу отримати доступ до вказівника на вказівник для масиву стека?


35

Будь ласка, подивіться на наступний код. Він намагається передати масив як char**a функції:

#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**?

Відповіді:


29

Бо testне вказівник.

&testотримує вам вказівник на масив типу char (*)[256], який не сумісний з char**(тому що масив не є вказівником). Це призводить до невизначеної поведінки.


3
Але чому компілятор Сі потім дозволити проходження що - то типу char (*)[256]в char**?
ComFreek

@ComFreek Я підозрюю, що з максимальними попередженнями та -Werror, це не дозволяє.
PiRocks

@ComFreek: Це дійсно не дозволяє. Я повинен змусити компілятора прийняти його, явно відкинувши його char**. Без цього складу він не компілюється.
Андреас

38

test- це масив, а не вказівник і &testє вказівником на масив. Це не вказівник на покажчик.

Можливо, вам сказали, що масив - це вказівник, але це неправильно. Ім'я масиву - це ім'я всього об’єкта - усіх елементів. Це не вказівник на перший елемент. У більшості виразів масив автоматично перетворюється в покажчик на його перший елемент. Це зручність, яка часто корисна. Але з цього правила є три винятки:

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

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

Для того, щоб отримати покажчик на покажчик charз 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.
Eric Postpischil

О, і в C11 був також _Alignofоператор, згаданий на додаток до sizeofта &. Цікаво, чому вони його зняли ...
Руслан

@Ruslan: Це було видалено, оскільки це була помилка. _Alignofприймає лише ім'я типу як операнд і ніколи не приймає масив чи будь-який інший об'єкт як операнд. (Я не знаю чому; це здається синтаксично і граматично, це могло б бути схожим sizeof, але це не так.)
Ерік Postpischil

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

Отже, ви отримуєте вина про сегментацію.


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]

1
Прийняття адреси testне те саме, що прийняття адреси test[0]. Перший має тип char (*)[256], а другий має тип char *. Вони не сумісні, і стандарт C дозволяє їм мати різні представлення.
Ерік Postpischil

Форматуючи покажчик з %p, його слід перетворити на void *(знову ж таки з міркувань сумісності та представлення).
Eric Postpischil

1
printchar(&test);може призвести до краху для вас, але поведінка не визначена стандартом C, і люди можуть спостерігати іншу поведінку за інших обставин.
Eric Postpischil

Re "Отже, кінцевою причиною помилки сегментації є те, що ця програма намагатиметься знеструмити абсолютну адресу 0x42 (також відому як" B "), яку, ймовірно, займає ОС.": Якщо є помилка сегменту, що намагається прочитати розташування, це означає, що там нічого не відображається, не те, що воно займає ОС. (За винятком того, що там може бути щось відображено як, скажімо, виконання лише без дозволу на читання, але це малоймовірно.)
Eric Postpischil

1
&test == &test[0]порушує обмеження в C 2018 6.5.9 2, оскільки типи не сумісні. Стандарт C вимагає реалізації для діагностики цього порушення, і результуюча поведінка не визначається стандартом C. Це означає, що ваш компілятор може створювати код, оцінюючи їх рівними, але інший компілятор може не робити.
Eric Postpischil

-4

Представництво char [256]залежить від реалізації. Це не повинно бути таким же, як char *.

Лиття &testтипу призводить char (*)[256]до char **невизначеної поведінки.

З деякими компіляторами він може робити те, що ви очікуєте, а на інших - ні.

Редагувати:

Після тестування з gcc 9.2.1 виявляється, що printchar((char**)&test)фактично передається test як передане значення char**. Це як би інструкція printchar((char**)test). У printcharфункції x- вказівник на перший char тесту масиву, а не подвійний вказівник на перший символ. Подвійна де-посилання 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має однакове значення.

Це справа компілятора. Я сумніваюся, що це визначено мовою.


1
Ви маєте на увазі, що представництво char (*)[256]залежить від реалізації? У char [256]цьому питанні представництво не доречне - це лише купа біт. Але, навіть якщо ви маєте на увазі представлення вказівника на масив, відрізняється від представлення вказівника на покажчик, що також не вистачає точки. Навіть якщо вони мають однакові представлення, код OP не буде працювати, оскільки вказівник на покажчик може бути скинутий вдвічі, як це робиться в printchar, але вказівник на масив не може, незалежно від представлення.
Eric Eric Postpischil

@EricPostpischil амплітуда від char (*)[256]до char **цього приймається компілятором, але не дає очікуваного результату, оскільки a char [256]не є атрибутом char *. Я припускав, що кодування відрізняється, інакше це дасть очікуваний результат.
chmike

Я не знаю, що ви маєте на увазі під «очікуваним результатом». Єдиною специфікацією стандарту C, яким повинен бути результат, є те, що якщо вирівнювання не є адекватним char **, поведінка не визначена, і, інакше, якщо результат буде перетворений назад char (*)[256], він порівнюється з рівнем вихідного вказівника. Під "очікуваним результатом" ви можете означати, що, якщо (char **) &testдалі буде перетворено на a char *, воно порівнюється з рівним &test[0]. Це не є малоймовірним результатом у реалізаціях, які використовують плоский адресний простір, але це не лише питання представлення.
Eric Postpischil

2
Крім того, "Лиття та тест типу char (*) [256] до char ** призводить до невизначеного поведінки". невірно. C 2018 6.3.2.3 7 дозволяє вказівник на тип об'єкта перетворити в будь-який інший вказівник на тип об'єкта. Якщо покажчик неправильно вирівняний для посиланого типу (тип посиланняchar ** є char *), поведінка не визначена. Інакше конверсія визначена, хоча значення лише частково визначене, за моїм коментарем вище.
Eric Eric Postpischil

char (*x)[256]це не те саме, що char **x. Причина xта *xдрук того самого значення вказівника полягає в тому, що xце просто вказівник на масив. Ваш *x - це масив , і використовуючи його в контексті покажчика, відхиляється назад до адреси масиву . Немає помилок компілятора (або що (char **)&testробить), лише трохи розумової гімнастики, щоб зрозуміти, що відбувається з типами. (cdecl пояснює це як "оголосити x як вказівник на масив 256 char"). Навіть використовуючи char*для доступу до представлення об'єкта char**не UB; він може псевдонім будь-що.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.