Зачем увеличивать указатели?

25

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

Не в традиционном смысле, я понимаю, что это такое, и почему они используются, и как они могут быть полезны, однако я не могу понять, насколько полезны инкрементные указатели, может ли кто-нибудь дать объяснение того, как инкрементный указатель является полезная концепция и идиоматический C ++?

Этот вопрос возник после того, как я начал читать книгу Bjarne Stroustrup « Путешествие по C ++ », мне порекомендовали эту книгу, потому что я хорошо знаком с Java, и ребята из Reddit сказали мне, что это будет хорошая книга «переключения» ,

INdek
источник
11
Указатель - просто итератор
Чарльз Сальвия
1
Это один из любимых инструментов для написания компьютерных вирусов, которые читают то, что им не следует читать. Это также один из наиболее распространенных случаев уязвимости в приложениях (когда кто-то увеличивает указатель на область, где они должны, затем читает или записывает ее)> См. Ошибку HeartBleed.
Сэм
1
@vasile Вот что плохо в указателях.
Cruncher
4
Хорошая / плохая вещь в C ++ состоит в том, что он позволяет вам сделать гораздо больше, прежде чем вызывать segfault. Обычно вы получаете segfault, когда пытаетесь получить доступ к памяти другого процесса, системной памяти или памяти защищенного приложения. Любой доступ к обычным страницам приложения разрешен системой, и только программист / компилятор / язык может применять разумные ограничения. C ++ в значительной степени позволяет вам делать все, что вы хотите. Что касается openssl, имеющего собственный менеджер памяти - это неправда. Он просто имеет стандартные механизмы доступа к памяти C ++.
Сэм
1
@INdek: Вы получите только segfault, если память, к которой вы пытаетесь получить доступ, защищена. Большинство операционных систем назначают защиту на уровне страницы, поэтому вы обычно можете получить доступ ко всему, что находится на странице, на которой начинается указатель. Если в ОС используется размер страницы 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
Есть несколько вещей, которые являются или могут рассматриваться как массив; строка текста - это массив символов. В некоторых случаях long 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или любом другом контейнере, который поддерживает итераторы.

Обратите внимание, что указатели представляют собой особый тип итератора: итератор с произвольным доступом , что означает, что они поддерживают увеличение, уменьшение и продвижение с помощью оператора +and -. Другие типы итераторов поддерживают только подмножество семантики указателей: двунаправленный итератор поддерживает как минимум увеличение и уменьшение; а вперед итераторы поддерживает , по меньшей мере , приращение. (Все типы итераторов поддерживают разыменование.) Для copyфункции требуется итератор, который, по крайней мере, поддерживает инкремент.

Вы можете прочитать о различных концепциях итераторов здесь .

Таким образом, инкрементные указатели являются идиоматическим способом C ++ для итерации по C-массиву или доступа к элементам / смещениям в C-массиве.

Чарльз Сальвиа
источник
3
Хотя я использую указатели, как в первом примере, я никогда не думал об этом как об итераторе, теперь это имеет большой смысл.
красители
1
«Цикл автоматически завершается, когда он встречается с нулем». Это ужасная идиома.
Чарльз Вуд
9
@CharlesWood, тогда, я думаю, вы найдете C довольно пугающим
Siler
7
@CharlesWood: альтернатива заключается в использовании длины строки в качестве переменной управления циклом, что означает двукратное прохождение строки (один раз для определения длины и один раз для копирования символов). Когда вы работаете на 1 МГц PDP-7, это действительно может начать складываться.
TMN
3
@INdek: во-первых, C и C ++ стараются любой ценой избегать внесения критических изменений - и я бы сказал, что изменение поведения строковых литералов по умолчанию было бы довольно сложно. Но самое главное, строки с нулевым символом в конце - это просто соглашение (за ним легко следовать тому факту, что строковые литералы заканчиваются нулем по умолчанию и функции библиотеки ожидают их), никто не мешает вам использовать подсчитанные строки в C - на самом деле, некоторые библиотеки C используют их (см., например, BSTR OLE).
Matteo Italia
16

Арифметика указателей в C ++, потому что она была в C. Арифметика указателей в C, потому что это нормальная идиома в ассемблере .

Существует множество систем, в которых «регистр приращения» быстрее, чем «загрузить постоянное значение 1 и добавить в регистр». Более того, довольно много систем позволяют вам «загрузить DWORD в A с адреса, указанного в регистре B, а затем добавить sizeof (DWORD) к B» в одной инструкции. В наши дни вы можете ожидать, что оптимизирующий компилятор решит эту проблему за вас, но в 1973 году такой возможности не было.

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

pjc50
источник