Які відмінності між змінною вказівника та еталонною змінною в C ++?


3262

Я знаю, що посилання є синтаксичним цукром, тому код легше читати та писати.

Але які відмінності?


100
Я думаю, що в пункті 2 має бути "Вказівник дозволений на NULL, але посилання - ні. Тільки неправильний код може створити NULL-посилання, а його поведінка не визначена."
Марк Рансом

19
Покажчики - це лише інший тип об'єкта, і, як і будь-який об’єкт у C ++, вони можуть бути змінною. Посилання, з іншого боку, ніколи не є об'єктами, а лише змінними.
Керрек СБ

19
Це компілюється без попереджень: int &x = *(int*)0;на gcc. Посилання дійсно може вказувати на NULL.
Кальмарій

20
посилання є змінним псевдонімом
Khaled.K

20
Мені подобається, як саме перше речення є тотальною помилкою. Посилання мають свою семантику.
Гонки легкості на орбіті

Відповіді:


1706
  1. Вказівник може бути призначений повторно:

    int x = 5;
    int y = 6;
    int *p;
    p = &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);

    Посилання не може бути і повинна бути призначена при ініціалізації:

    int x = 5;
    int y = 6;
    int &r = x;
  2. Вказівник має власну адресу пам’яті та розмір у стеці (4 байти на x86), тоді як посилання має ту саму адресу пам’яті (з оригінальною змінною), але також займає деякий простір у стеку. Оскільки посилання має таку саму адресу, що і сама вихідна змінна, можна подумати про посилання як інше ім'я для тієї ж змінної. Примітка. На що вказує вказівник, може бути на стеці чи купі. Дітто довідник. Моє твердження в цьому твердженні не в тому, що вказівник повинен вказувати на стек. Вказівник - це лише змінна, яка містить адресу пам'яті. Ця змінна є у стеці. Оскільки посилання має власний простір у стеці, а оскільки адреса така ж, як і змінна, на яку вона посилається. Більше про стек проти купи. Це означає, що існує справжня адреса посилання, про яку компілятор не скаже вам.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
  3. Ви можете мати покажчики на покажчики на покажчики, що пропонують додаткові рівні непрямості. Тоді як посилання пропонують лише один рівень непрямості.

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
  4. Вказівник може бути призначений nullptrбезпосередньо, тоді як посилання не може бути. Якщо ви постараєтесь досить, і знаєте, як це зробити, ви можете зробити адресу довідки nullptr. Так само, якщо ви досить стараєтесь, ви можете мати посилання на покажчик, і тоді ця посилання може містити nullptr.

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
  5. Покажчики можуть перебирати масив; ви можете використовувати ++для переходу до наступного елемента, на який вказує вказівник, та + 4для переходу до 5-го елемента. Це не має значення, якого розміру об'єкт на який вказує вказівник.

  6. Вказівник потрібно відкинути, *щоб отримати доступ до місця пам'яті, на яке він вказує, тоді як посилання може використовуватися безпосередньо. Вказівник на клас / структуру використовує ->для доступу до своїх членів, тоді як посилання використовує a ..

  7. Посилання не можуть бути заповнені в масив, тоді як покажчики можуть бути (Згадується користувачем @litb)

  8. Посилання на Const можуть бути прив’язані до тимчасових. Покажчики не можуть (не без деякої непрямості):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.

    Це робить const&більш безпечним для використання у списках аргументів тощо.


23
... але невизначення NULL не визначене. Наприклад, ви не можете перевірити, чи є посилання NULL (наприклад, & ref == NULL).
Пат Нотз

69
Число 2 не відповідає дійсності. Посилання - це не просто "інша назва цієї ж змінної". Посилання можуть бути передані до функцій, що зберігаються в класах тощо, таким чином, дуже схожим на покажчики. Вони існують незалежно від змінних, на які вони вказують.
Парк Дерек

31
Брайан, стек не має значення. Посилання та покажчики не повинні займати місця на стеку. Їх можна виділити на купу.
Дерек Парк

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

38
Ще одна важлива відмінність: посилання не можуть бути заповнені в масив
Йоханнес Шауб - ліб

382

Що таке посилання на C ++ ( для програмістів на C )

Посилання можна розглядати як постійний покажчик (не слід плутати з покажчиком на значення константи!) З автоматичною побічно, тобто компілятор буде застосовувати *оператор для вас.

Усі посилання повинні бути ініціалізовані з ненульовим значенням, або компіляція не вдасться. Отримати адресу посилання неможливо - адресовий оператор замість цього поверне адресу посиланого значення - а також не можна робити арифметику на посиланнях.

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

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

Розглянемо наступне твердження із поширених запитань C ++ :

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

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

Чому я вважаю посилання на C ++ корисними

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

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


17
@kriss: Ні, ви також можете отримати звисаючу посилання, повернувши автоматичну змінну за посиланням.
Бен Войгт

12
@kriss: Компілятор практично неможливо виявити в загальному випадку. Розглянемо функцію члена, яка повертає посилання на змінну члена класу: це безпечно і компілятор не повинен забороняти. Потім абонент, який має автоматичний примірник цього класу, викликає цю функцію-члена і повертає посилання. Престо: звисаючий довідник. І так, це призведе до неприємностей, @kriss: це моя суть. Багато людей стверджують, що перевага посилань над покажчиками полягає в тому, що посилання завжди дійсні, але це просто не так.
Бен Войгт

4
@kriss: Ні, посилання на об'єкт тривалості автоматичного зберігання сильно відрізняється від тимчасового об’єкта. У будь-якому випадку, я просто надавав зустрічний приклад вашої заяви про те, що ви можете отримати недійсну посилання лише шляхом відсилання недійсного вказівника. Крістоф правильний - посилання не є безпечнішими за покажчики, програма, яка використовує виключно посилання, все ще може порушити безпеку типу.
Бен Войгт

7
Посилання не є своєрідним покажчиком. Вони є новою назвою для існуючого об’єкта.
catphive

18
@catphive: вірно, якщо ви переходите за мовною семантикою, а не правда, якщо ви насправді дивитесь на реалізацію; C ++ - це набагато більш "чарівна" мова, якою є C, і якщо ви вилучите магію з посилань, ви закінчитеся вказівником
Крістоф

191

Якщо ви хочете бути дійсно педантичним, є одне, що ви можете зробити з посиланням, яке ви не можете зробити з покажчиком: продовжити термін експлуатації тимчасового об’єкта. У C ++, якщо ви пов'язуєте посилання const на тимчасовий об'єкт, термін експлуатації цього об'єкта стає терміном дії посилання.

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

У цьому прикладі s3_copy копіює тимчасовий об'єкт, який є результатом конкатенації. Тоді як s3_reference по суті стає тимчасовим об'єктом. Це дійсно посилання на тимчасовий об'єкт, який зараз має такий самий термін експлуатації, як і посилання.

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


5
але в чому справа для цього?
Ахмад Муштак

20
Ну, s3_copy створить тимчасовий, а потім скопіює його в s3_copy, тоді як s3_reference безпосередньо використовує тимчасове. Тоді, щоб бути дійсно педантичним, вам потрібно переглянути оптимізацію повернення, згідно з якою компілятору дозволено схилити конструкцію копії в першому випадку.
Метт Прайс

6
@digitalSurgeon: Магія там досить потужна. Термін експлуатації об'єкта подовжується фактом const &зв'язування, і лише тоді, коли посилання виходить за межі, викликається деструктор фактичного посиланого типу (порівняно з еталонним типом, який може бути базовим). Оскільки це посилання, між ними не відбудеться жодне нарізання.
Девід Родрігес - дрибес

9
Оновлення для C ++ 11: в останньому реченні повинно бути написано "Ви не можете прив'язувати посилання без значення const до тимчасового", оскільки ви можете прив'язувати посилання rvalue non-const до тимчасового, і воно має таку ж поведінку, що триває протягом життя.
Окталіст

4
@AhmadMushtaq: Ключовим використанням цього є похідні класи . Якщо немає спадщини, ви також можете використовувати значення семантики, яка буде дешевою або безкоштовною через RVO / переміщення конструкції. Але якщо ви Animal x = fast ? getHare() : getTortoise()тоді xзіткнетесь з класичною проблемою нарізки, тоді як Animal& x = ...будете працювати правильно.
Артур Такка

128

Крім синтаксичного цукру, посиланням є constвказівник ( не вказівник на a const). Ви повинні встановити, на що воно посилається, коли ви оголошуєте довідкову змінну, і ви не можете її змінити пізніше.

Оновлення: тепер, коли я подумаю про це ще трохи, є важлива різниця.

Ціль вказівника const можна замінити, взявши його адресу та використовуючи команду const.

Ціль посилання не може бути замінена жодним чином, ніж UB.

Це повинно дозволити компілятору зробити більш оптимізацію посилань.


8
Я думаю, що це найкраща відповідь на сьогоднішній день. Інші говорять про довідники та покажчики, ніби вони різні звірі, а потім викладають, як вони відрізняються за поведінкою. Це не робить їх легшими. Я завжди розумів посилання як на T* constрізний синтаксичний цукор (це може призвести до усунення багатьох * і & з вашого коду).
Карло Вуд

2
Msgstr "Ціль вказівника const можна замінити, взявши його адресу та використовуючи команду const." Це не визначена поведінка. Докладні відомості див. У розділі stackoverflow.com/questions/25209838/… .
dgnuff

1
Спроба змінити або референт посилання, або значення вказівника const (або будь-який скаляр const) - рівність незаконна. Що ви можете зробити: видалити кваліфікацію const, додану шляхом неявного перетворення: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);це нормально.
допитливий

1
Різниця тут UB проти буквально неможливої. У C ++ немає синтаксису, який би дозволив вам змінити, на які орієнтири.

Не неможливо, складніше, ви можете просто отримати доступ до області пам'яті вказівника, який моделює цю посилання і змінити її вміст. Це, безумовно, можна зробити.
Ніколя Буске

126

Всупереч поширеній думці, посилання може бути NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

Зрозуміло, це набагато важче зробити з посиланням - але якщо ви керуєте нею, ви розірвете волосся, намагаючись знайти його. Посилання не є по суті безпечними для C ++!

Технічно це недійсна посилання , а не нульова посилання. C ++ не підтримує нульові посилання як поняття, як ви можете знайти в інших мовах. Існують і інші види недійсних посилань. Будь-яка недійсна посилання збільшує спектр невизначеної поведінки , як і використання недійсного вказівника.

Фактична помилка полягає у перенаправлення вказівника NULL перед призначенням посилання. Але я не знаю жодного компілятора, який би генерував будь-які помилки за цієї умови - помилка поширюється до точки далі в коді. Ось що робить цю проблему такою підступною. Більшу частину часу, якщо ви перенаправляєте покажчик NULL, ви збиваєтесь прямо на цьому місці, і це не займе багато налагодження, щоб зрозуміти це.

Мій приклад вище короткий і надуманий. Ось більш реальний приклад.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

Я хочу ще раз зазначити, що єдиний спосіб отримати нульову посилання - це неправильний код, і як тільки ви це отримаєте, ви отримуєте не визначене поведінку. Він ніколи не має сенсу перевірити посилання нуля; наприклад, ви можете спробувати, if(&bar==NULL)...але компілятор може оптимізувати заяву поза існуванням! Дійсна посилання ніколи не може бути NULL, тому, з точки зору компілятора, порівняння завжди помилкове, і вилучити ifпункт як мертвий код вільно - це суть невизначеної поведінки.

Правильний спосіб уникнути неприємностей - уникати перенаправлення покажчика NULL для створення посилання. Ось автоматизований спосіб досягти цього.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

Старший погляд на цю проблему від когось із кращими навичками письма див. У Null References від Jim Hyslop та Herb Sutter.

Ще один приклад небезпеки перенаправлення нульового вказівника див. Розкриття невизначеної поведінки при спробі перенесення коду на іншу платформу Реймонда Чена.


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

9
марка має дійсний аргумент. аргумент про те, що вказівник може бути NULL і вам доведеться перевірити, також не є реальним: якщо ви говорите, що функція вимагає non-NULL, то абонент повинен це зробити. тож якщо той, хто телефонує, не звертається до невизначеної поведінки. так само, як і Марк з поганою посиланням
Йоганнес Шауб - ліб

13
Опис помилкове. Цей код може або не може створити посилання, яке є NULL. Його поведінка не визначена. Це може створити абсолютно дійсну посилання. Він може взагалі не створити посилання.
Девід Шварц

7
@David Schwartz, якби я говорив про те, як справи повинні працювати відповідно до стандарту, ви були б правильні. Але це не те, про що я говорю, я говорю про фактичну поведінку, що спостерігається з дуже популярним компілятором, і екстраполяцію на основі моїх знань про типові компілятори та архітектури процесора до того, що, можливо, станеться. Якщо ви вважаєте, що посилання перевершують покажчики, оскільки вони безпечніші і не вважаєте, що посилання можуть бути поганими, вас колись наткнуться на просту проблему, як і я.
Марк Викуп

6
Перенаправлення нульового покажчика неправильно. Будь-яка програма, яка робить це, навіть ініціалізувати посилання - неправильно. Якщо ви ініціалізуєте посилання з вказівника, ви завжди повинні перевірити, чи покажчик дійсний. Навіть якщо це вдається, базовий об'єкт може бути видалений у будь-який час, залишаючи посилання на посилання на неіснуючий об'єкт, правда? Що ви говорите, це хороші речі. Я думаю, що справжня проблема полягає в тому, що посилання НЕ потрібно перевіряти на "недійсність", коли ви бачите один, і вказівник повинен бути, як мінімум, стверджується.
t0rakka

115

Ви забули найважливішу частину:

член-доступ з покажчиками використовує ->
доступ члена з використанням посилань.

foo.barце явно перевершував foo->barтаким же чином , що VI є явно перевершує Emacs :-)


4
@Orion Edwards> членський доступ з покажчиками використовує ->> доступ для користувачів із використанням посилань. Це не на 100% правда. Ви можете мати посилання на вказівник. У цьому випадку ви отримаєте доступ до членів де-посилання вказівника, використовуючи -> struct Node {Node * next; }; Вузол * перший; // p - посилання на покажчик void foo (Node * & p) {p-> next = first; } Вузол * bar = новий Вузол; фу (бар); - ОП: Чи знайомі ви з поняттями rvalues ​​та lvalues?

3
Розумні покажчики мають і те, і інше. (методи класу розумних покажчиків) та -> (методи базового типу).
JBRWilkinson

1
@ user6105 Orion Edwards твердження насправді на 100% вірно. "отримати доступ до членів [the] де-посилання вказівника" Вказівник не має членів. Об'єкт, на який посилається вказівник, має членів, а доступ до них - саме те, що ->передбачає посилання на покажчики, так само, як і на сам покажчик.
Макс Трукса

1
чому це .і ->має щось спільне з vi та emacs :)
artm

10
@artM - це був жарт, і, мабуть, не має сенсу для носіїв англійської мови. Мої вибачення. Пояснити, чи краще vi від emacs, цілком суб'єктивно. Одні вважають, що vi набагато перевершує, а інші вважають навпаки. Аналогічно, я вважаю, що використовувати .краще, ніж використовувати ->, але так само, як vi vs emacs, це повністю суб'єктивно, і ви нічого не можете довести
Orion Edwards

74

Посилання дуже схожі на покажчики, але вони спеціально створені, щоб бути корисними для оптимізації компіляторів.

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

Як приклад:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

Оптимізуючий компілятор може зрозуміти, що ми отримуємо доступ до [0] і [1] досить багато. Було б радимо оптимізувати алгоритм для:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

Щоб здійснити таку оптимізацію, потрібно довести, що під час виклику нічого не може змінити масив [1]. Це зробити досить просто. i ніколи не менше 2, тому масив [i] ніколи не може посилатися на масив [1]. можливоModify () задається a0 як посилання (масив псевдоніму [0]). Оскільки немає «посилальної» арифметики, компілятор просто повинен довести, що можливоModify ніколи не отримує адресу x, і він довів, що масив нічого не змінює [1].

Він також повинен довести, що немає жодного способу, щоб майбутній виклик міг прочитати / записати [0], тоді як у нас є тимчасова копія реєстру в a0. Це часто буває доказів, оскільки в багатьох випадках очевидно, що посилання ніколи не зберігається в постійній структурі, як екземпляр класу.

Тепер те ж саме зробіть із покажчиками

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

Поведінка однакова; тільки зараз набагато важче довести, що можливоModify ніколи не модифікує масив [1], тому що ми вже дали йому вказівник; кішка поза сумкою. Тепер йому доведеться зробити набагато складніше доказ: статичний аналіз можливоModify, щоб довести, що він ніколи не пише в & x + 1. Він також повинен довести, що він ніколи не економить покажчик, який може посилатися на масив [0], що просто як хитрість.

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

Звичайно, забороняючи такі розумні оптимізації, компілятори дійсно перетворять посилання на покажчики при необхідності.

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

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

Зазвичай тимчасові об'єкти, як, наприклад, створений закликом до createF(5)руйнування, руйнуються в кінці виразу. Однак, прив’язуючи цей об'єкт до посилання, refC ++ продовжить термін експлуатації цього тимчасового об'єкта до тих пір, поки він refне вийде за межі.


Щоправда, тіло має бути видно. Однак визначити, що maybeModifyне приймає адреси нічого, пов'язаного з цим x, істотно простіше, ніж довести, що купа арифметики вказівника не відбувається.
Корт Аммон

Я вважаю, що оптимізатор вже робить, що "купа арифметики вказівника не виникає" перевіряє ще купу інших причин.
Бен Войгт

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

У мене є відчуття, що хтось помилково позначив застарілий коментар void maybeModify(int& x) { 1[&x]++; }, який обговорюють інші коментарі вище
Бен Войгт

68

Насправді, посилання не дуже схоже на вказівник.

Компілятор зберігає "посилання" на змінні, асоціюючи ім'я з адресою пам'яті; це його завдання перевести будь-яке ім’я змінної на адресу пам'яті при компілюванні.

Коли ви створюєте посилання, ви лише повідомляєте компілятору, що ви присвоюєте інше ім'я змінній вказівника; тому посилання не можуть "вказувати на нуль", тому що змінна не може бути і не бути.

Покажчики є змінними; вони містять адресу якоїсь іншої змінної або можуть бути нульовими. Важливо те, що вказівник має значення, тоді як у посилання є лише змінна, на яку він посилається.

Тепер кілька пояснень реального коду:

int a = 0;
int& b = a;

Тут ви не створюєте іншої змінної, яка вказує на a; ви просто додаєте інше ім’я до вмісту пам'яті, що містить значення a. Ця пам’ять тепер має два імена, aі b, і її можна вирішити за допомогою будь-якого імені.

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

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

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


2
Посилання - це посилання на l-значення, не обов'язково на змінну. Через це він набагато ближче до вказівника, ніж до реального псевдоніма (конструкція часу компіляції). Приклади виразів, на які можна посилатися, є * p або навіть * p ++

5
Правильно, я просто вказував на той факт, що посилання не завжди може штовхати нову змінну на стеку так, як буде новий покажчик.
Вінсент Роберт

1
@VincentRobert: Він буде діяти так само, як і вказівник ... якщо функція вкладена, і посилання, і вказівник будуть оптимізовані. Якщо є виклик функції, адресу об’єкта потрібно буде передати функції.
Бен Войгт

1
int * p = NULL; int & r = * p; посилання, що вказує на NULL; if (r) {} -> boOm;)
sree

2
Цей фокус на етапі компіляції здається приємним, поки ви не пам’ятаєте, що посилання можна передавати під час виконання, і тоді статичний псевдонім виходить з вікна. (І тоді посилання зазвичай реалізуються як покажчики, але стандарт не вимагає цього методу.)
underscore_d

45

Посилання ніколи не може бути NULL.


10
Дивіться відповідь Марка Рансома як зустрічний приклад. Це найчастіше стверджуваний міф про посилання, але це міф. Єдиною гарантією, яку ви маєте за стандартом, є те, що ви негайно маєте UB, коли у вас є посилання NULL. Але це схоже на те, «Цей автомобіль є безпечним, він ніколи не зможе вийти на дорогу (ми не беремо на себе відповідальність за те , що може статися , якщо ви будете направляти його з дороги в будь-якому випадку це може бути просто вибухнути ..).»
cmaster - відновити моніку

17
@cmaster: У дійсній програмі посилання не може бути нульовим. Але вказівник може. Це не міф, це факт.
користувач541686

8
@Mehrdad Так, дійсні програми залишаються в дорозі. Але немає ніякого бар'єру для руху, який би міг забезпечити виконання вашої програми. На великих ділянках дороги фактично не вистачає розмітки. Тож виходити з дороги вночі надзвичайно просто. І для налагодження таких помилок дуже важливо, щоб ви знали, що це може статися: нульова посилання може поширюватися до того, як вона збоїть вашу програму, як і нульовий покажчик. І коли у вас є такий код, як void Foo::bar() { virtual_baz(); }ці segfaults. Якщо ви не знаєте, що посилання можуть бути нульовими, ви не можете простежити нуль назад до його початку.
cmaster - відновити моніку

4
int * p = NULL; int & r = * p; посилання, що вказує на NULL; if (r) {} -> boOm;) -
sree

10
@sree int &r=*p;- невизначена поведінка. У цей момент, ви не маєте «кількість посилань вказує на NULL," у вас є програма , яка вже не може бути вмотивованою про взагалі .
cdhowie

35

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

Розглянемо ці два фрагменти програми. У першому ми призначаємо один вказівник іншому:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

Після призначення, ival, об'єкт, адресований pi, залишається незмінним. Призначення змінює значення pi, роблячи його вказувати на інший об'єкт. Тепер розглянемо аналогічну програму, яка призначає дві посилання:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

Це призначення змінює ival, значення, на яке посилається ri, а не саме посилання. Після присвоєння обидві посилання все ще відносяться до своїх оригінальних об'єктів, і значення цих об'єктів тепер є однаковим.


"посилання завжди посилається на об'єкт" просто абсолютно помилково
Бен Войгт

32

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

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

Крім цього, звичайно, є деякі практичні відмінності між вказівниками та посиланнями. Синтаксис їх використання, очевидно, відрізняється, і ви не можете "переставляти" посилання, мати посилання на небуття або вказувати на посилання.


27

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

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 

20

Не має значення, скільки місця займає, оскільки ви фактично не можете побачити будь-який побічний ефект (без виконання коду) того місця, який би він займав.

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

Наприклад:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

надрукує:

in scope
scope_test done!

Це мовний механізм, який дозволяє ScopeGuard працювати.


1
Ви не можете взяти адресу посилання, але це не означає, що вони фізично не займають місця. Забороняючи оптимізації, вони, безумовно, можуть.
Гонки легкості на орбіті

2
Незважаючи на те, що "Посилання на стек взагалі не займає місця" є явно помилковим.
Гонки легкості на орбіті

1
@Tomalak, ну, це залежить і від компілятора. Але так, мовляв, це трохи заплутано. Я гадаю, було б менш заплутаним просто зняти це.
MSN

1
У будь-якому конкретному випадку це може бути, а може і не. Тож "це не так", оскільки категоричне твердження є неправильним. Це я кажу. :) [я не можу пригадати, що стандарт говорить про це; правила довідкових членів можуть містити загальне правило "посилання можуть займати місце", але у мене немає копії стандарту зі мною тут, на пляжі: D]
Гонкості в Орбіті

20

На цьому ґрунтується підручник . Що написано, це робить більш зрозумілим:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

Просто пам'ятати про це,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

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

Подивіться на наступне твердження,

int Tom(0);
int & alias_Tom = Tom;

alias_Tomможна розуміти як alias of a variable(різний з typedef, що є alias of a type) Tom. Також добре забувати термінологію такого твердження - це створити посилання на Tom.


1
І якщо клас має посилання змінної, його слід ініціалізувати або з nullptr, або з дійсним об'єктом у списку ініціалізації.
Misgevolution

1
Формулювання у цій відповіді занадто заплутане для того, щоб мати користь. Також, @Misgevolution, ви серйозно не рекомендуєте читачам ініціалізувати посилання з nullptr? Ви насправді читали будь-яку іншу частину цієї теми, або ...?
підкреслюй_d

1
Моє погано, вибачте за ту дурню, яку я сказав. Я, мабуть, до цього часу був позбавлений сну. "ініціалізувати з nullptr" абсолютно неправильно.
Misgevolution

19

Посилання - це не інше ім'я, яке дається деякій пам'яті. Це незмінний покажчик, який автоматично де-посилається на використання. В основному він зводиться до:

int& j = i;

Це внутрішньо стає

int* const j = &i;

13
Про це не говорить стандарт C ++, і він не вимагає, щоб компілятор реалізував посилання так, як описано у вашій відповіді.
jogojapan

@jogojapan: Будь-який спосіб, який дійсний для компілятора C ++ для реалізації посилання, також є дійсним способом реалізації constвказівника. Ця гнучкість не доводить, що існує різниця між еталонним та вказівним.
Бен Войгт

2
@BenVoigt Це може бути правдою, що будь-яка дійсна реалізація однієї також є коректною реалізацією іншої, але це не видно очевидно з визначень цих двох понять. Гарна відповідь почалася б із визначень і продемонструвала, чому твердження про те, що вони є двома, є остаточними. Ця відповідь, здається, є якось коментарем до деяких інших відповідей.
jogojapan

Посилання - це інша назва, що дається об'єкту. У компіляторі допускається будь-яка реалізація, доки ви не зможете визначити різницю, це відомо як правило "як-якщо". Важлива частина тут полягає в тому, що ви не можете визначити різницю. Якщо ви зможете виявити, що вказівник не має місця для зберігання, компілятор помиляється. Якщо ви зможете виявити, що посилання не має місця для зберігання, компілятор все ще відповідає.
sp2danny

18

Пряма відповідь

Що таке посилання на C ++? Якийсь конкретний екземпляр типу, який не є типом об'єкта .

Що таке покажчик на C ++? Якийсь конкретний екземпляр типу, який є типом об'єкта .

З визначення ISO C ++ визначення типу об'єкта :

Об'єкт тип (можливо резюме -qualified) тип , який не є типом функції, а НЕ контрольний тип, а не резюме недійсним.

Це може бути важливо знати, тип об'єкта - це категорія всесвіту вищого рівня в C ++. Довідка також є категорією вищого рівня. Але вказівник - ні.

Покажчики та посилання згадуються разом у контексті складного типу . В основному це пов'язано з характером синтаксису декларатора, успадкованого від (і розширеного) C, на який немає посилань. (Крім того, існує більше ніж один вид декларатора посилань, починаючи з C ++ 11, тоді як покажчики все ще "єднані": &+ &&vs.. *) Тому складання мови, специфічної за допомогою "розширення", з подібним стилем C у цьому контексті є дещо розумним . (Я все ще буду стверджувати, що синтаксис деклараторів сильно витрачає синтаксичну виразність , робить як користувачів користувачів, так і реалізацію фрустрацією. Таким чином, всі вони не можуть бути вбудованимиу новому мовному дизайні. Це зовсім інша тема щодо дизайну ПЛ.)

В іншому випадку неістотно, що вказівники можна кваліфікувати як певні типи типів із посиланнями разом. Вони просто ділять занадто мало загальних властивостей, крім схожості синтаксису, тому в більшості випадків немає необхідності поєднувати їх.

Зауважте, що вищевказані висловлювання зазначають лише "покажчики" та "посилання" як типи. Є кілька зацікавлених питань щодо їх примірників (наприклад, змінні). Існує також занадто багато помилок.

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

  • Типи об'єктів можуть мати cvкваліфікатори вищого рівня . Посилання не можуть.
  • Змінні типи об'єктів займають сховище відповідно до абстрактної машинної семантики. Посилання не обов'язково займають сховище (детальну інформацію див. У розділі про помилкові уявлення нижче).
  • ...

Ще кілька спеціальних правил щодо посилань:

  • Декларатори з'єднань більш обмежують посилання.
  • Посилання можуть згортатися .
    • Спеціальні правила щодо &&параметрів (як "посилання на переадресацію"), засновані на згортанні посилань під час виведення параметра шаблону, дозволяють "ідеальне пересилання" параметрів.
  • Посилання мають спеціальні правила при ініціалізації. Термін служби змінної, оголошеної як еталонний тип, може бути різним для звичайних об'єктів за допомогою розширення.
    • BTW, кілька інших контекстів, таких як ініціалізація, включають std::initializer_listдеякі подібні правила продовження терміну експлуатації. Це ще одна банка глистів.
  • ...

Помилкові уявлення

Синтаксичний цукор

Я знаю, що посилання є синтаксичним цукром, тому код легше читати та писати.

Технічно це очевидно неправильно. Посилання не є синтаксичним цукром жодної іншої функції в C ++, тому що їх неможливо точно замінити іншими ознаками без будь-яких смислових відмінностей.

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

C ++ має лише кілька видів синтаксичних цукрів у цьому суворому розумінні. Один екземпляр - це (успадкований від C) вбудований (не перевантажений) оператор [], який визначається саме таким, що має однакові смислові властивості специфічних форм поєднання над вбудованим оператором одинарним *та бінарним+ .

Зберігання

Отже, і вказівник, і посилання використовують однаковий об'єм пам'яті.

Виклад вище є просто неправильним. Щоб уникнути подібних помилок, перегляньте натомість правила ISO C ++:

Від [intro.object] / 1 :

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

З [dcl.ref] / 4 :

Не визначено, потребує зберігання посилання чи ні.

Зауважте, це семантичні властивості.

Прагматика

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

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

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

  • Іноді мовні правила прямо вимагають використання конкретних типів. Якщо ви хочете використовувати ці функції, дотримуйтесь правил.
    • Конструктори копій вимагають конкретних типів cv - &тип посилання як тип 1-го параметра. (І зазвичай це слід constкваліфікувати.)
    • Конструктори, що рухаються, вимагають конкретних типів cv - &&тип посилання як тип 1-го параметра. (І зазвичай не повинно бути кваліфікаторів.)
    • Конкретні перевантаження операторів вимагають еталонних або нереференсних типів. Наприклад:
      • Перевантажений operator=функціями спеціальних членів вимагає типів посилань, подібних 1-му параметру конструкторів копіювання / переміщення.
      • Постфікс ++вимагає пустушки int.
      • ...
  • Якщо ви знаєте, що пропускна вартість (тобто використання нереференційних типів) є достатньою, використовуйте її безпосередньо, особливо, коли ви використовуєте реалізацію, яка підтримує копіювання C ++ 17 копій. ( Попередження : Однак вичерпно міркувати про необхідність може бути дуже складним .)
  • Якщо ви хочете керувати деякими ручками із власністю, використовуйте розумні вказівники типу unique_ptrта shared_ptr(або навіть з домашнім мовою самостійно, якщо вони вимагають, щоб вони були непрозорими ), а не необробленими покажчиками.
  • Якщо ви робите деякі ітерації в межах діапазону, використовуйте ітератори (або деякі діапазони, які ще не передбачені стандартною бібліотекою), а не сировинні покажчики, якщо ви не впевнені, що сировинні покажчики будуть краще (наприклад, для менших залежностей заголовка) в дуже конкретних справ.
  • Якщо ви знаєте, що значення пропускання є достатнім, і вам потрібна якась явна нульова семантика, використовуйте подібні обгортки std::optional, а не сирі вказівники.
  • Якщо ви знаєте, що значення пропускання не є ідеальним з вищезгаданих причин, і ви не хочете звести селектику, що зводиться до змін, використовуйте {lvalue, rvalue, forwarding} -references.
  • Навіть коли ви хочете, щоб семантика на зразок традиційного вказівника, часто є щось більш відповідне, як, наприклад, observer_ptrу бібліотеці Fundamental Fund TS.

Єдині винятки неможливо вирішити поточною мовою:

  • Коли ви реалізуєте розумні вказівники вище, можливо, вам доведеться мати справу з необробленими вказівниками.
  • Конкретні процедури взаємодії мови вимагають покажчиків, наприклад operator new. (Однак cv - void*все-таки зовсім інший і безпечніший порівняно зі звичайними вказівниками на об'єкти, оскільки він виключає несподівану арифметику вказівника, якщо ви не покладаєтесь на якесь невідповідне розширення на void*зразок GNU.)
  • Показники функцій можуть бути перетворені з лямбда-виразів без захоплення, тоді як посилання функцій не можуть. Для таких випадків ви повинні використовувати вказівники функцій у негенеричному коді, навіть якщо ви навмисно не хочете нульових значень.

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

  • Ви повинні обслуговувати API старого стилю (C).
  • Ви повинні відповідати вимогам ABI у конкретних реалізаціях C ++.
  • Ви повинні взаємодіяти під час виконання з різними мовними реалізаціями (включаючи різні збірки, час виконання мови та FFI деяких клієнтських мов високого рівня) на основі припущень про конкретні реалізації.
  • Ви повинні підвищити ефективність перекладу (компіляції та зв’язування) в деяких крайніх випадках.
  • Ви повинні уникати роздуття символів у деяких крайніх випадках.

Застереження щодо нейтральності мови

Якщо ви побачите це питання через якийсь результат пошуку Google (не характерний для C ++) , це, ймовірно, неправильне місце.

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

Посилання на C ++, ймовірно, не збережуть значення для різних мов. Наприклад, посилання взагалі не передбачають ненульових властивостей для таких значень, як вони в C ++, тому такі припущення можуть не працювати в деяких інших мовах (і зустріти приклади ви знайдете досить легко, наприклад, Java, C #, ...).

Серед загальних посилань на різних мовах програмування взагалі можуть бути деякі загальні властивості, але давайте залишимо це для деяких інших питань в ТА

(Побічна примітка: питання може бути важливим раніше, ніж будь-які мови, що стосуються "С-подібних", наприклад ALGOL 68 проти PL / I. )


17

Посилання на покажчик можливе в C ++, але зворотний не можливий означає, що вказівник на посилання неможливий. Посилання на покажчик забезпечує чистіший синтаксис для зміни вказівника. Подивіться на цей приклад:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

І розглянемо версію С вищевказаної програми. У C ви повинні використовувати покажчик на покажчик (множинна непрямість), і це призводить до плутанини і програма може виглядати складно.

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

Для отримання додаткової інформації про посилання на вказівник відвідайте наступне:

Як я вже сказав, вказівник на посилання неможливий. Спробуйте наступну програму:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

16

Я використовую посилання, якщо мені не потрібна будь-яка з цих:

  • Нульові покажчики можуть використовуватися як дозорне значення, часто дешевий спосіб уникнути перевантаження функцій або використання булінгу.

  • Ви можете робити арифметику за вказівником. Наприклад,p += offset;


5
Можна написати, &r + offsetде rбуло оголошено посилання
ММ

15

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

З повагою, & rzej


1
Посилання та покажчики - обидві ручки. Вони обидва дають вам семантичний характер, де ваш об'єкт передається посиланням, але ручка копіюється. Без різниці. (Є й інші способи мати ручки, наприклад ключ для пошуку у словнику)
Ben Voigt

Я також думав так. Але дивіться пов’язану статтю, яка описує, чому це не так.
Анджей

2
@Andrzj: Це просто дуже довга версія єдиного речення в моєму коментарі: Ручка скопійована.
Бен Войгт

Мені потрібно більше пояснень щодо цього "Ручка скопійована". Я розумію деяку основну ідею, але думаю, що фізично посилання та вказівник вказують на розташування пам'яті змінної. Це як псевдонім зберігає змінну значення та оновлює її, як значення змінної є зміною чи щось інше? Я новачок, і, будь ласка, не позначайте це дурним питанням.
Асим

1
@Andrzej False В обох випадках відбувається пропускна вартість. Посилання передається за значенням, а вказівник передається за значенням. Говорячи інакше, бентежить новачків.
Майлз Рут

14

Ризикуючи додати плутанину, я хочу ввести деякий вклад, я впевнений, що це в основному залежить від того, як компілятор реалізує посилання, але у випадку gcc ідея, що посилання може вказувати лише на змінну в стеці насправді не правильно, візьміть це для прикладу:

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

Які результати:

THIS IS A STRING
0xbb2070 : 0xbb2070

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

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

Які результати:

THIS IS A STRING

Тому посилання IS вказівник під кришкою, вони обидва просто зберігають адресу пам'яті, де адреса вказує, що не має значення, що ви думаєте, що станеться, якби я зателефонував std :: cout << str_ref; ПІСЛЯ виклику видалити & str_ref? Ну, очевидно, вона компілює добре, але викликає помилку сегментації під час виконання, оскільки вона більше не вказує на дійсну змінну, у нас, по суті, є зламана посилання, яка все ще існує (поки вона не виходить із сфери застосування), але марна.

Іншими словами, посилання - це не що інше, як покажчик, у якому відключена механіка вказівника, що робить його більш безпечним та простим у використанні (не випадкова математика вказівника, ніяке змішування '.' Та '->' тощо), якщо припускати, що ви не спробуйте будь-якої дурниці, як мої приклади вище;)

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

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

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


14

Ще одна відмінність полягає в тому, що ви можете мати вказівники на тип пустоти (а це означає вказівник на що-небудь), але посилання на недійсні заборонені.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

Не можу сказати, що я дуже задоволений цією різницею. Я дуже вважаю за краще, щоб це було дозволено із значенням посилання на що-небудь з адресою та в іншому випадку таку саму поведінку для посилань. Це дозволило б визначити деякі еквіваленти функцій бібліотеки С, як memcpy, використовуючи посилання.


13

Також посилання, яке є параметром функції, яка є вкладеною, може оброблятися інакше, ніж покажчик.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

Багато компіляторів, коли вбудовується версія вказівника перша, насправді змусить записати в пам'ять (адресу ми беремо прямо). Однак вони залишать посилання в реєстрі, який є більш оптимальним.

Звичайно, для функцій, які не є вкладеними вказівниками та посиланням, генерується один і той же код, і завжди краще передавати властивості за значенням, ніж за посиланням, якщо вони не змінюються і повертаються функцією.


11

Ще одне цікаве використання посилань - це надання аргументу за замовчуванням визначеного користувачем типу:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

Аромат за замовчуванням використовує "прив'язувати посилання const на тимчасовий" аспект посилань.


11

Ця програма може допомогти зрозуміти відповідь на питання. Це проста програма посилання "j" та вказівник "ptr", що вказує на змінну "x".

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

Запустіть програму і подивіться на вихід, і ви зрозумієте.

Крім того, запасіть 10 хвилин і перегляньте це відео: https://www.youtube.com/watch?v=rlJrrGV0iOg


11

Я відчуваю, що є ще один момент, який тут не висвітлювався.

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

Хоча це може здаватися поверхневим, я вважаю, що ця властивість є визначальною для ряду функцій C ++, наприклад:

  • Шаблони . Оскільки параметри шаблона мають тип качки, синтаксичні властивості типу - це все, що має значення, тому часто один і той же шаблон може використовуватися і з Tі T&.
    (або std::reference_wrapper<T>які досі покладаються на неявну роль у шаблонах T&)
    Шаблони, які охоплюють і те, T&і T&&ще більше поширені.

  • Значення . Розглянемо вислів str[0] = 'X';Без посилань, він би працював лише для c-рядків ( char* str). Повернення символу шляхом посилання дозволяє визначеним користувачем класам мати однакові позначення.

  • Конструктори копіювання . Синтаксично має сенс передавати об’єкти на копіювання конструкторів, а не вказівники на об’єкти. Але просто немає можливості конструктору копій взяти об'єкт за значенням - це призведе до рекурсивного виклику до того ж конструктора копій. Тут залишаються посилання як єдиний варіант.

  • Оператор перевантажує . За допомогою посилань можна ввести непрямий виклик оператора - скажімо, operator+(const T& a, const T& b)зберігаючи те саме позначення інфіксації. Це також працює для регулярних перевантажених функцій.

Ці пункти забезпечують значну частину C ++ та стандартної бібліотеки, тому це досить велика властивість посилань.


" неявний кидок " акторський склад - це синтаксична конструкція, вона існує в граматиці; акторський
склад

9

Існує дуже важлива нетехнічна різниця між вказівниками та посиланнями: Аргумент, переданий функції вказівником, набагато помітніший, ніж аргумент, переданий функції за допомогою non-const посилання. Наприклад:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

Повертаючись до C, дзвінок, який виглядає так, fn(x)може передаватися лише за значенням, тому його однозначно не можна змінювати x; щоб змінити аргумент, вам знадобиться передати вказівник fn(&x). Отже, якщо аргументу не передував &ви, ви знали, що він не буде змінений. (Зворотне, &означає, що модифіковано, було неправдою, тому що іноді доведеться передавати великі структури лише для читання за constвказівником.)

Деякі стверджують, що це така корисна функція при зчитуванні коду, що параметри вказівника завжди повинні використовуватися для змін параметрів, а не для constпосилань, навіть якщо функція ніколи не очікує a nullptr. Тобто, ті люди стверджують, що fn3()не можна допускати підписи функцій, як вище. Прикладом цього є інструкції Google щодо стилю C ++ .


8

Можливо, деякі метафори допоможуть; У контексті екрану робочого столу -

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

6

Різниця між вказівником та еталоном

Вказівник може бути ініціалізований на 0, а посилання - ні. Насправді, посилання також має посилатися на об’єкт, але покажчик може бути нульовим вказівником:

int* p = 0;

Але ми не можемо мати , int& p = 0;а також int& p=5 ;.

Насправді, щоб зробити це належним чином, ми повинні спочатку оголосити та визначити об’єкт, тоді ми можемо зробити посилання на цей об’єкт, тому правильна реалізація попереднього коду буде такою:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

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

Ще одна відмінність полягає в тому, що вказівник може вказувати на інший об’єкт, однак посилання завжди посилається на той самий об’єкт, давайте візьмемо цей приклад:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

Ще один момент: Коли у нас є такий шаблон, як шаблон STL, такий тип шаблону класу завжди повертає посилання, а не вказівник, щоб легко читати або призначати нове значення за допомогою оператора []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

1
Ми все ще можемо мати const int& i = 0.
Revolver_Ocelot

1
У цьому випадку посилання буде використовуватися лише в режимі читання, ми не можемо змінювати цю посилання const, навіть використовуючи "const_cast", оскільки "const_cast" приймає лише покажчик, не посилання.
dhokar.w

1
const_cast працює з посиланнями досить добре: coliru.stacked-crooked.com/a/eebb454ab2cfd570
Revolver_Ocelot

1
ви робите амплітуду для посилань, не передаваючи посилання, спробуйте це; const int & i =; const_cast <int> (i); я намагаюся відкинути стрімкість посилання, щоб зробити можливим запис і присвоєння нового значення посиланню, але це неможливо. будь ласка фокус !!
dhokar.w

5

Різниця полягає в тому, що нестабільна змінна вказівника (не плутати з покажчиком на константу) може змінюватися протягом певного часу під час виконання програми, вимагає використання семантики покажчика (&, *) операторів, тоді як посилання можуть бути встановлені при ініціалізації тільки (ось чому ви можете встановити їх лише у списку ініціалізатора конструктора, але не якось інше) та використовувати звичайні значення, що мають доступ до семантики. В основному посилання були введені, щоб забезпечити підтримку перевантаження операторів, як я читав у дуже старій книзі. Як хтось заявив у цій темі - покажчик може бути встановлений на 0 або будь-яке значення, яке ви хочете. 0 (NULL, nullptr) означає, що покажчик ініціалізується ні з чим. Це помилка дереференції нульового вказівника. Але насправді вказівник може містити значення, яке не вказує на якесь правильне розташування пам'яті. Посилання, у свою чергу, намагаються не дозволити користувачеві ініціалізувати посилання на те, на що не може бути посилання через те, що ви завжди надаєте для нього реальне значення правильного типу. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не заглиблюватися в цю деталь. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не заглиблюватися в цю деталь. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не копати цю деталь у деталях. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи.


4

простими словами, ми можемо сказати, що посилання є альтернативною назвою змінної, тоді як вказівник - це змінна, яка містить адресу іншої змінної. напр

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.