Указатели, ссылки и массивы в C++
Чтобы разобраться в том, что такое указатель, на первых порах приходится прикладывать нехилые усилия из-за слабого понимания принципов функционирования ОС 1 в целом. Я постараюсь описать общие идеи работы с указателями, ссылками и массивами в Си++ безотносительно различных сценариев работы с ними.
Указатель
Определение 2Для заданного типа T, тип T* является «указателем на T». Это означает, что переменные типа T* содержат адреса объектов типа T.
Таким образом, язык Си++ даёт возможность выделить и проинициализировать память значением, получить адрес значения, получить само значение и, конечно же, освободить занимаемую память:
Из-за склонности ОС лишь отмечать для себя, что память освобождена, могут возникать ситуации, когда результат не соотносится с логикой. Следующий пример будет работать нормально, хотя должна возникать ошибка:
Ошибка не возникает лишь потому, что уже «мусорное» значение в памяти по-прежнему сохранено в первоначальном виде. Хотя, если предположить, что между моментами освобождения памяти и выводом значения память компьютера полностью переписывается, то возникнет всеми любимая сегментейшн фолт — ошибка доступа к памяти. Чтобы избежать возможных проблем рекомендуется помечать указатель как пустой. Следующий пример сразу выявит ошибку:
Ссылка на значение
При инициализации переменной значением другой переменной неявно происходит вызов копирования одной переменной в другую. Таким образом на выходе будут существовать 2 различные переменные, значение которых одинаково, и изменение значение одной переменной не приведёт к изменению значение другой:
Ссылка & вместо копирования значения переменной задаст для этого значения ещё одно имя. Будут существовать 2 переменных, ссылающихся на одно и то же значение в памяти. Изменение значения через одну переменную приведёт к изменению значения второй. Вторая переменная будет алиасом первой, если такое определение будет понятней:
Также с помощью этого же оператора возможно получить адрес переменной:
Банковская метафораЕсли представить себе стек памяти как портмоне, то отделы для банкнот в них — статические переменных, а банковские карты играют роль указателей. Когда необходимо сохранить определённую сумму денег, то их можно либо непосредственно положить в отдел для банкнот, либо через терминал внести на банковский счёт. В первом случае деньги физически находятся в кошельке, тогда как во втором деньги поступают в банк, который выполняет роль кучи.
Когда происходит оплата карточкой, магазин связывается с банком по номеру карточки и требует удержать всю доступную сумму (если тратить, так всё сразу). Деньги физически остаются привязаны к счёту некоторое время и списанная сумма помечается как «задержанная» (англ. hold), поэтому владелец может до окончательного снятия суммы увидеть на своём балансе эти деньги, хотя их на самом деле уже нет ;-).
Динамические массивы
Когда рассматривались статические массивы, я умолчал про один существенный недостаток — размер такого массива должен быть известен уже на этапе компиляции. Другими словами, размер массива определяется числовой константой, будь то числовой литерал или же константная переменная.
Для случаев, когда программист хочет вычислять размер на этапе выполнения, используются динамические массивы:
Особое внимание следует уделить конструкции delete [] array; , которая удалит весь массив. Это возможно благодаря тому, что массив хранится в памяти непрерывным куском. Т.е. каждое новое значение может быть легко найдено, если известен адрес предыдущего и размер одного элемента:
На типе данных указатель определена операция вычитания (как указателя-число, так и указатель-указатель) и операция сложения (только указатель-число), поэтому возможно вручную сместиться относительно какого-либо адреса на требуемое количество байт.
Чтобы создать двумерный массив, необходимо выполнить примерно следующий код:
Вся соль кроется в первой строчке, которую следует читать следующим образом (справа-налево): создать массив из 10 указателей на целочисленный тип и сохранить адрес в указателе на указатель. Каждый, кто постигает смысл этой фразы, сразу же приступает к созданию 3-, 4- и так далее мерных массивов.
На поляхВ спецификации языка C# различаются массив массивов массивов… и многомерные массивы уже на уровне синтаксиса. Как видно из примеров выше, Си++ такой потрясающей особенностью не обладает и для него всё это просто указатели указателей указателей…
Строки как динамические массивы
Есть одна особенность в Си — делать эффективно, но непонятно. Эта особенность перекочевала и в Си++. Одной из таких штук является определение строки, как динамического массива чаров (англ. char). Чтобы записать строку достаточно записать:
Кстати, удалить такую строку через delete не получится (узнать почему). Массив строк в виде уже знаком, как аргумент главной функции char *argv[] , т.е. аргументы программы считываются, как строки. Например, следующая программа будет приветствовать человека по имени, или же сообщать, что не знакома, если аргументы отсутствуют:
Чтобы программа поприветствовала вас, её нужно запустить следующим образом: $ ./a Maksim Pelevim .
ОтступлениеИспользования строки-си (c-string), вообщем-то, неудобно во всех случаях. Вместо неё в Си++ давно придумали класс string .