Що означає "перенаправлення" вказівника?


540

Будь ласка, додайте приклад із поясненням.


це може допомогти вам: stackoverflow.com/questions/2795575/…
— Гаррі Радість


24
int *p;визначив би вказівник на ціле число і *pзнищив би цей покажчик, це означає, що він фактично отримає дані, на які вказує p.
— Пейман

4
Забава вказівника Бінкі ( cslibrary.stanford.edu/104 ) - ВЕЛИКЕ відео про покажчики, які можуть прояснити речі. @ Ерік - Ви рок для створення посилання на Стенфордську бібліотеку CS. Там так багато смакоти ...
— templatetypedef

6
Відповідь Гаррі є протилежною корисною тут.
— Джим Балтер

Відповіді:


731

Огляд основної термінології

Це , як правило , досить добре - якщо ви не програмуючи збірки - передбачити покажчик , що містить числова адреса пам'яті, з 1 зі посилання на другі байти в пам'яті процесу, 2 третіх, четверті 3 і так далі ....

  • Що сталося з 0 і першим байтом? Ну, ми до цього підемо пізніше - див. Нульові вказівки нижче.
  • Для більш точного визначення того, що зберігають покажчики, а також відношення пам’яті та адрес, див. У розділі "Докладніше про адреси пам'яті та чому вам, ймовірно, не потрібно знати" в кінці цієї відповіді.

Коли ви хочете отримати доступ до даних / значення в пам'яті, на яку вказує вказівник, - вмісту адреси з цим числовим індексом, тоді ви відмежуєте покажчик.

Різні комп'ютерні мови мають різні позначення, щоб сказати компілятору або інтерпретатору, що зараз ви зацікавлені в (поточному) значенні об'єкта - я зосередився нижче на C і C ++.

Вказівний сценарій

Розглянемо в C, враховуючи такий покажчик, як pнижче ...

const char* p = "abc";

... чотири байти з числовими значеннями, які використовуються для кодування букв 'a', 'b', 'c', і 0 байт для позначення кінця текстових даних, зберігаються десь у пам'яті та числовій адресі цього дані зберігаються в p. Таким чином, C кодує текст у пам'яті, відомий як ASCIIZ .

Наприклад, якщо випадковий літеральний рядок pзнаходився за адресою 0x1000 та 32-бітовим покажчиком на 0x2000, вміст пам'яті буде таким:

Memory Address (hex)    Variable name    Contents
1000                                     'a' == 97 (ASCII)
1001                                     'b' == 98
1002                                     'c' == 99
1003                                     0
...
2000-2003               p                1000 hex

Зверніть увагу , що не ім'я змінної / ідентифікатор адреси 0x1000, але ми можемо побічно посилатися на строковий літерал з допомогою покажчика , який зберігає його адресу: p.

Перенаправлення покажчика

Для посилання на символи p, на які ми вказуємо, ми відтягуємо, pвикористовуючи одне з цих позначень (знову ж таки, для C):

assert(*p == 'a');  // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
                     // p and 1 times the size of the things to which p points:
                     // In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b');  // Another notation for p[1]

Ви також можете переміщувати покажчики через дані, що вказуються, відстежуючи їх під час руху:

++p;  // Increment p so it's now 0x1001
assert(*p == 'b');  // p == 0x1001 which is where the 'b' is...

Якщо у вас є деякі дані, в які можна записати, ви можете робити такі дії:

int x = 2;
int* p_x = &x;  // Put the address of the x variable into the pointer p_x
*p_x = 4;       // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4

Вище ви, мабуть, знали під час компіляції, що вам потрібна змінна назва x, і код просить компілятор впорядкувати, де вона повинна зберігатися, гарантуючи, що адреса буде доступна через &x.

Перенаправлення та доступ до члена даних структури

У C, якщо у вас є змінна, яка є вказівником на структуру з членами даних, ви можете отримати доступ до цих членів за допомогою ->оператора перенаправлення:

typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159;  // Dereference and access data member x.d_
(*p).d_ *= -1;    // Another equivalent notation for accessing x.d_

Багатобайтові типи даних

Для використання вказівника комп'ютерній програмі також потрібне деяке розуміння типу даних, на який вказують - якщо для цього типу даних потрібно представити більше одного байта, то вказівник зазвичай вказує на байт з найменшими числами в даних.

Отже, дивлячись на трохи складніший приклад:

double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3);  // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4);  // Actually looks at bytes from address p + 1 * sizeof(double)
                       // (sizeof(double) is almost always eight bytes)
++p;                   // Advance p by sizeof(double)
assert(*p == 13.4);    // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8;       // Change sizes[3] from 19.4 to 29.8
                       // Note earlier ++p and + 2 here => sizes[3]

Покажчики на динамічно розподілену пам'ять

Іноді ви не знаєте, скільки пам'яті вам знадобиться до запуску вашої програми і не побачите, які дані на неї кинуті ... тоді ви можете динамічно розподіляти пам'ять, використовуючи malloc. Звичайна практика зберігати адресу в покажчику ...

int* p = (int*)malloc(sizeof(int)); // Get some memory somewhere...
*p = 10;            // Dereference the pointer to the memory, then write a value in
fn(*p);             // Call a function, passing it the value at address p
(*p) += 3;          // Change the value, adding 3 to it
free(p);            // Release the memory back to the heap allocation library

У C ++ розподіл пам'яті, як правило, виконується з newоператором, а розгортання з delete:

int* p = new int(10); // Memory for one int with initial value 10
delete p;

p = new int[10];      // Memory for ten ints with unspecified initial value
delete[] p;

p = new int[10]();    // Memory for ten ints that are value initialised (to 0)
delete[] p;

Дивіться також C ++ смарт-покажчики нижче.

Втрата та витік адрес

Часто вказівник може бути єдиним показником того, де в пам'яті є деякі дані або буфер. Якщо потрібне постійне використання цих даних / буфера або можливість викликати free()або deleteуникати протікання пам'яті, програміст повинен працювати над копією вказівника ...

const char* p = asprintf("name: %s", name);  // Common but non-Standard printf-on-heap

// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
    if (!isprint(*q))
        *q = '_';

printf("%s\n", p); // Only q was modified
free(p);

... або ретельно оркеструвати зміни будь-яких змін ...

const size_t n = ...;
p += n;
...
p -= n;  // Restore earlier value...
free(p);

C ++ розумні покажчики

У C ++ найкраще використовувати об'єкти розумних покажчиків для зберігання та керування покажчиками, автоматично розподіляючи їх під час запуску деструкторів смарт-покажчиків. Оскільки C ++ 11, стандартна бібліотека надає два, unique_ptrколи для одного виділеного об'єкта є один власник ...

{
    std::unique_ptr<T> p{new T(42, "meaning")};
    call_a_function(p);
    // The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete

... і shared_ptrдля власності на паї (з використанням довідкового підрахунку ) ...

{
    auto p = std::make_shared<T>(3.14, "pi");
    number_storage1.may_add(p); // Might copy p into its container
    number_storage2.may_add(p); // Might copy p into its container    } // p's destructor will only delete the T if neither may_add copied it

Нульові покажчики

В C NULLі 0- і додатково в C ++ nullptr- можна використовувати для вказівки, що покажчик наразі не містить адреси пам'яті змінної і не повинен бути відмежований або використаний у арифметиці вказівника. Наприклад:

const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
int c;
while ((c = getopt(argc, argv, "f:")) != -1)
    switch (c) {
      case f: p_filename = optarg; break;
    }
if (p_filename)  // Only NULL converts to false
    ...   // Only get here if -f flag specified

У C і C ++ так само, як і вбудовані числові типи не обов'язково за замовчуванням 0, а також boolsне false, вказівники не завжди встановлюються NULL. Все це встановлюється на 0 / false / NULL, коли вони є staticзмінними або (лише C ++) прямими або непрямими змінними членами статичних об'єктів або їх бази, або піддаються нульовій ініціалізації (наприклад, new T();і new T(x, y, z);виконують нульову ініціалізацію на членах T, включаючи покажчики, тоді як new T;не).

Крім того, при призначенні 0, NULLі nullptrдо покажчика біти в покажчику не обов'язково скидаються: покажчик може не містити «0» на апаратному рівні, або зверніться до адреси 0 в вашому віртуальному адресному просторі. Компілятор дозволено зберігати що - то ще там , якщо у неї є підстави, але все , що він робить - якщо ви прийшли разом і порівняти покажчик 0, NULL, nullptrабо інший покажчик , який був призначений який - або з тих, порівняння повинні працювати , як очікувалося. Отже, нижче вихідного коду на рівні компілятора "NULL" потенційно є трохи "магічним" у мовах C та C ++ ...

Більше про адреси пам’яті та чому вам, мабуть, не потрібно знати

Більш суворо, ініціалізовані покажчики зберігають бітовий шаблон, що ідентифікує адресу NULLабо (часто віртуальну ) пам'яті.

Простий випадок, коли це числове зміщення у весь віртуальний адресний простір процесу; у більш складних випадках вказівник може бути відносно якоїсь конкретної області пам'яті, яку ЦП може обрати на основі регістрів "сегмента" процесора або певного способу ідентифікатора сегмента, закодованого у бітовій схемі, та / або шукає в різних місцях залежно від інструкції машинного коду за допомогою адреси.

Наприклад, int*правильно ініціалізований для вказівки на intзмінну може - після кастингу до float*- пам'яті доступу до пам'яті "GPU" досить відрізнятися від пам'яті, де знаходиться intзмінна, а потім одного разу передаватися та використовуватись як покажчик функції, який може вказувати далі виразні кодові машини для зберігання пам'яті для програми (з числовим значенням int*фактично випадкового, недійсного вказівника в цих інших областях пам'яті).

Мови програмування 3GL, такі як C і C ++, як правило, приховують таку складність:

  • Якщо компілятор надає вам вказівник на змінну або функцію, ви можете вільно її знешкодити (доки змінна не знищена / не розміщена між тим), і це проблема компілятора, чи потрібно, наприклад, реєстр певного сегмента процесора заздалегідь відновити, або відома інструкція машинного коду, що використовується

  • Якщо ви отримуєте вказівник на елемент масиву, ви можете використовувати арифметику вказівника для переміщення в будь-який інший масив, або навіть для формування адреси, що знаходиться в кінці масиву, що є законним для порівняння з іншими вказівниками на елементи у масиві (або які аналогічно переміщені за арифметикою вказівника на одне і те ж значення минулого кінця); знову на C і C ++, компілятор повинен забезпечити це "просто працює"

  • Конкретні функції ОС, наприклад, спільне відображення пам’яті, можуть давати вам покажчики, і вони «просто працюватимуть» в межах діапазону адрес, який має для них сенс

  • Спроби переміщення легальних покажчиків за ці межі або присвоєння довільних цифр покажчикам або використання покажчиків, переданих на непов'язані типи, зазвичай мають невизначене поведінку , тому слід уникати в бібліотеках та додатках вищого рівня, але коду для ОС, драйверів пристроїв тощо ., можливо, доведеться покладатися на поведінку, що не визначена стандартом C або C ++, що, тим не менш, добре визначено їх конкретною реалізацією або обладнанням.


є p[1] і *(p + 1) тотожна ? Тобто чи створює p[1] та *(p + 1)генерує однакові інструкції?
— Pacerier

2
@Pacerier: з 6.5.2.1/2 в проекті стандарту N1570 C (вперше я знайшов в Інтернеті) "Визначення оператора підписки [] полягає в тому, що E1 [E2] ідентичний (* ((E1) + (E2)) ) ". - Я не уявляю жодної причини, чому компілятор не одразу перетворив би їх на однакові представлення на ранній стадії компіляції, застосувавши ті ж самі оптимізації після цього, але не бачу, як хтось може точно довести, що код був би ідентичним не опитуючи жодного написаного компілятора.
— Тоні Делрой

3
@Honey: значення 1000 hex занадто велике, щоб кодувати в одному байті (8 біт) пам'яті: ви можете зберігати непідписані числа від 0 до 255 в одному байті. Отже, ви просто не можете зберігати 1000 шістнадцяткових разів на "просто" адресу 2000. Натомість 32-розрядна система використовує 32 біти - це чотири байти - з адресами з 2000 по 2003 рік. 64-бітова система використовує 64 біт - 8 байт - з 2000 по 2007 рік. Базовою адресою pє лише 2000: якби у вас був інший покажчик, pвін повинен був би зберігати 2000 у своїх чотирьох або восьми байтах. Сподіваюся, що це допомагає! Ура.
— Тоні Делрой

1
@TonyDelroy: Якщо об'єднання uмістить масив arr, і gcc, і clang визнають, що lvalue u.arr[i]може отримати доступ до того ж сховища, що й інші члени об'єднання, але не визнає, що lvalue *(u.arr+i)може це зробити. Я не впевнений, чи вважають автори цих компіляторів, що останній посилається на UB, або що перший викликає UB, але вони все одно повинні це обробити корисно, але вони чітко розглядають два вирази як різні.
— supercat

3
Я рідко бачив покажчики та їх використання в межах C / C ++, так стисло та просто пояснено.
— kayleeFrye_onDeck

102

Перенаправлення покажчика означає отримання значення, яке зберігається в пам'яті, вказаному вказівником. Для цього використовується оператор * і називається оператором перенаправлення.

int a = 10;
int* ptr = &a;

printf("%d", *ptr); // With *ptr I'm dereferencing the pointer. 
                    // Which means, I am asking the value pointed at by the pointer.
                    // ptr is pointing to the location in memory of the variable a.
                    // In a's location, we have 10. So, dereferencing gives this value.

// Since we have indirect control over a's location, we can modify its content using the pointer. This is an indirect way to access a.

 *ptr = 20;         // Now a's content is no longer 10, and has been modified to 20.

15
Вказівник не вказує на значення , він вказує на об'єкт .
— Кіт Томпсон

51
@KeithThompson Вказівник не вказує на об’єкт, він вказує на адресу пам'яті, де знаходиться об'єкт (можливо, примітив).
— mg30rg

4
@ mg30rg: Я не впевнений, яку відмінність ти робиш. Значення вказівника - це адреса. Об'єктом, за визначенням, є "область зберігання даних у середовищі виконання, вміст якої може представляти значення". А що ви розумієте під "примітивом"? Стандарт C не використовує цей термін.
— Кіт Томпсон

6
@KeithThompson Я ледь не зауважив, що ти насправді не додав значення відповіді, ти лише терпів термінологію (і робив це теж неправильно). Значення вказівника, безумовно, є адресою, саме так воно "вказує" на адресу пам'яті. Слово "об'єкт" у нашому світі OOPdriven може ввести в оману, оскільки його можна трактувати як "екземпляр класу" (так, мені не було відомо, що питання позначено як [C], а не [C ++]), і я використав це слово "примітивні", як у протилежному "copmlex" (структура даних, як структура чи клас).
— mg30rg

3
Дозвольте додати до цієї відповіді, що оператор індексів масиву []також відмежує вказівник ( a[b]визначається як значення *(a + b)).
— cmaster - відновлення моніки

20

Вказівник - це "посилання" на значення .. так само, як номер виклику бібліотеки є посиланням на книгу. "Перенаправлення" номера дзвінка фізично відбувається через та витягування цієї книги.

int a=4 ;
int *pA = &a ;
printf( "The REFERENCE/call number for the variable `a` is %p\n", pA ) ;

// The * causes pA to DEREFERENCE...  `a` via "callnumber" `pA`.
printf( "%d\n", *pA ) ; // prints 4.. 

Якщо книги там немає, бібліотекар починає кричати, вимикає бібліотеку, і пара людей збирається розслідувати причину того, що людина збирається знайти книгу, якої там немає.


18

Простими словами, перенаправлення означає доступ до значення з певного місця пам'яті, на яке цей вказівник вказує.


7

Код та пояснення з Основи вказівника :

Операція з відміни починається у вказівника і слідує за стрілкою, щоб отримати доступ до пункту. Мета може полягати в тому, щоб подивитися на стан пуанти або змінити стан пуанти. Операція скидання на покажчик працює лише в тому випадку, якщо вказівник має pointee - pointee повинен бути призначений і покажчик повинен бути встановлений, щоб вказувати на нього. Найпоширенішою помилкою коду вказівника є забуття налаштування пункту. Найпоширеніший збій часу виконання через цю помилку в коді - це невдала операція скидання. У Java неправильна відмітка буде ввічливо позначена системою виконання. У компільованих мовах, таких як C, C ++ та Pascal, неправильна дереференція іноді може вийти з ладу, а в інші часи пошкодити пам'ять якимось тонким, випадковим чином.

void main() {   
    int*    x;  // Allocate the pointer x
    x = malloc(sizeof(int));    // Allocate an int pointee,
                            // and set x to point to it
    *x = 42;    // Dereference x to store 42 in its pointee   
}

Ви фактично повинні виділити пам'ять на те, куди має вказувати х. У вашому прикладі є невизначена поведінка.
— Пейман

3

Я думаю, що всі попередні відповіді помилкові, оскільки вони стверджують, що перенаправлення означає доступ до фактичного значення. Вікіпедія натомість дає правильне визначення: https://en.wikipedia.org/wiki/Dereference_operator

Він працює над змінною вказівника і повертає значення l, еквівалентне значенню за адресою вказівника. Це називається "перенаправлення" вказівника.

Однак, ми можемо знеструмити покажчик, не отримавши жодного разу доступ до значення, на яке він вказує. Наприклад:

char *p = NULL;
*p;

Ми знецінили покажчик NULL, не отримавши доступ до його значення. Або ми могли б зробити:

p1 = &(*p);
sz = sizeof(*p);

Знову перенаправлення, але ніколи не отримуючи доступу до цінності. Такий код НЕ буде збій: Збій відбувається, коли ви фактично отримуєте доступ до даних за допомогою недійсного вказівника. Однак, на жаль, згідно зі стандартом, перенаправлення недійсного покажчика є невизначеною поведінкою (за кількома винятками), навіть якщо ви не намагаєтесь торкнутися фактичних даних.

Отже, коротше: перенаправлення покажчика означає застосування оператора dereference до нього. Цей оператор просто повертає l-значення для подальшого використання.


добре, ви знецінили покажчик NULL, що призведе до помилки сегментації.
— arjun gaur

Крім того, ви шукали "оператора перенаправлення", а не "перенаправлення покажчика", що фактично означає отримання значення / доступу до значення в місці пам'яті, на яке вказує вказівник.
— arjun gaur

Ти намагався? Я зробив. Наступне не дає збоїв: `#include <stdlib.h> int main () {char * p = NULL; * р; повернути 0; } `
— stsp

1
@stsp Це те, що код не виходить з ладу зараз не означає, що він не буде в майбутньому або в іншій системі.

1
*p;викликає невизначеність поведінки. Незважаючи на те, що ви праві, що перенаправлення не має доступу до значення як таке , код *p; дійсно має значення.
— ММ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.