Лабораторная работа 5 Работа с указателями. Обработка числовых данных. Программирование пользовательских функций
Скачать 103 Kb.
|
Лабораторная работа №5Работа с указателями. Обработка числовых данных. Программирование пользовательских функций. Цель работы:
Теоретические сведения. Указатели. Работая с памятью, программист оперирует понятиями имя и значение переменной – это работа на высоком уровне абстракции. На машинном (низком уровне абстракции), компьютер оперирует понятиями «адрес участка памяти», выделенный под переменную и «содержимое участка памяти». Определяя переменную, например: int var =1; программа заставляет компилятор выделять память, при этом имя переменной var адресует участок памяти, а константа 1 определяет значение, которое запишется по этому адресу. Имея в своем распоряжении адрес переменной, программисту необходимо с ним работать: сохранять, передавать и преобразовывать, для этой цели и служат указатели. Указатели - это переменные, значениями которых являются адреса других объектов - переменных, массивов, функций. Указатели служат для обращения к области памяти, отведенной под другую переменную. Итак, для того, чтобы работать с указателем, необходимы, по крайней мере, две переменные – сам указатель и переменная на которую он будет ссылаться. Подобно любой другой переменной, указатель нужно создать (объявить) и задать начальное значение (установить указатель), после чего он будет указывать (ссылаться) на переменную. При объявлении указателя перед именем ставится знак *. Пример: int x, *p_i,; // объявить переменную x и указатель p_i double f, *p_d; // объявить переменную f и указатель p_d p_i = &x ; // установить указатели p_d = &f ; Обратите внимание, что указатель должен быть того же типа, что и переменная, на которую он ссылается. Операция &x означает: "получить адрес переменной x". Оператор p_d = &f заносит значение в указатель, он означает следующее: адрес переменной f занести в указатель p_d (указатель установлен на переменную f). Установка указателя на объект – это обязательный этап работы с любым указателем. Будьте внимательны, неустановленный указатель главный источник неприятностей. После того, как указатель установлен, можно обращаться к объекту, на который он указывает, для этой цели служит специальная операция –разыменование «*». Операция * рассматривает свой операнд как адрес и позволяет обратиться к содержимому этого адреса. Следует отметить, что указатели могут использоваться в выражениях, подобно любым другим переменным. Пример: int x, *ptr; ptr = &x; // ptr ссылается на x *ptr= *ptr +1; // аналог: x = x+1 *ptr = *ptr*5; // x=x*5 Арифметика указателей. Работая с указателем программист имеет возможность совершать действия двух различных типов :
Манипуляции с адресом часто называют арифметикой указателей или адресной арифметикой. Это очень удобный инструмент для работы с различными программными объектами, позволяющий легко манипулировать массивами как простых, так и сложных типов данных. Арифметические операции с адресами имеют ряд ограничений, которые мы и рассмотрим подробнее. Присваивание. Операцию присваивания можно применять :
Никакие другие типы присваивания с указателями недопустимы. Пример: int *p,*k,*z,date=2007; p=&date; // установка указателя на переменную date k=p; // значение указателя p заносится в указатель k z=NULL; // нулевое значение указателя Инкремент и декремент. Операции ++ и – применённые к указателю перемещают ссылку на следующий (или предыдущий) объект, позволяя последовательно перебирать элементы массива в прямом или обратном направлении. Пример: float M[10], *k; char Q[60],*s; s=&Q[0]; // установить указатель на Q[0] k=&M[10]; // установить указатель на M[10] s++; // переместить указатель на Q[1] k--; // переместить указатель на M[9] Заметим, что в данном примере обращение к элементам массива нет, мы лишь манипулируем с их адресами. Для того, чтобы обратиться к объекту, например, прочитать элемент массива, нужно применить операцию разыменования Сложение. Два указателя нельзя складывать, однако к указателю можно прибавить целую величину. Если помнить, что указатели содержат адреса, то подобные ограничения вполне объяснимы, действительно, складывать между собой два разных адреса не имеет смысла, а вот прибавить к адресу некоторое число, означает переместиться вперед, «перешагнув» через несколько объектов. Пример: Вывести каждый четвертый элемент массива на печать. … for (k=&dig[0] ; k<&M[N-1]; k=k+3) printf("\t%f",*k); // печать элемента массива Вычитание. Два указателя можно вычитать друг из друга, если они одного типа. Эта операция дает расстояние между объектами. Из адреса указателя можно вычесть целую величину, таким образом, перемещаясь в сторону уменьшения адресов. Пример: Вывести каждый четвертый элемент массива на печать в обратном порядке. … for (k=&dig[N-1] ; k>&M[0]; k=k-3) printf("\t%f",*k); // печать элемента массива Пример: Вычитая адрес первого элемента из адреса последнего, получаем расстояние между этими объектами, то есть длину массива. … int x[5],*i , *r, j ; i = &x[0] ; k = &x[4] ; j = k-i ; // j=5 Указатели и массивы. В языке С между указателями и массивами существует тесная связь. Рассмотрим что происходит, когда объявляется массив, например int array[25]:
Массив остается безымянным, а доступ к его элементам осуществляется через указатель array. Следует отметить, что указатель array является константным, его значение нельзя менять, то есть он всегда указывает на нулевой элемент массива (его начало). Пример: Имя массива можно приравнять к указателю, так как оно также является указателем. … int arrey[25], *ptr; ptr = array; // установка указателя ptr на начало массива … Оператор ptr=array можно заменить на эквивалентный ему: ptr=&array[0]. К массиву можно обратиться двумя способами:
Пример: Сравнение двух способов доступа к элементам массива. // определение массива mas и указателя q char mas[100] , *q; // установка указателя на начало массива q = mas; // Доступ к элементам массива // (следующие записи эквивалентны): mas[0]; // *q mas[10]; // *(q+10) mas[n-1]; // *(q+n-1) Указатели в параметрах функции. Переменные, объявленные в функциях - это локальные (временные) переменные, которые уничтожаются при выходе из функции, а значит, их значения теряются. Когда же нужно сохранить вычисляемые значения требуется располагать переменные за пределами функции. Поэтому возникает необходимость обращения к таким переменным, определенным за пределами функции, которые являются внешними по отношению к функции объектами. Для этой цели служат указатели, и это одно из важнейших применений указателя - организация связи функции с внешней средой или использование указателя в качестве параметра функции. Указатели в параметрах функции имеют три важных применения:
Если функция должна изменять внешнюю память, то параметры следует передавать через указатели. Пример: Функция изменяет переменную в вызывающей программе. #include // ввести число с клавиатуры и записать во внешнюю переменную, адресуемую через указатель p void f (int *p) { printf ("\nx="); scanf("%d",p); } void main() { int x=0,*q; f(&x); // вызов функции f() printf("x=%d",x); } Функция f() имеет один параметр – указатель на тип int, который устанавливается фактическим параметром при вызове функции, то есть при вызове f(&x) происходит установка указателя p= &x, таким образом, функция f() получает доступ к внешней переменной x через указатель p (функция заносит в переменную значение, введенное с клавиатуры). Подобным образом могут изменяться и внешние массивы. В функцию передаётся указатель на массив, через который и ведётся работа с его элементами. Примеры программирования. Подробнее теорию с примерами по использованию указателей смотрите в файле: «Лекция (указатели,ссылки_применение)» Пример: Создать массив с помощью датчика случайных чисел, распечатать на экране в виде матрицы и найти его сумму. #include #include void main() {//____________________________________________________ // Область инициализации, здесь объявляются все объекты, // размещенные в памяти компьютера const int N=10; // размер массива const int col=5; // число колонок при выводе массива на экран // выделить память для различных объектов double *k; // указатель на тип double double dig[N], s;// массив dig и переменные s и i int i ; //___________________ конец области инициализации__ // Заполнить массив случайными числами for (k=dig; k *k=rand()/3.; // печать исходного массива for (k=dig,i=0; k { printf("%8.2f",*k); if ((i+1)%col) printf("\t"); else printf("\n"); } // суммирование массива for (s=0,k=dig; k s=s+ *k; // печать результата printf ("\n===============\ns=%f\n",s); } Рассмотрим цикл, заполняющий массив for (k=dig; k<dig+N; k++) :
В цикле происходит перебор всех элементов массива от первого до последнего, тело цикла состоит их одного оператора *k=rand()/3. Указатель k ссылается на очередной элемент, то есть содержит адрес этого элемента, а операция *k – обращение к этому элементу. Функция rand() возвращает случайное число целого типа, путем несложного вычисления преобразуем его в вещественное число. Поэтому оператор *k=rand()/3. не что иное, как запись в массив случайного вещественного числа. В цикле печати, кроме указателя k, адресующего элементы массива, используется счётчик выведенных чисел i, который используется для форматирования. Если i кратно col, то необходимо закончить строку (вывести на печать символ «перевод строки»). Суммирование массива проводится в отдельном цикле. Пример: Решим ту же задачу с применением функций. Потребуются три функции: для заполнения массива, для вывода массива на экран и для суммирования массива. #include #include //____________________ область определения функций________ // функция заполнения массива void initmas (double *p, int n) { double *tp; // рабочий указатель(локальная переменная) for (tp=p; tp < (p+n); tp++) *tp=rand()/3.; } // функция печати массива void printmas (double *p, int n, int k) { int i; for (i=0; i { printf("%8.2f",*p); if ((i+1)%k) printf("\t"); else printf("\n"); } } // функция вычисления суммы double summas (double *p, int n) { double fs; //сумма (локальная переменная) double *tp; // рабочий указатель(локальная переменная) for (fs=0, tp=p; tp < (p+n); tp++) fs=fs+ *tp; // суммирование массива return fs; // возврат суммы в вызывающую программу } //_________ конец области определения функций_______________ void main() {// область определения объектов( переменных и массива)_______ // Константы : const int N=10; // размер массива const int col=5; // число колонок массива на экранt // Массив dig и переменная s double dig[N], s; //_________________________________________________________ // вызовы функций : initmas(dig,N); // заполнение массива dig printmas(dig,N,col); // печать в 5 колонок s = summas(dig,N); // вычисление суммы printf ("s=%f\n",s); } Все необходимые объекты (dig[] и s) определены в вызывающей программе, поэтому память, в которой располагаются эти объекты является внешней, по отношению к трём пользовательским функциям : initmas(), printmas() и summas(). Разберем подробно работу с внешним массивом на примере функции initmas(). Чтобы функция имела доступ к массиву dig(), необходимо передать указатель на массив (адрес начала) в качестве параметра функции (первый параметр) и длину массива (второй параметр). При вызове функции (смотрите оператор initmas(dig, N) в функции main()) происходит инициализация формальных параметров (p и n) функции initmas(), при входе в функцию параметры получают следующие значения : p = dig , n = N, то есть указатель p направлен на начало массива, а n содержит длину массива. Для текущей работы потребуется ещё один указатель (tp), текущая работа заключается в перемещении указателя на очередной элемент массива. Рассмотрим заголовок цикла for (tp=p; tp < (p+n); tp++)
Теперь понятно, почему потребовался дополнительный указатель tp, в самом деле, если на каждом проходе цикла необходимо проверять условие выхода из цикла, то следует где-то хранить адрес начала массива для того, чтобы вычислить адреса конца массива (p+n), а значит перемещать указатель p нельзя. Следует отметить, что когда мы работаем с массивами через указатель, всегда требуется как минимум два указателя – один для хранения «начала отсчета», а другой для текущей работы - это типовой стандартный подход к решению задачи. Две другие функции printmas() и summas() используют те же принципы работы с указателями. Обратите внимание, на то, как упростилась функция main(), логика выполнения программы стала хорошо видна, остались только основные переменные. Все «детали работы» и вспомогательные переменные отошли к функциям, или как часто говорят «скрыты в функциях», и это существенно экономит как память компьютера, так и время на разработку программы. В самом деле, стоит вспомнить, что функцию можно многократно вызывать из различных частей программы с различными параметрами. Поэтому однажды написанная функция может многократно использоваться. Следующий пример показывает, как можно использовать созданные функции. Вопросы.
Варианты заданий. Общие требования.
|