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


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.