Как объявить одномерный массив c. Массивы

Пожалуйста, приостановите работу AdBlock на этом сайте.

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

Рис.1 Переменные и массивы. Аналогия с коробками.

На картинке выше изображено три массива:

  • целочисленный массив из 8 элементов с именем arr_int
  • вещественный массив из 11 элементов с именем arr_float
  • символьный массив из 6 элементов с именем arr_char

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

Обратите внимание!

Нумерация элементов массива начинается с нуля, а не с единицы.

Объявление и инициализация массива

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

Листинг 1.

Int arr_int; double arr_float; float number;

На имя массива накладываются ограничения, аналогичные тем, которые накладываются на имя переменной.

Правило именования массивов

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

Вот ещё несколько примеров объявления массивов:

Листинг 2.

Int grades, order; double prices;

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

Листинг 3.

Int arr_int = {2, 5, 5, 3, 4}; double arr_float = {1.2, -2.3, 4.5, 3.83, 0.01, -0.12, 44.2, 123.7, 23.44, -3.7, 7};

Если нужно присвоить нулевые значения всем элементам массива, то можно сделать вот так:

Листинг 4.

Double arr = {0};

Работа с отдельными элементами массива

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

Давайте, например, выведем элементы массива из пяти элементов на экран.

Листинг 5.

#include int main(void){ int arr = {2, 4, 3, 5, 5}; printf("%d %d %d %d %d\n",arr, arr, arr, arr, arr); return(0); }

Конечно, если массив будет очень большой, то выводить его поэлементно подобным образом то ещё удовольствие. Да и с маленькими массивами так никто не делает. Лучше и правильнее использовать циклы. Например:

Листинг 6.

#include int main(void){ int arr = {0}; for(int i = 0; i < 100; i = i + 1){ arr[i] = 2*i; } for(int i = 0; i < 100; i = i + 1){ printf("%d\t",arr[i]); } return(0); }

Программа в первом цикле сохраняет в массив первую сотню чётных чисел, а во втором цикле выводит их на экран.

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

Листинг 7.

#include #include #include int main(void) { srand(time(NULL)); int count = {0}; int rand_number; for (int i = 0; i < 100000; i = i + 1){ rand_number = rand()%3; count = count + 1; } for(int i = 0; i < 3; i = i + 1){ printf("%d - %d\n", i, count[i]); } return 0; }

Обратите внимание на приём, который используется в этой программе.
В нулевом элементе массива хранится количество выпадений числа 0 , в первом элементе – количество выпадений числа 1 , во втором элементе – числа 2 . То есть само сгенерированное число позволяет определить, к какому элементу массива необходимо добавить единичку. Поэтому необходимость в операторе выбора switch отпадает. Удобно, не так ли?

  • 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

Массив в Си - это набор элементов одного типа, обратиться к которым можно по индексу. Элементы массивов в C расположены друг за другом в памяти компьютера.

Простой пример создания и заполнения массива в C:

// @author Subbotin B.P..h> void main(void) { int nArr; nArr = 1; nArr = 2; nArr = 3; printf("\n\tArray\n\n"); printf("nArr\t=\t%d\n", nArr); printf("nArr\t=\t%d\n", nArr); printf("nArr\t=\t%d\n", nArr); return 0; }

Получаем:

В примере объявляем массив, содержащий элементы типа int:

здесь имя массива nArr, количество элементов массива равно трём, тип элементов массива int.

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

Присвоим значение второму элементу массива, а второй элемент имеет индекс один:

Присвоим значение третьему элементу массива, а третий элемент имеет индекс два:

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

printf("nArr\t=\t%d\n", nArr);

Чтоб получить элемент массива, надо указать имя массива и индекс элемента:

это первый элемент массива, ведь у первого элемета индекс ноль.

Присвоим значение третьего элемента массива переменной int a:

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

Теперь общее правило объявления массивов в Си: при объявлении массива нужно указать его имя, тип элементов, количество элементов. Количество элементов есть натуральное число, т.е. целое положительное. Ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива. Вот примеры объявлений массивов в C:

int nArr; // Объявлен массив, предназначенный для хранения ста целых чисел;
float fArr; // Объявлен массив, предназначенный для хранения 5-ти чисел типа float;
char cArr; // Объявлен массив, предназначенный для хранения двух символов;

Ошибкой будет объявить массив с переменным количеством элементов:

Int varElem;
int nArr; // Ошибка! Количество элементов нельзя задавать переменной;

Но можно задавать количество элементов постоянной величиной: или непосредственным целым положительным числом 1, 2, 3... или константой:

Const int arrayLength = 3;
int nArr;

При объявлении массива в Си его сразу можно инициализировать:

int nMassiv = {1, 2, 3};

Можно не указывать количество элементов массива в квадратных скобках, если инициализируются все элементы массива:

int nMassiv = {1, 2, 3};

количество элементов будет в этом случае определено автоматически.

Можно определить лишь часть элементов массива при его объявлении:

int nMassiv = {1, 2};

в этом примере первые два элемента массива инициализированы, а третий не определен.

Пример массива символов:

char cArr = {"S", "B", "P"};

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

Int ind = 0;
char cr = cArr;

Это используется при работе с циклами. Пример:

// @author Subbotin B.P..h> void main(void) { const int arrayLength = 3; int nArr; for(int inn = 0; inn < 3; inn++) { nArr = inn + 1; } printf("\n\tArray\n\n"); for(int inn = 0; inn < 3; inn++) { printf("nArr[%d]\t=\t%d\n", inn, nArr); } return 0; }

В примере в первом цикле заполняем массив элементами типа int, а во втором цикле выводим эти элементы на экран.

Массивы чрезвычайно важная тема в 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 ++ ) //заполняем и выводим значения на экран

Объявление массива в Си
Массив (Array) относится к вторичным типам данных. Массив в Си представляет собой коллекция явно определенного размера элементов определенного типа. то есть в отличие от массивов в Ruby массивы в Си являются однотипными (хранят данные только одного типа) и имеют заранее определенную длину (размер).

В Си массивы можно грубо разделить на 2 типа: массив чисел и массив символов. Разумеется, такое деление абсолютно условное ведь символы — это также целые числа. Массивы символов также имеют несколько иной синтаксис. Ниже приведены примеры объявления массивов:

Int arr; int a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; char ch = {"R","u","b","y","D","e","v",".","r","u"}; char ch2 = "сайт";

В первом случае мы объявляем массив целых чисел (4 байта на число) размером в 100 элементов. Точнее мы резервируем память для хранения 100 элементов типа int.

Во втором случае мы определяем массив из 10 целочисленных элементов и сразу же присваиваем элементам массива значения.

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

В последнем случае мы также объявляем массив символов с помощью специального — более лаконичного синтаксиса. Массивы ch и ch2 практически идентичны, но есть одно отличие. Когда для создания массива мы используем синтаксис со строковой константой, то в конец массива символов автоматически добавляется символ \0, при использовании стандартного синтаксиса обявления массива мы должны самостоятельно добавлять \0 в качестве последнего элемента массива символов. Символ \0 (null) используется для идентификации конца строки. О страках мы поговорим более подробно в отдельной статье.

Обращение к элементам массива в Си

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

#include int main() { int arr; int a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; char ch = {"R","u","b","y","D","e","v",".","r","u"} ; char ch2 = "сайт"; printf("%d\n", arr); printf("%c\n", ch); }

Код printf(«%d\n», a); напечатает 2, а не 1 потому, что индексация массивов начинается с 0 и лишнее подтверждение тому строка printf(«%c\n», ch); , которая напечатает символ «R» — нулевой элемент массива ch.

В общем случае объявление массива имеет следующий синтаксис:

тип_данных имя_переменной[<количество_элементов>] = <список, элементов, массива>

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

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

Указатели в Си
Типы данных необходимы для того, чтобы мочь выделить кусок памяти определенного размера для хранения данных и определения того, что это за данные ибо без явного определения непонятно является ли набор нулей и единиц числом, символом или чем-нибудь еще. В этом случае переменная является ссылкой на фрагмент памяти определенного размера и типа, например, int переменная ссылается на определенную область памяти объемом 4 байта, в которой хранится целое число, а char переменная ссылается на область памяти объемом 1 байт в которой хранится символ (код символа).

Чтобы получить адрес на который ссылается переменная мы используем специальный оператор & — оператор адреса (address operator), пример:

#include int main() { int arr; int a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; char ch = {"R","u","b","y","D","e","v",".","r","u"} ; char ch2 = "сайт"; int num = 100500; printf("%p\n", &arr); }

Строка printf(«%p\n», &arr); напечатает 0xbfbbe068. 0xbfbbe068 — шестнадцатеричное представление адреса памяти где хранится число 100500.

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

#include int main() { int a, b; b = a = 10; printf("A: %p\n", &a); printf("B: %p\n", &b); }

$ ./program
A: 0xbfe32008
B: 0xbfe3200c

В примере выше мы присваиваем переменным a и b одинаковое значение — число 10, но переменные a и b ссылаются на две разные области памяти, то есть мы сохраняем в памяти число 10 два раза. Если мы изменим значение переменной b, то оно это не отразится на переменной a и наоборот. Это отличается от того, как мы работаем с переменными в Ruby, где переменные — это ссылки на объекты хранимые в памяти, и при присваивании в стиле a = b = 10 мы получаем один объект — число 10 и две ссылки на него.

#include int main() { int a = 10; int * b = &a; printf("A:\n\taddress: %p\n\tvalue: %d\n",&a, a); printf("B:\n\taddress: %p\n\tvalue: %d\n",b, *b); }

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

$ ./program
A:
address: 0xbfed0fa8
value: 10
B:
address: 0xbfed0fa8
value: 10

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

На самом деле в Си нет массивов в привычном для многих людей понимании. Любой массив в Си — это просто ссылка на нулевой элемент массива. Пример:

#include int main() { int a = {10,20,30}; printf("a-Address:%p\n", &a); printf("a-Address:%p\n", &a); printf("a-Value:%d\n", a); printf("a-Size:%d\n", sizeof(a)); }

Результат:

$ ./program
a-Address:0xbfc029b4
a-Address:0xbfc029b4
a-Value:10
a-Size:12

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

Когда мы запускаем программу, то операционная система предоставляет программе два особых объема памяти — стек (stack) и кучу (heap). В нашей программе используется только стек. Стек хранит значения упорядочено. Когда мы создаем массив, мы на самом деле создаем указатель ну нулевой элемент коллекции элементов и резервируем память для N-количества элементов. В примере выше мы создали коллекцию из 3 элементов типа int, т.е. каждый элемент занимает 4 байта памяти. Когда мы воспользовались функцией sizeof(), которая возвращает размер в байтах переданного ей аргумента, то получили значение 12 т.е. массив занимает 12 байт памяти: 3элемента * 4 байта. Поскольку для хранения элементов коллекции используется стек — элементы сохраняются по порядку, то есть занимают соседние области стека, а это означает, что мы можем перемещаться по коллекции зная позицию элемента и размер коллекции. Пример:

#include int main() { int a = {10,20,30,40,10}, i; for(i = 0; i <= sizeof(a)/sizeof(int); i++) printf("a[%d] has %d in %p\n", i, a[i], &a[i]); }

Результат:

$ ./program
a has 10 in 0xbfbeda88
a has 20 in 0xbfbeda8c
a has 30 in 0xbfbeda90
a has 40 in 0xbfbeda94
a has 10 in 0xbfbeda98
a has 5 in 0xbfbeda9c

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

#include int main() { int a = {10,20,30,40,10}, i; int * b = a; for(i = 0; i <= sizeof(a)/sizeof(int); i++) printf("a[%d] has %d in %p\n", i, *(b + i), b + i); }

Примечания

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

2. Использование квадратных скобой с указанием индексов элементов массива — это такой синтаксический сахар в Си для более удобного и понятного обращения к элементам коллекции.

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

4. Адрес 1 элемента массива больше адреса 0 элемента массива на объем памяти выделяемой под хранение элемента данного типа. Мы работаем с элементами типа int, для хранения каждого из которых используется 4 байта. Адрес элемента массива в памяти и вообще любых данных — это адрес первого байта выделяемой под его хранение памяти.

5. Для упрощения понимания представьте, что память компьютера — это огромный кинотеатр, где места пронумерованы от 0 до, скачем 1_073_741_824. У данных типа char задница нормального размера и они помещаются в одном кресле (занимают один байт), а у толстых посетителей типа long double задницы огромные и каждый из них вмещается только на 10 сидениях. Когда у толстых посетителей кинотеатра спрашивают номер их места, они говорят только номер первого кресла, а количество и номера всех остальных кресел можно легко вычислить исходя из комплекции посетителя (типа данных). Массивы можно представить в виде групп однотипных посетителей кинотеатра, например группа худеньких балерин типа char из 10 человек займет 10 мест потому, что char вмещается в одном кресле, а группа любителей пива состоящая из 5 человек типа long int займет 40 байт.

6. У операторов & и * имеется несколько популярных названий, но вы можете называть их хоть Васей и Петей. Главное, что стоит запомнить — это:

& — показывает номер первого занятого посетителем кинотеатра сидения. То есть адрес первого занимаемого байта.

* — позволяет обратиться к посетителю сищящему на определенном месте. То есть позволяет получить значение, что хранится по определенному адресу в памяти.

На этом статья окончена, но не окончена тема массивов и указателей, а тем более изучения всего языка Си.

Responses

  1. anonymouse says:

    Проверь, действительно ли эти два массива одинаковы:
    char ch = {‘R’,"u’,"b’,"y’,"D’,"e’,"v’,’.’,"r’,"u’};
    char ch2 = «сайт»;
    Во втором случае массив содержит на один элемент больше, /0, который используется как ограничитель при печати, копировании строк и так далее.

  2. admin says:

    На самом деле оба массива содержат символ \0 в качестве 10 элемента, по этому они действительно одинаковы, но о символе \0 я расскажу в отдельной статье посвященной символьным массивам и строкам.

  3. anonymouse says:

    Да, ты оказался прав, я написал тот комментарий до того как сам проверил вот этот код в GCC:
    #include
    #include

    int main(void)
    {
    char ch = {‘R’,"u’,"b’, ‘y’, ‘D’, ‘e’, ‘v’, ‘.’, ‘r’, ‘u’};
    char ch2 = «сайт»;

    printf(«%x\n», ch[ strlen(ch) ]);

    return 0;
    }
    Печатает ноль.

  4. admin says:

    Самое интересно, что если верить спецификации ANSI C, то ты прав ведь там ничего не сказано об автодобавлении нулевого символа в конец массива символов созданного стандартным для массивов способом (и в K&R это в обоих вариантах делается явно). Думаю, это или отличие в С99 или в компиляторе дело, так как производители компиляторов реализуют возможности С99 в основном частично, а некоторые добавляют что-то свое. Теперь понятно, почему выбор компилятора так важен. Нужно будет над этим вопросом попозже поработать и написать статью о различиях компиляторов Си, поддержке ими С99 и различиях между ANSI C и C 99.

  5. admin says:

    Провел расследование, все таки я дезинформировал тебя. В традиционном синтаксисе \0 не добавляется, это просто такое совпадение, что следующим в стеке идет символ \0, но он не относится к массиву символов. Если использовать strlen() то явно видна разница в 1 символ между традиционным синтаксисом создания массива. где символы просто перечисляются и использованием строковой константы. null-символ добавляется автоматически только в конец массива символов созданного при помощи строковой констранты.

  6. andr says:

    Очень много вводящих в заблуждение высказываний. Такими статьями начинающих программистов только портить. :)
    Например, «В 5 элементе коллекции, которого мы на самом деле не объявляли хранится общее количество элементов коллекции.», — вот это невиданные сказки. В языке С нигде не хранится длина массивов. В этом примере происходит выход за пределы массива ‘a’, т.к. для 5-ти элементов массива последний индекс == 4, а ты его индексируешь до 5-ти. Тем самым обращаешься по адресу переменной i, которую компилятор разместил в памяти сразу после массива, поэтому на последнем цикле (когда i == 5) и получаешь в результате 5-ку на выходе.

    «Как я уже говорил, в Си нету традиционных массивов потому, я называю их коллекциями для того, чтобы подчеркнуть эту особенность Си.» — это совсем что-то непонятное. Что такое «традиционные массивы»? Коллекции, кстати, это более широкий термин. Под определение коллекций подходят массивы, списки, матрицы, стеки и даже хеш-таблицы. Зачем вводить неподходящие термины и вводить в заблуждение читателей?

  7. admin says:

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

  8. faustman says:

    Про худеньких балерин и группу любителей пива хорошо сказал!))

  9. Myname says:

    А у меня gcc a, которое, вы говорите, хранит количество элементов, выдает значение 32767.