Все остальные операции над указателями запрещены. Министерство финансов Российской Федерации Всероссийская государственная налоговая академия ИНФОРМАТИКА И ПРОГРАММИРОВАНИЕ Бакалавры: 230700 "Прикладная информатика" Язык программирования С++ Лабораторная работа № 15 ПРОГРАММИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ УКАЗАТЕЛЕЙ Автор профессор кафедры "Прикладной информатики в экономике" кандидат технических наук Л.К. Кузнецов Мая 2012 г. Москва ВГНА Лабораторная работа № 15 Программирование с использованием указателей Цель работы: Ознакомиться: Ø с понятием указатель; Ø с моделями памяти в языке С++; Ø с видами структур; Ø с понятием структурированной переменной; Ø с правилами создания динамических массивов на языке С++; Ø с приемами обработки динамических массивов; Ø с операциями динамического выделения и освобождения памяти; Ø с операциями косвенного обращения к элементам массива. Изучить типовые алгоритмы обработки информации с использованием указателей. Научитьсяразрабатывать и отлаживать в среде программирования программы с использованием указателей. Получить практические навыки выделения, перераспределения и освобождение памяти при работе с динамическими массивами. 1. КРАТКИЕ ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ Ниже приводятся минимальные сведения, необходимые только для выполнения лабораторной работы 1.1. Понятие указателя В языке Си существует два способа доступа к переменной: ссылка на переменную по имени и использование механизма указателей. Механизм указателей в Си – одна из наиболее привлекательных для профессиональных программистов особенностей языка. Указатель – это переменная, которая может содержать адрес некоторого объекта в памяти компьютера, например, адрес другой переменной. Через указатель, установленный на переменную, можно обращаться к участку оперативной памяти (ОП), отведенной компилятором под ее значение. Указатель – это переменная, значением которой является адрес некоторого объекта (обычно другой переменной) в памяти компьютера. Подобно тому, как переменная типа char имеет в качестве значения символ, а переменная типа int – целочисленное значение, переменная типа указателя имеет в качестве значения адрес ячейки оперативной памяти. Допустимые значения для переменной-указателя – множество адресов оперативной памяти компьютера. Указатель является одной из наиболее важных концепций языка C. Правильное понимание и использование указателей особенно необходимо для составления хороших программ по следующим причинам: · указатели являются средством, при помощи которого функции могут изменять значения передаваемых в нее аргументов; · при помощи указателей выполняется динамическое распределение памяти; · указатели позволяют повысить эффективность программирования; · указатели обеспечивают поддержку динамических структур данных (двоичные деревья, связные списки). Однако указатель может вызвать и ряд затруднений, например, если указатель содержит неправильное значение, программа может быть неработоспособной. Можно легко ошибиться при использовании указателей; к тому же ошибки, связанные с неправильными значениями указателей, найти очень трудно. Указатель – переменная, значением которой является адрес ячейки памяти. То есть указатель ссылается на блок данных из области памяти, причём на самое его начало. Указатель может ссылаться на переменную или функцию. Для этого нужно знать адрес переменной или функции. Так вот, чтобы узнать адрес конкретной переменной в С++ существует унарная операция взятия адреса "&". Такая операция извлекает адрес объявленных переменных, для того, чтобы его присвоить указателю. Указатель-переменная (или просто указатель) – это переменная, предназначенная для хранения адреса в памяти. Указатель-константа – это значение адреса оперативной памяти. В языке Си определены две специальные операции для доступа к переменным через указатели: операция ― "&" и операция ―"*". Результатом операции ―"&" является адрес объекта, к которому эта операция применяется. Например, &var1 дает адрес, по которому var1 хранится в памяти (точнее адрес первого байта var1). Операция ―"*" – это операция обращения к содержимому памяти по адресу, хранимому в переменной-указателе или равному указателю-константе. Признаком переменной-указателя для компилятора является наличие в описании переменной двух компонентов: 1) типа объекта данных, для доступа к которому используется указатель (или, как часто говорят, на который ссылается указатель); 2) символа ―"*" перед именем переменной. В совокупности тип и ―"*" воспринимаются компилятором как особый тип данных – "указатель на что-либо". Таким образом, описание int var1, *ptr приводит к появлению переменной var1 и указателя-переменной ptr. Указатель ptr имеет тип *int, т.е. тип ― "указатель на целое". Место, выделяемое под такой тип компилятором, зависит от модели памяти. Операцию ―"*", пытаясь выразить словами смысл выражения, можно заменить фразой ― "взять содержимое по адресу, равному значению указателя". Например, оператор присваивания *ptr = *ptr2 + 4 можно интерпретировать так: взять содержимое памяти по адресу, равному значению указателя ptr2, прибавить к этому содержимому 4, а результат поместить по адресу, равному значению указателя ptr. Число байтов, извлекаемых из памяти и участвующих в операции, определяется компилятором исходя из типа, на который ― "указывает" указатель. Указатель -это переменная, значением которой является адрессдругой переменной. Так как указатель может ссылаться на переменные разных типов, с указателем в языке Си связывается тип того объекта, на который он ссылается. Для описания указателей используется операция косвенной адресации *. Например, указатель целого типа ukописывается так : int *uk.Унарная операция &, примененная к некоторой переменной, показывает, что нам нужен адресс этой переменной, а не ее текущее значение. Если переменная ukобъявлена как указатель, то оператор присваивания uk=&xозначает: "взять адресс переменной x и присвоить его значение переменной-указателю uk". Как вы уже знаете, программы на C++ хранят переменные в памяти. Указатель представляет собой адрес памяти, который указывает (или ссылается) на определенный участок. Из урока 10 вы узнали, что для изменения параметра внутри функции ваша программа должна передать адрес параметра (указатель) в функцию. Далее функция в свою очередь использует переменную-указатель для обращения к участку памяти. Некоторые программы, созданные вами в нескольких предыдущих уроках, использовали указатели на параметры. Аналогично этому, когда ваши программы работают с символьными строками и массивами, они обычно используют указатели, чтобы оперировать элементами массива. Так как применение указателей является общепринятым, очень важно, чтобы вы хорошо понимали их использование. Таким образом, этот урок рассматривает еще один аспект применения указателей. К концу данного урока вы освоите следующие основные концепции: Ø Для простоты (для уменьшения кода) многие программы трактуют символьную строку как указатель и манипулируют содержимым строки, используя операции с указателями. Ø Когда вы увеличиваете переменную-указатель (переменную, которая хранит адрес), C++ автоматически увеличивает адрес на требуемую величину (на 1 байт для char, на 2 байта для int, на 4 байта для float и т.д.). Ø Ваши программы могут использовать указатели для работы с массивами целочисленных значений или значений с плавающей точкой. Операции с указателями широко используются в C++. Выберите время для эксперимента с программами, представленными в этом уроке. 1.2. Объявление указателя Понимание и правильное использование указателей является основой для создания профессиональных программна языке С. Указатель — это переменная, которая предназначена для хранения и использования в программе адреса некоторого объекта. Здесь имеется в виду адрес в памяти компьютера. Адрес представляет собой простое целое число, но его нельзя трактовать как переменную или константу целого типа. Если переменная по смыслу является указателем, то она должна быть соответствующим образом объявлена. Форма объявления указателя следующая: Тип *Имя_переменной; //. В этом объявлении тип — некоторый допустимый для языка С (С++) тип данных. Это тип той переменной, адрес которой будет хранить указатель. Знак * означает, что следующая за ним переменная является указателем. Например: char *ch; int *temp, i, *j; float *pf, f; //. Здесь объявлены указатели ch,temp,j,pf, переменная i типа int и переменная f типа float. С указателями связаны две специальные операции: & и *. Обе эти операции являются унарными, т. е. имеют один операнд, перед которым они ставятся. Операция & соответствует по смыслу операции взятия (определения) адреса. Операция * является операцией взятия (определения) значения по указанному адресу. Данные операции нельзя спутать с соответствующими им по написанию бинарными операциями, поразрядным AND и операцией умножения, так как они являются унарными, что всегда видно из контекста программы. Для всех переменных выделяются участки памяти размером, соответствующим типу переменной. Программист имеет возможность работать непосредственно с адресами, для чего определен соответствующий тип данных – указатель. Указатель имеет следующий формат: тип *имя_указателя; Например: int *a; double *b, *d; char *c; Знак «звездочка» относится к имени указателя. Значение указателя соответствует первому байту участка памяти, на который он ссылается. На один и тот же участок памяти может ссылаться любое число указателей. В языке С существует три вида указателей: 1. Указатель на объект известного типа. Содержит адрес объекта определенного типа. Например: int *ptr; 2. Указатель типа void. Применяется, еcли тип объекта заранее не определен. Например: void *vptr; 3. Указатель на функцию. Адрес, по которому передается управление при вызове функции. Например: void (*func)(int); *func указатель на функцию, принимающей аргумент int и не возвращающей никаких значений. При объявлении указателя очень важным является базовый тип, который сообщает компилятору сколько байт памяти занимает переменная, на которую указывает данный указатель. Простейшие действия с указателями проиллюстрируем на примере следующей программы/ Пример 15.1 # include <iostream.h> int main (void) { //Объявляем переменные х и у float x= 12.3, у; // Объявляем указатель p float *p; // Присваиваем указателю р адрес переменной х р=&х; // Переменной у присваиваем значение // переменной, адрес которой // содержит указатель р у=*p; // Выводим на экран х и у cout << "x=" << x << " y=" << y << endl; // Увеличиваем на 1 значение, // взятое по указателю р (*р)++; // Выводим на экран х и у cout << "x=" << x << " y=" << y << endl; // Добавляем 1 к произведению значения, // взятого по указателю р на у у=1+(*р)*у; // Выводим на экран х и у cout << "x=" << x << " y=" << y << endl; } // конец программы. К указателям можно применять операцию присваивания, если они являются указателями одного типа. Пример 15.2 # include <iostream.h> int main(void) {int x=12; int *p, *g; p=&x; g=p; printf("%p\n",p); printf("%p\n",g); printf("%d\n%d\n",x,*g); } // конец программы. В этом примере приведена спецификация формата %р функции printf(), которая используется для вывода адреса памяти в шестнадцатеричной форме. Нельзя создать переменную типа void, но можно создать указатель на такой тип. Указателю на void можно присвоить значение указателя любого другого типа. При обратном присваивании необходимо использовать явное преобразование указателя на void. Например, рассмотрим следующий фрагмент: void *pv; float f, *pf; pf=&f; pv=pf; pf=(float*)pv; // . Здесь указатель pv приводится к типу (float*), то есть к указателю на переменную типа float. В языке С допустимо присвоить указателю любой адрес памяти. Однако если объявлен указатель на целое (int *р), а по адресу, который присвоен данному указателю, находится переменная х другого типа, то при компиляции программы будет выдано сообщение об ошибке в строке р=&х. Эту ошибку можно исправить, преобразовав адрес переменной х к нужному типу указателя явным преобразованием типа: p=(int*)&x; но при этом теряется информация о типе переменной х. Над указателями можно производить арифметические операции: сложение и вычитание. Арифметические действия над указателями имеют свои особенности. Рассмотрим программу: #include <stdio.h> int main(void) { int *p; int x=12; р=&х; printf("%p\n%p",p,++p); } //. При выполнении этой программы увидим, что в результате операции ++р значение указателя р увеличиться на 2, а не на 1. Это правильно, так как следующее значение указателя указывает на адрес следующего целого, а не на следующий адрес (целое занимает 2 байта). Таким образом, при каждой операции ++р значение указателя будет увеличиваться на количество байт, занимаемых переменной базового типа указателя. К указателям можно прибавлять или вычитать некоторое целое. Пусть указатель р имеет значение 4000 и указывает на целое. Тогда в результате выполнения оператора р=р+5; значение указателя станет равным 4010. Общая формула для вычисления значения указателя после выполнения операции р=р+n; будет иметь вид: р=р+n*Размер_в_байтах_переменной_базового_типа. Аналогичны правила вычитания целых констант из значения указателя. Можно также вычитать один указатель из другого. Другие арифметические операции над указателями запрещены, т. е. нельзя складывать два указателя, умножать друг на друга, делить и т. д. Указатели можно сравнивать. Применимы все 6 операций сравнения. Сравнение p<g дает "истину если адрес, находящийся в р, меньше адреса, находящегося в g. В языке С допускается использование указателей, указывающих на указатель [4]. «В этом случае описание имеет следующий вид: int **point; //. Здесь point имеет тип указатель на указатель int. Соответственно, чтобы получить целочисленное значение переменной, на которую указывает point, надо в выражении использовать **point. Рассмотрим пример: #include <stdio.h> int main(void) { int i; int *pi; int **ppi; i=12; pi=&i; ppi=π printf("i = %d pi = %p ppi = %p **ppi = %d\n",i,pi,ppi,**ppi); } //. После того как указатель был объявлен, но до того как ему было присвоено значение, указатель содержит неопределённое значение. Попытка использовать такое значение может вызвать ошибку при выполнении программы и даже нарушить работу операционной системы.» Всем указателям, не используемым в данный момент целесообразно присваивать предопределённое значение NULL. Оно является в некотором смысле «заземлением» указателя. Обращение к объектам любого типа в языке C может проводиться по имени, как мы до сих пор делали, и по указателю (косвенная адресация). Перед использованием указатель должен быть инициирован либо конкретным адресом, либо значением NULL (0) – отсутствие указателя. С указателями связаны две унарные операции: & и *. Операция & означает «взять адрес», а операция разадресации * – «значение, расположенное по адресу», например: int x, *y; // х – переменная типа int , у – указатель типа int y = &x; // y – адрес переменной x *y = 1; // по адресу y записать 1, в результате x = 1 При работе с указателями можно использовать операции сложения, вычитания и сравнения, причем выполняются они в единицах того типа, на который установлен указатель. Операции сложения, вычитания и сравнения (больше/меньше) имеют смысл только для последовательно расположенных данных – массивов. Операции сравнения «==» и «!=» имеют смысл для любых указателей, т.е. если два указателя равны между собой, то они указывают на одну и ту же переменную. Указатели используются для передачи по ссылке данных, что намного ускоряет процесс обработки этих данных (в том случае, если объём данных большой), так как их не надо копировать, как при передаче по значению, то есть, используя имя переменной. В основном указатели используются для организации динамического распределения памяти, например при объявлении массива, не надо будет его ограничивать в размере. Ведь программист заранее не может знать, какого размера нужен массив тому или иному пользователю, в таком случае используется динамическое выделение памяти под массив. Любой указатель необходимо объявить перед использованием, как и любую переменную. //объявление указателя /*тип данных*/ * /*имя указателя*/; Принцип объявления указателей такой же, как и принцип объявления переменных. Отличие заключается только в том, что перед именем ставится символ звёздочки "*". Визуально указатели отличаются от переменных только одним символом. При объявлении указателей компилятор выделяет несколько байт памяти, в зависимости от типа данных отводимых для хранения некоторой информации в памяти. Чтобы получить значение, записанное в некоторой области, на которое ссылается указатель нужно воспользоваться операцией разыменования указателя "*". Необходимо поставить звёздочку перед именем и получим доступ к значению указателя. Разработаем программу, которая будет использовать указатели. // pointer1.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include <iostream> using namespace std; int main(int argc, char* argv[]) { int var = 123; // инициализация переменной var числом 123 int *ptrvar = &var; // указатель на переменную var (присвоили адрес переменной указателю) cout << "&var = " << &var << endl;// адрес переменной var содержащийся в памяти, извлечённый операцией взятия адреса cout << "ptrvar = " << ptrvar << endl;// адрес переменной var, является значением указателя ptrvar cout << "var = " << var << endl; // значение в переменной var cout << "*ptrvar = " << *ptrvar << endl; // вывод значения содержащегося в переменной var через указатель, операцией разименования указателя system("pause"); return 0; } В строке 8 объявлен и инициализирован адресом переменной var указатель ptrvar. Можно было сначала просто объявить указатель, а потом его инициализировать, тогда были бы две строки: int *ptrvar; // объявление указателя ptrvar = &var; // инициализация указателя В программировании принято добавлять к имени указателя приставку ptr, таким образом, получится осмысленное имя указателя, и уже с обычной переменной такой указатель не спутаешь. Результат работы программы ( рис. 15. 1).  Рис. 15.1. Указатели в С++ Итак, программа показала, что строки 9 и 10 выводят идентичный адрес, то есть адрес переменной var, который содержится в указателе ptrvar. Тогда как операция разыменования указателя *ptrvar обеспечивает доступ к значению, на которое ссылается указатель. 1.2.4. Итак, указатель – это новый тип данных. Для него определены понятия константы, переменной, массива. Как и любую переменную, указатель необходимо объявить. Объявление указателя состоит из имени базового типа, символа * (звездочка) и имени переменной. Общая форма объявления указателя: тип *имя; Тип указателя определяет тип объекта, на который указатель будет ссылаться, например, int *p1; Фактически указатель любого типа может ссылаться на любое место в памяти, но выполняемые над указателем операции существенно зависят от его типа. Так, если объявлен указатель типа int *, компилятор предполагает, что любой адрес, на который он ссылается, содержит переменную типа int, хотя это может быть и не так. Следовательно, объявляя указатель, необходимо убедиться в том, что его тип совместим с типом объекта, на который он будет ссылаться. 1.3. Операции над указателями Для указателей-переменных разрешены некоторые операции: присваивание, инкремент, декремент, сложение, вычитание, сравнение. Язык Си разрешает операцию сравнения указателей одинакового типа. При выполнении присваивания значение указателя в правой части выражения пересылается в ячейку памяти, отведенную для указателя в левой части. Важной особенностью арифметических операций с указателями является то, что физическое увеличение или уменьшение его значения зависит от типа указателя, т.е. от размера того объекта, на который указатель ссылается. Если к указателю, описанному как type *ptr прибавляется или отнимается константа N, значение ptr изменяется на N*sizeof(type). Разность двух указателей type *ptr1, *ptr2 – это разность их значений, поделенная на sizeof(type). Такие правила арифметических операций с указателями вытекают из того, что указатель в Си неявно рассматривается как указатель на начало массива однотипных элементов. Продвижение указателя вперед или назад совпадает с увеличением или уменьшением индекса элемента. К указателям можно применять две унитарные операции: 1. & (взятие адреса). Указатель получает адрес переменной. Данная операция применима к переменным, под которые выделен соответствующий участок памяти. Например: int *ptr, var=1; // ptr – указатель, var – переменная ptr = &var; // В ptr заносится адрес var 2. * (операция разадресации). Предназначена для доступа к значению, расположенному по данному адресу. *ptr = 9; // В ячейку памяти, с адресом ptr записывается значение 9 var = *ptr; // Переменной var присваивается значение, // расположенное по адресу ptr Над указателями можно выполнять арифметические операции сложения, инкремента (увеличения на 1), вычитания, декремента (уменьшения на 1) и операции сравнения (>, >=, <, <=, ==, !=). При выполнении арифметических операций с указателями автоматически учитывается размер данных, на которые он указывает. Например: ptr++; // Сдвиг указателя ptr на один элемент вперед (*ptr)++; // (или ++*ptr;) Увеличение на 1 значения переменной, // на которую указывает указатель ptr *ptr = NULL; // Очистка указателя рtr1 Указатели, как правило, используются при работе с динамической памятью (heap или «куча»). Для работы с динамической памятью в языке С определены следующие функции: malloc, сalloc, realloc и free. В языке C++ для выделения и освобождения памяти определены операции new и delete соответственно. Используют две формы операций: 1. Тип *указатель = new тип (значение); – выделение участка памяти в соответствии с указанным типом и занесение туда заданного значения. delete указатель; – освобождение выделенной памяти. 2. Тип *указатель = new тип[n]; – выделение участка памяти размером n блоков указанного типа. delete [ ] указатель; – освобождение выделенной памяти. Пример работы с одномерным динамическим массивом: int *a; // Объявление указателя a a = new int[n]; // Выделение n блоков памяти целого типа … // Работа с массивом a delete [] a; // Освобождение выделенной памяти Над указателями определено 5 основных операций. § Определение адреса указателя: &p, где p – указатель (&p – адрес ячейки, в которой находится указатель). § Присваивание. Указателю можно присвоить адрес переменной p=&q, где p – указатель, q – идентификатор переменной. § Определение значения, на которое ссылается указатель: *p (операция косвенной адресации). § Увеличение (уменьшение) указателя. Увеличение выполняется как с помощью операции сложения (+), так и с помощью операции инкремента (++). Уменьшение – с помощью операции вычитания (–) либо декремента (––). Например, пусть p1 – указатель, тогда р1++ перемещает указатель на: o 1 байт, если *p1 имеет тип char; o 4 байта, если *p1 имеет тип int (в 32 разрядной операционной системе) или 2 байта (в 16 разрядной операционной системе); o 4 байта, если *p1 имеет тип float. § Разность двух указателей. Пусть р1 и р2 – указатели одного и того же типа. Можно определить разность р1 и р2, чтобы найти, на каком расстоянии друг от друга находятся элементы массива. Пример 15.12.программы. Даны адреса переменных &a=63384,&b=64390,&c=64404. Что напечатает ЭВМ? # include <stdio.h> int main() { float a,*p1; int b,*p2; char c,*p3; a=2.5; b=3; c='A'; p1=&a; p2=&b; p3=&c; p1++; p2++; p3++; printf("\n p1=%u, p2=%u, p3=%u",p1,p2,p3); return 0; } Ответ: р1=63388, р2=64392, р3=64405. Операции адресной арифметики подчиняются следующим правилам. После увеличения значения переменной-указателя на 1 данный указатель будет ссылаться на следующий объект своего базового типа. После уменьшения – на предыдущий объект. Для всех указателей адрес увеличивается или уменьшается на величину, равную размеру объекта того типа, на который они указывают. Поэтому указатель всегда ссылается на объект с типом, тождественным базовому типу указателя. Применительно к указателям на объект типа char операции адресной арифметики выполняются как обычные арифметические операции, потому что длина объекта char всегда равна 1. Операции адресной арифметики не ограничены увеличением (инкрементом) и уменьшением (декрементом). К указателям, например, можно добавлять или вычитать из них целые числа. При операции вычитания двух указателей можно определить количество объектов, расположенных между адресами, на которые указывают эти два указателя. При этом необходимо, чтобы указатели имели один и тот же тип. Кроме того, стандартом C допускается сравнение двух указателей. Как правило, сравнение указателей может оказаться полезным только тогда, когда два указателя ссылаются на общий объект, например, на массив. Указатели можно сравнивать, причём не, только на равенство или неравенство, ведь адреса могут быть меньше или больше относительно друг друга. Разработаем программу, которая будет сравнивать адреса указателей. // pointer.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include <iostream> using namespace std; int main(int argc, char* argv[]) { int var1 = 123; // инициализация переменной var1 числом 123 int var2 = 99; // инициализация переменной var2 числом 99 int *ptrvar1 = &var1; // указатель на переменную var1 int *ptrvar2 = &var2; // указатель на переменную var2 cout << "var1 = " << var1 << endl; cout << "var2 = " << var2 << endl; cout << "ptrvar1 = " << ptrvar1 << endl; cout << "ptrvar2 = " << ptrvar2 << endl; if (ptrvar1 > ptrvar2) // сравниваем значения указателей, то есть адреса переменных cout << "ptrvar1 > ptrvar2" << endl; if (*ptrvar1 > *ptrvar2) // сравниваем значения переменных, на которые ссылаются указатели cout << "*ptrvar1 > *ptrvar2" << endl; system("pause"); return 0; } Результат работы программы (рис. 15. 2).  Рис. 15.2. Указатели в С++ В первом случае, мы сравнивали адреса переменных, и, причём адрес второй переменной, всегда меньше адреса первой переменной. При каждом запуске программы адреса выделяются разные. Во втором случае мы сравнивали значения этих переменных используя операцию разыменования указателя. Из арифметических операций, чаще всего используются операции сложения, вычитания, инкремент и декремент, так как с помощью этих операций, например в массивах, вычисляется адрес следующего элемента. Все остальные операции над указателями запрещены. 1.4. Передача параметра по ссылке В языке С все аргументы функции передаются по значению. Это значит, что при вызове функции в системный стек помещаются фактические значения аргументов функции. В функции создаются локальные копии аргументов, значения которых считываются из стека. Далее функция использует эти локальные переменные и может изменять их. При выходе из функции локальные переменные уничтожаются, изменённые значения параметров теряются. Таким образом, в языке С вызванная функция не может изменить значения переменных, указанных в качестве фактических параметров при обращении к ней. Например, функция swap(), которая должна менять значения параметров местами, не будет этого делать: void swap(int a, int b) { int tmp=a; a=b; b=tmp; } //. Но тем не менее функцию можно приспособить для изменения аргументов. Для этого необходимо в качестве параметра передавать адрес переменной, которую надо изменять, т. е. передавать указатель на переменную. Такой приём в языке С называется передачей параметра через указатель. Вызванная функция должна описывать аргумент как указатель и обращаться к фактической переменной косвенно, через разыменование указателя. Теперь функцию swap() мож- но описать следующим образом: void swap(int *a, int *b) { int tmp=*a; *a = *b; *b=tmp; } //. Тогда для обмена местами значений переменных x и y необходимо использовать функцию swap(&x,&y). Ещё одним способом косвенной передачи параметра функции является передача по ссылке. Тогда аргументами функции swap() будут «ссылки» на другие переменные void swap(int &a, int &b) { int tmp=a; a = b; b=tmp; } //. При обращении к функции в качестве фактических параметров необходимо указывать имена переменных x и y: swap(x,y). Подробнее о ссылках можно почитать в [2]. 1.5. Динамическое выделение памяти Помимо рассмотренных элементарных действий с указателями с их помощью можно также создавать новые переменные и выделять память динамически в ходе работы программы. Для этого предназначен оператор new: указатель = new тип; например, int *p; // указатель на целое p = new int; // выделяем память. После операции new указывается тип данных, которые будут храниться в выделяемой области памяти, адрес начала выделенной области присваивается указателю. После этого можно пользоваться выделенными ячейками, например *p = 155; // присвоили значение. Когда динамически выделенная память больше не нужна её можно освободить с помощью оператора delete: delete указатель; например, delete p; // освободили память. Удаления указателя при этом не происходит, память, адрес которой находится в указателе, объявляется свободной. Для создания массивов с переменной размерностью используется динамическое размещение данных, декларируемых указателями. Для работы с динамической памятью используются стандартные функции библиотеки alloc.h: void *malloc(size) и void *calloc(n, size) – выделяют блок памяти размером size и n´sizeбайт соответственно; возвращают указатель на выделенную область, при ошибке – значение NULL; void free(bf); – освобождает ранее выделенную память с адресом bf. Другим, более предпочтительным подходом к динамическому распределению памяти является использование операций языка С++ new и delete. Операцияnewвозвращает адрес ОП, отведенной под динамически размещенный объект, при ошибке – NULL, а операция deleteосвобождает память. 1.5.1.Динамическое размещение одномерного массива Минимальный набор действий, необходимых для динамического размещения одномерного массива действительных чисел размером n: double *а; . . . а = new double[n]; // Захват памяти для n элементов . . . delete []а; // Освобождение памяти 1.5.2. Динамическое размещение двухмерного массива Минимальный набор действий, необходимых для динамического размещения двухмерного массива действительных чисел размером n´m: int i, n, m; // n, m – размеры массива double **a; a = new double *[n]; // Захват памяти под указатели for(i=0; i<n; i++) a[i] = new double [m]; // и под элементы . . . for(i=0; i<n; i++) delete []a[i]; // Освобождение памяти delete []a; Для современных компиляторов (версий старше «6») для освобождения памяти достаточно записать только delete []a; 1.5.3. Динамические массивы При традиционном определении массива: тип имя_массива [количество_элементов]; общее количество памяти, выделяемой под массив, задается определением и равно количество_элементов * sizeof(тип). Но иногда бывает нужно, чтобы память под массив выделялась для решения конкретной задачи, причем ее размеры заранее не известны и не могут быть фиксированы. Формирование массивов с переменными размерами можно организовать с помощью указателей и средств динамического распределения памяти двумя способами: 1) с использованием библиотечных функций, описанных в заголовочных файлах alloc.h и stdlib.h (стандартный Си); 2) с использованием операций new и delete (Си++). 1.5.4. Формирование динамических массивов с использованием библиотечных функций Для выделения и освобождения динамической памяти используются функции Функция | Прототип и краткое описание | malloc | void * malloc(unsigned s) Возвращает указатель на начало области динамической памяти длиной в s байт, при неудачном завершении возвращает NULL | calloc | void * calloc(unsigned n, unsigned m) Возвращает указатель на начало области динамической памяти для размещения n элементов длиной по m байт каждый, при не удачном завершении возвращает NULL | realloc | void * realloc(void * p, unsigned s) Изменяет размер блока ранее выделенной динамической памяти до размера s байт, р-адрес начала изменяемого блока, при неудачном завершении возвращает NULL | free | void *free(void p) Освобождает ранее выделенный участок динамической памяти, р – адрес первого байта | Пример 15.45 Функция для формирования одномерного динамического массива int * make_mas(int n) ( int *mas; mas=(int*)malloc(n*sizeof(int)); for(int i=0;i<n;i++) mas[i]=random(10); return mas; } Для выделения памяти используется функция malloc, параметром которой является размер выделяемого участка памяти равный n*sizeof(int). Так как функция malloc возвращает нетипизированный указатель void*, то необходимо выполнить преобразование полученного нетипизированного указателя в указатель int*. Освободить выделенную память можно функцией free(mas). 1.5.5. Формирование динамических массивов с использованием операций new и delete Для динамического распределения памяти используются операции new и delete. Операция new имя_типа или new имя_типа инициализатор позволяет выделить и сделать доступны свободный участок памяти, размеры которого соответствуют типу данных, определяемому именем типа. В выделенный участок заносится значение определяемое инициализатором, который не является обязательным параметром. В случае успешного выделения памяти операция возвращает адрес начала выделенного участка памяти, если участок не может быть выделен, то возвращается NULL. Примеры: 1) int *i; i=new int(10); 2) float *f; f=new float; 3) int *mas=new[5]; В примерах 1, 2 показано как выделить память под скалярные переменные, пример 3 показывает выделение памяти под массив переменных. Операция delete указатель освобождает участок памяти ранее выделенный операцией new. Пример: Функция для формирования двумерного динамического массива int ** make_matr(int n) { int **matr; int i,j; matr=new int*[n]; for (i=0;i<n;i++) { matr[i]=new int[n]; for (j=0;j<n;j++) matr[i][j]=random(10); } return matr; } При формировании матрицы сначала выделяется памяти для массива указателей на одномерные массивы, а затем в цикле с параметром выделяется память под n одномерных массивов.  Чтобы освободить память необходимо выполнить цикл для освобождения одномерных массивов for(int i=0;i<n;i++) delete matr[i]; После этого освобождаем память на которую указывает указатель matr delete [] matr; 1.5.6. Динамические массивы Для работы с динамической памятью используют указатели. С их помощью осуществляется доступ к участкам динамической памяти, которые называются динамическими переменными. Динамические переменные создаются с помощью специальных функций и операций. Они существуют либо до конца работы программ, либо до тех пор, пока не будут уничтожены с помощью специальных функций или операций. Для создания динамических переменных используют операцию new, определенную в C++: указатель = new имя_типа[инициализатор]; где инициализатор – выражение в круглых скобках. Операция new позволяет выделить и сделать доступным участок динамической памяти, который соответствует заданному типу данных. Если задан инициализатор, то в этот участок будет занесено значение, указанное в инициализаторе. int* x=new int(5); Для удаления динамических переменных используется операция delete, определенная в C++: delete указатель; где указатель содержит адрес участка памяти, ранее выделенный с помощью операции new. delete x; Операция new при использовании с массивами имеет следующий формат: new тип_массива Такая операция выделяет для размещения массива участок динамической памяти соответствующего размера, но не позволяет инициализировать элементы массива. Операция new возвращает указатель, значением которого служит адрес первого элемента массива. При выделении динамической памяти размеры массива должны быть полностью определены. //выделение динамической памяти 100*sizeof(int) байт int* a = new int[100]; При формирование матрицы сначала выделяется память для массива указателей на одномерные массивы, а затем в цикле с параметром выделяется память под n одномерных массивов. /*выделение динамической памяти под двумерный динамический массив*/ int** form_matr(int n,int m) { int **matr=new int*[n];//выделение памяти по массив указателей for(int i=0;i<n;i++) //выделение памяти 100*sizeof(int) байт для массива значений matr[i]=new int [m]; return matr;//возвращаем указатель на массив указателей } *matr[0] | *matr[1] | *matr[2] | ….. | *matr[n] |  Рис.15.1. Выделение памяти под двумерный массив Изменять значение указателя на динамический массив надо аккуратно, т. к. этот указатель затем используется при освобождении памяти с помощью операции delete. /*освобождает память, выделенную под массив, если а адресует его начало*/ delete[] a; Удаление из динамической памяти двумерного массива осуществляется в порядке обратном его созданию, т. е. сначала освобождается память, выделенная под одномерные массивы с данными, а затем память, выделенная под одномерный массив указателей. int find(int **matr,int m,int I) { for(int i=0;i<m;i++) if(matr[I][i]<0) return 1; return 0; } При удалении из динамической матрицы строк или столбцов создается новая матрица нужного размера, в которую переписываются данные из старой матрицы. Затем старая матрица удаляется. int **del(int **matr,int &n,int m) {//удаление четных строк int k=0,t=0; for(int i=0;i<n;i++) if(i % 2!=0)k++;//количество нечетных строк //выделяем память под новую матрицу int **matr2=form_matr(k,m); for(i=0;i<n;i++) if(i % 2!=0) { //если строка нечетная, то переписываем ее в новую матрицу for(int j=0;j<m;j++) matr2[t][j]=matr[i][j]; t++; } n=t;//изменяем количество строк //возвращаем указатель на новую матрицу как результат функции return matr2; } 1.5.7. Динамические массивы Если количество элементов массива определяется в процессе выполнения программы, используют динамическое выделение оперативной памяти компьютера. Если до начала работы программы неизвестно, сколько в массиве элементов, в программе используют динамические массивы. Память под них выделяется с помощью оператора new во время выполнения программы. Адрес начала массива хранится в переменной, называемой указателем. Например. int n=20; int *a = new int[n]; Здесь описан указатель a на целую величину, которому присваивается адрес начала непрерывной области динамической памяти, выделенной с помощью оператора new. Выделяется столько памяти, сколько необходимо для хранения n величин типа int. Величина n может быть переменной. Примечание: Обнуление памяти при ее выделении не происходит. Инициализировать динамический массив нельзя. Обращение к элементу динамического массива осуществляется так же, как и к элементам обычного массива. Например: a[0], a[1], …, a[9]. Можно обратиться к элементу массива другим способом: *(a+1), …,*(a+9), *(a+i), т.к. в переменной – указателе a хранится адрес начала массива. Дляполучения адреса, например, 9 – го его элемента к этому адресу прибавляется 9·sizeof(int) (9 умножить на·длину элемента типа int), т.е. к начальному адресу a прибавляется смещение 9. Затем с помощью операции *(разадресации) выполняется выборка значения из указанной области памяти. После использования массива выделенная динамическая память освобождается с помощью оператора: delete [ ] имя массива. Так например, для одномерного массива a: delete [ ] a; . Время "жизни" динамического массива определяется с момента выделения динамической памяти до момента ее освобождения. 1.5.8. Динамические массивы Если до начала работы программы неизвестно, сколько в массиве элементов, в программе используют динамические массивы. Память под них выделяется с помощью операции new или функции malloc во время выполнения программы. Адрес начала массива хранится в переменной, называемой указателем. Например. Примечание. Обнуление памяти при ее выделении не происходит. Инициализировать динамический массив нельзя. Обращение к элементу динамического массива осуществляется также, как и к элементам обычного массива. Например, a[0], a[1], …, a[19], c[2][5] и т.д. Другой способ обращения к элементу b[i][j] двумерного массива имеет вид: *(*(b+i)+j). Поясним это. Двумерный массив представляет собой массив массивов, т.е., это массив, каждый элемент которого является массивом. Имя двумерного массива также является константным указателем на начало массива. Например, int b[5][10] является массивом, состоящим из 5-ти массивов. Для обращения к b[i][j] сначала требуется обратиться к i-ой строке массива, т.е., к одномерному массиву b[i]. Для этого надо к адресу начала массива b прибавить смещение, равное номеру строки i: b+i (при сложении указателя b с i учитывается длина адресуемого элемента, т.е., i·(n·sizeof(int)), т.к. элементом массива b[i] является строка, состоящая из n элементов типа int). Затем требуется выполнить разадресацию: *(b+i). Получим массив из 10 элементов. Далее требуется обратиться к j-му элементу полученного массива Для получения его адреса опять применяется сложение указателя с j: *(b+i)+j (на самом деле прибавляется j·sizeof(int)). Затем применяется операция разадресации: *(*(b+i)+j). Т.о., получаем формулу для вычисления адреса элемента b[i][j]: b+k·i·n+k·j=b+k·(i·n+j), где k – длина в байтах одного элемента массива, b – адрес начала массива. Эта формула может быть использована в дальнейшем, например для организации передачи в подпрограмму двумерного массива переменной размерности. Приведем универсальный способ выделения памяти под двумерный массив, когда обе его размерности задаются на этапе выполнения программы: int i, m, n; //i – номер строки, m, n – количество строк и столбцов puts("Введите m и n"); scanf("%d %d",&m, &n); // Ввод m, n int **b=new int *[m]; //1 for (i=0; i<m; i++) //2 b[i]=new int[n]; //3 Здесь в операторе //1 объявляется переменная типа "указатель на указатель на int" и выделяется память под массив указателей на строки массива (m строк). В операторе //2 организуется цикл для выделения памяти под каждую строку массива. В операторе //3 выделяется память под каждую строку массива и i-му элементу массива указателей на строки присваивается адрес начала участка памяти, выделенного под строку двумерного массива. Каждая строка массива состоит из n элементов типа int. Примечание. Для выделения динамической памяти для вещественного двумерного массива достаточно в приведенном фрагменте программы в стоках //1, //3 имя типа int поменять на имя типа float. 1.5.9. Освобождение выделенной динамической памяти. Динамическая память освобождается с помощью операции delete[] имя массива, например, для одномерного массива, delete[]a; Освобождение памяти, выделенной для двумерного массива b, выглядит следующим образом: for (i=0; i<n; i++) delete [ ] b[i]; delete [ ] b; Время "жизни" динамического массива определяется с момента выделения динамической памяти до момента ее освобождения. 1. Алгоритмы обработки двумерных массивов Примечание. Ввод-вывод динамической матрицы отличается от ввода-вывода статической матрицы лишь описанием матрицы. 1.5.10. Описание динамической строки Для размещения строки в динамической памяти, необходимо описать указатель на Char, а затем выделить память с помощью new или malloc (new предпочтительнее). Например, char *S3=new char[m]; //m должно быть уже определено Или char *S3; S3=new char[m]; Примечание. Динамические строки, как и другие массивы, нельзя инициализировать. Например, оператор, сhar *S4="На нуль делить нельзя"; Создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно. S4 – адрес первого символа строковой константы. 1.5.11. Объявление динамического массива Массивы, создаваемые в динамической памяти, будем называть динамическими (размерность становится известна в процессе выполнения программы). При описании массива после имени в квадратных скобках задается количество его элементов (размерность), например int a[10]. Размерность массива может быть задана только константой или константным выражением. При описании массив можно инициализировать, то есть присвоить его элементам начальные значения, например: int а[10] = {1, 1, 2, 2, 5, 100}; Если инициализирующих значений меньше, чем элементов в массиве, остаток массива обнуляется, если больше — лишние значения не используются. Элементы массивов нумеруются с нуля, поэтому максимальный номер элемента всегда на единицу меньше размерности. Номер элемента указывается после его имени в квадратных скобках, например, а[0], а[3]. Если до начала работы программы неизвестно, сколько в массиве элементов, в программе следует использовать динамические массивы. Память под них выделяется с помощью операции new или функции malloc в динамической области памяти во время выполнения программы. Адрес начала массива хранится в переменной, называемой указателем. Например: int n = 10; int *mass1 = new int[n]; Во второй строке описан указатель на целую величину, которому присваивается адрес начала непрерывной области динамической памяти, выделенной с помощью операции new. Выделяется столько памяти, сколько необходимо для хранения n величин типа int. Величина n может быть переменной. Инициализировать динамический массив нельзя. Обращение к элементу динамического массива осуществляется так же, как и к элементу обычного. Если динамический массив в какой-то момент работы программы перестает быть нужным и мы собираемся впоследствии использовать эту память повторно, необходимо освободить ее с помощью операции delete[], например: delete [] a; (размерность массива при этом не указывается). delete[] mass1; При необходимости создания многомерных динамических массивов сначала необходимо с помощью операции new выделить память под n указателей (вектор, элемент которого - указатель), при этом все указатели располагаются в памяти последовательно друг за другом. После этого необходимо в цикле каждому указателю присвоить адрес выделенной области памяти размером, равным второй границе массива mass2=new int*[row]; // mass2 - указатель на массив указателей на одно //мерные массивы for(i=0;i<row;i++) mass2[i]=new int[col]; // каждый элемент массива //указывает на одномерный for (i=0; i<row;i++) for (j=0;j<col;j++) Освобождение памяти от двумерного динамического массива: for(i=0;i<row;i++) //удаление всех одномерных delete[] mass2[i]; // массивов delete[] mass2; // удаление массива указателей на одномерные массивы 1.6. Связь указателей и массивов Существует тесная связь между массивами и указателями. Она заключается в том, что в объявленном массиве его имя является указателем на массив, а точнее, на первый элемент массива. Таким образом, если был объявлен массив int t[31]; то t является указателем на массив, а операторы pl=t; и pl=&t[0]; приведут к одному и тому же результату. Для того чтобы получить значение 8-го элемента массива t можно написать t[7] или *(t+7). Результат будет один и тот же. Преимущество второго варианта заключается в том, что арифметические операции над указателями выполняются быстрее, чем действия с элементами массива. Для большей наглядности использования указателей решим следующий пример без использования указателей и с использованием указателей. Пример 15.1 Дана последовательность температур t0 ... t30. Организовать массив для хранения этой последовательности. Определить среднемесячную температуру. Вариант 1. Без использования указателей #include <iostream.h> int main(void) { float t[31]; //описание массива int i; //параметр цикла for float s; //сумма элементов for (i=0;i<=30;i++) //заполнение массива { cout << "введите элемент с номером" << i; cin >> t[i]; }; s=0; //обнуление суммы for (i=0;i<=30;i++) s=s+t[i]; //вычисление суммы cout << "среднемесячная температура" << s/31; } // конец программы. Вариант 2. С использованием указателей #include <iostream.h> int main(void) { float t[31]; //описание массива int i; //параметр цикла for float s; //сумма элементов for (i=0;i<=30;i++) //заполнение массива { cout << "введите элемент с номером" << i; cin >> *(t+i); }; s=0; //обнуление суммы for (i=0;i<=30;i++) s=s+*(t+i); //вычисление суммы cout << "среднемесячная температура" << s/31; } // конец программы. В языке C массивы и указатели тесно связаны друг с другом. Например, когда объявляется массив в виде int a[25], то при этом не только выделяется память для 25 элементов массива, но и формируется указатель с именем a, значение которого равно адресу первого по счету (нулевого) элемента массива. Доступ к элементам массива может осуществляться через указатель с именем a. С точки зрения синтаксиса языка указатель a является константой, значение которой можно использовать в выражениях, но изменить это значение нельзя. Поскольку имя массива является указателем-константой, допустимо, например, такое присваивание: int a[25]; int *ptr; ptr=a; В этом примере в переменную-указатель ptr записывается адрес начала массива a, т. е. адрес первого элемента массива. Также справедливы следующие соотношения: например, имеется массив a[N], тогда истинными будут следующие сравнения: a==&a[0]; *a==a[0]. Указатели можно увеличивать или уменьшать на целое число: ptr=a+1; Теперь указатель ptr будет указывать на второй элемент массива a, что эквивалентно &a[1]. При увеличении указателя на единицу адрес, который он представляет, увеличивается на размер объекта связанного с ним типа, например: int a[25]; int *ptr=a; ptr+=3; Первоначально указатель ptr указывал на начало массива a. После прибавления к переменной ptr числа 3 значение указателя увеличилось на 3*sizeof(int), а указатель ptr теперь будет указывать на четвертый элемент массива a. Указатель можно индексировать точно так же, как и массив. На самом деле компилятор преобразует индексацию в арифметику указателей, например, ptr[3]=10 представляется как *(ptr+3)=10. К указателям типа void арифметические операции применять нельзя, так как им не ставится в соответствие размер области памяти. Таким образом, в языке C для доступа к элементам массива существует два различных способа. Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например, a[7]=3 или a[i+2]=5. При таком способе доступа записываются два выражения, причем второе выражение заключается в квадратные скобки. Первое из этих выражений должно быть указателем, а второе – выражением целого типа. Указатель, используемый в индексном выражении, не обязательно должен быть константой, указывающей на какой-либо массив, это может быть и переменная-указатель. В частности, после выполнения присваивания ptr=a доступ к седьмому элементу массива можно получить как с помощью константы-указателя a в форме a[7], так и переменной-указателя ptr в форме ptr[7]. Второй способ доступа к элементам массива связан с использованием адресных выражений и операции косвенной адресации в форме *(a+3)=10 или *(a+i+2)=5. При реализации на компьютере первый способ приводится ко второму, т. е. индексное выражение приводится к адресному. Для приведенных примеров обращение к элементу массива a[3] преобразуется в *(a+3). Для доступа к начальному элементу массива, т. е. к элементу с нулевым индексом, можно использовать просто значение указателя a или ptr, поэтому любое из присваиваний *a=2; a[0]=2; *(a+0)=2; *ptr=2; ptr[0]=2; *(ptr+0)=2; присваивает начальному элементу массива значение 2. Многомерные массивы в языке C – это массивы массивов, т. е. массивы, элементами которых, в свою очередь, являются массивы. При объявлении таких массивов в памяти компьютера создается несколько различных объектов. Например, при выполнении объявления двумерного массива int a2[4][3] в программе создается указатель a2, который определяет в памяти местоположение первого элемента массива и, кроме того, является указателем на массив из четырех указателей. Каждый из этих четырех указателей содержит адрес одномерного массива, представляющего собой строку двумерного массива и состоящего из трех элементов типа int, и позволяет обратиться к соответствующей строке массива. Таким образом, объявление a2[4][3] порождает в программе три разных объекта: указатель с идентификатором a2, безымянный массив из четырех указателей и безымянный массив из двенадцати чисел типа int. Для доступа к безымянным массивам используются адресные выражения с указателем a2. Доступ к элементам массива указателей осуществляется с указанием одного индексного выражения в форме a2[2] или *(a2+2). Для доступа к элементам двумерного массива чисел типа int должны быть использованы два индексных выражения в форме a2[1][2] или эквивалентных ей *(*(a2+1)+2) и (*(a2+1))[2]. Следует учитывать, что с точки зрения синтаксиса языка C указатель a2 и указатели a2[0], a2[1], a2[2], a2[3] являются константами, и их значения нельзя изменять во время выполнения программы. Размещение трехмерного массива происходит аналогично. Так, например, объявление float a3[3][4][5] порождает в программе, кроме самого трехмерного массива из 60 чисел типа float, массив из четырех указателей на тип float, массив из трех указателей на массив указателей на float и указатель на массив массивов указателей на float. При размещении элементов многомерных массивов они располагаются в памяти подряд по строкам, т. е. быстрее всего изменяется последний индекс, а медленнее – первый. Такой порядок дает возможность обращаться к любому элементу многомерного массива, используя адрес его начального элемента и только одно индексное выражение. Например, обращение к элементу a2[1][2] можно осуществить при помощи указателя ptr2, объявленного в форме int *ptr2=a2[0], как обращение ptr2[1×3+2] (здесь 1 и 2 – это индексы используемого элемента, а 3 – число элементов в строке) или как ptr2[5]. Заметим, что внешне похожее обращение a2[6] выполнить невозможно, так как указателя с индексом 6 не существует. Для обращения к элементу a3[2][3][4] из трехмерного массива тоже можно использовать указатель, описанный как float *ptr3=a3[0][0], с одним индексным выражением в форме ptr3[2×20+3×5+4] или ptr3[59]. Указатели и массивы тесно связаны между собой. Идентификатор массива является указателем на его первый элемент, т.е. для массива int a[10], выражения a и a[0] имеют одинаковые значения, т.к. адрес первого (с индексом 0) элемента массива – это адрес начала размещения его элементов в ОП. Пусть объявлены – массив из 10 элементов и указатель типа double: double a[10], *p; если p = a; (установить указатель p на начало массива a), то следующие обращения: a[i] , *(a+i) и *(p+i) эквивалентны, т.е. для любых указателей можно использовать две эквивалентные |