Я знаю, що посилання є синтаксичним цукром, тому код легше читати та писати.
Але які відмінності?
int &x = *(int*)0;
на gcc. Посилання дійсно може вказувати на NULL.
Я знаю, що посилання є синтаксичним цукром, тому код легше читати та писати.
Але які відмінності?
int &x = *(int*)0;
на gcc. Посилання дійсно може вказувати на NULL.
Відповіді:
Вказівник може бути призначений повторно:
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;
Вказівник має власну адресу пам’яті та розмір у стеці (4 байти на x86), тоді як посилання має ту саму адресу пам’яті (з оригінальною змінною), але також займає деякий простір у стеку. Оскільки посилання має таку саму адресу, що і сама вихідна змінна, можна подумати про посилання як інше ім'я для тієї ж змінної. Примітка. На що вказує вказівник, може бути на стеці чи купі. Дітто довідник. Моє твердження в цьому твердженні не в тому, що вказівник повинен вказувати на стек. Вказівник - це лише змінна, яка містить адресу пам'яті. Ця змінна є у стеці. Оскільки посилання має власний простір у стеці, а оскільки адреса така ж, як і змінна, на яку вона посилається. Більше про стек проти купи. Це означає, що існує справжня адреса посилання, про яку компілятор не скаже вам.
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2);
Ви можете мати покажчики на покажчики на покажчики, що пропонують додаткові рівні непрямості. Тоді як посилання пропонують лише один рівень непрямості.
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);
Вказівник може бути призначений 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
Покажчики можуть перебирати масив; ви можете використовувати ++
для переходу до наступного елемента, на який вказує вказівник, та + 4
для переходу до 5-го елемента. Це не має значення, якого розміру об'єкт на який вказує вказівник.
Вказівник потрібно відкинути, *
щоб отримати доступ до місця пам'яті, на яке він вказує, тоді як посилання може використовуватися безпосередньо. Вказівник на клас / структуру використовує ->
для доступу до своїх членів, тоді як посилання використовує a .
.
Посилання не можуть бути заповнені в масив, тоді як покажчики можуть бути (Згадується користувачем @litb)
Посилання на Const можуть бути прив’язані до тимчасових. Покажчики не можуть (не без деякої непрямості):
const int &x = int(12); //legal C++
int *y = &int(12); //illegal to dereference a temporary.
Це робить const&
більш безпечним для використання у списках аргументів тощо.
Посилання можна розглядати як постійний покажчик (не слід плутати з покажчиком на значення константи!) З автоматичною побічно, тобто компілятор буде застосовувати *
оператор для вас.
Усі посилання повинні бути ініціалізовані з ненульовим значенням, або компіляція не вдасться. Отримати адресу посилання неможливо - адресовий оператор замість цього поверне адресу посиланого значення - а також не можна робити арифметику на посиланнях.
Програмісти C можуть не сподобатися посилань на C ++, оскільки це більше не буде очевидним, коли відбувається непрямість або якщо аргумент передається за значенням або за вказівником, не дивлячись на підписи функцій.
Програмісти на C ++ можуть не подобатися використовувати покажчики, оскільки вони вважаються небезпечними - хоча посилання насправді не безпечніші, ніж постійні покажчики, за винятком самих тривіальних випадків, - не вистачає зручності автоматичного опосередкування та мають різну смислову конотацію.
Розглянемо наступне твердження із поширених запитань C ++ :
Навіть незважаючи на те, що посилання часто реалізовується за допомогою адреси на основній мові збірки, будь ласка, не думайте про посилання як на кумедний вказівник на об'єкт. Довідка - це об'єкт. Це не вказівник на об’єкт, ані копія об'єкта. Він є об'єктом.
Але якщо об'єктом насправді було посилання, то як можуть існувати звисаючі посилання? У некерованих мовах неможливо, щоб посилання були "безпечнішими", ніж покажчики - зазвичай просто немає способу надійного псевдонімування значень через межі області!
Виходячи з фону C, посилання на C ++ може виглядати дещо дурною концепції, але все ж слід використовувати їх замість покажчиків , де це можливо: Автоматична опосередкованість є зручним, а також посилання стають особливо корисними при роботі з RAII - але не з - за який - або передбачуваної безпеки перевага, а швидше тому, що вони роблять написання ідіоматичного коду менш незручним.
RAII є однією з центральних концепцій C ++, але вона нетривіально взаємодіє з семантикою копіювання. Передача об'єктів за посиланням дозволяє уникнути цих проблем, оскільки жодне копіювання не пов'язане. Якщо посилань не було в мові, вам доведеться використовувати вказівники, які є більш громіздкими, тим самим порушуючи принцип мовної розробки, що рішення найкращої практики має бути простішим за альтернативи.
Якщо ви хочете бути дійсно педантичним, є одне, що ви можете зробити з посиланням, яке ви не можете зробити з покажчиком: продовжити термін експлуатації тимчасового об’єкта. У 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
він не зможе компілювати. Ви не можете прив’язати нестандартну посилання на тимчасовий об'єкт, а також не можете взяти його адресу з цього питання.
const &
зв'язування, і лише тоді, коли посилання виходить за межі, викликається деструктор фактичного посиланого типу (порівняно з еталонним типом, який може бути базовим). Оскільки це посилання, між ними не відбудеться жодне нарізання.
Animal x = fast ? getHare() : getTortoise()
тоді x
зіткнетесь з класичною проблемою нарізки, тоді як Animal& x = ...
будете працювати правильно.
Крім синтаксичного цукру, посиланням є const
вказівник ( не вказівник на a const
). Ви повинні встановити, на що воно посилається, коли ви оголошуєте довідкову змінну, і ви не можете її змінити пізніше.
Оновлення: тепер, коли я подумаю про це ще трохи, є важлива різниця.
Ціль вказівника const можна замінити, взявши його адресу та використовуючи команду const.
Ціль посилання не може бути замінена жодним чином, ніж UB.
Це повинно дозволити компілятору зробити більш оптимізацію посилань.
T* const
різний синтаксичний цукор (це може призвести до усунення багатьох * і & з вашого коду).
int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
це нормально.
Всупереч поширеній думці, посилання може бути 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.
Ще один приклад небезпеки перенаправлення нульового вказівника див. Розкриття невизначеної поведінки при спробі перенесення коду на іншу платформу Реймонда Чена.
Ви забули найважливішу частину:
член-доступ з покажчиками використовує ->
доступ члена з використанням посилань.
foo.bar
це явно перевершував foo->bar
таким же чином , що VI є явно перевершує Emacs :-)
->
передбачає посилання на покажчики, так само, як і на сам покажчик.
.
і ->
має щось спільне з vi та emacs :)
.
краще, ніж використовувати ->
, але так само, як vi vs emacs, це повністю суб'єктивно, і ви нічого не можете довести
Посилання дуже схожі на покажчики, але вони спеціально створені, щоб бути корисними для оптимізації компіляторів.
Як приклад:
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)
руйнування, руйнуються в кінці виразу. Однак, прив’язуючи цей об'єкт до посилання, ref
C ++ продовжить термін експлуатації цього тимчасового об'єкта до тих пір, поки він ref
не вийде за межі.
maybeModify
не приймає адреси нічого, пов'язаного з цим x
, істотно простіше, ніж довести, що купа арифметики вказівника не відбувається.
void maybeModify(int& x) { 1[&x]++; }
, який обговорюють інші коментарі вище
Насправді, посилання не дуже схоже на вказівник.
Компілятор зберігає "посилання" на змінні, асоціюючи ім'я з адресою пам'яті; це його завдання перевести будь-яке ім’я змінної на адресу пам'яті при компілюванні.
Коли ви створюєте посилання, ви лише повідомляєте компілятору, що ви присвоюєте інше ім'я змінній вказівника; тому посилання не можуть "вказувати на нуль", тому що змінна не може бути і не бути.
Покажчики є змінними; вони містять адресу якоїсь іншої змінної або можуть бути нульовими. Важливо те, що вказівник має значення, тоді як у посилання є лише змінна, на яку він посилається.
Тепер кілька пояснень реального коду:
int a = 0;
int& b = a;
Тут ви не створюєте іншої змінної, яка вказує на a
; ви просто додаєте інше ім’я до вмісту пам'яті, що містить значення a
. Ця пам’ять тепер має два імена, a
і b
, і її можна вирішити за допомогою будь-якого імені.
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
Під час виклику функції компілятор, як правило, генерує простори пам'яті для аргументів, які потрібно скопіювати. Підпис функції визначає пробіли, які слід створити, і дає ім'я, яке слід використовувати для цих пробілів. При оголошенні параметра як посилання просто вказується компілятору використовувати вхідний простір змінної пам'яті замість виділення нового простору пам'яті під час виклику методу. Може здатися дивним, що ваша функція буде безпосередньо маніпулювати змінною, оголошеною в області виклику, але пам’ятайте, що при виконанні компільованого коду більше немає області; є просто рівна пам'ять, і ваш код функції може маніпулювати будь-якими змінними.
Зараз можуть бути деякі випадки, коли ваш компілятор може не знати посилання при компілюванні, як, наприклад, при використанні зовнішньої змінної. Таким чином, посилання може бути або не може бути реалізована як вказівник у базовому коді. Але в наведених вами прикладах він, швидше за все, не буде реалізований за допомогою вказівника.
Посилання ніколи не може бути NULL
.
void Foo::bar() { virtual_baz(); }
ці segfaults. Якщо ви не знаєте, що посилання можуть бути нульовими, ви не можете простежити нуль назад до його початку.
int &r=*p;
- невизначена поведінка. У цей момент, ви не маєте «кількість посилань вказує на NULL," у вас є програма , яка вже не може бути вмотивованою про взагалі .
Хоча і посилання, і покажчики використовуються для опосередкованого доступу до іншого значення, є дві важливі відмінності між посиланнями та покажчиками. Перший полягає в тому, що посилання завжди посилається на об’єкт: Помилка визначення посилання без ініціалізації. Поведінка присвоєння є другою важливою відмінністю: Призначення посилання змінює об'єкт, до якого посилання пов'язана; це не відновлює посилання на інший об'єкт. Після ініціалізації посилання завжди посилається на той самий базовий об'єкт.
Розглянемо ці два фрагменти програми. У першому ми призначаємо один вказівник іншому:
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, а не саме посилання. Після присвоєння обидві посилання все ще відносяться до своїх оригінальних об'єктів, і значення цих об'єктів тепер є однаковим.
Існує семантична різниця, яка може здатися езотеричною, якщо ви не знайомі з вивченням комп'ютерних мов абстрактно або навіть академічно.
На найвищому рівні ідея посилань полягає в тому, що вони є прозорими "псевдонімами". Ваш комп'ютер може використовувати адресу, щоб змусити їх працювати, але ви не повинні турбуватися про це: ви повинні вважати їх "просто іншим ім'ям" для існуючого об'єкта, і синтаксис це відображає. Вони суворіші за покажчики, тому ваш компілятор може надійніше попередити вас, коли ви збираєтеся створити звисаючу посилання, ніж коли ви збираєтеся створити звисаючий покажчик.
Крім цього, звичайно, є деякі практичні відмінності між вказівниками та посиланнями. Синтаксис їх використання, очевидно, відрізняється, і ви не можете "переставляти" посилання, мати посилання на небуття або вказувати на посилання.
Посилання є псевдонімом для іншої змінної, тоді як вказівник містить адресу пам'яті змінної. Посилання зазвичай використовуються як параметри функції, так що переданий об'єкт є не копією, а самим об'єктом.
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.
Не має значення, скільки місця займає, оскільки ви фактично не можете побачити будь-який побічний ефект (без виконання коду) того місця, який би він займав.
З іншого боку, одна основна відмінність між посиланнями та покажчиками полягає в тому, що тимчасові часописи, призначені 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 працювати.
На цьому ґрунтується підручник . Що написано, це робить більш зрозумілим:
>>> 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
.
nullptr
? Ви насправді читали будь-яку іншу частину цієї теми, або ...?
Посилання - це не інше ім'я, яке дається деякій пам'яті. Це незмінний покажчик, який автоматично де-посилається на використання. В основному він зводиться до:
int& j = i;
Це внутрішньо стає
int* const j = &i;
const
вказівника. Ця гнучкість не доводить, що існує різниця між еталонним та вказівним.
Що таке посилання на C ++? Якийсь конкретний екземпляр типу, який не є типом об'єкта .
Що таке покажчик на C ++? Якийсь конкретний екземпляр типу, який є типом об'єкта .
З визначення ISO C ++ визначення типу об'єкта :
Об'єкт тип (можливо резюме -qualified) тип , який не є типом функції, а НЕ контрольний тип, а не резюме недійсним.
Це може бути важливо знати, тип об'єкта - це категорія всесвіту вищого рівня в C ++. Довідка також є категорією вищого рівня. Але вказівник - ні.
Покажчики та посилання згадуються разом у контексті складного типу . В основному це пов'язано з характером синтаксису декларатора, успадкованого від (і розширеного) C, на який немає посилань. (Крім того, існує більше ніж один вид декларатора посилань, починаючи з C ++ 11, тоді як покажчики все ще "єднані": &
+ &&
vs.. *
) Тому складання мови, специфічної за допомогою "розширення", з подібним стилем C у цьому контексті є дещо розумним . (Я все ще буду стверджувати, що синтаксис деклараторів сильно витрачає синтаксичну виразність , робить як користувачів користувачів, так і реалізацію фрустрацією. Таким чином, всі вони не можуть бути вбудованимиу новому мовному дизайні. Це зовсім інша тема щодо дизайну ПЛ.)
В іншому випадку неістотно, що вказівники можна кваліфікувати як певні типи типів із посиланнями разом. Вони просто ділять занадто мало загальних властивостей, крім схожості синтаксису, тому в більшості випадків немає необхідності поєднувати їх.
Зауважте, що вищевказані висловлювання зазначають лише "покажчики" та "посилання" як типи. Є кілька зацікавлених питань щодо їх примірників (наприклад, змінні). Існує також занадто багато помилок.
Відмінності категорій вищого рівня вже можуть виявити багато конкретних відмінностей, не пов'язаних безпосередньо з покажчиками:
cv
кваліфікатори вищого рівня . Посилання не можуть.Ще кілька спеціальних правил щодо посилань:
&&
параметрів (як "посилання на переадресацію"), засновані на згортанні посилань під час виведення параметра шаблону, дозволяють "ідеальне пересилання" параметрів.std::initializer_list
деякі подібні правила продовження терміну експлуатації. Це ще одна банка глистів.Я знаю, що посилання є синтаксичним цукром, тому код легше читати та писати.
Технічно це очевидно неправильно. Посилання не є синтаксичним цукром жодної іншої функції в C ++, тому що їх неможливо точно замінити іншими ознаками без будь-яких смислових відмінностей.
(Аналогічно, лямбда-вираз s не є синтаксичним цукром жодної іншої функції в C ++, тому що його неможливо точно моделювати за допомогою "невизначених" властивостей, таких як порядок декларування захоплених змінних , що може бути важливим, оскільки порядок ініціалізації таких змінних може бути суттєвий.)
C ++ має лише кілька видів синтаксичних цукрів у цьому суворому розумінні. Один екземпляр - це (успадкований від C) вбудований (не перевантажений) оператор []
, який визначається саме таким, що має однакові смислові властивості специфічних форм поєднання над вбудованим оператором одинарним *
та бінарним+
.
Отже, і вказівник, і посилання використовують однаковий об'єм пам'яті.
Виклад вище є просто неправильним. Щоб уникнути подібних помилок, перегляньте натомість правила ISO C ++:
Від [intro.object] / 1 :
... Об'єкт займає область зберігання як у період його будівництва, так і впродовж усього періоду його знищення. ...
З [dcl.ref] / 4 :
Не визначено, потребує зберігання посилання чи ні.
Зауважте, це семантичні властивості.
Навіть те, що покажчики недостатньо кваліфіковані, щоб їх можна було скласти разом із посиланнями у розумінні мовної конструкції, все ж є деякі аргументи, що дозволяють робити вибір між ними в інших контекстах, наприклад, при виборі типів параметрів.
Але це ще не вся історія. Я маю на увазі, що є більше речей, ніж покажчики проти посилань, які ви повинні враховувати.
Якщо вам не доведеться дотримуватися таких надто конкретних виборів, у більшості випадків відповідь коротка: у вас немає необхідності використовувати вказівники, тому ви цього не робите . Покажчики зазвичай є досить поганими, оскільки вони передбачають занадто багато речей, яких ви не очікуєте, і вони будуть покладатися на занадто багато неявних припущень, що підривають ремонтопридатність та (навіть) портативність коду. Непотрібно покладатися на покажчики, безумовно, поганий стиль, і цього слід уникати в розумінні сучасного C ++. Перегляньте своє призначення, і ви нарешті виявите, що вказівник є особливістю останніх сортів у більшості випадків.
&
тип посилання як тип 1-го параметра. (І зазвичай це слід const
кваліфікувати.)&&
тип посилання як тип 1-го параметра. (І зазвичай не повинно бути кваліфікаторів.)operator=
функціями спеціальних членів вимагає типів посилань, подібних 1-му параметру конструкторів копіювання / переміщення.++
вимагає пустушки int
.unique_ptr
та shared_ptr
(або навіть з домашнім мовою самостійно, якщо вони вимагають, щоб вони були непрозорими ), а не необробленими покажчиками.std::optional
, а не сирі вказівники.observer_ptr
у бібліотеці Fundamental Fund TS.Єдині винятки неможливо вирішити поточною мовою:
operator new
. (Однак cv - void*
все-таки зовсім інший і безпечніший порівняно зі звичайними вказівниками на об'єкти, оскільки він виключає несподівану арифметику вказівника, якщо ви не покладаєтесь на якесь невідповідне розширення на void*
зразок GNU.)Тож на практиці відповідь така очевидна: коли сумніваєтесь, уникайте покажчиків . Використовувати покажчики потрібно лише тоді, коли є чіткі причини, що більше нічого не підходить. За винятком кількох виняткових випадків, згаданих вище, такі варіанти вибору майже завжди не є суто специфічними для C ++ (але, можливо, залежать від мови). Такими прикладами можуть бути:
Якщо ви побачите це питання через якийсь результат пошуку Google (не характерний для C ++) , це, ймовірно, неправильне місце.
Посилання в C ++ є досить «дивною», так як вона, по суті , не першим класом: вони будуть розглядатися як об'єкти або функції іменуються таким чином , вони не мають жодних шансів на підтримку деяких операцій першого класу , як буде лівий операндом Оператор доступу членів незалежно від типу об'єкта, що посилається. Інші мови можуть мати або не мати подібних обмежень щодо своїх посилань.
Посилання на C ++, ймовірно, не збережуть значення для різних мов. Наприклад, посилання взагалі не передбачають ненульових властивостей для таких значень, як вони в C ++, тому такі припущення можуть не працювати в деяких інших мовах (і зустріти приклади ви знайдете досить легко, наприклад, Java, C #, ...).
Серед загальних посилань на різних мовах програмування взагалі можуть бути деякі загальні властивості, але давайте залишимо це для деяких інших питань в ТА
(Побічна примітка: питання може бути важливим раніше, ніж будь-які мови, що стосуються "С-подібних", наприклад ALGOL 68 проти PL / I. )
Посилання на покажчик можливе в 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;
}
Я використовую посилання, якщо мені не потрібна будь-яка з цих:
Нульові покажчики можуть використовуватися як дозорне значення, часто дешевий спосіб уникнути перевантаження функцій або використання булінгу.
Ви можете робити арифметику за вказівником. Наприклад,p += offset;
&r + offset
де r
було оголошено посилання
Існує одна принципова відмінність між вказівниками та посиланнями, про які я не бачив, щоб хтось згадав: посилання дозволяють проходити семантику проходження посилання в аргументах функції. Покажчики, хоча спочатку це не видно: вони лише надають семантику прохідних значень. Це було дуже красиво описано в цій статті .
З повагою, & rzej
Ризикуючи додати плутанину, я хочу ввести деякий вклад, я впевнений, що це в основному залежить від того, як компілятор реалізує посилання, але у випадку 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? Ну, очевидно, вона компілює добре, але викликає помилку сегментації під час виконання, оскільки вона більше не вказує на дійсну змінну, у нас, по суті, є зламана посилання, яка все ще існує (поки вона не виходить із сфери застосування), але марна.
Іншими словами, посилання - це не що інше, як покажчик, у якому відключена механіка вказівника, що робить його більш безпечним та простим у використанні (не випадкова математика вказівника, ніяке змішування '.' Та '->' тощо), якщо припускати, що ви не спробуйте будь-якої дурниці, як мої приклади вище;)
Тепер незалежно від того, як компілятор обробляє посилання, він завжди матиме якийсь вказівник під кришкою, тому що посилання має посилатися на певну змінну за певною адресою пам'яті, щоб вона працювала, як очікувалося, цього не обійти (отже термін 'довідник').
Єдине головне правило, яке важливо пам’ятати при посиланнях, - це те, що вони повинні бути визначені під час декларування (за винятком посилання в заголовку; у цьому випадку воно має бути визначене в конструкторі, після того, як об'єкт, який він міститься, є побудовано, це визначити занадто пізно).
Пам'ятайте, мої приклади, наведені вище, - це лише те, що в прикладах, що демонструють, що таке посилання, ви ніколи не хочете використовувати посилання цими способами! Для правильного використання довідки вже тут багато відповідей, які вдарили нігтем по голові
Ще одна відмінність полягає в тому, що ви можете мати вказівники на тип пустоти (а це означає вказівник на що-небудь), але посилання на недійсні заборонені.
int a;
void * p = &a; // ok
void & p = a; // forbidden
Не можу сказати, що я дуже задоволений цією різницею. Я дуже вважаю за краще, щоб це було дозволено із значенням посилання на що-небудь з адресою та в іншому випадку таку саму поведінку для посилань. Це дозволило б визначити деякі еквіваленти функцій бібліотеки С, як memcpy, використовуючи посилання.
Також посилання, яке є параметром функції, яка є вкладеною, може оброблятися інакше, ніж покажчик.
void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
int testptr=0;
increment(&testptr);
}
void increftest()
{
int testref=0;
increment(testref);
}
Багато компіляторів, коли вбудовується версія вказівника перша, насправді змусить записати в пам'ять (адресу ми беремо прямо). Однак вони залишать посилання в реєстрі, який є більш оптимальним.
Звичайно, для функцій, які не є вкладеними вказівниками та посиланням, генерується один і той же код, і завжди краще передавати властивості за значенням, ніж за посиланням, якщо вони не змінюються і повертаються функцією.
Ще одне цікаве використання посилань - це надання аргументу за замовчуванням визначеного користувачем типу:
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 на тимчасовий" аспект посилань.
Ця програма може допомогти зрозуміти відповідь на питання. Це проста програма посилання "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
Я відчуваю, що є ще один момент, який тут не висвітлювався.
На відміну від покажчиків, посилання синтаксично еквівалентні об'єкту, на який вони посилаються, тобто будь-яка операція, яка може бути застосована до об'єкта, працює для посилання та з точно таким же синтаксисом (виняток, звичайно, є ініціалізацією).
Хоча це може здаватися поверхневим, я вважаю, що ця властивість є визначальною для ряду функцій C ++, наприклад:
Шаблони . Оскільки параметри шаблона мають тип качки, синтаксичні властивості типу - це все, що має значення, тому часто один і той же шаблон може використовуватися і з T
і T&
.
(або std::reference_wrapper<T>
які досі покладаються на неявну роль у
шаблонах T&
)
Шаблони, які охоплюють і те, T&
і T&&
ще більше поширені.
Значення . Розглянемо вислів str[0] = 'X';
Без посилань, він би працював лише для c-рядків ( char* str
). Повернення символу шляхом посилання дозволяє визначеним користувачем класам мати однакові позначення.
Конструктори копіювання . Синтаксично має сенс передавати об’єкти на копіювання конструкторів, а не вказівники на об’єкти. Але просто немає можливості конструктору копій взяти об'єкт за значенням - це призведе до рекурсивного виклику до того ж конструктора копій. Тут залишаються посилання як єдиний варіант.
Оператор перевантажує . За допомогою посилань можна ввести непрямий виклик оператора - скажімо, operator+(const T& a, const T& b)
зберігаючи те саме позначення інфіксації. Це також працює для регулярних перевантажених функцій.
Ці пункти забезпечують значну частину C ++ та стандартної бібліотеки, тому це досить велика властивість посилань.
Існує дуже важлива нетехнічна різниця між вказівниками та посиланнями: Аргумент, переданий функції вказівником, набагато помітніший, ніж аргумент, переданий функції за допомогою 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 ++ .
Вказівник може бути ініціалізований на 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 "="
const int& i = 0
.
Різниця полягає в тому, що нестабільна змінна вказівника (не плутати з покажчиком на константу) може змінюватися протягом певного часу під час виконання програми, вимагає використання семантики покажчика (&, *) операторів, тоді як посилання можуть бути встановлені при ініціалізації тільки (ось чому ви можете встановити їх лише у списку ініціалізатора конструктора, але не якось інше) та використовувати звичайні значення, що мають доступ до семантики. В основному посилання були введені, щоб забезпечити підтримку перевантаження операторів, як я читав у дуже старій книзі. Як хтось заявив у цій темі - покажчик може бути встановлений на 0 або будь-яке значення, яке ви хочете. 0 (NULL, nullptr) означає, що покажчик ініціалізується ні з чим. Це помилка дереференції нульового вказівника. Але насправді вказівник може містити значення, яке не вказує на якесь правильне розташування пам'яті. Посилання, у свою чергу, намагаються не дозволити користувачеві ініціалізувати посилання на те, на що не може бути посилання через те, що ви завжди надаєте для нього реальне значення правильного типу. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не заглиблюватися в цю деталь. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не заглиблюватися в цю деталь. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи. Хоча існує маса способів зробити ініціалізацію змінної посилання до неправильного місця пам'яті - вам краще не копати цю деталь у деталях. На рівні машини і покажчик, і еталон працюють рівномірно - за допомогою покажчиків. Скажімо, у суттєвих посиланнях - синтаксичний цукор. Посилання rvalue відрізняються від цього - вони є природними об'єктами стека / купи.
простими словами, ми можемо сказати, що посилання є альтернативною назвою змінної, тоді як вказівник - це змінна, яка містить адресу іншої змінної. напр
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 */