Язык C++ позволяет выполнять целочисленные операции сложения/вычитания с указателями. Если ptr
указывает на целое число, то ptr + 1
является адресом следующего целочисленного значения в памяти после ptr
. ptr - 1
— это адрес предыдущего целочисленного значения (перед ptr
).
Адресная арифметика
Обратите внимание, ptr + 1
не возвращает следующий любой адрес памяти, который находится сразу после ptr
, но он возвращает адрес памяти следующего объекта, тип которого совпадает с типом значения, на которое указывает ptr
. Если ptr
указывает на адрес памяти целочисленного значения (размер которого 4 байта), то ptr + 3
будет возвращать адрес памяти третьего целочисленного значения после ptr
. Если ptr
указывает на адрес памяти значения типа char, то ptr + 3
будет возвращать адрес памяти третьего значения типа char после ptr
.
При вычислении результата выражения адресной арифметики (или «арифметики с указателями») компилятор всегда умножает целочисленный операнд на размер объекта, на который указывает указатель. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int value = 8; int *ptr = &value; std::cout << ptr << '\n'; std::cout << ptr+1 << '\n'; std::cout << ptr+2 << '\n'; std::cout << ptr+3 << '\n'; return 0; } |
Результат на моем компьютере:
002CF9A4
002CF9A8
002CF9AC
002CF9B0
Как вы можете видеть, каждый последующий адрес увеличивается на 4. Это связано с тем, что размер типа int на моем компьютере составляет 4 байта.
Та же программа, но с использованием типа short вместо типа int:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { short value = 8; short *ptr = &value; std::cout << ptr << '\n'; std::cout << ptr+1 << '\n'; std::cout << ptr+2 << '\n'; std::cout << ptr+3 << '\n'; return 0; } |
Результат:
002BFA20
002BFA22
002BFA24
002BFA26
Поскольку тип short занимает 2 байта, то каждый следующий адрес больше предыдущего на 2.
Расположение элементов массива в памяти
Используя оператор адреса &
, мы можем легко определить, что элементы массива расположены в памяти последовательно. То есть, элементы 0, 1, 2 и т.д. размещены рядом (друг за другом):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int main() { int array[] = { 7, 8, 2, 4, 5 }; std::cout << "Element 0 is at address: " << &array[0] << '\n'; std::cout << "Element 1 is at address: " << &array[1] << '\n'; std::cout << "Element 2 is at address: " << &array[2] << '\n'; std::cout << "Element 3 is at address: " << &array[3] << '\n'; return 0; } |
Результат на моем компьютере:
Element 0 is at address: 002CF6F4
Element 1 is at address: 002CF6F8
Element 2 is at address: 002CF6FC
Element 3 is at address: 002CF700
Обратите внимание, каждый из этих адресов по отдельности занимает 4 байта, как и размер типа int на моем компьютере.
Индексация массивов
Мы уже знаем, что элементы массива расположены в памяти последовательно. Из урока №82 мы знаем, что фиксированный массив может распадаться на указатель, который указывает на первый элемент (элемент под индексом 0) массива.
Также мы уже знаем, что добавление единицы к указателю возвращает адрес памяти следующего объекта этого же типа данных.
Следовательно, можно предположить, что добавление единицы к идентификатору массива приведет к возврату адреса памяти второго элемента (элемента под индексом 1) массива. Проверим на практике:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int array [5] = { 7, 8, 2, 4, 5 }; std::cout << &array[1] << '\n'; // выведется адрес памяти элемента под индексом 1 std::cout << array+1 << '\n'; // выведется адрес памяти указателя на массив + 1 std::cout << array[1] << '\n'; // выведется 8 std::cout << *(array+1) << '\n'; // выведется 8 (обратите внимание на скобки, они здесь обязательны) return 0; } |
При разыменовании результата выражения адресной арифметики скобки необходимы для соблюдения приоритета операций, поскольку оператор *
имеет более высокий приоритет, чем оператор +
.
Результат выполнения программы на моем компьютере:
001AFE74
001AFE74
8
8
Оказывается, когда компилятор видит оператор индекса []
, он, на самом деле, конвертирует его в указатель с операцией сложения и разыменования! То есть, array[n]
— это то же самое, что и *(array + n)
, где n
является целочисленным значением. Оператор индекса []
используется в целях удобства, чтобы не нужно было всегда помнить о скобках.
Использование указателя для итерации по массиву
Мы можем использовать указатели и адресную арифметику для выполнения итераций по массиву. Хотя обычно это не делается (использование оператора индекса, как правило, читабельнее и менее подвержено ошибкам), следующий пример показывает, что это возможно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> int main() { const int arrayLength = 9; char name[arrayLength] = "Jonathan"; int numVowels(0); for (char *ptr = name; ptr < name + arrayLength; ++ptr) { switch (*ptr) { case 'A': case 'a': case 'E': case 'e': case 'I': case 'i': case 'O': case 'o': case 'U': case 'u': ++numVowels; } } std::cout << name << " has " << numVowels << " vowels.\n"; return 0; } |
Как это работает? Программа использует указатель для прогона каждого элемента массива поочередно. Помните, что массив распадается в указатель на первый элемент массива? Поэтому, присвоив name
для ptr
, сам ptr
стал указывать на первый элемент массива. Каждый элемент разыменовывается с помощью выражения switch, и, если текущий элемент массива является гласной буквой, то numVowels
увеличивается. Для перемещения указателя на следующий символ (элемент) массива в цикле for используется оператор ++
. Работа цикла завершится, когда все символы будут проверены.
Результат выполнения программы:
Jonathan has 3 vowels.
Скажите, а как правильно сдвинуть указатель на некую переменную?
Допустим, у меня есть буфер, и указатель типа uint8_t* MyPointer, указывающий куда-то в буфер.
У меня есть переменная int32_t MyOffset , содержащая значение, насколько требуемые мне данные сдвинуты относительно MyPointer.
Если я записываю MyPointer =+ MyOffset; — код компилится и нормально работает, но с предупреждением «Warning: assigment to uint8_t * {aka ‘unsigned char *’} from int32_t » {aka ‘long int’} makes pointer from integer without a cast [-Wint-conversion]»
Если я пытаюсь избавиться от предупреждения, наивно записывая что-то вроде MyPointer =+ (uint8_t *)MyOffset; , или там MyPointer =+ (uint8_t *)(void *)MyOffset; — код вообще отказывается компилироваться, с ошибкой «wrong type of argument».
Как сделать рабочий код без ворнингов?
Оператор сложение с присваиванием x += y (у тебя наоборот)
Желающим посмаковать префиксы и постфиксы посвящается сей код — попробуйте раскоментировать строки и предсказать на основе адресной арифметики какие значения будут в каждом элементе массива:
Успеха!
У меня получилось понять, что префиксный и постфиксный инкременты как бы сдвигают индексацию массива вправо. Нулевой элемент "теряется", а последний элемент появляется заполненным "мусором". Префиксный и постфиксный декременты сдвигают индексацию массива влево, при этом "находятся" потерянные первые элементы с записанными ранее значениями.
Как это называется и где про это почитать? 😀
Мне удалось разобраться самому. Пришлось повторить Урок 40, чтобы вспомнить, что инкремент прибавляет единицу и присваивает (!) новое значение объекту. Таким образом, в этой программе выполняется переопределение значения указателя на нулевой элемент массива, из-за чего массив елозит вдоль памяти. Это чертовски неудобно и потенциально опасно и так лучше не делать.
Неожиданно, вот эта штука for (char *ptr = name; ptr < name + arrayLength; ++ptr) смещает начало массива, походу, переписывает, раз первый байт тот же, но символы сдвигают влево, и они там есть, ptr-1, ptr-2.
В примере с char , хоть и выводит всю строку , но указывает на первый символ, что логично.
В цикле for у тебя ++ptr грубо говоря значит тоже самое, что и ptr = ptr + 1. Т.е. ptr каждый раз присваивается новое значение. Так что ничего выходящего за рамки не происходит
Добрый день,
Можете подробнее разъяснить:
"Обратите внимание, ptr + 1 не возвращает следующий любой адрес памяти, который находится сразу после ptr, но он возвращает адрес памяти следующего объекта, тип которого совпадает с типом значения, на которое указывает ptr. "
Получается, что если по порядку есть адрес памяти не соответствующего типа — его перепрыгивает?
адрес состоит из комплекта байт, а переменная из n байт (например bool из 1 байта). Вот он(компилятор) и состыковывает подряд вагончики в единый состав (для була берет однобайтные вагончики). Как-то так…
Да, данный адрес несоответствующего типа компилятор просто "перепрыгивает" в поисках следующего соответствующего типу адреса.
разыменованный указатель, по-видимому, можно представлять с индексом, как элемент массива?
т.е. можно так?:
и это будет равнозначно?
Нет, не будет. Проверяем:
Да, у меня выходит одно и то же:
Результат:
11
11
-858993460
-858993460
Объясните:
1. почему
int *pointer=&y выводит адрес y( при cout<<pointer;)
а char *pointer=&y выводит сам символ??( при cout<<pointer;). Почему не адрес?
2. как образуются 16-ричные адреса, почему они зависят от размера переменной? т.е. почему если друг за другом идут две 4-байтные переменные, то адреса их различаются на 4, а не просто по порядку +1?
сам механизм хочется понять, гугл не помогает.
короче, выяснилось
1й вопрос: ответ в следующем уроке
2й вопрос: адрес по сути — это адрес 1 байта. 4-х байтная переменная занимает 4 адреса друг за другом. А когда называем ее адрес — это адрес первого ее байта. Поправьте, если не так.
Именно так. В оперативной памяти нумеруется каждый байт. Если переменная занимает 4 байта, адрес следующей будет больше на 4.
Вы уверены, что последний пример будет работать одозначно? Здесь недостаточно "Хотя обычно это не делается". Мало того, этот код может сносно работать при текущей архитектуре. Но переход к другой архитектуре вызовет очень много проблем. Хотел бы я посмотреть на производительность этого цикла например с адресами типа far из времен древнего ДОСа.
Но даже это еще не все. Если мне не изменяет память, конструкция "name + arrayLength" уже выходит за границы объекта. Что нам говорит по этому поводу стандарт? Результат не определен. Сравнение указателей гарантировано работает только внутри одного объекта. А дальше — как бог на душу положит.
все верно, выходит. Только вот в цикле стоит < значит сравнение указателей корректно. А в остальном вы, пожалуй, правы
Сравнение указателя с адресом, следующим сразу за последним элементом массива, гарантировано стандартом. Но только сравнение, а не обращение по такому адресу.
Спасибо за статью!
Пожалуйста.
А вот интересный момент, допустим есть указатель: int32_t* pntr, указывающий на определенный адрес и мы к нему прибавим, например, +next, который int8_t next = 1. Таким образом pntr должен всё также сдвинуться на 4 байта.