Чому покажчики збільшення?


25

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

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

Це запитання виникло після того, як я почав читати книгу "Екскурсія по C ++" від Bjarne Stroustrup. Мені було рекомендовано цю книгу, тому що я досить добре знайомий з Java, і хлопці з Reddit сказали мені, що це буде добре "переключення" книги .


11
Вказівник - просто ітератор
Чарльз Сальвія

1
Це один з улюблених інструментів запису комп'ютерних вірусів, які читають те, що вони не повинні читати. Це також один з найпоширеніших випадків вразливості в додатках (коли збільшується покажчик повз область, де вони повинні, а потім читає чи записує)> Перегляньте помилку HeartBleed.
Сем

1
@vasile Це те, що погано стосується покажчиків.
Cruncher

4
Приємна / погана річ у програмі C ++ полягає в тому, що вона дозволяє зробити набагато більше, перш ніж викликати segfault. Зазвичай ви отримуєте сегмент за замовчуванням, намагаючись отримати доступ до пам'яті іншого процесу, системної пам'яті або захищеної пам’яті додатків. Будь-який доступ до звичайних сторінок додатків системою дозволений, і програміст / компілятор / мова має право застосовувати розумні межі. C ++ в значній мірі дозволяє робити все, що завгодно. Що стосується openssl, який має власного менеджера пам'яті - це неправда. Він просто має за замовчуванням механізми доступу до пам'яті C ++.
Сем

1
@INdek: Ви можете отримати лише сегмент за замовчуванням, якщо буде захищена пам'ять, до якої ви намагаєтесь отримати доступ. Більшість операційних систем призначають захист на рівні сторінки, тому зазвичай ви можете отримати доступ до всього, що знаходиться на сторінці, на якій починається ваш покажчик. Якщо ОС використовує 4K розмір сторінки, це досить велика кількість даних. Якщо ваш покажчик починається десь у купі, хтось здогадається, скільки даних ви могли отримати доступ.
TMN

Відповіді:


46

Коли у вас є масив, ви можете встановити вказівник, який би вказував на елемент масиву:

int a[10];
int *p = &a[0];

Тут pвказується на перший елемент a, який є a[0]. Тепер ви можете збільшити покажчик, щоб вказати на наступний елемент:

p++;

Тепер pвказує на другий елемент, a[1]. Ви можете отримати доступ до елемента тут, використовуючи *p. Це відрізняється від Java, де вам доведеться використовувати цілу змінну індексу для доступу до елементів масиву.

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


23
Так, за допомогою C ++ ви несете відповідальність за уникнення помилок програмування, таких як доступ поза межами масиву.
Грег Х'югілл

9
Ні, посилення вказівника, який вказує на що-небудь, крім елемента масиву, є невизначеним поведінкою. Однак якщо ви робите щось низьке та не портативне, то збільшення покажчика зазвичай є не що інше, як доступ до наступної речі в пам’яті, що б там не сталося.
Грег Х'югілл

4
Є кілька речей, які є або можуть бути розцінені як масив; рядок тексту - це, власне, масив символів. У деяких випадках довгий int трактується як масив байтів, хоча це може легко зашкодити вам.
AMADANON Inc.

6
Це вказує на тип , але поведінка описана в 5.7 Оператори добавок [expr.add]. Зокрема, 5,7 / 5 говорить про те, що переходити будь-де за межі масиву, окрім того, що знаходиться в кінці, є UB.
Марно

4
Останній абзац: Якщо і операнд вказівника, і результат вказують на елементи одного об'єкта масиву, оцінка не повинна створювати переповнення; інакше поведінка не визначена . Отже, якщо результат не є ні в масиві, ні в кінці кінця, ви отримуєте UB.
Марно

37

Збільшення покажчиків є ідіоматичним C ++, оскільки семантика покажчиків відображає фундаментальний аспект філософії дизайну, що стоїть за стандартною бібліотекою C ++ (заснована на STL Олександра Степанова )

Тут важливою концепцією є те, що STL розроблений навколо контейнерів, алгоритмів та ітераторів. Покажчики - просто ітератори .

Звичайно, можливість збільшення (або додавання / віднімання з) покажчиків повертається до C. Багато алгоритмів маніпулювання C-рядками можна записати просто за допомогою арифметики вказівника. Розглянемо наступний код:

char string1[4] = "abc";
char string2[4];
char* src = string1;
char* dest = string2;
while ((*dest++ = *src++));

Цей код використовує арифметику вказівника для копіювання нульового завершення C-рядка. Цикл автоматично припиняється, коли він стикається з нулем.

За допомогою C ++ семантика вказівників узагальнена до поняття ітераторів . Більшість контейнерів стандарту C ++ забезпечують ітератори, які можуть бути доступні через beginта endчлен функцій. Ітератори поводяться як вказівники, оскільки вони можуть збільшуватися, дереференціюватися, а іноді і зменшуватися, або просуватися.

Щоб повторити std::string, ми б сказали:

std::string s = "abcdef";
std::string::iterator it = s.begin();
for (; it != s.end(); ++it) std::cout << *it;

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

std::string s1 = "abcdef";
std::vector<char> buf;
std::copy(s1.begin(), s1.end(), std::back_inserter(buf));

Цей код копіює рядок у вектор. copyФункція являє собою шаблон , який буде працювати з будь-яким ітератора , який підтримує збільшується (який включає в себе прості покажчики). Ми могли б використовувати ту саму copyфункцію на звичайному C-рядку:

   const char* s1 = "abcdef";
   std::vector<char> buf;
   std::copy(s1, s1 + std::strlen(s1), std::back_inserter(buf));

Ми могли б використовувати copyна умовах std::mapабо std::setабо будь-який призначений для користувача контейнер , який підтримує ітератори.

Зверніть увагу , що покажчики типу конкретного ітератора: випадковий итератор доступу , що означає , що вони підтримують збільшення, декремент і просування вперед з +і -оператором. Інші типи ітераторів підтримують лише підмножину семантики вказівників: двонаправлений ітератор підтримує принаймні збільшення та декрементацію; а вперед ітератори підтримує , щонайменше , приріст. (Усі типи ітераторів підтримують перенаправлення.) copyФункція вимагає ітератора, який принаймні підтримує збільшення.

Про різні концепції ітератора ви можете прочитати тут .

Отже, збільшувальні покажчики - це ідіоматичний спосіб C ++ для ітерації через C-масив або доступу до елементів / зсувів у C-масиві.


3
Хоча я використовую вказівники, як у першому прикладі, я ніколи не думав про це як про ітератор, зараз це має багато сенсу.
барвники

1
"Цикл автоматично припиняється, коли він стикається з нулем." Це жахливий ідіом.
Чарльз Вуд

9
@CharlesWood, то, мабуть, ви повинні знайти C досить жахливим
Siler

7
@CharlesWood: Альтернативою є використання довжини рядка як змінної керування циклом, що означає перехід рядка двічі (один раз для визначення довжини та один раз для копіювання символів). Якщо ви працюєте на 1 МГц PDP-7, це дійсно може почати додаватися.
TMN

3
@INdek: Перш за все, C і C ++ намагаються уникнути будь-якої ціни, щоб ввести зміни, - і я б сказав, що зміна поведінки рядкових літералів за замовчуванням було б досить модифікацією. Але найголовніше, що нульові завершені рядки - це лише умова (полегшується вслід за тим, що літеральні рядки за замовчуванням закінчуються нулем і що їх очікують функції бібліотеки), ніхто не заважає вам використовувати перелічені рядки в С - власне, кілька бібліотек C використовують їх (див., наприклад, BSTR OLE).
Matteo Italia

16

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

Існує безліч систем, де "приріст реєстру" швидше "завантаження постійного значення 1 і додавання до реєстру". Більше того, досить багато систем дозволяють "завантажувати DWORD в A з адреси, вказаної в регістрі B, а потім додавати sizeof (DWORD) до B" в одній інструкції. Ці дні ви можете очікувати на оптимізаційний компілятор, щоб розібратися в цьому для вас, але це було насправді не таким варіантом у 1973 році.

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

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