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

Обновлено: 06.07.2024

В первой статье были описаны приёмы работы с простейшим видом массивов — одномерным (линейным) массивом. В этой, второй статье будут рассмотрены многомерные массивы. В основном, речь пойдёт о двумерных массивах. Но приведённые примеры легко экстраполируются на массивы любой размерности. Также как и в первой статье, будут рассматриваться только массивы в стиле C/C++, без использования возможностей STL.

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

Классика жанра

Определение автоматических многомерных массивов

Определение автоматических многомерных массивов почти полностью совпадает с определением одномерных массивов (о чём было рассказано в первой статье), за исключением того, что вместо одного размера может быть указано несколько:

В этом примере определяется двумерный массив из 3 строк по 5 значений типа int в каждой строке. Итого 15 значений типа int .

Во втором примере определяется трёхмерный массив, содержащий 3 матрицы, каждая из которых состоит из 5 строк по 2 значения типа int в каждой строке.

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

Инициализация

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

В примере показана статическая инициализация прямоугольного массива. Весь список инициализирующих значений заключён в фигурные скобки. Значения для каждой из 3 строк заключены в свою пару из фигурных скобок, значения для каждого из 5 столбцов для каждой строки перечислены через запятую.

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

Заполнение массива значениями

Многомерный массив заполняется значениями с помощью вложенных циклов. Причём, как правило, количество циклов совпадает с размерностью массива:

В этом примере каждому элементу массива присваивается значение, первая цифра которого указывает номер строки, а вторая цифра — номер столбца для этого значения (нумерация с 1).

Вывод значений массива на консоль

В продолжение предыдущего примера можно написать:

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

Для трёхмерного массива можно написать код, использующий те же приёмы:

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

Расположение в памяти

Для многомерного C-массива выделяется единый блок памяти необходимого размера: размер_массива1 * размер_массива2 * . * размер_массиваN * sizeof(тип_элемента_массива) .

Значения располагаются последовательно. Самый левый индекс изменяется медленнее всего. Т.е. для трёхмерного массива сначала располагаются значения для первой (индекс 0) матрицы, затем для второй и т.д. Значения для матриц располагаются построчно (ср. со статической инициализацией массива выше).

Имя (идентификатор) многомерного C-массива является указателем на первый элемент массива (так же как и для одномерных массивов)

Если код из последнего примера немного изменить:

поставить точку останова на return и посмотреть под отладчиком память, отведённую под переменную ary , то будет видно, что значения, расположенные в памяти, последовательно возрастают:

Поскольку все значения многомерного C-массива располагаются последовательно, то, пользуясь адресной арифметикой, можно сделать следующий хак:

В последнем фрагменте осуществляется доступ к значениям двумерного массива как к одномерному массиву. Цивилизованное решение реализуется через union .

Из двух примеров, приведённых выше, следует, что работу с двумерным или многомерным массивом (в понимании на более высоком уровне абстракции) технически можно организовать посредством одномерного массива соответствующего размера:

Этот приём достаточно распространён. Его выгода в том, что массив ary[DIM1 * DIM2] не обязательно должен быть выделен автоматически. Его можно выделять и динамически. Но при этом логически рассматривать как C-массив.

Вышеприведённый код написан в духе чистого C. В C++ обычно такие вещи прячут в класс, оставляя снаружи лаконичный интерфейс без всяких следов адресной арифметики.

Неродные близнецы

Создание и уничтожение динамических многомерных массивов

Как правило, работа с такими массивами осуществляется следующим образом:

(1) Для доступа к двумерному массиву объявляется переменная ary типа указатель на указатель на тип (в данном случае это указатель на указатель на int ).

(2) Переменная инициализируется оператором new , который выделяет память для массива указателей на int .

(3) В цикле каждый элемент массива указателей инициализируется оператором new , который выделяет память для массива типа int .

Освобождение памяти происходит строго в обратном порядке: сначала уничтожаются массивы значений типа int , а затем уничтожается массив указателей.

Работа с динамическим многомерным массивом синтаксически полностью совпадает с работой с многомерным C-массивом.

Пример кода для трёхмерного массива:

Где собака порылась

Работа с динамическим многомерным массивом синтаксически полностью совпадает с работой с многомерным C-массивом. (Цитирую предыдущий раздел.) Синтаксически — да, но между этими массивами есть глубокое различие, о котором начинающие программисты часто забывают.

Во-первых, для динамического массива выделяется другой объём памяти.

Если посчитать, сколько памяти будет выделяться для двумерного массива из примера выше, то получится: первый оператор new выделил память для 3 указателей, второй оператор new в цикле трижды выделил память для 5 элементов типа int . Т.е. получилось, что выделили памяти для 15 значений типа int и для 3 значений типа указатель на int . Для C-массива компилятором была выделена память только для 15 значений типа int . (Всяческие выравнивания и прочие оптимизации не учитываем!)

Во-вторых, память, выделенная для динамического массива, не непрерывна. Следовательно, хак №1 (обращение с двумерным массивом как с одномерным) работать не будет.

В-третьих, передача многомерных массивов в функции и работа с ними будет отличаться для динамических массивов и C-массивов.

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

Динамический многомерный массив НЕ является C-массивом.

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

Ещё раз о предосторожности

Из вышеизложенного следует, что нужно чётко отличать многомерные C-массивы вида

от массивов указателей на массивы.

Иногда внешние отличия весьма незначительны. К примеру С-строка — это одномерный массив элементов типа char , заканчивающийся нулевым байтом. Как реализовать массив строк?

Это — пример определения и инициализации двумерного C-массива

А здесь определён и инициализирован одномерный (!) массив указателей на массивы элементов типа char .

Вся информация, доступная через переменную month , занимает 13 блоков памяти: массив из 12 указателей и 12 блоков памяти, адреса которых хранятся в указателях, содержащих С-строки с названиями месяцев. И нет никакой гарантии, что 12 блоков памяти с С-строками будут расположены в памяти последовательно и в порядке, соответствующем перечислению в инициализаторе.

И, в заключение, ещё одно предостережение.

Поскольку многомерные C-массивы, как правило, занимают большой объём памяти, их надо с особой осторожностью объявлять внутри функций, в том числе в main() . И с осторожностью в n-ной степени в рекурсивных функциях. Можно легко получить переполнение стека и, как следствие, аварийное завершение программы.

Многомерные массивы при работе с функциями

Поскольку многомерные C-массивы и многомерные динамические массивы — совершенно разные типы данных, то и при работе с функциями подходы будут разные.

Передача в функцию многомерного C-массива

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

Форма (1) — наиболее распространённая.

Форма (2). При передаче многомерного C-массива в функцию можно не указывать длину самого левого измерения. Компилятору для расчёта доступа к элементам массива эта информация не нужна.

Как всегда в C/C++, параметр передаётся в функцию по значению. Т.е. в функции доступна копия фактического параметра. Поскольку имя C-массива является указателем на его первый элемент (т.е. адресом первого элемента), то в функцию передаётся копия адреса начала массива. Следовательно, внутри функции можно изменять значения элементов массива, т.к. доступ к ним осуществляется через переданный адрес, но нельзя изменить адрес начала массива, переданный в качестве параметра, т.к. это — копия фактического параметра.

Возвратить многомерный C-массив из функции в качестве результата стандартными средствами невозможно.

Передача в функцию многомерного динамического массива

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

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

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

Массив указателей используется в каждой программе, которая может получать входную информацию из командной строки (или при её вызове от операционной системы). Одна из классических форм функции main() имеет вид:

Аргументами функции являются количество строк argc (размер массива указателей) и массив указателей на строки — argv . Т.е. argv — это массив указателей на массивы значений типа char .

Пожалуй это всё, что я хотел рассказать в этой статье. Надеюсь, что кто-то сочтёт её полезной для себя.

xkcd как всегда великолепен

Чтобы разобраться в том, что такое указатель, на первых порах приходится прикладывать нехилые усилия из-за слабого понимания принципов функционирования ОС 1 в целом. Я постараюсь описать общие идеи работы с указателями, ссылками и массивами в Си++ безотносительно различных сценариев работы с ними.

Указатель

Определение 2

Таким образом, язык Си++ даёт возможность выделить и проинициализировать память значением, получить адрес значения, получить само значение и, конечно же, освободить занимаемую память:

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

Ссылка на значение

При инициализации переменной значением другой переменной неявно происходит вызов копирования одной переменной в другую. Таким образом на выходе будут существовать 2 различные переменные, значение которых одинаково, и изменение значение одной переменной не приведёт к изменению значение другой:

Ссылка & вместо копирования значения переменной задаст для этого значения ещё одно имя. Будут существовать 2 переменных, ссылающихся на одно и то же значение в памяти. Изменение значения через одну переменную приведёт к изменению значения второй. Вторая переменная будет алиасом первой, если такое определение будет понятней:

Также с помощью этого же оператора возможно получить адрес переменной:

Банковская метафора

Если представить себе стек памяти как портмоне, то отделы для банкнот в них — статические переменных, а банковские карты играют роль указателей. Когда необходимо сохранить определённую сумму денег, то их можно либо непосредственно положить в отдел для банкнот, либо через терминал внести на банковский счёт. В первом случае деньги физически находятся в кошельке, тогда как во втором деньги поступают в банк, который выполняет роль кучи.

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

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

Для случаев, когда программист хочет вычислять размер на этапе выполнения, используются динамические массивы:

Особое внимание следует уделить конструкции delete [] array; , которая удалит весь массив. Это возможно благодаря тому, что массив хранится в памяти непрерывным куском. Т.е. каждое новое значение может быть легко найдено, если известен адрес предыдущего и размер одного элемента:

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

Чтобы создать двумерный массив, необходимо выполнить примерно следующий код:

Вся соль кроется в первой строчке, которую следует читать следующим образом (справа-налево): создать массив из 10 указателей на целочисленный тип и сохранить адрес в указателе на указатель. Каждый, кто постигает смысл этой фразы, сразу же приступает к созданию 3-, 4- и так далее мерных массивов.

На полях

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

Есть одна особенность в Си — делать эффективно, но непонятно. Эта особенность перекочевала и в Си++. Одной из таких штук является определение строки, как динамического массива чаров (англ. char). Чтобы записать строку достаточно записать:

Кстати, удалить такую строку через delete не получится (узнать почему). Массив строк в виде уже знаком, как аргумент главной функции char *argv[] , т.е. аргументы программы считываются, как строки. Например, следующая программа будет приветствовать человека по имени, или же сообщать, что не знакома, если аргументы отсутствуют:

Чтобы программа поприветствовала вас, её нужно запустить следующим образом: $ ./a Maksim Pelevim .

Отступление

Использования строки-си (c-string), вообщем-то, неудобно во всех случаях. Вместо неё в Си++ давно придумали класс string .

каково общее правило для понимания более сложных деклараций?

третий такой же, как и первый.

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

использовать ключевое слово cdecl программа, как предложено K & R.

это работает и в другую сторону.

Я не знаю, есть ли у него официальное название, но я называю его правой-левой штукой(TM).

начните с переменной, затем идите вправо, влево и вправо. и так далее.

arr1-это массив из 8 указателей на целые.

arr2-указатель (скобка блок справа-слева) на массив из 8 целых чисел.

arr3-это массив из 8 указателей на целое число.

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

ответ для последних двух также можно вычесть из золотого правила в C:

объявление следует за использованием.

что произойдет, если вы dereference arr2? Вы получаете массив из 8 целых чисел.

что произойдет, если вы возьмете элемент из arr3? Вы получаете указатель на целое число.

это также помогает при работе с указателями на функции. Взять sigjuice по пример:

что происходит, когда вы разыменование x? Вы получаете функцию, которую вы можете вызвать без аргументов. Что происходит, когда вы называете это? Он вернет указатель на float.

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

edit: example

кстати, я просто наткнулся на следующую ситуацию: функция, которая имеет статическую матрицу и которая использует арифметику указателя, чтобы увидеть, если указатель строки выходит за рамки. Пример:

обратите внимание, что значение границы никогда не изменяется, поэтому компилятор может оптимизировать это. Это отличается от того, что вы могли бы изначально использовать: const int (*border)[3] : это объявляет границу как указатель на массив из 3 целых чисел, который не будет изменять значение, пока существует переменная. Однако этот указатель может быть указан на любой другой такой массив в любое время. Вместо этого мы хотим такого поведения для аргумента (потому что эта функция не изменяет ни одно из этих целых чисел). Декларации следует использовать.

Собирая информацию для написания этой статьи, вспомнилось мне моё первое знакомство с указателями – грусть-печаль была… Поэтому после прочтения нескольких разделов по этой теме из разных книг о программировании на C++, было решено пойти иным путем и изложить тему Указатели C++ в той последовательности, в которой я считаю нужным. Сразу дам вам короткое определение и будем рассматривать указатели в работе – на примерах. В следующей статье (Указатели С++. Часть 2) будут изложены нюансы, применение указателей со строками в стиле Си (символьными массивами) и основное, что следует запомнить.

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

Рассмотрев следующие примеры, вы поймете главное – зачем нам нужны в программировании указатели, как их объявлять и применять.

Допустим, в программе нам необходимо создать целочисленный массив, точный размер которого нам не известен до начала работы программы. То есть мы не знаем какое количество чисел понадобится пользователю внести в этот массив. Конечно, мы можем подстраховаться и объявить массив на несколько тысяч элементов (к примеру на 5 000). Этого (по нашему субъективному мнению) должно хватить пользователю для работы. Да – действительно – этого может быть достаточно. Но не будем забывать, что этот массив займет в оперативной памяти много места (5 000 * 4 (тип int) = 20 000 байт). Мы то подстраховались, а пользователь будет заполнять только 10 элементов нашего массива. Получается, что реально 40 байт в работе, а 19 960 байт напрасно занимают память.

В стандартную библиотечную функцию sizeof() передаем объявленный массив arrWithDigits строка 10. Она вернёт на место вызова размер в байтах, который занимает этот массив в памяти. На вопрос “Сколько чисел вы введете в массив?” ответим – 10. В строке 15, выражение amount * sizeof(int) станет равнозначным 10 * 4, так как функция sizeof(int) вернет 4 (размер в байтах типа int). Далее введем числа с клавиатуры и программа покажет их на экран. Получается, что остальные 4990 элементов будут хранить нули. Так что нет смысла их показывать.

указатели с++, указатели c++, new, delete

Главная информация на экране: массив занял 20 000 байт, а реально для него необходимо 40 байт. Как выйти из этой ситуации? Возможно, кому-то захочется переписать программу так, чтобы пользователь с клавиатуры вводил размер массива и уже после ввода значения объявить массив с необходимым количеством элементов. Но это невозможно реализовать без указателей. Как вы помните – размер массива должен быть константой. То есть целочисленная константа должна быть инициализирована до объявления массива и мы не можем запросить её ввод с клавиатуры. Поэкспериментируйте – проверьте.

указатели с++, указатели c++, new, delete

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

указатели с++, указатели c++, new, delete

Тут нас предупреждают о том, что размером массива НЕ может быть значение обычной переменной. Необходимо константное значение!

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

Пользователь вводит значение с клавиатуры – строка 12. Ниже определён указатель: int * arrWithDigits Эта запись означает, что arrWithDigits – это указатель. Он создан для хранения адреса ячейки, в которой будет находиться целое число. В нашем случае arrWithDigits будет указывать на ячейку массива с индексом 0. Знак * – тот же что применяется при умножении. По контексту компилятор “поймет”, что это объявление указателя, а не умножение. Далее следует знак = и оператор new , который выделяет участок памяти. Мы помним, что у нас память должна быть выделена под массив, а не под одно число. Запись new int [ sizeOfArray ] можно расшифровать так: new (выдели память) int (для хранения целых чисел) [sizeOfArray] (в количестве sizeOfArray ).

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

В строке 25 применяется оператор delete . Он освобождает выделенную оператором new память. Так как new выделил память под размещение массива, то и при её освобождении надо дать понять компилятору, что необходимо освободить память массива, а не только его нулевой ячейки, на которую указывает arrWithDigits. Поэтому между delete и именем указателя ставятся квадратные скобки [] – delete [ ] arrWithDigits ; Следует запомнить, что каждый раз, когда выделяется память с помощью new , необходимо эту память освободить используя delete. Конечно, по завершении программы память, занимаемая ей, будет автоматически освобождена. Но пусть для вас станет хорошей привычкой использование операторов new и delete в паре. Ведь в программе могут располагаться 5-6 массивов например. И если вы будете освобождать память, каждый раз, когда она уже не потребуется в дальнейшем в запущенной программе – память будет расходоваться более разумно.

Допустим в нашей программе мы заполнили массив десятью значениями. Далее посчитали их сумму и записали в какую-то переменную. И всё – больше мы с этим массивом работать уже не будем. Программа же продолжает работу и в ней создаются новые динамические массивы для каких-то целей. В этом случае целесообразно освободить память, которую занимает первый массив. Тогда при выделении памяти под остальные массивы эта память может быть использована в программе повторно.

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

Читайте также: