Обработка массивов в си. Программирование

До сих пор мы работали с простыми типами данных – логический (boolean), целый (integer , word , byte , longint), вещественный (real), символьный (char). Любой алгоритм можно запрограммировать с помощью этих четырех базовых типов. Но для обработки информации о многообразном реальном мире требуются данные, имеющие более сложное строение. Такие сложные конструкции, основанные на простейших скалярных типах, называются структурами. Структура – некоторый составной тип данных, составленный из базовых скалярных. Если структура не изменяет своего строения на протяжении всего выполнения программы, в которой она описана, то такую структуру называют статической.

Массив – однородная совокупность элементов

Самой распространенной структурой, реализованной практически во всех языках программирования, является массив.

Массивы состоят из ограниченного числа компонент, причем все компоненты массива имеют один и тот же тип, называемый базовым. Структура массива всегда однородна. Массив может состоять из элементов типа integer , real или char , либо других однотипных элементов. Из этого, правда, не следует делать вывод, что компоненты массива могут иметь только скалярный тип.

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

Индекс массива

Номер элемента массива называется индексом . Индекс – это значение порядкового типа, определенного, как тип индекса данного массива. Очень часто это целочисленный тип (integer , word или byte), но может быть и логический и символьный.

Описание массива в Паскале. В языке Паскаль тип массива задается с использованием специального слова array (англ. – массив), и его объявление в программе выглядит следующим образом:

Type < имя _ типа >= array [ I ] of T;

где I – тип индекса массива, T – тип его элементов.

Можно описывать сразу переменные типа массив, т.е. в разделе описания переменных:

Var a,b: array [ I ] of T;

Обычно тип индекса характеризуется некоторым диапазоном значений любого порядкового типа: I 1 .. I n . Например, индексы могут изменяться в диапазоне 1..20 или " a ".." n ".

При этом длину массива Паскаля характеризует выражение:

ord (I n)- ord (I 1)+1.

Вот, например, объявление двух типов: vector в виде массива Паскаля из 10 целых чисел и stroka в виде массива из 256 символов:

Type
Vector=array of integer;
Stroka=array of char;

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

Опишем переменные типа vector и stroka:

Вычисление индекса массива Паскаля

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

Этот механизм – весьма мощное средство программирования. Но он порождает распространенную ошибку: результат вычислений может оказаться за пределами интервала допустимых значений индекса, то есть будет произведена попытка обратиться к элементу, которого не существует. Эта типичная ошибка называется «выход за пределы массива».

Пример программы с ошибкой массива Паскаля

Program primer _ error ;
Type
vector=array of word;
var
n: integer;
a: vector;
begin
n:=45;
a:=25;
end .

Хотя данная программа полностью соответствует синтаксису языка, и транслятор «пропустит» ее, на стадии выполнения произойдет ошибка выхода за пределы массива Паскаля. При n =45 выражение n *2=90, компьютер сделает попытку обратиться к элементу массива a , но такого элемента нет, поскольку описан массив размерностью 80.

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

Из всего этого следует сделать вывод: программисту надо быть очень аккуратным при работе с индексами массива.

Основные действия с массивами Паскаля

Как известно, определение типа данных означает ограничение области допустимых значений, внутреннее представление в ЭВМ, а также набор допустимых операций над данными этого типа. Мы определили тип данных как массив Паскаля. Какие же операции определены над этим типом данных? Единственное действие, которое можно выполнять над массивами целиком, причем только при условии, что массивы однотипны, – это присваивание. Если в программе описаны две переменные одного типа, например,

Var
a , b: array of real ;

то можно переменной a присвоить значение переменной b (a:= b). При этом каждому элементу массива a будет присвоено соответствующее значение из массива b . Все остальные действия над массивами Паскаля производятся поэлементно (это важно!) .

Ввод массива Паскаля

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

Пример фрагмента программы ввода массива Паскаля

Var
A: array of integer ;
Begin
For i:=1 to 10 do
Readln (a[i]); { ввод i- го элемента производится с клавиатуры }

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

Пример фрагмента программы заполнения массива Паскаля случайными числами

Var
I: byte ; {переменная I вводится как индекс массива}
Begin
For i:=1 to 10 do
A [ i ]:= random (10); { i -му элементу массива присваивается «случайное» целое число в диапазоне от 0 до 10}

Вывод массива Паскаля

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

Пример фрагмента программы вывода массива Паскаля

Var
A: array of integer;
I: byte ; {переменная I вводится как индекс массива}
Begin
For i:=1 to 10 do
Write (a [ i ]," "); {вывод массива осуществляется в строку, после каждого элемента печатается пробел}

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

Пример программы вывода массива Паскаля в столбик

Var
A: array of integer;
I: byte ; {переменная I вводится как индекс массива}
Begin
For i:=1 to 10 do
Writeln ("a[", i,"]=", a[i]); { вывод элементов массива в столбик }

На экране мы увидим, к примеру, следующие значения:

a =2
a =4
a =1 и т.д.

Пример решения задачи с использованием массивов Паскаля

Задача: даны два n -мерных вектора. Найти сумму этих векторов.

Решение задачи:

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

    var a , b: array of integer ;

  • Выходными данными будут элементы результирующего массива, назовем его c . Тип результирующего массива также должен быть целочисленным.
  • Кроме трех массивов нам потребуется переменная – параметр цикла и индекс массива, назовем ее i , а также переменная n для определения количества элементов в каждом массиве.

Ход решения задачи:

  • определим количество элементов (размерность) массивов, введем значение n ;
  • введем массив a ;
  • введем массив b ;
  • в цикле, перебирая значения индекса i от 1 до n , вычислим последовательно значения элементов массива c по формуле:

    c [ i ]= a [ i ]+ b [ i ];

  • выведем на экран полученный массив.

Текст программы:

Пример программы суммирования векторов

Program summa;
Var
a, b, c: array of integer;
I, n: byte;
Begin
Write ("введите размерность массивов:");
Readln(n);
For i:=1 to n do
Readln (a[i]); { ввод массива a}
For i:=1 to n do
Readln (b[i]); { ввод массива b}
For i:=1 to n do
C[i]:=a[i]+b[i]; { вычисление суммы массивов }
For i:=1 to n do
write (c[i]," "); { вывод массива с }
end.

  • Tutorial

В этом посте я постараюсь окончательно разобрать такие тонкие понятия в C и C++, как указатели, ссылки и массивы. В частности, я отвечу на вопрос, так являются массивы C указателями или нет.

Обозначения и предположения

  • Я буду предполагать, что читатель понимает, что, например, в C++ есть ссылки, а в C - нет, поэтому я не буду постоянно напоминать, о каком именно языке (C/C++ или именно C++) я сейчас говорю, читатель поймёт это из контекста;
  • Также, я предполагаю, что читатель уже знает C и C++ на базовом уровне и знает, к примеру, синтаксис объявления ссылки. В этом посте я буду заниматься именно дотошным разбором мелочей;
  • Буду обозначать типы так, как выглядело бы объявление переменной TYPE соответствующего типа. Например, тип «массив длины 2 int"ов» я буду обозначать как int TYPE ;
  • Я буду предполагать, что мы в основном имеем дело с обычными типами данных, такими как int TYPE , int *TYPE и т. д., для которых операции =, &, * и другие не переопределены и обозначают обычные вещи;
  • «Объект» всегда будет означать «всё, что не ссылка», а не «экземпляр класса»;
  • Везде, за исключением специально оговоренных случаев, подразумеваются C89 и C++98.

Указатели и ссылки

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

Int x; int *y = &x; // От любой переменной можно взять адрес при помощи операции взятия адреса "&". Эта операция возвращает указатель int z = *y; // Указатель можно разыменовать при помощи операции разыменовывания "*". Это операция возвращает тот объект, на который указывает указатель

Также напомню следующее: char - это всегда ровно один байт и во всех стандартах C и C++ sizeof (char) == 1 (но при этом стандарты не гарантируют, что в байте содержится именно 8 бит:)). Далее, если прибавить к указателю на какой-нибудь тип T число, то реальное численное значение этого указателя увеличится на это число, умноженное на sizeof (T) . Т. е. если p имеет тип T *TYPE , то p + 3 эквивалентно (T *)((char *)p + 3 * sizeof (T)) . Аналогичные соображения относятся и к вычитанию.

Ссылки . Теперь по поводу ссылок. Ссылки - это то же самое, что и указатели, но с другим синтаксисом и некоторыми другими важными отличиями, о которых речь пойдёт дальше. Следующий код ничем не отличается от предыдущего, за исключением того, что в нём фигурируют ссылки вместо указателей:
int x; int &y = x; int z = y;

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

Lvalue . Те выражения, которым можно присваивать, называются lvalue в C, C++ и многих других языках (это сокращение от «left value», т. е. слева от знака равенства). Остальные выражения называются rvalue. Имена переменных очевидным образом являются lvalue, но не только они. Выражения a , some_struct.some_field , *ptr , *(ptr + 3) - тоже lvalue.

Удивительный факт состоит в том, что ссылки и lvalue - это в каком-то смысле одно и то же. Давайте порассуждаем. Что такое lvalue? Это нечто, чему можно присвоить. Т. е. это некое фиксированное место в памяти, куда можно что-то положить. Т. е. адрес. Т. е. указатель или ссылка (как мы уже знаем, указатели и ссылки - это два синтаксически разных способа в C++ выразить понятие адреса). Причём скорее ссылка, чем указатель, т. к. ссылку можно поместить слева от знака равенства и это будет означать присваивание объекту, на который указывает ссылка. Значит, lvalue - это ссылка.

Окей, но ведь (почти любая) переменная тоже может быть слева от знака равенства. Значит, (такая) переменная - ссылка? Почти. Выражение, представляющее собой переменную - ссылка.

Иными словами, допустим, мы объявили int x . Теперь x - это переменная типа int TYPE и никакого другого. Это int и всё тут. Но если я теперь пишу x + 2 или x = 3 , то в этих выражениях подвыражение x имеет тип int &TYPE . Потому что иначе этот x ничем не отличался бы от, скажем, 10, и ему (как и десятке) нельзя было бы ничего присвоить.

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

Принцип «любое lvalue - ссылка» - тоже моя выдумка. А вот принцип «любая ссылка - lvalue» - вполне законный, общепризнанный принцип (разумеется, ссылка должна быть ссылкой на изменяемый объект, и этот объект должен допускать присваивание).

Теперь, с учётом наших соглашений, сформулируем строго правила работы со ссылками: если объявлено, скажем, int x , то теперь выражение x имеет тип int &TYPE . Если теперь это выражение (или любое другое выражение типа ссылка) стоит слева от знака равенства, то оно используется именно как ссылка, практически во всех остальных случаях (например, в ситуации x + 2) x автоматически конвертируется в тип int TYPE (ещё одной операцией, рядом с которой ссылка не конвертируется в свой объект, является &, как мы увидим далее). Слева от знака равенства может стоять только ссылка. Инициализировать (неконстантную) ссылку может только ссылка.

Операции * и & . Наши соглашения позволяют по-новому взглянуть на операции * и &. Теперь становится понятно следующее: операция * может применяться только к указателю (конкретно это было всегда известно) и она возвращает ссылку на тот же тип. & применяется всегда к ссылке и возвращает указатель того же типа. Таким образом, * и & превращают указатели и ссылки друг в друга. Т. е. по сути они вообще ничего не делают и лишь заменяют сущности одного синтаксиса на сущности другого! Таким образом, & вообще-то не совсем правильно называть операцией взятия адреса: она может быть применена лишь к уже существующему адресу, просто она меняет синтаксическое воплощение этого адреса.

Замечу, что указатели и ссылки объявляются как int *x и int &x . Таким образом, принцип «объявление подсказывает использование» лишний раз подтверждается: объявление указателя напоминает, как превратить его в ссылку, а объявление ссылки - наоборот.

Также замечу, что &*EXPR (здесь EXPR - это произвольное выражение, не обязательно один идентификатор) эквивалентно EXPR всегда, когда имеет смысл (т. е. всегда, когда EXPR - указатель), а *&EXPR тоже эквивалентно EXPR всегда, когда имеет смысл (т. е. когда EXPR - ссылка).

Массивы

Итак, есть такой тип данных - массив. Определяются массивы, например, так:
int x;
Выражение в квадратных скобках должно быть непременно константой времени компиляции в C89 и C++98. При этом в квадратных скобках должно стоять число, пустые квадратные скобки не допускаются.

Подобно тому, как все локальные переменные (напомню, мы предполагаем, что все примеры кода находятся внутри функций) находятся на стеке, массивы тоже находятся на стеке. Т. е. приведённый код привёл к выделению прямо на стеке огромного блока памяти размером 5 * sizeof (int) , в котором целиком размещается наш массив. Не нужно думать, что этот код объявил некий указатель, который указывает на память, размещённую где-то там далеко, в куче. Нет, мы объявили массив, самый настоящий. Здесь, на стеке.

Чему будет равно sizeof (x) ? Разумеется, оно будет равно размеру нашего массива, т. е. 5 * sizeof (int) . Если мы пишем
struct foo { int a; int b; };
то, опять-таки, место для массива будет целиком выделяться прямо внутри структуры, и sizeof от этой структуры будет это подтверждать.

От массива можно взять адрес (&x), и это будет самый настоящий указатель на то место, где этот массив расположен. Тип у выражения &x , как легко понять, будет int (*TYPE) . В начале массива размещён его нулевой элемент, поэтому адрес самого массива и адрес его нулевого элемента численно совпадают. Т. е. &x и &(x) численно равны (тут я лихо написал выражение &(x) , на самом деле в нём не всё так просто, к этому мы ещё вернёмся). Но эти выражения имеют разный тип - int (*TYPE) и int *TYPE , поэтому сравнить их при помощи == не получится. Но можно применить трюк с void * : следующее выражение будет истинным: (void *)&x == (void *)&(x) .

Хорошо, будем считать, я вас убедил, что массив - это именно массив, а не что-нибудь ещё. Откуда тогда берётся вся эта путаница между указателями и массивами? Дело в том, что имя массива почти при любых операциях преобразуется в указатель на его нулевой элемент.

Итак, мы объявили int x . Если мы теперь пишем x + 0 , то это преобразует наш x (который имел тип int TYPE , или, более точно, int (&TYPE)) в &(x) , т. е. в указатель на нулевой элемент массива x. Теперь наш x имеет тип int *TYPE .

Конвертирование имени массива в void * или применение к нему == тоже приводит к предварительному преобразованию этого имени в указатель на первый элемент, поэтому:
&x == x // ошибка компиляции, разные типы: int (*TYPE) и int *TYPE (void *)&x == (void *)x // истина x == x + 0 // истина x == &(x) // истина

Операция . Запись a[b] всегда эквивалентна *(a + b) (напомню, что мы не рассматриваем переопределения operator и других операций). Таким образом, запись x означает следующее:

  • x эквивалентно *(x + 2)
  • x + 2 относится к тем операциям, при которых имя массива преобразуется в указатель на его первый элемент, поэтому это происходит
  • Далее, в соответствии с моими объяснениями выше, x + 2 эквивалентно (int *)((char *)x + 2 * sizeof (int)) , т. е. x + 2 означает «сдвинуть указатель x на два int"а»
  • Наконец, от результата берётся операция разыменования и мы извлекаем тот объект, который размещён по этому сдвинутому указателю

Типы у участвовавших выражений следующие:
x // int (&TYPE), после преобразования типа: int *TYPE x + 2 // int *TYPE *(x + 2) // int &TYPE x // int &TYPE

Также замечу, что слева от квадратных скобок необязательно должен стоять именно массив, там может быть любой указатель. Например, можно написать (x + 2) , и это будет эквивалентно x . Ещё замечу, что *a и a всегда эквивалентны, как в случае, когда a - массив, так и когда a - указатель.

Теперь, как я и обещал, я возвращаюсь к &(x) . Теперь ясно, что в этом выражении сперва x преобразуется в указатель, затем к этому указателю в соответствии с вышеприведённым алгоритмом применяется и в результате получается значение типа int &TYPE , и наконец, при помощи & оно преобразуется к типу int *TYPE . Поэтому, объяснять при помощи этого сложного выражения (внутри которого уже выполняется преобразование массива к указателю) немного более простое понятие преобразования массива к указателю - это был немного мухлёж.

А теперь вопрос на засыпку : что такое &x + 1 ? Что ж, &x - это указатель на весь массив целиком, + 1 приводит к шагу на весь этот массив. Т. е. &x + 1 - это (int (*))((char *)&x + sizeof (int )) , т. е. (int (*))((char *)&x + 5 * sizeof (int)) (здесь int (*) - это int (*TYPE)). Итак, &x + 1 численно равно x + 5 , а не x + 1 , как можно было бы подумать. Да, в результате мы указываем на память, которая находится за пределами массива (сразу после последнего элемента), но кого это волнует? Ведь в C всё равно не проверяется выход за границы массива. Также, заметим, что выражение *(&x + 1) == x + 5 истинно. Ещё его можно записать вот так: (&x) == x + 5 . Также будет истинным *((&x)) == x , или, что тоже самое, (&x) == x (если мы, конечно, не схватим segmentation fault за попытку обращения за пределы нашей памяти:)).

Массив нельзя передать как аргумент в функцию . Если вы напишите int x или int x в заголовке функции, то это будет эквивалентно int *x и в функцию всегда будет передаваться указатель (sizeof от переданной переменной будет таким, как у указателя). При этом размер массива, указанный в заголовке будет игнорироваться. Вы запросто можете указать в заголовке int x и передать туда массив длины 3.

Однако, в C++ существует способ передать в функцию ссылку на массив:
void f (int (&x)) { // sizeof (x) здесь равен 5 * sizeof (int) } int main (void) { int x; f (x); // OK f (x + 0); // Нельзя int y; f (y); // Нельзя, не тот размер }
При такой передаче вы всё равно передаёте лишь ссылку, а не массив, т. е. массив не копируется. Но всё же вы получаете несколько отличий по сравнению с обычной передачей указателя. Передаётся ссылка на массив. Вместо неё нельзя передать указатель. Нужно передать именно массив указанного размера. Внутри функции ссылка на массив будет вести себя именно как ссылка на массив, например, у неё будет sizeof как у массива.

И что самое интересное, эту передачу можно использовать так:
// Вычисляет длину массива template size_t len (t (&a)[n]) { return n; }
Похожим образом реализована функция std::end в C++11 для массивов.

«Указатель на массив» . Строго говоря, «указатель на массив» - это именно указатель на массив и ничто другое. Иными словами:
int (*a); // Это указатель на массив. Самый настоящий. Он имеет тип int (*TYPE) int b; int *c = b; // Это не указатель на массив. Это просто указатель. Указатель на первый элемент некоего массива int *d = new int; // И это не указатель на массив. Это указатель
Однако, иногда под фразой «указатель на массив» неформально понимают указатель на область памяти, в которой размещён массив, даже если тип у этого указателя неподходящий. В соответствии с таким неформальным пониманием c и d (и b + 0) - это указатели на массивы.

Многомерные массивы . Если объявлено int x , то x - это не массив длины 5 неких указателей, указывающих куда-то далеко. Нет, x теперь - это единый монолитный блок размером 5 x 7, размещённый на стеке. sizeof (x) равен 5 * 7 * sizeof (int) . Элементы располагаются в памяти так: x , x , x , x , x , x , x , x и так далее. Когда мы пишем x , события развиваются так:
x // int (&TYPE), после преобразования: int (*TYPE) x // int (&TYPE), после преобразования: int *TYPE x // int &TYPE
То же самое относится к **x . Замечу, что в выражениях, скажем, x + 3 и **x + 3 в реальности извлечение из памяти происходит только один раз (несмотря на наличие двух звёздочек), в момент преобразования окончательной ссылки типа int &TYPE просто в int TYPE . Т. е. если бы мы взглянули на ассемблерный код, который генерируется из выражения **x + 3 , мы бы в нём увидели, что операция извлечения данных из памяти выполняется там только один раз. **x + 3 можно ещё по-другому записать как *(int *)x + 3 .

А теперь посмотрим на такую ситуацию:
int **y = new int *; for (int i = 0; i != 5; ++i) { y[i] = new int; }

Что теперь есть y? y - это указатель на массив (в неформальном смысле!) указателей на массивы (опять-таки, в неформальном смысле). Нигде здесь не появляется единый блок размера 5 x 7, есть 5 блоков размера 7 * sizeof (int) , которые могут находиться далеко друг от друга. Что есть y ?
y // int **&TYPE y // int *&TYPE y // int &TYPE
Теперь, когда мы пишем y + 3 , извлечение из памяти происходит два раза: извлечение из массива y и последующее извлечение из массива y , который может находиться далеко от массива y. Причина этого в том, что здесь не происходит преобразования имени массива в указатель на его первый элемент, в отличие от примера с многомерным массивом x. Поэтому **y + 3 здесь не эквивалентен *(int *)y + 3 .

Объясню ещё разок. x эквивалентно *(*(x + 2) + 3) . И y эквивалентно *(*(y + 2) + 3) . Но в первом случае наша задача найти «третий элемент во втором ряду» в едином блоке размера 5 x 7 (разумеется, элементы нумеруются с нуля, поэтому этот третий элемент будет в некотором смысле четвёртым:)). Компилятор вычисляет, что на самом деле нужный элемент находится на 2 * 7 + 3 -м месте в этом блоке и извлекает его. Т. е. x здесь эквивалентно ((int *)x) , или, что то же самое, *((int *)x + 2 * 7 + 3) . Во втором случае сперва извлекает 2-й элемент в массиве y, а затем 3-й элемент в полученном массиве.

В первом случае, когда мы делаем x + 2 , мы сдвигаемся сразу на 2 * sizeof (int ) , т. е. на 2 * 7 * sizeof (int) . Во втором случае, y + 2 - это сдвиг на 2 * sizeof (int *) .

В первом случае (void *)x и (void *)*x (и (void *)&x !) - это один и тот же указатель, во втором - это не так.

Массивы чрезвычайно важная тема в C++. В программах они используются очень часто и разобраться в этой теме необходимо досконально. Сразу вас обрадую – понять и научиться применять массивы достаточно просто даже начинающему.

Итак, зачем же нужны массивы и что они из себя представляют? К настоящему моменту вы уже хорошо знаете, что данные программы хранятся в объявленных нами . Но бывает так, что программе необходимо хранить сотни (а то и больше) переменных однотипных данных, а также необходимо с ними работать – присваивать значения, изменять их и т.д.

К примеру, надо хранить порядковые номера строк. Согласитесь – любому станет страшно от мысли, что надо создать пятьсот переменных типа int, каждой дать уникальное имя и присвоить значение от 1-го до 500-та. (мне уже страшно:) В таком случае, массивы нас просто спасут.

Отметим основное и перейдем к практическому примеру:

  • массив в С++ – это совокупность определенного количества однотипных переменных, имеющих одно имя. К примеру, int array ; . Эта запись означает, что мы объявили массив с именем array , который содержит в себе 3 переменные типа int ;
  • переменные массива называют элементами;
  • каждый элемент имеет свой уникальный индекс – свой порядковый номер. Используя индекс мы можем обращаться к конкретному элементу. ВАЖНО – индексация элементов массива начинается с 0 . Так в массиве int array первый элемент имеет индекс 0 , а последний – 2 . Чтобы обратиться, например, к нулевому элементу массива и изменить его значение, надо указать имя массива и в квадратных скобках указать индекс элемента – array = 33 .

Рассмотрим пример:

массивы C++

// в этой программе создаем массив с размером size, // с помощью цикла for вносим данные во все ячейки // массива и отображаем их содержимое на экран #include using namespace std; int main() { setlocale(LC_ALL, "rus"); const int SIZE = 10; //объявляем константу int firstArray; //объявляем массив с количеством элементов SIZE for (int i = 0; i < SIZE ; i++) //заполняем и отображаем значения на экран { firstArray[i] = i + 1; // на первом шаге цикла firstArray присвоить 1 (0 + 1) cout << i << "-я ячейка хранит число " << firstArray[i] << endl; } cout << endl; return 0; }

// в этой программе создаем массив с размером size,

// с помощью цикла for вносим данные во все ячейки

// массива и отображаем их содержимое на экран

#include

using namespace std ;

int main ()

setlocale (LC_ALL , "rus" ) ;

const int SIZE = 10 ; //объявляем константу

int firstArray [ SIZE ] ; //объявляем массив с количеством элементов SIZE

for (int i = 0 ; i < SIZE ; i ++ ) //заполняем и отображаем значения на экран

firstArray [ i ] = i + 1 ; // на первом шаге цикла firstArray присвоить 1 (0 + 1)

cout << i << "-я ячейка хранит число " << firstArray [ i ] << endl ;

cout << endl ;

return 0 ;

В строке 12 мы определяем целочисленную константу SIZE , которая будет хранить размер массива (определённое нами, количество его элементов). В строке 13 объявляем массив: указываем тип данных, которые будут храниться в ячейках массива, даем имя и указываем размер в квадратных скобках.

Важно, что в квадратные скобки мы можем записывать только целые константные значения. Надо либо сразу вписать целое число в квадратные скобки при объявлении массива (int firstArray; ), либо определить целочисленную константу до объявления массива и ввести в квадратные скобки имя этой константы (как в нашем примере).

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

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

Можете попробовать в нашем примере внести любую другую цифру в константу SIZE . И вы увидите, что программа будет прекрасно работать – создаст массив на столько элементов, на сколько вы укажете, внесет данные и отобразит их на экране.

А если массив совсем небольшой, к примеру на 5 элементов, инициализировать его можно сразу при объявлении:

Так элементу с индексом 0 – firstArray – будет присвоено значение 11 , а последнему элементу массива firstArray – значение 1 5 . Есть такая фишка – вы можете не указывать размер массива в квадратных скобках и сделать такую запись:

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

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

Следует запомнить, что такая инициализация возможна только для заполнения нулями. Если необходимо заполнить элементы массива какими-либо другими числами, лучше применять цикл. В C++11 (стандарт кодирования) при использовании списковой инициализации (инициализации с фигурными скобками) разрешается даже отбросить знак = .

Хочется показать еще один прием инициализации при создании массива. К примеру, для массива из 30-ти элементов нам надо внести значения 33 и 44 только в ячейки с индексом 0 и 1 соответственно, а остальные заполнить нулями. Тогда делаем так:

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

Организовать заполнение массива можно и при помощи оператора cin :

for (int i = 0; i < size; i++) //заполняем и выводим значения на экран { cout << "Введите значение в ячейку №" << i << " :"; cin >> firstArray[i]; }

for (int i = 0 ; i < size ; i ++ ) //заполняем и выводим значения на экран

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

В каких ситуациях может возникать необходимость в таких структурах данных? Эти массивы могут применяться для представления деревьев, у которых узлы могут иметь произвольное число потомков. Таковым может быть, например, генеалогическое дерево. Вершины первого уровня -Fathers , представляющие отцов, могут задаваться одномерным массивом, так что Fathers [ i ] - этоi -й отец. Вершины второго уровня представляются массивом массивов -Children , так чтоChildren [ i ] - это массив детейi -го отца, аChildren [ i ][ j ] - это j-й ребенокi -го отца. Для представления внуков понадобится третий уровень, так чтоGrandChildren [ i ][ j ][ k ] будет представлять к -го внукаj -го ребенка i -го отца.

Есть некоторые особенности в объявлении и инициализации таких массивов. Если при объявлении типа многомерных массивов для указания размерности использовались запятые, то для изрезанных массивов применяется более ясная символика - совокупности пар квадратных скобок; например, int [ ] задает массив, элементы которого - одномерные массивы элементов типа int .

Сложнее с созданием самих массивов и их инициализацией. Здесь нельзя вызвать конструкторnew int , поскольку он не задает изрезанный массив. Фактически нужно вызывать конструктор для каждого массива на самом нижнем уровне. В этом и состоит сложность объявления таких массивов. Начнем с формального примера:

//массив массивов - формальный пример
//объявление и инициализация
int [ ] jagger = new int [ ] {
new int[ ] {5, 7, 9, 11},
new int[ ] {2, 8},
new int[ ] {6, 12, 4}
};

Массивjagger имеет всего два уровня. Можно считать, что у него три элемента, каждый из которых является массивом. Для каждого такого массива необходимо вызвать конструкторnew , чтобы создать внутренний массив. В данном примере элементы внутренних массивов получают значение, будучи явно инициализированы константными массивами. Конечно, допустимо и такое объявление:

int [ ] jagger 1 = new int [ ] {
new int ,
new int ,
new int
};

В этом случае элементы массива получат при инициализации нулевые значения. Реальную инициализацию нужно будет выполнять программным путем. Стоит заметить, что в конструкторе верхнего уровня константу 3 можно опустить и писать простоnew int [ ] . Вызов этого конструктора можно вообще опустить - он будет подразумеваться:

int [ ] jagger 2 = {
new int,
new int,
new int
};

А вот конструкторы нижнего уровня необходимы. Еще одно важное замечание - динамические массивы возможны и здесь. В общем случае, границы на любом уровне могут быть выражениями, зависящими от переменных. Более того, допустимо, чтобы массивы на нижнем уровне были многомерными.

Массив это структура данных, представленная в виде группы ячеек одного типа, объединенных под одним единым именем. Массивы используются для обработки большого количества однотипных данных. Имя массива является , что такое указатели расскажу немного позже. Отдельная ячейка данных массива называется элементом массива. Элементами массива могут быть данные любого типа. Массивы могут иметь как одно, так и более одного измерений. В зависимости от количества измерений массивы делятся на одномерные массивы, двумерные массивы, трёхмерные массивы и так далее до n-мерного массива. Чаще всего в программировании используются одномерные и двумерные массивы, поэтому мы рассмотрим только эти массивы.

Одномерные массивы в С++

Одномерный массив — массив, с одним параметром, характеризующим количество элементов одномерного массива. Фактически одномерный массив — это массив, у которого может быть только одна строка, и n-е количество столбцов. Столбцы в одномерном массиве — это элементы массива. На рисунке 1 показана структура целочисленного одномерного массива a . Размер этого массива — 16 ячеек.

Рисунок 1 — Массивы в С++

Заметьте, что максимальный индекс одномерного массива a равен 15, но размер массива 16 ячеек, потому что нумерация ячеек массива всегда начинается с 0. Индекс ячейки – это целое неотрицательное число, по которому можно обращаться к каждой ячейке массива и выполнять какие-либо действия над ней (ячейкой).

//синтаксис объявления одномерного массива в С++: /*тип данных*/ /*имя одномерного массива*/; //пример объявления одномерного массива, изображенного на рисунке 1: int a;

где, int —целочисленный ;

А — имя одномерного массива;
16 — размер одномерного массива, 16 ячеек.

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

//ещё один способ объявления одномерных массивов int mas, a;

Объявлены два одномерных массива mas и а размерами 10 и 16 соответственно. Причём в таком способе объявления все массивы будут иметь одинаковый тип данных, в нашем случае — int .

// массивы могут быть инициализированы при объявлении: int a = { 5, -12, -12, 9, 10, 0, -9, -12, -1, 23, 65, 64, 11, 43, 39, -15 }; // инициализация одномерного массива

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

Int a={5,-12,-12,9,10,0,-9,-12,-1,23,65,64,11,43,39,-15}; // инициализации массива без определения его размера.

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

// array.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include << "obrabotka massiva" << endl; int array1 = { 5, -12, -12, 9, 10, 0, -9, -12, -1, 23, 65, 64, 11, 43, 39, -15 }; // объявление и инициализация одномерного массива cout << "indeks" << "\t\t" << "element massiva" << endl; // печать заголовков for (int counter = 0; counter < 16; counter++) //начало цикла { //вывод на экран индекса ячейки массива, а затем содержимого этой ячейки, в нашем случае - это целое число cout << "array1[" << counter << "]" << "\t\t" << array1 << endl; } system("pause"); return 0; }

// код Code::Blocks

// код Dev-C++

// array.cpp: определяет точку входа для консольного приложения. #include using namespace std; int main(int argc, char* argv) { cout << "obrabotka massiva" << endl; int array1 = { 5, -12, -12, 9, 10, 0, -9, -12, -1, 23, 65, 64, 11, 43, 39, -15 }; // объявление и инициализация одномерного массива cout << "indeks" << "\t\t" << "element massiva" << endl; // печать заголовков for (int counter = 0; counter < 16; counter++) //начало цикла { //вывод на экран индекса ячейки массива, а затем содержимого этой ячейки, в нашем случае - это целое число cout << "array1[" << counter << "]" << "\t\t" << array1 << endl; } return 0; }

В строках 10 — 11 объявлен и проинициализирован целочисленный одномерный массив с именем array1 , размер которого равен 16 ячейкам, то есть такой массив может хранить 16 чисел. Любая обработка массива осуществима только совместно с циклами. Какой цикл выбрать для обработки массива — это вам решать. Но лучше всего для этой задачи подходит . Переменную-счётчик counter будем использовать для обращения к элементам одномерного массива array1 . В условии продолжения цикла for стоит строгий знак неравенства, так как шестнадцатого индекса в одномерном массиве array1 нет. А так как нумерация ячеек начинается с нуля, то элементов в массиве 16. В теле цикла for оператор cout печатает элементы одномерного массива (см. Рисунок 2).

Obrabotka massiva indeks element massiva array1 5 array1 -12 array1 -12 array1 9 array1 10 array1 0 array1 -9 array1 -12 array1 -1 array1 23 array1 65 array1 64 array1 11 array1 43 array1 39 array1 -15 Для продолжения нажмите любую клавишу. . .

Рисунок 2 — Массивы в С++

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

// array_sum.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include << "Enter elementi massiva: " << endl; int sum = 0; for (int counter = 0; counter < 10; counter++) // цикл для считывания чисел cin >> << "array1 = {"; for (int counter = 0; counter < 10; counter++) // цикл для вывода элементов массива cout << array1 << " "; // выводим элементы массива на стандартное устройство вывода for (int counter = 0; counter < 10; counter++) // цикл для суммирования чисел массива sum += array1; // суммируем элементы массива cout << "}\nsum = " << sum << endl; system("pause"); return 0; }

// код Code::Blocks

// код Dev-C++

// array_sum.cpp: определяет точку входа для консольного приложения. #include using namespace std; int main(int argc, char* argv) { int array1; // объявляем целочисленный массив cout << "Enter elementi massiva: " << endl; int sum = 0; for (int counter = 0; counter < 10; counter++) // цикл для считывания чисел cin >> array1; // считываем вводимые с клавиатуры числа cout << "array1 = {"; for (int counter = 0; counter < 10; counter++) // цикл для вывода элементов массива cout << array1 << " "; // выводим элементы массива на стандартное устройство вывода for (int counter = 0; counter < 10; counter++) // цикл для суммирования чисел массива sum += array1; // суммируем элементы массива cout << "}\nsum = " << sum << endl; return 0; }

Перед тем как выполнять обработку массива его необходимо объявить, причём размер одномерного массива равен 10, так как это оговорено условием задачи. В переменной sum будем накапливать сумму элементов одномерного массива. Первый цикл for заполняет объявленный одномерный массив, введёнными с клавиатуры числами, строки 12 — 13 . Переменная счётчик counter используется для последовательного доступа к элементам одномерного массива array1 , начиная с индекса 0 и до 9-го включительно. Второй цикл for выводит на экран элементы массива, строки 15 — 16 . Третий цикл for последовательно считывает элементы одномерного массива и суммирует их, сумма накапливается в переменной sum , строки 17 — 18 . Результат работы программы смотреть на рисунке 3.

Enter elementi massiva: 0 1 2 3 4 5 6 7 8 9 array1 = {0 1 2 3 4 5 6 7 8 9 } sum = 45 Для продолжения нажмите любую клавишу. . .

Рисунок 3 — Массивы в С++

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

Двумерные массивы в С++

До этого момента мы рассматривали одномерные массивы, которыми не всегда можно ограничиться. Допустим, необходимо обработать некоторые данные из таблицы. В таблице есть две характеристики: количество строк и количество столбцов. Также и в двумерном массиве, кроме количества элементов массива, есть такие характеристики как, количество строк и количество столбцов двумерного массива. То есть, визуально, двумерный массив — это обычная таблица, со строками и столбцами. Фактически двумерный массив — это одномерный массив одномерных массивов. Структура двумерного массива, с именем a , размером m на n показана ниже (см. Рисунок 4).

Рисунок 4 — Массивы в С++

где, m — количество строк двумерного массива;
n — количество столбцов двумерного массива;
m * n — количество элементов массива.

// синтаксис объявления двумерного массива /*тип данных*/ /*имя массива*/;

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

  • тип данных;
  • имя массива.

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

// пример объявление двумерного массива: int a;

  • a — имя целочисленного массива
  • число в первых квадратных скобках указывает количество строк двумерного массива, в данном случае их 5;
  • число во вторых квадратных скобках указывает количество столбцов двумерного массива, в данном случае их 3.

// инициализация двумерного массива: int a = { {4, 7, 8}, {9, 66, -1}, {5, -5, 0}, {3, -3, 30}, {1, 1, 1} };

В данном массиве 5 строк, 3 столбца. после знака присвоить ставятся общие фигурные скобочки, внутри которых ставится столько пар фигурных скобочек, сколько должно быть строк в двумерном массиве, причём эти скобочки разделяются запятыми. В каждой паре фигурных скобочек записывать через запятую элементы двумерного массива. Во всех фигурных скобочках количество элементов должно совпадать. Так как в массиве пять строк, то и внутренних пар скобочек тоже пять. Во внутренних скобочках записаны по три элемента, так как количество столбцов — три. Графически наш массив будет выглядеть, как двумерная таблица (см. Рисунок 5).

Рисунок 5 — Массивы в С++

В каждой ячейке двумерного массива a показано значение, в нижнем правом углу показан адрес данной ячейки. Адресом ячейки двумерного массива является имя массива, номер строки и номер столбца.

Разработаем несложную программу, на обработку двумерного массива, которая называется «Лабиринт». Лабиринт должен быть построен на основе двумерного массива. Размер лабиринта выберем на свое усмотрение.

// array2.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include < 33; i++) //переключение по строкам { for (int j = 0; j < 20; j++)// переключение по столбцам if (mas[i][j] == 1) { // вывести два раза символ (номер которого 176 в таблице аски) в консоль cout << static_cast(176); cout << static_cast(176); } else cout << " "; // вывести два пробела cout << endl; } system("pause"); return 0; }

// код Code::Blocks

// код Dev-C++

// array2.cpp: определяет точку входа для консольного приложения. #include using namespace std; int main(int argc, char* argv) { // 1-условно "стенки лабиринта" // 2-"правильный путь, выход из лабиринта" // 0-"ложный путь" int mas = { {1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,}, // инициализация двумерного массива {1,2,1,0,0,1,0,1,2,2,2,1,1,1,1,0,0,0,0,1,}, {1,2,1,1,0,1,0,1,2,1,2,2,2,2,1,0,1,1,0,1,}, {1,2,2,2,2,2,2,1,2,1,1,1,1,2,1,0,0,1,0,1,}, {1,1,1,1,1,1,2,1,2,1,0,0,1,2,1,1,0,1,0,1,}, {1,0,0,1,0,0,2,2,2,1,1,0,0,2,0,0,0,1,0,1,}, {1,0,1,1,0,1,1,1,1,1,0,0,1,2,1,1,1,1,0,1,}, {1,0,0,0,0,0,0,0,0,1,1,1,1,2,1,0,0,0,0,1,}, {1,1,1,1,1,1,0,1,1,1,2,2,2,2,1,0,1,1,1,1,}, {1,1,0,0,0,1,0,0,1,1,2,1,1,1,1,0,0,0,0,1,}, {1,0,0,1,0,0,0,0,0,1,2,2,2,2,1,1,1,1,0,1,}, {1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,0,0,0,1,}, {1,2,2,2,2,2,2,2,2,2,2,2,2,2,1,0,1,1,1,1,}, {1,2,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,}, {1,2,1,0,0,0,1,2,2,2,1,0,0,0,0,0,1,1,0,1,}, {1,2,1,1,1,1,1,2,1,2,1,1,1,0,1,0,0,0,0,1,}, {1,2,1,2,2,2,1,2,1,2,2,2,1,1,1,1,1,1,1,1,}, {1,2,1,2,1,2,1,2,1,0,1,2,2,2,2,2,2,2,2,1,}, {1,2,1,2,1,2,1,2,1,0,1,1,1,1,1,1,1,1,2,1,}, {1,2,1,2,1,2,1,2,1,0,0,0,0,0,0,0,0,0,2,1,}, {1,2,1,2,1,2,2,2,1,0,1,1,1,1,1,1,0,1,2,1,}, {1,2,1,2,1,1,1,1,1,0,0,0,1,0,1,0,0,1,2,1,}, {1,2,1,2,2,1,0,0,1,1,1,0,0,0,1,0,1,1,2,1,}, {1,2,1,1,2,1,1,0,0,0,0,0,1,0,1,0,0,1,2,1,}, {1,2,1,1,2,1,0,0,1,1,1,1,1,1,1,1,1,1,2,1,}, {1,2,1,1,2,1,1,0,1,2,2,2,2,2,2,2,2,2,2,1,}, {1,2,1,1,2,1,0,0,1,2,1,1,1,1,1,1,1,1,1,1,}, {1,2,1,1,2,1,0,1,1,2,1,1,1,1,1,1,1,1,2,2,}, {1,2,1,1,2,1,0,0,1,2,1,1,2,2,2,2,2,2,2,1,}, {1,2,1,1,2,1,0,1,1,2,1,1,2,1,1,1,1,1,1,1,}, {1,2,1,1,2,1,0,0,1,2,1,1,2,1,0,0,0,1,0,1,}, {1,2,2,2,2,1,0,1,1,2,2,2,2,0,0,1,0,0,0,1,}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,} }; // два цикла - внутренний и внешний, осуществляющие обращение к каждому элементу массива for (int i = 0; i < 33; i++) //переключение по строкам { for (int j = 0; j < 20; j++)// переключение по столбцам if (mas[i][j] == 1) { // вывести два раза символ (номер которого 176 в таблице аски) в консоль cout << static_cast(176); cout << static_cast(176); } else cout << " "; // вывести два пробела cout << endl; } return 0; }

Правильный и ложный пути можно было бы обозначать одной и той же цифрой, например, нулём, но для наглядности правильный путь обозначен цифрой 2. Инициализация массива выполнялась вручную, только для того, что бы упростить программу. Так как в программе выполняется обработка двумерного массива, нужны два цикла, для переключения между элементами двумерного массива. Первый цикл for выполняет переключение между строками двумерного массива. Так как строк в двумерном массиве 33, то и переменная-счетчик i инкрементируется от 0 до 33, строка 46 . Внутри первого цикла стоит цикл for , который переключается между элементами строки двумерного массива. В теле второго цикла for внутри выполняетcя унарная операция преобразования типа данных — static_cast<>() , которая печатает символ , под номером 176. операция преобразования типов данных дублируется для увеличения ширины лабиринта. Результат работы программы (см. Рисунок 6).

Рисунок 6 — Массивы в С++



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: