Как сделать ссылку на массив

Массивы в C++

Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это четвертая статья из серии, первые три, посвященные перегрузке в C++, находятся здесь, здесь и здесь.

Эта статья посвящена массивам. Массивы можно отнести к наиболее древним слоям C++, они пришли из первых версий C. Тем не менее, массивы вошли в объектно-ориентированную систему типов C++, хотя и с определенными оговорками. Программисту важно знать об этих особенностях, чтобы избежать потенциальных ошибок. В статье также рассмотрено другое наследие C – тривиальные типы и неинициализированные переменные. Часть нововведений C++11 затрагивают работу с массивами, все эти новые возможности также подробно описаны. Итак, попробуем рассказать о массивах все.

Оглавление

1. Общие положения

Массив является простейшим агрегатным типом. Он моделирует набор однотипных элементов, расположенных подряд в непрерывном отрезке памяти. Массивы в той или иной форме поддерживаются практически всеми языками программирования и неудивительно, что они появились в первых версиях C и затем стали частью C++.

1.1. Объявление массивов

Если T некоторый тип, N константа или выражение, вычисляемое во время компиляции, то инструкция

Такие массивы еще называют встроенными массивами (regular arrays), чтобы подчеркнуть отличие от других вариантов массивов, термин «массив» используется в программировании и в том числе в C++ очень широко.

Вот примеры правильных объявлений массивов:

А вот примеры некорректных объявлений массивов:

Выход за границы массива не контролируется, ошибка может привести к неопределенному поведению.

В одной инструкции можно объявить несколько массивов, но размер должен быть указан для каждого.

Для типов массивов можно вводить псевдонимы. Можно использовать традиционный вариант с ключевым словом typedef :

или более современный (C++11) с ключевым словом using :

После этого массивы объявляются как простые переменные:

Это будет то же самое, что

1.2. Операторы и стандартные функции для работы с массивами

Для работы с массивами можно использовать оператор sizeof и несколько стандартных функций и макросов.

Оператор sizeof возвращает полный размер массива в байтах, то есть размер элемента умноженный на размер массива.

А также в стандартных алгоритмах:

1.3. Размещение в памяти

Если массив объявлен статически, то есть в глобальной области видимости, в области видимости пространства имен или в качестве статического члена класса, то он размещается в статической памяти. Массивам, объявленным локально, память выделяется на стеке. (Естественно, надо учитывать ограниченный размер стека при выборе размера локальных массивов.) Нестатические члены класса размещаются в границах экземпляра класса. Динамические массивы (см. раздел 6) размещаются в динамической памяти.

1.4. Ограничения на типы элементов массивов

Нельзя объявить массив ссылок.

Вместо этого можно использовать массив константных указателей.

(Синтаксис инициализации массивов будет обсуждаться в разделе 3.2.)

Нельзя объявить массив функций.

Вместо этого можно использовать массив указателей на функцию.

Квалификатор const не применим к типу массива, а только к типам его элементов.

2. Сведение и копирование массивов

В данном разделе рассматриваются особенности массивов, которые выделяют их из общей системы типов C++.

2.1. Сведение

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

Но все же сведение можно отнести к сишным архаизмам и с ним надо быть внимательным и аккуратным, иначе можно столкнуться с не самыми приятными неожиданностями.

Вот как сведение влияет на объявления функций. Функции

не являются перегруженными функциями — это одно и то же. Размер надо передавать дополнительным параметром или использовать специальное соглашение для определения размера (например, завершающий ноль для строк).

При внешнем связывании массива также происходит сведение.

Для размера также надо использовать дополнительную переменную или использовать специальное соглашение для определения размера.

При объявлении переменной с помощью ключевого слова auto также происходит сведение.

При конкретизации шаблона функции

тип параметра шаблонной функции также будет выведен как указатель, если аргумент является массивом.

Сведение вызывает дополнительные проблемы при использовании наследования. (В C ведь нет наследования.) Рассмотрим пример.

Следующий код компилируется без ошибок и предупреждений.

2.2. Копирование

Наряду со сведением (и тесно связанная с ним) есть еще одна особенность типа массива, которая делает его в некотором смысле «неполноценным». Массивы не поддерживают привычный синтаксис инициализации и присваивания, основанный на семантике копирования:

Также функция не может возвращать массив.

Но если массив является членом класса/структуры/объединения, то проблемы с копированием (а также сведение) отсутствуют.

Для этой структуры компилятор сгенерирует копирующий конструктор по умолчанию и соответствующий оператор присваивания, которые без проблем скопируют массив.

3. Инициализация массивов

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

3.1. Тривиальные типы и неинициализированные переменные

Конструкторы и деструкторы можно назвать ключевыми элементами объектной модели С++. При создании объекта обязательно вызывается конструктор, а при удалении — деструктор. Но проблемы совместимости с С вынудили сделать некоторое исключение, и это исключение называется тривиальные типы. Они введены для моделирования сишных типов и сишного жизненного цикла переменных, без обязательного вызова конструктора и деструктора. Сишный код, если он компилируется и выполняется в С++, должен работать так же как в С. К тривиальным типам относятся числовые типы, указатели, перечисления, а также классы, структуры, объединения и массивы, состоящие из тривиальных типов. Классы и структуры должны удовлетворять некоторым дополнительным условиям: отсутствие пользовательского конструктора, деструктора, копирования, присваивания, виртуальных функций.

Переменная тривиального типа будет неинициализированной, если не использовать какой-нибудь вариант явной инициализации. Для тривиального класса компилятор может сгенерировать конструктор по умолчанию и деструктор. Конструктор по умолчанию обнуляет объект, деструктор ничего не делает. Но этот конструктор будет сгенерирован и использован только, если использовать какой-нибудь вариант явной инициализации, иначе переменная останется неинициализированной.

Неинициализированная переменная устроена следующим образом: если она объявлена в области видимости пространства имен (глобально), будет иметь все биты нулевыми, если локально, или создана динамически, то получит случайный набор битов. Понятно, что использование такой переменной может привести к непредсказуемому поведению программы. Массивы достаточно часто имеют тривиальный тип и поэтому эта проблема для них весьма актуальна.

Неинициализированные константы тривиального типа выявляет компилятор, иногда он выявляет и другие неинициализированные переменные, но с этой задачей лучше справляются статические анализаторы кода.

3.2. Синтаксис инициализации массивов

3.2.1. Общие положения

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

Со времен C массивы можно было инициализировать с помощью синтаксиса агрегатной инициализации:

В С++11 появилась универсальная инициализация (uniform initialization) и теперь можно инициализировать так:

Для универсальной инициализации также можно использовать =, и различать эти два типа инициализации не всегда просто, а, скорее всего, не очень нужно.

Размер массива можно не указывать, тогда он определится по числу инициализаторов.

Если размер массива указан, то число инициализаторов не должно быть больше размера массива. Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию (который, естественно, должен быть), в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.

Массивы констант тривиального типа требуют обязательного списка инициализации.

Число инициализаторов может быть меньше размера массива, в этом случае оставшиеся элементы инициализируются конструктором по умолчанию.

Символьные массивы можно инициализировать строковым литералом.

Размер такого массива будет на единицу больше числа символов строки, нужно хранить завершающий нулевой символ.

3.2.2. Инициализация членов класса

В С++11 появилась возможность инициализировать массивы, являющиеся нестатическими членами класса. Это можно сделать двумя способами: непосредственно при объявлении или в списке инициализации членов при определении конструктора.

Правда в этом случае надо всегда явно задавать размер массива, неявное определение размера через список инициализации не разрешается.

Статические массивы, как и ранее, можно инициализировать только при определении, размер массива может быть определен через список инициализации.

3.2.3. Требования к инициализаторам

Выражения, стоящие в списке инициализации, вычисляются непосредственно перед инициализацией, они не обязаны быть известными на стадии компиляции (конечно, за исключением массивов, объявленных как constexpr ). Требования к элементам списка инициализации такие же как и к аргументу функции, имеющей параметр того же типа, что и элемент массива — должно существовать неявное преобразование от типа элемента списка инициализации к типу элемента массива. Пусть у нас есть объявление массива:

Наличие нужного преобразования эквивалентно корректности инструкции

Элемент списка инициализации может быть сам списком инициализации. В этом случае корректность этой инструкции также гарантирует корректную инициализацию элемента массива.

Этот пример также демонстрирует как с помощью списка инициализации мы можем создать массив для типа у которого нет конструктора по умолчанию. Но в этом случае число инициализаторов должно совпадать с размером массива.

4. Указатели и ссылки на массивы

4.1. Указатели на массивы

Пусть у нас объявлен массив

Указатель на этот массив объявляется и инициализируется следующим образом:

Указатель на массив — это не указатель на первый элемент (хотя побитово они, конечно, совпадают), здесь нет никакого сведения. Это полноценный тип, который «знает» размер массива. Поэтому при инициализации размеры должны совпадать.

При инкременте указатель на массив увеличивается на размер всего массива, а не на размер элемента.

Для доступа к элементу массива через указатель надо использовать оператор * и индексатор.

При использовании псевдонимов можно получить более привычный синтаксис объявления указателя на массив.

Понимание указателей на массивы необходимо для правильной работы с многомерными массивами, которые подробно будут рассмотрены далее.

4.2. Ссылки на массивы

Пусть у нас объявлен массив

Ссылка на этот массив объявляется и инициализируется следующим образом:

Также ссылку на массив можно инициализировать разыменованным указателем на массив.

Как и указатель, ссылка «знает» размер массива. Поэтому при инициализации размеры должны совпадать.

Доступ к элементу массива через ссылку осуществляется так же, как и через идентификатор массива.

Ссылки на массивы как раз и являются теми средствами, с помощью которых можно обойти сведение.

При использовании псевдонимов можно получить более привычный синтаксис объявления ссылки на массив.

При конкретизации шаблона функции

тип параметра шаблонной функции также будет выведен как ссылка на массив, если аргумент является массивом.

Особенно удобно использовать шаблоны с выводом типа и размера массива.

5. Многомерные массивы

Если T некоторый тип, N и M выражения, допустимые для определения размера массива, то инструкция

Сведение преобразует массив к указателю на элемент. Для двумерного массива этот элемент сам является массивом, а значит двумерный массив сводится к указателю на массив.

Таким образом, при передаче двумерного массива в функцию следующие варианты объявления соответствующего параметра эквивалентны:

Это означает, что внешний размер двумерного массива теряется и его надо передавать отдельным параметром.

При использовании псевдонимов можно получить более лаконичный синтаксис объявления двумерных массивов.

Это то же самое, что

Двумерные массивы инициализируются следующим образом:

Можно получить указатель на двумерный массив:

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

Двумерный массив хорошо согласуется с математическими матрицами. В объявлении

6. Динамические массивы

В C++ отсутствует тип «динамический массив». Имеются только операторы для создания и удаления динамического массива, доступ к нему осуществляется через указатели на начало массива (своего рода полное сведение). Размер такого массива надо хранить отдельно. Динамические массивы желательно инкапсулировать в C++ классы.

6.1. Создание и удаление динамического массива

Если T некоторый тип, n переменная, значение которой может определяются в процессе выполнения программы, то инструкция

Если тип T тривиальный, то элементы будут иметь случайное значение, в противном случае для инициализации элементов будет использован конструктор по умолчанию.

В C++11 появилась возможность использовать список инициализации.

Если число инициализаторов больше размера массива, то лишние не используются (компилятор может выдать ошибку, если значение n известно на стадии компиляции). Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию, в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.

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

6.2. Управление жизненным циклом динамического массива

6.3. Многомерные динамические массивы

При использовании псевдонимов можно получить более лаконичный синтаксис.

Используя перегрузку оператора [] легко создать класс, который хранит данные в одномерном массиве, но при этом предоставляет интерфейс многомерного массива. Вот пример предельно упрощенного класса матрицы.

Вот пример использования:

7. Частичная специализация шаблонов для массивов

Вывод: base pointer array

8. Стандартные альтернативы массивам

Стандартная библиотека предоставляет несколько классов (точнее шаблонов классов), которые рекомендуется использовать вместо массивов.

Этот шаблон поддерживает индексатор и традиционный интерфейс стандартного контейнера.

Список литературы

[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.

Источник

Урок №88. Ссылки

Обновл. 16 Авг 2020 |

До этого момента мы успели рассмотреть 2 основных типа переменных:

обычные переменные, которые хранят значения напрямую;

указатели, которые хранят адрес другого значения (или null), для доступа к которым выполняется операция разыменования указателя.

Ссылки — это третий базовый тип переменных в языке C++.

Ссылки

Ссылка — это тип переменной в языке C++, который работает как псевдоним другого объекта или значения. Язык C++ поддерживает 3 типа ссылок:

Ссылки на неконстантные значения (обычно их называют просто «ссылки» или «неконстантные ссылки»), которые мы обсудим на этом уроке.

Ссылки на константные значения (обычно их называют «константные ссылки»), которые мы обсудим на следующем уроке.

В C++11 добавлены ссылки r-value, о которых мы поговорим чуть позже.

Ссылка (на неконстантное значение) объявляется с использованием амперсанда ( & ) между типом данных и именем ссылки:

В этом контексте амперсанд не означает «оператор адреса», он означает «ссылка на».

Ссылки в качестве псевдонимов

Ссылки обычно ведут себя идентично значениям, на которые они ссылаются. В этом смысле ссылка работает как псевдоним объекта, на который она ссылается, например:

Результат выполнения программы:

В примере, приведенном выше, объекты ref и value обрабатываются как одно целое. Использование оператора адреса с ссылкой приведет к возврату адреса значения, на которое ссылается ссылка:

Краткий обзор l-value и r-value

На уроке №10 мы уже рассматривали, что такое l-value и r-value. l-value — это объект, который имеет определенный адрес памяти (например, переменная x ) и сохраняется за пределами одного выражения. r-value — это временное значение без определенного адреса памяти и с областью видимости выражения (т.е. сохраняется в пределах одного выражения). В качестве r-values могут быть как результаты выражения (например, 2 + 3 ), так и литералы.

Инициализация ссылок

Ссылки должны быть инициализированы при создании:

В отличие от указателей, которые могут содержать нулевое значение, ссылки нулевыми быть не могут.

Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть инициализированы константными l-values или r-values:

Обратите внимание, во втором случае вы не можете инициализировать неконстантную ссылку константным объектом. В противном случае, вы бы могли изменить значение константного объекта через ссылку, что уже является нарушением понятия «константа».

После инициализации изменить объект, на который указывает ссылка — нельзя. Рассмотрим следующий фрагмент кода:

Обратите внимание, во втором стейтменте ( ref = value2; ) выполняется не то, что вы могли бы ожидать! Вместо переприсваивания ref (ссылаться на переменную value2 ), значение из value2 присваивается переменной value1 (на которое и ссылается ref ).

Ссылки в качестве параметров в функциях

Ссылки чаще всего используются в качестве параметров в функциях. В этом контексте ссылка-параметр работает как псевдоним аргумента, а сам аргумент не копируется при передаче в параметр. Это в свою очередь улучшает производительность, если аргумент слишком большой или затратный для копирования.

На уроке №82 мы говорили о том, что передача аргумента-указателя в функцию позволяет функции при разыменовании этого указателя напрямую изменять значение аргумента.

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

Результат выполнения программы:

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

Основным недостатком использования неконстантных ссылок в качестве параметров в функциях является то, что аргумент должен быть неконстантным l-value (т.е. константой или литералом он быть не может). Мы поговорим об этом подробнее (и о том, как это обойти) на следующем уроке.

Ссылки как более легкий способ доступа к данным

Второе (гораздо менее используемое) применение ссылок заключается в более легком способе доступа к вложенным данным. Рассмотрим следующую структуру:

Таким образом, следующие два стейтмента идентичны:

Ссылки позволяют сделать ваш код более чистым и понятным.

Ссылки vs. Указатели

Ссылка — это тот же указатель, который неявно разыменовывается при доступе к значению, на которое он указывает («под капотом» ссылки реализованы с помощью указателей). Таким образом, в следующем коде:

*ptr и ref обрабатываются одинаково. Т.е. это одно и то же:

Поскольку ссылки должны быть инициализированы корректными объектами (они не могут быть нулевыми) и не могут быть изменены позже, то они, как правило, безопаснее указателей (так как риск разыменования нулевого указателя отпадает). Однако они немного ограничены в функциональности по сравнению с указателями.

Если определенное задание может быть решено с помощью как ссылок, так и указателей, то лучше использовать ссылки. Указатели следует использовать только в тех ситуациях, когда ссылки являются недостаточно эффективными (например, при динамическом выделении памяти).

Заключение

Ссылки позволяют определять псевдонимы для других объектов или значений. Ссылки на неконстантные значения могут быть инициализированы только неконстантными l-values. Они не могут быть переприсвоены после инициализации. Ссылки чаще всего используются в качестве параметров в функциях, когда мы хотим изменить значение аргумента или хотим избежать его затратного копирования.

Поделиться в социальных сетях:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *