diff --git a/cpp/cpp_chapter_0142/text.md b/cpp/cpp_chapter_0142/text.md index 89a09779..8b3187e1 100644 --- a/cpp/cpp_chapter_0142/text.md +++ b/cpp/cpp_chapter_0142/text.md @@ -543,7 +543,7 @@ bool is_sorted(int arr[], std::size_t n, std::function comp) } ``` -### Длина массива +### Длина массива {#block-array-length} Давайте получим длину массива, разделив его размер на размер типа элемента: @@ -560,7 +560,13 @@ std::println("{}", len); Для получения размера массива `offsets` в байтах мы применили к нему оператор [sizeof](https://en.cppreference.com/w/cpp/language/sizeof.html). В предыдущей главе мы [использовали](/courses/cpp/chapters/cpp_chapter_0141/#block-sizeof) `sizeof` для того, чтобы узнать, сколько байт выделяется под тип. Но `sizeof` можно применять не только для типов, но и для переменных. -Деление размера массива на размер его элемента — это способ определения длины, который работает во всех версиях C++, но не отличается удобством. В C++17 появилась функция [std::size()](https://en.cppreference.com/w/cpp/iterator/size.html), упрощающая задачу: +Запись `sizeof(offsets) / sizeof(int)` не слишком удобна, потому что явно задействует тип элемента массива. Более каноничным считается получение длины массива делением размера массива на размер нулевого элемента. Это абсолютно безопасно, потому что сишные массивы нулевой длины запрещены. + +```cpp +std::size_t len = sizeof(offsets) / sizeof(offsets[0]); +``` + +Оба описанных способа работают во всех версиях C++, но не отличаются удобством. В C++17 появилась функция [std::size()](https://en.cppreference.com/w/cpp/iterator/size.html), упрощающая задачу: ```cpp {.example_for_playground .example_for_playground_006} int offsets[] = {-1, 0, -1, 2}; @@ -573,7 +579,7 @@ std::println("{}", len); 4 ``` -Оба способа получения длины **не работают** внутри функций, в которые массив передаётся. Виной тому низведение массива (array to pointer decay) — неявное приведение сишного массива к указателю на его первый элемент. Указатель — это переменная, которая хранит адрес другой переменной. Низведение массива происходит при передаче его в функцию: {#block-array-to-pointer-decay} +Ни один из способов получения длины **не работает** внутри функций, в которые массив передаётся. Виной тому низведение массива (array to pointer decay) — неявное приведение сишного массива к указателю на его первый элемент. Указатель — это переменная, которая хранит адрес другой переменной. Низведение массива происходит при передаче его в функцию: {#block-array-to-pointer-decay} ```cpp {.example_for_playground} import std; @@ -659,7 +665,7 @@ Accelerometer measurement #1 Кроме того, отсутствует удобный способ избежать случайного выхода за границы сишного массива. Зато в контейнере `std::array` такой способ есть. Метод `at()`, в отличие от оператора `[]`, проверяет индекс и в случае выхода за границы бросает исключение. -Сишные массивы нельзя присваивать друг другу и сравнивать через операторы `=` и `==`. Все присваивания и сравнения выполняются поэлементно, в цикле. +Сишные массивы нельзя присваивать друг другу и сравнивать через операторы `=` и `==`. Все присваивания и сравнения выполняются поэлементно, в цикле. {#block-compare} ### Советы по работе с массивами {#block-advice} diff --git a/cpp/cpp_chapter_0162/text.md b/cpp/cpp_chapter_0162/text.md index f3b58ed0..aaf6b4d2 100644 --- a/cpp/cpp_chapter_0162/text.md +++ b/cpp/cpp_chapter_0162/text.md @@ -1,3 +1,512 @@ -## Глава 16.2. Адресная арифметика +# Глава 16.2. Адресная арифметика -Глава находится в разработке. \ No newline at end of file +Итак, указатель — это переменная, хранящая адрес. И размер указателя не зависит от типа, на который он ссылается. Зачем же в момент объявления сообщать компилятору об этом типе? Какая разница, чей адрес содержит указатель — `bool`, `long int` или `std::queue`? + +## Зачем указателю знать свой тип данных + +Память упрощенно можно представить как массив ячеек по 1 байту. Кстати, именно поэтому нельзя завести переменную размером в 3 или 11 бит при условии, что байт равен 8 бит. + +Допустим, у нас есть три локальных переменных размером в 1, 4 и 8 байт: + +```cpp +int ret_code = -5; +bool retry = false; +bool * p = &retry; +``` + + +![Переменные в адресном пространстве](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-16-2/illustrations/cpp/pointers_and_addresses.jpg) {.illustration} + + +И если бы мы захотели по указателю `p` обновить значение `retry`, но компилятор имел бы _неправильное_ представление о размере этой переменной, то вместо перезаписи 1 байта мы бы перезаписали 4, 8 или больше. Мы бы повредили значения соседних переменных. А возможно, даже ячеек, в которых расположен сам указатель `p`! Такая ситуация называется **повреждением памяти** (memory corruption). + +Еще одна причина, по которой компилятору важно знать о типе указателя — это **адресная арифметика.** + +## Что такое адресная арифметика + +С помощью указателей можно перемещаться по памяти. В частности, перебирать элементы массива. Для этого к указателю применяются арифметические операции `+` и `-`, работающие _с учетом типа._ + +Допустим, у нас есть указатель `p` типа `T *` и целое число `n`. Тогда к `p` применимы арифметические операции: +- `p + n`. Сложение указателя с целым числом увеличивает адрес на это число, домноженное на размер типа `T`. +- `p - n`. Вычитание из указателя целого числа уменьшает адрес на значение `n`, домноженное на размер типа. +- `p - p_other`. Вычитание из указателя другого указателя дает количество элементов типа `T` между ними. Операция имеет смысл для указателей одного типа, ссылающихся на непрерывный блок памяти. + +Также к указателям применимо сравнение операторами `>`, `>=`, `<`, `<=`, `==` и `!=`. При этом происходит сравнение адресов, на которые они указывают. Сравнение на больше-меньше имеет смысл только в случае, если указатели ссылаются на одну и ту же область памяти. + +Разберем подробнее каждую из арифметических операций. Удобнее всего это делать на примере указателей на элементы сишных массивов. Заодно узнаем, что такое сишные строки. + +## Указатели на элементы массива + +Элементы контейнера `std::array` и сишных массивов расположены друг за другом [в непрерывной области памяти.](/courses/cpp/chapters/cpp_chapter_0142/#block-c-array-under-the-hood) Убедимся в этом: переберем в цикле все элементы массива и выведем их адреса. Для консольного вывода в виде таблицы используем [спецификаторы форматирования.](https://en.cppreference.com/w/cpp/utility/format/spec.html) + +```cpp +import std; + +int main() +{ + std::println("Size of int: {} bytes", sizeof(int)); + std::array pow_series = {16, 32, 64, 128, 256}; + + std::println("\nArray of ints:"); + std::string line(30, '-'); + std::println("{}", line); + std::println("{:>2} {:>16} {:>9}", 'i', "address", "value"); + std::println("{}", line); + + for (std::size_t i = 0; i < pow_series.size(); ++i) + { + std::println("{:>2} {:>16} {:>9}", + i, + static_cast(&pow_series[i]), + pow_series[i]); + } + + std::println("{}", line); +} +``` +``` +Size of int: 4 bytes + +Array of ints: +------------------------------ + i address value +------------------------------ + 0 0x7ffe9438f630 16 + 1 0x7ffe9438f634 32 + 2 0x7ffe9438f638 64 + 3 0x7ffe9438f63c 128 + 4 0x7ffe9438f640 256 +------------------------------ +``` + +Как видите, разность между адресами соседних элементов совпадает с размером типа элемента. + +Чтобы присвоить указателю адрес элемента массива, к этому нему применяется оператор взятия адреса `&`: + +```cpp +std::array arr = {-1, 0, 1}; + +int * p = &arr[0]; +``` + +Для получения указателя _на нулевой элемент сишного массива_ есть более лаконичная запись: + +```cpp +int arr[] = {-1, 0, 1}; + +int * p = arr; +``` + +Здесь срабатывает неявное приведение типов: тип массива `int[3]` приводится к типу указателя `int *` на нулевой элемент. + +Синтаксис языка позволяет применять к указателю оператор `[]` и через него обращаться к элементам массива: + +```cpp +std::uint64_t seed[] = { + 9621534751069176051UL, + 2054564862222048242UL + }; + +std::uint64_t * p = seed; +std::println("{} {}", *p, p[1]); +``` +``` +9621534751069176051 2054564862222048242 +``` + +Что выведет этот код? Напишите `err` в случае ошибки или `ub` в случае неопределенного поведения. {.task_text} + +Вспомните [способы](/courses/cpp/chapters/cpp_chapter_0142/#block-array-length) определения длины массива. {.task_text} + +```cpp +int arr[] = {1, 2}; +std::println("{}", sizeof(arr) / sizeof(*arr)); +``` + +```consoleoutput {.task_source #cpp_chapter_0162_task_0080} +``` +. {.task_hint} +```cpp {.task_answer} +2 +``` + +## Нуль-терминированные строки + +Вы уже знакомы с классом строки `std::string` из стандартной библиотеки. У него [около двадцати](https://en.cppreference.com/w/cpp/string/basic_string/basic_string.html) перегрузок конструктора. Наверное, наиболее популярна перегрузка для инициализации литералом в двойных кавычках: + +```cpp +std::string protocol = "UART"; +``` + +Но [какой тип](https://en.cppreference.com/w/cpp/language/string_literal.html) у самого литерала? Перед вами [нуль-терминированная строка.](https://ru.wikipedia.org/wiki/%D0%9D%D1%83%D0%BB%D1%8C-%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B0) По сути это статический массив символов с типом `const char[n]`, где `n` — длина литерала + 1. + +Например, у литерала `"UART"` тип `const char[5]`. Дополнительный элемент отводится под [завершающий ноль](https://en.wikipedia.org/wiki/Null_character) (terminating null character) — маркер конца строки с кодом `0`. В C++ это управляющий символ `\0`: + +```cpp +char null_char = '\0'; +``` + +В обиходе нуль-терминированные строки называют сишными. Создание такой строки из литерала равносильно инициализации массива набором символов, но к литералу не нужно добавлять завершающий ноль вручную: + +```cpp +const char * c_str = "C-like string"; + +const char c_arr[] = {'C', + '-', + 'l', 'i', 'k', 'e', + ' ', + 's', 't', 'r', 'i', 'n', 'g', + '\0'}; + +std::println("{}", c_str); +std::println("{}", c_arr); +``` +``` +C-like string +C-like string +``` + +Записывать строку в двойных кавычках удобнее, чем перечислять посимвольно. Поэтому определяйте строку как массив только чтобы избежать добавления терминирующего нуля. + +При передаче в функцию сишная строка приводится к указателю (array-to-pointer decay). Но в отличие от массива, она не требует передачи дополнительного параметра — длины. Не обязательно знать длину строки, чтобы избежать выхода за ее границы: достаточно найти завершающий `\0`. + +В стандартной библиотеке C++ есть [функции](https://en.cppreference.com/w/cpp/header/cstring.html) для работы с сишными строками. Например, [std::strcmp()](https://en.cppreference.com/w/cpp/string/byte/strcmp.html) для сравнения строк. Ведь строки, как и массивы, [нельзя](/courses/cpp/chapters/cpp_chapter_0142/#block-compare) сравнивать напрямую такими операторами как `==` или `>`. + +```cpp +// strcmp() возвращает 0, если строки равны. +// -1, если первая строка лексикографически +// меньше второй и 1, если она больше. +// int strcmp( const char* lhs, const char* rhs ); + +int main() +{ + char proto1[] = "UART"; + char proto2[] = "I2C"; + std::println("{}", std::strcmp(proto1, proto2)); +} +``` +``` +1 +``` + +## Прибавление к указателю целого числа + +Перебирать сишный массив можно не только через индексы, но и с помощью указателя. Для этого указатель увеличивается на требуемое количество элементов: `++p`, `p++`, `p += n` или `p = p + n`. + +Когда к указателю прибавляется целое число, то адрес увеличивается на соответствующее значение, умноженное на размер типа данных. Если прибавить к указателю число, не превышающее длину массива, то он будет ссылаться на один из последующих элементов: + +```cpp +double thresholds[]{0.009, 0.01, 0.5, 1.5}; + +double * p = &thresholds[0]; +std::println("{}", *p); // 0.009 + +++p; +std::println("{}", *p); // 0.01 + +p += 2; +std::println("{}", *p); // 1.5 + +++p; // Выход за пределы массива +std::println("{}", *p); // UB +``` +``` +0.009 +0.01 +1.5 +??? UB +``` + + +![Перемещение по массиву с помощью указателя](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-16/illustrations/cpp/pointer_to_array_element.jpg) {.illustration} + + +Важно следить, чтобы указатель не вышел за границы массива. Обращение по нему приведет к UB. + +Стандарт [определяет,](https://timsong-cpp.github.io/cppwp/std23/expr.sub#2) что запись `arr[i]` эквивалентна выражению `*(arr + i)`. Разберем по шагам, что в нем происходит: +1. Внутри круглых скобок массив неявно приводится к указателю. +2. К хранящемуся в указателе адресу прибавляется число `i`, умноженное на размер типа. +3. Затем к нему применяется разыменование `*`. Мы получаем значение массива по индексу `i`. + +```cpp +std::uint8_t cmyk_color[] = {73, 45, 0, 4}; + +std::println("cmyk_color[3] == {}. *(cmyk_color + 3) == {}", + cmyk_color[3], + *(cmyk_color + 3)); +``` +``` +cmyk_color[3] == 4. *(cmyk_color + 3) == 4 +``` + +А теперь следите за руками. Оператор `+` [коммутативный:](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BC%D0%BC%D1%83%D1%82%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D1%81%D1%82%D1%8C) `a + b` и `b + a` — это одно и то же. То есть `*(arr + i)` можно записать как `*(i + arr)`. Это в свою очередь соответствует записи `i[arr]`. Скомпилируется ли код вида `i[arr]`? Да! Следует ли использовать такую форму записи в промышленном коде? Категорически нет! Она эзотерическая и сбивающая с толку. Зачем тогда о ней знать? Исключительно потому что о ней любят спрашивать на собеседованиях. + +Что выведет этот код? Напишите `err` в случае ошибки или `ub` в случае неопределенного поведения. {.task_text} + +```cpp +std::unit8_t rgb_color[] = {66, 135, 245}; + +std::println("{}", 1[rgb_color]); +``` + +```consoleoutput {.task_source #cpp_chapter_0162_task_0010} +``` +. {.task_hint} +```cpp {.task_answer} +135 +``` + +Итак, выражение `arr[i]` эквивалентно выражению `*(arr + i)`. Которое в свою очередь можно заменить на `*(ptr + i)` и даже `ptr[i]`, если `ptr` ссылается на нулевой элемент массива. + +Эквивалентны ли выражения `&arr[i]` и `ptr + i`? `y/n` {.task_text} + +```consoleoutput {.task_source #cpp_chapter_0162_task_0020} +``` +. {.task_hint} +```cpp {.task_answer} +y +``` + +Обойдем массив с помощью указателей: + +```cpp +const std::size_t n = 3; +std::size_t sizes[n] = {16384, 32768, 65536}; + +std::size_t * ptr = sizes; + +for (std::size_t i = 0; i < n; ++i) +{ + std::println("i={}. Addr={}. Val={}", + i, static_cast(ptr + i), *(ptr + i)); +} +``` +``` +i=0. Addr=0x7fffc75c9740. Val=16384 +i=1. Addr=0x7fffc75c9748. Val=32768 +i=2. Addr=0x7fffc75c9750. Val=65536 +``` + +В стандартной библиотеке есть функция [std::strchr()](https://en.cppreference.com/w/cpp/string/byte/strchr.html), которая принимает сишную строку и символ. Она возвращает указатель на первое вхождение символа в строку. Завершающий ноль _участвует_ в поиске. Если символ не найден, функция возвращает `nullptr`. {.task_text} + +Напишите свою реализацию функции. Будем считать, что в нее не может быть передан `nullptr`. {.task_text} + +```cpp {.task_source #cpp_chapter_0162_task_0030} +const char * strchr(const char * str, int ch) +{ + +} +``` +. {.task_hint} +```cpp {.task_answer} + +``` + +Применим адресную арифметику, чтобы посмотреть, какие адреса в памяти занимают переменные. Заведем шаблонную функцию `show_used_memory()`, которая принимает указатель на переменную любого типа `T`. Внутри функции приведем тип указателя `const T *` к типу `const char *` с помощью [reinterpret_cast](https://en.cppreference.com/w/cpp/language/reinterpret_cast.html). Это нужно, чтобы при инкременте указателя адрес увеличивался ровно на 1 байт. Выражение `reinterpret_cast(expr)` приводит тип `expr` к типу `T`. + +```cpp +import std; + +template +void show_used_memory(const T * ptr, const std::string & name) +{ + const char * address = reinterpret_cast(ptr); + + std::println("\n|{:<10}|{:p}|", + name, static_cast(address)); + + for (auto i = 1; i < sizeof(T); ++i) + { + std::println("|{:<10}|{:p}|", + ' ', static_cast(address + i)); + } +} + +int main() +{ + int ret_code = -5; + bool retry = false; + bool * p = &retry; + + show_used_memory(&retry, "retry"); + show_used_memory(&ret_code, "ret_code"); + show_used_memory(&p, "p"); +} +``` +``` +|retry |7ffdb51deffb| + +|ret_code |7ffdb51deffc| +| |7ffdb51deffd| +| |7ffdb51deffe| +| |7ffdb51defff| + +|p |7ffdb51df000| +| |7ffdb51df001| +| |7ffdb51df002| +| |7ffdb51df003| +| |7ffdb51df004| +| |7ffdb51df005| +| |7ffdb51df006| +| |7ffdb51df007| +``` + +## Вычитание из указателя целого числа + +Вычитание целых чисел из указателей работает по той же схеме, что и сложение: из адреса отнимается значение, умноженное на размер типа. + +```cpp +int random_numbers[] = {-67, 9, 22, 18}; + +int * p = &random_numbers[3]; +std::println("Address: {}. Value: {:0x}", + static_cast(p), *p); + +p--; +std::println("Address: {}. Value: {:0x}", + static_cast(p), *p); + +p -= 2; +std::println("Address: {}. Value: {:0x}", + static_cast(p), *p); + +--p; // Выход за пределы массива +std::println("Address: {}. Value: {:0x}", + static_cast(p), *p); // UB +``` +``` +Address: 0x7ffe027539dc. Value: 12 +Address: 0x7ffe027539d8. Value: 16 +Address: 0x7ffe027539d0. Value: -43 +??? UB +``` + + +![Перемещение по массиву с помощью указателя](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-16-2/illustrations/cpp/pointer_to_array_substraction.jpg) {.illustration} + + +Напишите функцию `reverse()`, которая принимает указатель на сишную строку. Она должна перевернуть строку, то есть расположить ее символы в обратном порядке. Считаем, что в функцию не может быть передан `nullptr`. {.task_text} + +```cpp {.task_source #cpp_chapter_0162_task_0040} +void reverse(char * str) +{ + +} +``` +. {.task_hint} +```cpp {.task_answer} +void reverse(char * str) +{ + char * start = str; + char * end = start; + + while (*end != '\0') + ++end; + + --end; + + while (start < end) + { + std::swap(start, end); + ++start; + --end; + } +} +``` + +## Вычитание указателей + +Указатели можно вычитать один из другого. Для этого они должны иметь одинаковый тип и ссылаться на области непрерывного участка памяти. Например, указывать на элементы одного и того же массива. Разность между указателями равна количеству объектов заданного типа между ними, а вовсе не количеству байт! При вычитании указателей компилятор делит получившееся значение на размер типа данных. Поэтому через разность указателей на элементы массива можно определять расстояние между ними. + +```cpp +int http_non_retriable_errors[] = { + 400, // Bad Request + 401, // Unauthorized + 404, // Not found + 403, // Forbidden + 501, // Not implemented + 405, // Method not allowed +}; + +// Указатель на нулевой элемент массива: +int * p1 = http_non_retriable_errors; + +int * p2 = &http_non_retriable_errors[5]; + +std::size_t dist = p2 - p1; + +std::println("{}", dist); +``` +``` +5 +``` + + +![Вычитание указателей](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/cpp-chapter-16-2/illustrations/cpp/pointer_substraction.jpg) {.illustration} + + +Что выведет этот код, если размер `int` равен 4 байта? Напишите `err` в случае ошибки компиляции или `ub` в случае неопределенного поведения. {.task_text} + +```cpp +import std; + +std::size_t size_ptr(int * buf) +{ + return sizeof(buf); +} + +int main() +{ + int raw_data[5] = {}; + std::size_t a = &raw_data[4] - raw_data; + std::size_t b = sizeof(raw_data); + + std::println("{} {} {} {}", + a, + b, + b == size_ptr(raw_data)); +} +``` + +```consoleoutput {.task_source #cpp_chapter_0162_task_0050} +``` +. {.task_hint} +```cpp {.task_answer} +4 20 false +``` + +В стандартной библиотеке есть функция `std::strlen()`, которая принимает сишную строку и возвращает ее длину без учета завершающего нулевого символа. {.task_text} + +Напишите свою реализацию функции. Будем считать, что в нее не может попасть `nullptr`. {.task_text} + +```cpp {.task_source #cpp_chapter_0162_task_0060} +std::size_t strlen(const char * str) +{ + +} +``` +Заведите указатель на начало строки: `const char * end = str`. В цикле, пока значение по указателю не станет равным `'\0'`, увеличивайте указатель. Затем верните разность между указателем на `'\0'` и указателем на начало строки. В соответствии с правилами адресной арифметики получившееся значение равно количеству элементов между двумя указателями. {.task_hint} +```cpp {.task_answer} +std::size_t strlen(const char * str) +{ + const char * end = str; + + while (*end != '\0') + ++end; + + return end - str; +} +``` + +Вы можете сравнить свое решение задачи с вариантом, предлагаемом [в разделе cppreference,](https://en.cppreference.com/w/cpp/string/byte/strlen.html) про `std::strlen()`. + + +---------- + +## Резюме +- Сишная строка (нуль-терминированная строка) — это массив `char`, завершенный символом `\0`. +- Адресная арифметика заключается в применении к указателям операций сложения и вычитания, которые работают с учетом типа указателя. +- Прибавление к указателю целого числа увеличивает значение адреса на это число, домноженное на размер типа. +- Вычитание из указателя целого числа уменьшает значение адреса на число, домноженное на размер типа. +- Вычитание из указателя другого указателя имеет смысл, если оба указателя ссылаются на общий участок памяти и имеют одинаковый тип. +- Вычитание одного указателя из другого возвращает количество элементов типа `T` между ними. diff --git a/illustrations/cpp/pointer_substraction.jpg b/illustrations/cpp/pointer_substraction.jpg new file mode 100644 index 00000000..d1b0c511 Binary files /dev/null and b/illustrations/cpp/pointer_substraction.jpg differ diff --git a/illustrations/cpp/pointer_to_array_substraction.jpg b/illustrations/cpp/pointer_to_array_substraction.jpg new file mode 100644 index 00000000..eac123ba Binary files /dev/null and b/illustrations/cpp/pointer_to_array_substraction.jpg differ diff --git a/illustrations/cpp/pointers_and_addresses.jpg b/illustrations/cpp/pointers_and_addresses.jpg index aba9791a..b8a0804c 100644 Binary files a/illustrations/cpp/pointers_and_addresses.jpg and b/illustrations/cpp/pointers_and_addresses.jpg differ