Архив рубрики: Указатели и ссылки в C++

В этой рубрике собраны уроки об указателях и ссылках в C++

Указатели в контейнерах. STL (часть 16)




указатели в контейнерах, STL, программирование для начинающих

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

Примечание: Степень выраженности таких эффектов зависит и от типа контейнера, и от рассматриваемых операций. Например, операции взаимного обмена 2-х элементов в ходе сортировки потребует 3-х копирований для контейнера типа vector и не потребует дополнительных копирований для контейнера list. Но операции начального помещения элемента в контейнер (push_back(), insert() и др.) всегда выполняются копированием.

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

Всё работает благополучно, как и ранее — сортируем набор записей по произвольным полям и в любом порядке:

16-1

(Обратите внимание, что завершать это приложение нужно по Ctrl+D — End Of File … в Widows, наверное, по Ctrl+Z.)

Но! … Специально были оставлены отладочные «следы» срабатываний конструкторов и деструкторов записей, и записи конструируются 8 раз, а деструктор срабатывает только 4, для локального массива в точке выхода из блока. Локальный массив для нас вообще не представляет интереса. Он введен для упрощения примера только как набор инициализирующих значений. А вот для записей, помещаемых в контейнер, уничтожение записей не происходит, и мы получаем откровенную утечку памяти. Но хуже того, после того как мы удаляем элемент из контейнера (не предпринимая дополнительных действий), мы и не сможем удалить запись, вызвав для неё delete. Это потому, что после вызова erase() мы потеряли к записи единственный путь доступа через итератор (в коде показан цикл с erase(), так наглядней, что эквивалентно clear(), эффект которого, будет тем же самым).

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

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

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

Здесь некоторую помощь могут оказать умные указатели из последних стандартов C++ (shared_ptr или weak_ptr, но не unique_ptr и не старый добрый и проблемный auto_ptr), нам для этого в предыдущем коде достаточно сменить 4 строчки:

В Windows для shared_ptr необходим #include <memory> , а в других системах не обязательно.

И поведение приложения существенно изменится:

16-2

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

Рассылка новых уроков по программированию:

Бинарные деревья (кратко о главном)




бинарное дерево с++, бинарное дерево c++, иерархия, двоичное деревоВообще, надо признаться, что тема иерархических структур весьма широка. О ней можно спорить много и долго, и так и не прийти к общему знаменателю, четко показывающему что такое иерархия и как с ней работать, по каким канонам в смысле. Я не стану вдаваться в ужасные дебри математического Тартара. Это никому кроме академиков не интересно, и в практическом обозримом слишком тонко.

Итак. В качестве примера дерева возьмем двоичное дерево. Что такое дерево вообще? Это некий набор данных, которые указывают на другие данные. Динамический список. Кстати списки это частный вид дерева. Причем двоичного. Дерево состоит из веток (узлов) и листьев (элементов). Листья — это узлы, которые не указывают ни на кого, у них нет веточек. Бинарное дерево — это дерево, в котором у ветки может быть не более двух листьев или веток. Отсюда и его название — «бинарное» значит «двоичное», т.е. элементов два или меньше. Но никак не более двух.

Вот пример такого бинарного дерева (в картинках Яндекса нашел):

А вот обычное дерево:

У обычного дерева узлов у веток больше может быть.

Не буду вдаваться в классификации деревьев. Это тоже большой объем информации. Об этом можно почитать в Википедии, какие они бывают. Можно набрать в поиске слово «Граф» и почитать. Главное не наткнуться на настоящего графа, у которого бывают графини. По четвергам и субботам :)

Основное правило формирования бинарного дерева в С++: Если значение узла больше добавляемого — добавляется ветка справа, иначе создается ветка слева.

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

Поле Data представляет данные, на основании которых строится дерево. Точнее один из элементов данных. Поля Branch описывают левую и правую ветки дерева, и являются указателями на такую же структуру.

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

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

Строка. Обычная строка Си, которую будем разводить по веткам. Узлы и листы будут содержать символы, по кодам символов будем же определять вправо или влево строить дерево. Хоть я и взял в строку цифровые символы, это не важно. С таким же успехом можно написать туда строку «Они убили Кенни».

Как должна работать программа? Возьмем символы «12384» из строки. Сначала программа видит , что дерево без корня. Его еще нет. Нужно посеять семя, чтоб оно выросло. Первый символ соответственно станет корнем. Второй символ — «2» будет сравниваться с корнем. По коду его значение больше. Значит у корня должна вырасти ветка вправо (RightBranch) в нашем случае. Далее идет тройка. У нас уже есть две ветки. Программа должна пройтись по ним, проверить: 3 больше 1? — Да. Пойти вправо. Там двойка. 3 больше 2? Так точно. После двойки нет веток, значит нужно создать ветку вправо для узла «2».

Дальше идет восемь. Что с ним делать? То же самое. От единицы пройти через двойку до тройки и так же отрастить ветку право.

После восьмерки идет 4. Это значение пройдет корень. Поскольку оно больше, программа должна посмотреть, есть ли у корня ветка справа. Есть — пойти по ней. Пройти «2» и «3», ибо 4 больше по значению. Дальше справа встретится число 8. Оно меньше. Значит «4» уже пойдет от восьмерки не вправо, а влево.

Простейший код построения дерева (с иерархической структурой) будет выглядеть так:

Обратите внимание: В функции три основных условия:

  1. Если узел-ветка не создана — создать его, наполнить данными и указать NULL (0) для его веток;
  2. Если передаваемые данные больше чем данные текущего узла — попытаться построить его правую ветку, или если она уже есть просто пройти по ней;
  3. То же самое для левой ветки, если данные меньше по значению.

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

Все! А чего вы думали? Древо готово. Иггдрасиль знаменитый вполне мог быть создан вот такими вот 14-ю строками на С++. Или какой там божественно-космический язык применялся? Если вы думаете, что посадить дерево сложно — вот вам очевидное невероятное. Нет. Ничего сложного нет. Ну может быть N-мерное дерево будет чуть более сложнее в реализации…

В данном случае чтоб понять принцип его построения не нужны тонны кода. Не обязательно рассматривать вставку между узлами, чтоб к примеру упорядочить дерево или разрабатывать код для балансировки дерева. Принцип в своей Naked сущности очень прост. И если при написании какого-то паука для серфинга по сайтам в интернете или разных 3D дизайнерских программ типа тех, что разрабатывают Autodesk, знания деревьев и графов нужны сумасшедшие, то в случае простой базовой основы достаточно и чего-то попроще.

Осталось пожалуй для соблюдения «феншуя» написать код обхода дерева и вывода на экран

и освобождения дерева:

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

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

На всякий случай поясню еще один финт в выводе:

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

Для этого определяем глобальную переменную int tabs=0; которая будет считать количество отступов, и по факту (кстати!) являться носителем значения уровня узла, т.е. номером его вложенности. Сколько у этого узла родителей-веток вплоть до корня. Это понятие тоже можно встретить в литературе про деревья. Так что оно там (в коде имеется ввиду) не только для наведения красоты на экране.

Вот полный код (при запуске дерево «растет» не сверху вниз, как на рисунках, а слева направо. Не хотелось усложнять код):

На экране:

бинарное дерево с++, бинарное дерево c++, иерархия, двоичное дерево

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

Видео. Автор — «mycodeschool»:

Ссылки в C++





ссылки в C++, ссылочные переменные с++

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

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

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

Набирайте следующий пример и всё должно стать понятным:

Начнем с функции change(). Вызвав ее в строке 17 исходного кода, передаем в неё переменные по значению. Функция благополучно создаст копии этих переменных, запишет в них данные и, завершая свою «нелегкую» работу, уничтожит эти копии. В итоге никаких изменений с передаваемыми переменными не произойдёт.

Теперь посмотрите на определение функции changeRef(), в которую параметры передаются по ссылке — строки 45 — 49. Как говорилось выше, мы использовали & , чтобы сообщить компилятору о том, что принимаемые параметры — это ссылки. Внутри самой функции мы обращаемся к ссылкам, как к обычным переменным. То есть нам не надо применять операцию разыменования, если надо изменить значение переменной:

ссылки в C++, ссылочные переменные с++

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

Напоследок — передача по указателю. Сразу к определению changePtr() — строки 51 — 55. Тут есть то, чего мы не использовали при передаче по ссылке — разыменование указателей для изменения значений переменных:

ссылки в C++, ссылочные переменные с++

Так как функция должна принять переменные по указателю, а указатель хранит адрес, то при вызове (строка 25) передаём в неё адреса переменных:

ссылки в C++, ссылочные переменные с++

Результат работы программы:

ссылки в C++, ссылочные переменные с++

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

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

ссылки в C++, ссылочные переменные с++Так в программе мы можем использовать имя ссылки для последующего обращения к данным, которые хранит aaa. Это если например кто-то до вас написал код и дал название переменной aaa. Этот программист знает, что она хранит количество ящиков. А вы, для своего удобства, дали этой переменной псевдоним amountOfBoxes и используете это новое имя, дописывая какой-то новый код в программу.

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




Подписывайтесь на уведомления о новых статьях на нашем сайте:


Согласен получать уведомления от purecodecpp.com на мой e-mail

Указатели С++. Часть 2





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

В первой части мы рассмотрели, как используя указатель и оператор new можно выделить участок памяти необходимого размера непосредственно в процессе работы программы. Так же узнали, как этот участок памяти можно освободить, используя delete. Увидели как параметры передаются в функцию по указателю. И то, что это даёт возможность внести изменения в значение переменных, которые передаются в функцию.

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

Перед тем, как набирать код, чтобы вас не «засыпало» ошибками — установите свежую версию среды Microsoft Visual Studio. Например Microsoft Visual Studio 2013 Express. Если вы используете более раннюю версию, то вместо функций strcat_s() применяйте strcat() и strcpy() вместо strcpy_s().

Идем по порядку. В строке 4 находится прототип функции. Её задача — выделить новый участок памяти для строки, в конец которой запишется другая строка. Об этом мы поговорим ниже, когда дойдем до определения этой функции. Переходим к строкам 11 — 12. В них определены переменные, которые будут хранить значения длины строк "строка 1 " и "+ строка 2". Длина подсчитывается с помощью встроенной функции strlen(). Она вернет значение длины строки без учета символа \0 . Поэтому при инициализации переменных strSize1 и strSize2 мы прибавляем к тому что вернет strlen(…) единицу.

В строке 14 определён указатель pStr1 на первую строку. Чтобы записать строку, сначала необходимо выделить под нее память: char* pStr1 = new char[strSize1]; Ниже копируем строку в выделенную память: strcpy_s(pStr1, strSize1, "строка 1 "); . Первый параметр, который принимает функция strcpy_s() — указатель на адрес куда надо скопировать строку; второй — размер строки; третий — сама строка. Второй указатель pStr2 определяем точно так же.

В стр. 20 — 21, просим показать на экран записанные строки. Для этого достаточно обратиться к ним по имени указателей. Указатель хранит адрес нулевой ячейки. Когда мы просим показать его на экран — будут поочередно отображаться символы массива (участка памяти, который был выделен ранее), пока не встретится символ конца строки — \0 .

Стр. 23-24 — смотрим сколько байт памяти занимает каждая строка. Строка 26 исключена комментарием. В ней осуществлена попытка дописать вторую строку в первую. Попробуйте удалить // и откомпилировать программу. Выполнение программы прервется ошибкой. Вы увидите на экране следующее:

указатели на строки с++, указатели на строки c++, new, deleteКонечно — ведь выделенный участок памяти под первую строку слишком мал, для того чтобы дописать в него еще какие-либо данные. В строке 28 записываем в переменную requiredSize реально необходимый размер памяти для записи двух строк: int requiredSize = (strSize1 + strSize2) - 1; Единицу отнимаем потому, что в переменных strSize1 и strSize2 уже включены два \0 , а нам необходим только один.

Переместимся к определению функции giveNewMem() — стр. 42 — 51. Она примет первую строку (указатель на неё) и целое число (достаточный размер памяти). Смотрите — нам надо в этой функции выделить новый участок памяти для записи в него символов. Значит функция должна вернуть указатель на этот новый участок памяти. Так мы сможем записать в указатель pStr1 новый адрес, по которому расположена память для записи двух строк. Поэтому в заголовке функции пишем так char* giveNewMem(char *pstr1, int reqSize)

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

Создаем новый указатель и сразу выделяем под него память, достаточную для размещения символов обеих строк (стр. 44). Далее копируем в выделенную память символы первой строки: strcpy_s(strInFunc, reqSize, pstr1); Строка скопирована — нужно освободить память, которую она занимала, чтобы не произошло утечки памяти (стр. 48). Возвращаем из функции указатель на новый участок памяти: return strInFunc;

Получается, когда мы вызываем эту функцию из main() (стр. 31) pStr1 = giveNewMem(pStr1, requiredSize); — в указатель pStr1 запишется адрес нового участка памяти, который способен вместить две строки. Осталось только дописать в эту память вторую строку (стр. 33) и показать её на экран. Перед выходом из программы освобождаем память. Сначала ту что была выделена в функции, а потом ту где располагается вторая строка.

Откомпилируем программу:

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

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

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

Зачем нужны указатели в C++?

  • с помощью указателей, возможно выделение динамической памяти. Так память под данные выделяется в процессе работы программы, а не на этапе компиляции. Это очень выгодно, когда мы не знаем до начала работы программы сколько реально будет использовано данных (переменных). Чтобы выделять память и освобождать ее, используются операторы new и delete.
  • указатели часто используются для доступа к объемным участкам данных из функций. К данным символьных и числовых массивов например, которые определены вне функции. Такой подход пришел из программирования на языке C. В C++ относительно числовых переменных и массивов удобнее использовать передачу по ссылке. Вскоре мы с вами рассмотрим эту тему. Относительно строк в стиле Си лучше применять передачу по указателю.
  • используя указатели — мы работаем с памятью по адресам напрямую. Это быстрее, чем обращение по имени переменных.

Объявление указателей, взятие адреса, разыменование

Рассмотрим подробно на примере:

В строке 8 определена обычная целочисленная переменная. В строке 9 закомментировано объявление и инициализация указателя. Попробуйте удалить знаки комментирования // и откомпилировать. Нам сообщат об ошибке. Все правильно. Ведь указатель должен хранить адрес (шестнадцатеричное число), а не значение (десятеричное число или символ). Для того, чтобы получить адрес переменной используется операция взятия адреса & (амперсанд: Shift + 7). В строке 10 создаем указатель и записываем в него адрес переменной:

инициализация указателя

Чтобы определить указатель, надо объявить его тип, за типом поставить звездочку * и дать ему имя. Инициализировать указатель можно только адресом. Если вы не присваиваете адрес указателю при его объявлении, обязательно инициализируйте его нулем. Тогда такой указатель не сможет привести к сложным ошибкам, которые тяжело найти. Он просто ни на что не будет указывать.

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

разыменование указателя

Только в потоке cout она трактуется иначе — не так как при объявлении указателя. Она помогает обратиться к данным, хранящимся по адресу. Это продемонстрировано в строке 15 исходного кода.

В строке 18 определен целочисленный массив на пять элементов. Ниже определен указатель. Обратите внимание — чтобы инициализировать указатель адресом массива, операция & не применяется. Это потому, что имя массива ссылается на адрес нулевой ячейки. Получается что запись

инициализация указателя

вполне нормально компилируется. В переменную-указатель pFirstArr будет записан адрес нулевой ячейки массива firstArr. Эта запись аналогична следующей

инициализация указателя

В строке 25 показан пример обращения к данным элементов массива через указатель. Используется нотация массивов. То есть нам не надо применять операцию разыменования, чтобы обратиться к данным массива:

cout << "pfirstArr[0] = " << pFirstArr[0] << endl << endl;

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

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

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

инициализация указателя, разыменование указателя, & взятие адреса

Еще одно отступление от темы, чтобы подбодрить тех, кому тяжело дается тема указателей :) Вы не одни. Всё приходит с практикой! И к вам дойдет! Не паникуйте, если это выглядит слишком запутанным для вас. Решайте как можно больше задач по программированию. Даже если вы что-то будете делать не так в процессе написания кода, наши замечательные среды разработки дадут об этом знать.

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

Указатели, как параметры (аргументы) функций:




Подписывайтесь на уведомления о новых статьях на нашем сайте:


Согласен получать уведомления от purecodecpp.com на мой e-mail

Указатели С++. Часть 1





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

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

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

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

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

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

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

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

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

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

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

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

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

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

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

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

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

В заголовке (строка 27) и прототипе функции (строка 5), добавляем операцию * перед именами параметров. Это говорит о том, что функция получит адреса, а не значения переменных. При вызове функции из main() добавляем перед именами передаваемых переменных операцию & (амперсанд — Shift + 7). & означает взятие адреса. Вы помните, что указатель хранит адрес. Поэтому мы не можем передать обычное значение, если в заголовке указано, что функция примет указатель. Используя & перед именами переменных, функция получит их адреса.

В теле функции, при вводе значений в переменные, необходимо использовать разыменование указателей. Делается это с помощью всё той же операции * : cin >> *varForCh1; Так мы внесем изменения в значения переменных, а не в адреса. Проверим работу программы:

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

Всё получилось — значения переменных были изменены в функции.

Такой способ (передача параметров в функцию через указатель) широко использовался в программировании на C. В C++ всё чаще используют передачу параметров в функцию по ссылке. Там отпадает необходимость использовать разыменование * и взятие адреса & переменных. Поэтому использовать передачу параметров по ссылке удобней. Этот способ мы с вами рассмотрим в следующих уроках.

Не переживайте, если что-то не совсем понятно. Вы получили много новой информации в этом уроке — и это вполне нормально, что не всё воспринялось сразу. Понимание указателей придет с практикой. Мы еще поговорим об указателях во второй части к этой статье и порешаем задачи. Так что все будет нормально.

По возможности посмотрите видео об указателях (с 12-й минуты):

Параметры (аргументы) функций:




Подписывайтесь на уведомления о новых статьях на нашем сайте:


Согласен получать уведомления от purecodecpp.com на мой e-mail