ToxicSpider
Бритуля - Богиня

Условие:

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

Программа должна:
1. Распечатать содержимое всех переменных, включая указатели (ВАЖНО! кроме содержимого указателя на char!)
2. Распечатать адреса всех переменных, включая указатели (ВАЖНО! кроме адреса переменной char!)
3. Только для указателей: распечатать содержимое переменных, на которые указывают указатели (через указатели)
4. Только для указателей: изменить содержимое переменных, на которые они указывают (через указатели)
5. Распечатать все заново, то есть повторить с 1 по 3 пункт.




Код:









Результат:










Давайте представим себе такую ситуацию: вы пишете программу, которая хранит в себе цены в магазине. В этом магазине всего 3 вещи и все они имеют порядковые номера. Заходит покупатель, смотрит на вещь, рядом написан его номер. Он вводит этот номер в компьютер и узнает ее стоимость. Как мы напишем это?

Ну, в начале мы объявим переменные:

int first = 0;
int second = 0;
int third = 0;


заполним цены, а потом, через switch-case высветим высветим их стоимость, в зависимости от запроса. Вроде, неплохое решение, правда? А если таких вещей 30? 300? 3000? Мы объявим 3000 переменных? Должно быть какое-то более элегантное решение, скажете вы... я не могу с вами не согласится и на этой лекции мы как раз его узнаем.

У нас замечательно идет сравнение памяти с ящичками, давайте так и продолжим. Давайте немного модернизируем некоторые из наших ящичков: они будут длинными и разбитыми на деления. Каждое деление будет пронумеровано. Цена первой вещи будет лежать в первом делении, второй - во втором и так далее. Весь ящичек будет носить одно название: "Цены".

Теперь получается более удобно! Нам говорят: "Скажи цену на второй предмет". Так... мы идем к ящичку "цены", какой предмет? Второй? Ага... второе деление... достаем от туда число и это число - и есть интересующая нас цена. НО! Так как программисты - люди необычные и у них все не так, как у людей, то первый ящичек получит цифру 0, второй - 1 и так далее))) То есть счет мы начинаем с нуля, а не с единицы. Этому есть, конечно, причина, но о ней мы поговорим позже.

Такой "длинный ящичек" с сгруппированными переменными ОДНОГО ТИПА носит название "Массив" (англ. "array"). Массив объявляется следующим образом: мы пишем тип переменных, пишем имя, далее мы открываем квадратную скобку (клавиша с русской буквой х), пишем число, которое означает количество делений в нашем ящичке, а после закрываем скобку (клавиша с русской буквой ъ), к примеру:

int prices [30]; // я объявил массив с 30-ю ячейками.

prices [5] = 18; // я положил в 6-ой ящик число 18 (почему в 6-ой? написано же 5-ть.... напоминание: начинаем с нуля!)

int curr_num = prices [3]; // я создал переменную curr_num и в нее положил то число, которое лежит в массиве prices, в 4-ой ячейке.

ОЧЕНЬ важное замечание! Во время объявления массива НЕЛЬЗЯ использовать переменную в качестве размера! То есть:

int num = 0;

cin >> num;

int arr [num]; // так писать НЕЛЬЗЯ!!!! Нужно использовать ТОЛЬКО константу!

Вы подумаете, что объявлять массивы, не зная ДО запуска программы их размера, вообще нельзя? Можно, но не так. Как объявляются массивы с размером, который, например, должен ввести пользователь с клавиатуры - мы узнаем позже.

Примечание: есть шанс, что вы попробуете написать int arr [num]; и компилятор скомпилирует это... Это произойдет только по одной причине: потому что конкретный компилятор, с которым мы работаем - умеет это делать, НО! Это фича ТОЛЬКО этого компилятора, все остальные не скомпилируют этот код, поэтому - не привыкайте к халяве)))

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

int array[5];

array[0] = 11;
array[1] = 12;
array[2] = 13;
array[3] = 14;
array[4] = 15;


Окей, мы создали массив чисел размером 5 ячеек и ВСЕ (убедитесь, что все) - заполнили числами. Как это выглядит теоретически? Вот так:



Это еще пока не взгляд в память, а только логическая схема. Давайте разберем: красным написано имя массива, черным - содержимое, а синим - "номер ячейки". Вроде все понятно, правда? А если подумать? Каким образом в памяти хранится 5 ячеек, с одинаковым именем? Как это сделано на самом деле? Это ОЧЕНЬ важно понять! Как именно это сделано на самом деле!

С++ гарантирует, что как только вы создадите массив, он выделит количество ячеек, которые вы указали ПОДРЯД! То есть один за другим! Допустим, мы создали массив элементов и каждый элемент хранится в одном байте. К примеру - массив char. Один char хранится в одном байте. Теперь, если это массив - то эти байты будут расположены один за другим! Если первый элемент получил адрес в памяти, скажем, 17, то ГАРАНТИРОВАНО!!! второй получит 18! и так далее. Это ОЧЕНЬ ВАЖНО ПОНЯТЬ!

А вот имя массива - это имя указателя, который указывает на первый элемент! То есть, другими словами, как только вы написали:

char arr[3];

случилось следующее: операционная система выделила 3 ячейки под 3 элемента, взяла адрес первого элемента, выделила ЕЩЕ одну ячейку под указатель на этот тип и туда положила адрес первого элемента! То, что я сейчас написал и выделил жирным - НУЖНО ЗНАТЬ И ПОНИМАТЬ как таблицу умножения! Читайте лекцию от начала и до сих пор, пока это на 100% не засядет в голове! Естественно, вся эта память была выделена на стеке! То есть, как только алгоритм выйдет за пределы блока, в котором объявлен массив - ВЕСЬ массив, включая указатель на него (его имя) перестанет существовать!

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

char arr [3];

arr [0] = 'a';
arr [1] = 'b';
arr [2] = 'c';


Память:



Как мы видим, что arr на самом деле, это имя указателя, хранящего в памяти адрес первого элемента. Обратим внимание на адреса элементов: все три элемента идут подряд в памяти!

Примечание: я привел в пример массив char по той причине, что char занимает один байт. Если бы это был int, который лежит в 2-х или в 4-х байтах, то пришлось бы числа адресов писать с пропуском 2 или 4, чтобы быть до конца честным, а это могло бы ввести путанницу.

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

char arr [3];
char * p;

p = arr;


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

Можно написать так:

char arr [3] = { 'a', 'b', 'c' };

Этот код выполнил следующее действие: он создал массив из 3-х char, в первую ячкейку положил букву 'a', во вторую - 'b', а в третью - 'c'. Однако, у этого способа есть минус: число, которое стоит в квадратных скобках, должно быть не меньше, чем число элементов, которые мы кладем. То есть программист должен постоянно сидеть и считать. С++ избавил его от этой необходимости, а именно? Можно писать так:

char arr [ ] = { 'a', 'b', 'c' };

то есть - если есть присвоение первоначального значения таким образом: { 'a', 'b', 'c' } или, для, например массива int - {1, 2, 3}; - нет необходимости писать его размер в квадратных скобках, компилятор сам посчитает сколько нужно!

НО! Иногда нам нужно обнулить весь массив.... то есть:

int arr [ ] = {0, 0, 0};

а если 300 ячеек? Это же убийство! А как мы знаем - обнуление важный элемент.... так вот, в случае, если массив надо "забить" нулями, во время объявления, то можно написать так:

int arr [300] = {0};

только один ноль! Но тогда требуется размер в квадратных скобках)))) И самое главное - это работает ТОЛЬКО с нулями! То есть, написав int arr [300] = {5}; - мы создадим массив размером 300 элементов, а в первую ячейку положим 5. Дальше будет мусор.



Стринги


Прежде чем мы перейдем к играм с указателями, я хочу познакомить вас с особым типом массива - массивом char. Да, мы уже видели его чуть ранее, но! У него есть маленький секрет! Часто, в программах нужно сохранять слово, а не букву, логично, да? Как сохранять слово? Нужно просто объявить массив char и побуквенно положить в него буквы. То есть слово "word" должно выглядеть примерно так:

char array [ ] = { 'w', 'o', 'r', 'd' };

НО! тут есть маленькая оговорочка.... если пользоваться таким способом, то всегда надо знать, какой размер у слова, чтобы допустим, его распечатать. То есть, там, где мы его распечатаем - нужно 2 параметра: сам массив и его размер, чтобы после буквы 'd' остановиться. Таскать за собой 2 параметра - неудобно. Поэтому, из ситуации вышли следующим образом: после последней буквы ставится 0. Как мы знаем, что 0 и '0' - две разные вещи, то есть нет никаких проблем с этим, даже, если строка должна содержать ноль как ее часть, например: "есть 0 свободных ячеек", так как 0 посередине строки - это вот такой ноль: '0'.

У ноль, который обозначает конец строки пишут так: '\0' (с бекслешем) НО! 0 и '\0' - одно и тоже.

Массив char с нулем в конце называется "стринг"

То есть наше слово выглядит на самом деле вот так:

char array [ ] = { 'w', 'o', 'r', 'd' , '\0'}; // обратите внимание!!! об этом всегда нужно помнить! Если мы хотим создать стринг, то у него всегда должно быть на одну больше ячеек, чем букв, которые составляют слово или предложение. В этой, последней ячейке должен быть ноль!

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

char str [ ] = "hello world";

или так:

char *str = "hello world";

В таком виде программа создаст стринг, то есть массив char нужного размера, плюс ноль в конце. Так, как это полагается делать.

Распечатать стринг на экран можно так:

cout << str << endl;

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

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




Арифметика указателей


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

Пример:

int arr [ ] = {1, 2, 3};
int * p = arr;


cout << *p << endl; // на экран будет выведено 1

p = p + 2;

cout << *p << endl; // на экран будет выведено 3

p = p - 1;

cout << *p << endl; // на экран будет выведено 2

*p = 48;

теперь наш arr выглядит так:

{1, 48, 3}

Разберем:
в начале, р указывает на первую ячейку массива. Действие p = p + 2 законно, потому что мы знаем, что есть три элемента. И у нас есть еще одно знание: эти элементы лежат в памяти подряд, один за другим. Теперь р указывает на 3-ю ячейку, а после p = p - 1 - на вторую.

Некоторые внимательные скажут:
позвольте.... int хранится в 2-х байтах.... то есть, если в р лежит адрес первого байта, первого int, то сделав действие p = p + 2 мы перескочим на середину второго int...

Ответ: нет. Потому что p указывает на int и программа знает, что если мы прибавляем адрес int, то нужно перескакивать 2 байта на каждую прибавленную единицу. То есть - эти действия полностью законны и отработают так, как и должны. Программист не должен переживать по этому поводу.



Многоярусные массивы и массивы указателей


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

int * arr [3] = {0};

содержит в себе 3 указателя на int


Более неожиданно, это то, что можно создавать многоярусные массивы в моем примере я покажу двухярусный, но нет никаких проблем создать 3-х, 4-х и так далее, по тому же принципу, то есть:

int arr [2] [3];

arr [0][0] = 1;
arr [0][1] = 2;
arr [0][2] = 3;
arr [1][0] = 4;
arr [1][1] = 5;
arr [1][2] = 6;


Создаст следующее (схема):





Точно так же этот массив можно создать и инициализировать сразу:

int arr [ ] [ ] = { {1, 2, 3} , {4, 5, 6} };

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



Домашнее задание


1. Объявите массив float, длиной 3 элемента, заполните его начальными значениями, отличными от нуля.
2. Распечатайте значения.
3. Объявите указатель на float и присвойте ему адрес первого элемента в массиве, созданном на шаге (1)
4. Распечатайте значения массива, созданного на шаге (1) через указатель.
5. Поменяйте значение 2-го элемента "напрямую"
6. Поменяйте значение 3-го элемента через указатель, объявленный на шаге (3)
7. Выполните шаги 2 и 4 еще раз.

Удачи!

@темы: C++