Тема 3.5. Указатели и ссылки 3.5.webp
Понятия указателя, стека и кучи

Хранение переменных в памяти

Программа, которая выводит размер переменных разных типов:

351_01

Оператор sizeof – специальная инструкция C++, которая возвращает размер своего аргумента в байтах.

Результат работы программы (при использовании другого компилятора можно получить другие результаты):

351_02

Значения переменных хранятся в оперативной памяти. Оперативная память разбита на байты, каждый из которых имеет свой адрес. Адрес записывается в шестнадцатеричном формате.

351_03

Таким образом, переменная типа int последовательно займет ячейки памяти с адресами 0x60FE98, 0x60FE99, 0x60FE9A, 0x60FE9B.

И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x60FE98.

Программа, демонстрирующая расположение переменных в памяти:

351_04

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

351_05

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

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

Например, если бы при создании игры жанра "Shooter" использовался этот метод работы с данными, то игрокам пришлось бы перезагружать свои высоконагруженные системы после нескольких секунд работы игры. Дело в том, что игрок в каждый момент времени видит различные объекты на экране монитора – все они занимают какое-то место в оперативной памяти компьютера. Если не уничтожать неиспользуемые объекты – обломки, пыль, тени, моменты выстрелов и т.п. – то очень скоро они заполнят весь объем ресурсов ПК. По этим причинам в языке C++ (и во многих других языках) имеется указатель.

Указатель (англ. – pointer) – это переменная, которая содержит адрес ячейки оперативной памяти, адрес другой переменной (т.е. ее расположение в памяти).

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

Указатели имеют две сферы применения:

  • Использование выгоды косвенной адресации: экономия памяти. Делая указатель на файл, мы читаем его из памяти, а не загружаем в ОЗУ. Передавая указатель на переменную в функцию, мы не делаем копию этой переменной и редактируем ее напрямую. Указатели используют для хранения адресов точек входа для подпрограмм в процедурном программировании и для подключения динамических подключаемых библиотек;
  • Методы динамического управления памятью. Выделяется место в так называемой куче (динамической памяти), а переменные, для которых таким образом выделили память, называют динамическими.

351_06

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

Размер кучи – размер памяти, выделенной операционной системой для хранения кучи (под кучу).

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

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

Стек (англ. stack – стопка) – абстрактный тип данных, представляющий собой список элементов, организованных по принципу LIFO (англ. last in – first out, “последним пришел – первым вышел”):

351_06_2

" Куча – большой комод, в который можно класть вещи (оператор new). Но чтобы комод не лопнул, надо из него ненужное удалять (оператор delete). Чтобы с вещью, положенной в комод, можно было общаться, дается веревочка (указатель p = new X).

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

Ну а очередь… – кто первый встал, того и валенки."

(с) cyberforum

Переполнение стека

Переполнение стека (англ. stack overflow) возникает, когда в стеке вызовов хранится больше информации, чем он может вместить.

351_06_3

fn() пытается бесконечно вызывать сама себя. Это приводит к переполнению стека (если в fn() добавить еще переменных, то переполнение произойдет быстрее):

351_06_4

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

351_06_5

Переменная num объявляется и инициализируется. После чего объявляется переменная-указатель pNum. Затем указателю pNum присваивается адрес переменной num. Таким образом обе переменные можно использовать для доступа к одному и тому же месту в памяти.

351_07

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

int*  x;

int  *  x;

int  *x;

Компилятору безразлично, какой из способов используется программистом. Среда разработки Visual Studio явно рекомендует 1-й способ.

351_08


Но если в одной строке объявляются однотипные указатели и переменные, звездочка рядом с типом может привести к путанице:

float* x, y; // x – указатель, y – обычная переменная типа float.

Такой способ не самый наглядный.

float *x, y, *z; //здесь описаны указатели на вещественные числа - x и z, а также вещественная переменная y.

Поэтому рекомендуется объявлять каждую переменную в отдельной строке.

Примеры:

Обращения к переменным через указатель и напрямую

  • Использования обычных переменных:

351_09

351_10

  • Оперирования динамическими переменными посредством указателей:

351_11

351_12

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

delete удалит данные по этому адресу, но указатель всё еще указывает на этот адрес. Поэтому после этого указатель нужно обнулить.

Выделение памяти с помощью  оператора new имеет вид:

тип_данных *имя_указателя = new тип_данных;

Пример:

int* a = new int;

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

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

тип_данных *имя_указателя = new тип_данных (значение);

Пример:

int *b = new int (5);

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

int *ptrVar = new int;

int *pArray = new int [50]; // динамический массив

delete [] pArray;

delete ptrVar;

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

ptrVar = 0;

или (то же самое)

ptrVar = NULL; // NULL – это скрипт, который просто заменяет значение NULL на 0.

ptrVar = nullptr; // лучше использовать nullptr, т.к. nullptr != 0

Пример:

Освобождения памяти с помощью оператора delete:

#include 

using namespace std;

int main()

{

// Выделение памяти

int *a = new int;

int *b = new int;

float *c = new float;

// ... Любые действия программы ...

// Освобождение выделенной памяти

delete c;

delete b;

delete a;

return 0;

}

При использовании оператора delete  для указателя – знак * не используется.

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

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

Пример:

351_13

В 8 строке создается объект в динамической памяти. Код в 8 строке выполняется 1000 раз, причем каждый следующий раз адрес нового объекта перезаписывает значение, хранящееся в указателе px. В 11 строке выполняется удаление объекта, созданного на последней итерации цикла. Однако первые 999 объектов остаются в динамической памяти, и одновременно в программе не остается переменных, которые хранили бы адреса этих объектов. Т.е. в 11 строке невозможно ни получить доступ к первым 999 объектам, ни удалить их. Итого, утечка памяти здесь составляет 999*500*8 байт (почти 4 МБ). И программа скомпилируется без ошибок – об утечке памяти компилятор не сообщит.

Операции с указателями

В следующей таблице offset (англ. – смещение) имеет тип int (операции, близкие к сложению и вычитанию, такие как ++ и +=, которые также могут применяться к указателям):

ОперацияРезультатДействие
pointer + offsetУказательВычисляет адрес элемента, расположенного через offset элементов после pointer
pointer - offsetУказательОперация, противоположная сложению
pointer2 - pointer1СмещениеВычисляет количества элементов между pointer1 и pointer2

Суммирование двух указателей является в C++ некорректной операцией. Также бессмысленно умножать или делить, возводить их в квадрат или извлекать квадратный корень. Например, есть квартал, в котором все дома пронумерованы по порядку. Дом, следующий за домом Курчатова 7, будет иметь адрес Курчатова 8 (или Курчатова 6, если идти в противоположную сторону). Очевидно, что в таком случае через четыре дома от Курчатова 7 будет находиться дом с адресом Курчатова 11. Адрес этого дома можно записать так:

7 Kurchatov + 4 = 11 Kurchatov

И наоборот, если поинтересоваться, сколько домов находится между домами 7 и 11, ответом будет 4:

11 Kurchatov – 7 Kurchatov = 4

Понятно, что любой дом находится относительно самого себя на расстоянии нуль домов:

7 Kurchatov – 7 Kurchatov = 0

Складывать дома 7 и 11 не имеет смысла, поэтому и указатели суммировать бессмысленно.

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

Массив по сути является адресом его первого элемента. Имя массива – это указатель на элемент массива с индексом [0].

351_15

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

351_16

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

351_17

Массив тоже очень “похож” на городской квартал. Каждый элемент массива выступает в качестве дома в этом квартале. Дома – элементы массива – отсчитываются по порядку от начала квартала. Дом на углу улицы отстоит на 0 домов от угла, следующий дом отстоит на 1 дом от угла и т.д. Пользуясь терминологией массивов, можно сказать, что, например, house[0] представляет собой дом по адресу Kurchatov 7, house[1] – дом по адресу Kurchatov 8 и т.д.

Созданы два указателя:

351_18

В *pMyArray записывается то, что хранится в myArray (т.е. адрес первого элемента),

и в *pElement записывается адрес первого элемента.

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

Обход массива можно представить следующим образом:

351_19

Итак, после выполнения строки:

351_20

указатель pElement может, например, содержать адрес 0029FE54.

Можно прибавить к этому адресу целочисленное смещение и перейти к необходимому элементу массива:

351_21

И таким образом:

СмещениеРезультатСоответствуетЗначение
+ 00029FE54myArray [0]2
+ 10029FE58myArray [1]4
+ 20029FE5CmyArray [2]6
......... 
+ n0029FE54+nmyArray [n] 

Поскольку * имеет более высокий приоритет, чем сложение, операция *pMyArray+n привела бы к сложению n со значением, на которое указывает pMyArray. Чтобы выполнить сначала сложение и лишь затем переход к переменной по указателю, следует использовать скобки.

Выражение *(pMyArray+n) возвращает элемент, который находится по адресу pMyArray плюс n элементов.

В действительности соответствие между двумя формами выражений настолько строго, что C++ рассматривает элемент массива myArray[n] как *(pMyArray+n), где pMyArray указывает на первый элемент массива myArray. C++ интерпретирует myArray[n] как *(&myArray[0]+n). Таким образом, если дано int myArray[20], то myArray определяется как &myArray[0].

Имя массива, записанное без индекса элемента, интерпретируется как адрес нулевого элемента массива (или просто адрес массива). Таким образом, можно упростить приведенную ранее запись, поскольку myArray[n] С++ интерпретирует как *(myArray + n).

Использование операций над указателями для адресации внутри массива

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

351_23

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

351_24

Этот вариант функции displayArray() начинается с создания указателя на первый элемент массива ar. После этого функция считывает все элементы массива по порядку. При каждом выполнении оператора for происходит вывод текущего элемента из массива ar. Этот элемент находится по адресу pAr, который (адрес) увеличивается на единицу при каждом выполнении цикла.

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

351_25

Если попытаться разыменовать:

351_26

Чтобы получить адреса элементов, нужно использовать &:

351_27

Использование указателей – более распространенная практика, чем работа с массивами.

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

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

351_28

int* const a – это указатель на массив целых чисел.

Параметры, передаваемые в функцию, становятся константами. Теперь в функции fillArray() нельзя изменить значение size и нельзя в a выделить память для нового массива:

351_29

Если сделать константой еще и указатель, то вообще нельзя будет даже заполнить массив:

351_30

Т.о., при передаче массива и его размера в функцию showArray() используются такие константы, которые не позволят случайно или злонамеренно изменить полученный массив в теле функции.

Пример:

351_31

Указатель на функцию

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

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

тип (*имя_указателя) (параметры)

здесь тип представляет тип возвращаемого функцией значения;

имя_указателя представляет произвольно выбранный идентификатор в соответствии с правилами о наименовании переменных;

параметры определяют тип и название параметров через запятую при их наличии.

Пример определения указателя на функцию: void (*fn) ()

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

351_32

Значит в функции main() нужно вызвать функцию RealizeOrder, а в качестве параметра в скобках указать нужный филиал. В 17 строке будет вызвана функция RealizeOrder, и в 9 строке в указатель на функцию *fn запишется адрес функции FilialMoscow. Далее сработает 11 строка, которая приведет к выполнению функции FilialMoscow в 4 строке (потому что fn хранит в себе адрес именно этой функции – FilialMoscow).

Затем построился еще один филиал, и можно теперь и для него сделать функцию заказа канцелярии:

351_33

Теперь можно в 23 строке менять содержимое в (), выбирая вызов нужной функции или добавить возможность выбора:

351_34

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

Для этого используется следующий формальный синтаксис:

тип (*имя_массива[размер]) (параметры)

Пример:

double (*ar[]) (int, int)

Здесь ar представляет массив указателей на функции, каждая из которых обязательно должна принимать два параметра типа int и возвращать значение типа double.

Использование указателей

1. Дан указатель double **p = 0; Выполнить следующие задания (решения можно оформлять внутри функции main):

  • создайте конструкцию, изображенную на рисунке;
  • выведите число, указанное в квадратике, на экран;
  • после этого удалите все динамические объекты.

352_01

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

352_02

2. Объявите указатель на массив типа double и предложите пользователю выбрать его размер.

Далее напишите четыре функции:

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

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

352_03

3. Объявите указатель на массив типа int и выделите память для 12-ти элементов. Необходимо написать функцию, которая поменяет значения четных и нечетных ячеек массива. Например, есть массив из 4 элементов:

Исходные данные массива

ячейка 0

ячейка 1

ячейка 2

ячейка 3

1

2

3

4

Данные после работы функции

ячейка 0

ячейка 1

ячейка 2

ячейка 3

2

1

4

3

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

352_04

4. Объявить и заполнить двумерный динамический массив случайными числами от 10 до 50. Показать его на экран. Для заполнения и показа на экран написать отдельные функции. (подсказка: функции должны принимать три параметра — указатель на динамический массив, количество строк, количество столбцов). Количество строк и столбцов выбирает пользователь.

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

352_05

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

6. Написать программу с использованием указателей на функцию для решения следующей задачи. Для кафедры ИТ требуется рассчитывать среднюю оценку по информатике по всем 10 студентам группы; для кафедры истории требуется рассчитывать разницу между максимальной и минимальной оценкой; для кафедры филателии нужно выяснить, превышает ли разница между максимальной и минимальной оценками 2 балла. Массив оценок формируется рандомно (от 2 до 5).

Результаты работы программы должны быть следующими:

352_06

352_07

352_08

Объявление ссылки

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

353_01

Отличия указателя от ссылки:

1. В отличие от указателей, у ссылок нет оператора разыменования. Они и так выводят значение.

2. С помощью арифметики указателей можно изменить адрес, на который указывает указатель:

353_02

Для ссылок нет понятия арифметики ссылок. Следующий код приведет к увеличению значения переменной, а не адреса (&xref выводит адрес переменной x):

353_03

3. Указатель может быть неинициализированным, а ссылка – нет.

353_04

4. Указатель может ссылаться на 0, на NULL или на nullptr, а ссылка такой тип данных хранить не может:

353_05

Можно создать цепочку указателей:

int x = 22;

int *px = &x;  // указатель px хранит адрес переменной x

int **ppx = &px; // указатель ppx хранит адрес указателя px

353_06

Здесь указатель ppx хранит адрес указателя px. Поэтому через выражение *ppx (разыменование) можно получить значение, которое хранится в указателе px – адрес переменной x. А через выражение **ppx можно получить значение по адресу из px, то есть значение переменной x.

Можно организовать взаимодействие ссылок и указателей:

353_07

Получается цепочка ссылок. Если изменить в конце цепочки значение, то изменение по цепочке перейдет к переменной x:

353_08

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

353_09

Передача аргументов

Передача аргументов по значению

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

354_01

Передача значений указателей

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

354_02

Передача аргументов по ссылке

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

354_03

В этом примере функция fn() получает не значение переменной x, а ссылку на нее и, в свою очередь, записывает 27 в переменную x, на которую ссылается ссылка x.

Область видимости

Если можно передать функции указатель, то можно и вернуть его как результат работы функции. Функция, которая должна вернуть некоторый адрес, объявляется следующим образом:

double *fn(void);

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

Этот фрагмент программы будет скомпилирован, но не будет корректно работать:

354_04

Проблема в том, что переменная x объявлена внутри функции fn2(). Следовательно, в момент возврата адреса x из fn2() самой переменной x уже не существует, и адрес ссылается на память, которая может быть занята чем-то другим.

Чтобы не возникло такой ошибки видимости: заданы два разных указателя с одинаковым именем pX, которые содержат в итоге один и тот же адрес:

354_05

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

Адрес, возвращенный из функции fn2(), записывается в 12 строке в указатель pX (который локально объявлен в fn1()) и потом используется в 14 строке для записи значения 1.7 в эту же память, ранее выделенную в куче в fn2(). В 18 строке обнуляется указатель pX из fn1().

Объявление и использование массивов указателей

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

354_06

Например, элемент pA[0] является указателем на переменную n1.

В 28 строке – вывод размера массива указателей: указатель занимает в памяти 4 байта, поэтому размер массива указателей из 4 элементов 4*4=16 байт (а не 8*4=32).

Можно сразу  при объявлении массива указателей этот массив инициализировать адресами:

354_07

Описание динамического массива

Динамическим называется массив, размер которого может изменяться во время исполнения программы. Возможность изменения размера отличает динамический массив от статического, размер которого задаётся на момент компиляции программы.

355_01

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

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

Двумерный динамический массив

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

Утечка памяти:

355_02

delete[] a; приведет к тому, что rows уничтожатся, а cols потеряются в куче.

355_03

Чтобы избежать утечки памяти:

355_04

Получится:

355_05

Заполнение и вывод этого двумерного динамического массива:

355_06

Результат:

355_07

Копирование динамического массива

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

355_08

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

355_09

Правильный алгоритм действий:

Были два массива разного размера. Нужно, чтобы массив b стал именно копией массива a. Для этого нужно удалить массив b (вернуть память куче). Затем снова выделить память под b, но уже для другого количества элементов (при этом память может оказаться выделенной как по тому же адресу, где был b, так и по любому другому). Поэлементно скопировать (в цикле) a в b:

355_10

Результат:

355_11

Решение:

355_12

Изменение размера массива

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

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

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

Результат добавления элемента в конец массива:

355_16

Решение:

355_17

Результат удаления последнего элемента:

355_19

Добавим в предыдущую программу еще одну функцию:

355_20

Дополним main():

355_21

Работа с динамическими массивами
  1. Создать динамический массив, получив его длину от пользователя. Заполнить массив псевдослучайными числами. Вывести массив. Вернуть память в кучу.
  2. Создать двумерный динамический массив, получив его параметры от пользователя. Заполнить массив псевдослучайными числами. Вывести массив. Вернуть память в кучу.
  3. Написать программу, которая меняет местами два динамических массива разного размера.
  4. Написать программу, создающую массив из 10 псевдослучайных целых чисел из отрезка [-50;50]. Вывести на экран весь массив и на отдельной строке — значение минимального элемента массива. Для обхода массива использовать указатели (нельзя обращаться к элементам массива по индексам).
  5. С одномерным массивом, состоящим из n вещественных элементов, выполнить следующее: преобразовать массив таким образом, чтобы сначала располагались все элементы, целая часть которых лежит в интервале [а,b], а потом – все остальные.
Тренажёр