Файл: Обязательные результаты обучения.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 11.01.2024

Просмотров: 24

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

ЛАБОРАТОРНАЯ РАБОТА 5.
УКАЗАТЕЛИ НА ПРОСТЕЙШИЕ ТИПЫ. ОПЕРАЦИИ НАД
УКАЗАТЕЛЯМИ. НИЗКОУРОВНЕВОЕ ПРОГРАММИРОВАНИЕ
Просвещение следует внедрять с умеренностью,
по возможности избегая кровопролития.
М.Е. Салтыков-Щедрин
ОБЯЗАТЕЛЬНЫЕ РЕЗУЛЬТАТЫ ОБУЧЕНИЯ
Знать понятия:
- "указатель", "статическая переменная", "динамическая переменная",
"операции над указателями".
Знать семантику:
- функций языка С: calloc(), malloc(), free();
- операций языка C++: new, delete.
ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ
Понятие "указатель"
Указатель, всякий снарядъ, орудiе, приспособленье, для
указанья чего-либо: рука на столбе или доска съ надписью,
указывающая дорогу; всякая стрълка, на часахъ, на
градусникахъ и пр; оглавленье книги, или опись книгамъ,
дъламъ, бумагамъ, по коей можно отыскать, что нужно.
В.Даль
Среди многочисленных понятий языка C имеется одно, которое можно отнести к разряду особо сложных, речь идёт об указателях.
Определение.
Указатель — это переменная, значением которой является целое число, являющееся адресом некоторого программного объекта (в этом случае говорят, что
указатель ссылается на программный объект).
Наиболее важной идеей, которая является отправной точкой для освоения всего последующего учебного материала, является то, что указатель является
переменной. Более точно, это не просто переменная, а специальный тип
переменной, значения которой содержат адреса (в частности, указатель может содержать адрес переменной).
Указатели должны быть проинициализированы либо при объявлении, либо с помощью оператора присваивания. Хорошим стилем программирования является такое программирование, при котором указатели указывают на что-то
конкретное, прежде чем их переопределяют. Другими словами, всегда инициализируйте указатели.
Указатель может быть проинициализирован нулём, символической
константой NULL или значением адреса, при этом:
(1) указатель со значением NULL не указывает ни на что;
(2) значение 0 является единственным целым числом, которое может быть присвоено переменной-указателю непосредственно. Инициализация указателя значением 0 эквивалентна инициализации указателя константой NULL, однако
использование NULL предпочтительнее (когда присваивается значение 0, то происходит его преобразование к указателю соответствующего типа).
Определение.
Операцией нахождения адреса называется унарная операция "&", которая, будучи применённой к идентификатору переменной, возвращает её адрес. Другими словами, сказанное можно записать так:
<Адрес переменной>=&<Идентификатор переменной>
Операндом операции нахождения адреса должна быть переменная; эта операция не может применяться к константам, выражениям или к переменным, объявленным с модификатором register (так, конструкции вида &(х-3) или &5 запрещены с точки зрения синтаксиса).
Определение.
Операцией косвенной адресации (операцией разыменования, операцией
раскрытия ссылки) называется унарная операция "*", которая рассматривает свой операнд как адрес программного объекта и обращается по этому адресу, возвращая его содержимое.
Другими словами, сказанное можно записать так:
<Содержимое объекта>=*<Адрес объекта>.
Иногда процесс использования операции * называется разыменованием
указателя.
Замечание (важное).
Только в умелых руках указатели "ведут себя послушно и правильно", разрешая использовать всю мощь косвенных обращений.
Новичок должен обращаться с указателями как "сапёр с неразорвавшейся миной": одно неосторожное "движение" - и программа "самоликвидируется".
Недаром в языке Java от явных указателей просто отказались.
Описание переменных типа "указатель"
Кто вытверживает слова, не поняв их смысла, скоро
их забывает, ибо слова, как говорит Гомер, крылаты
и легко улетают, если их не удерживает груз смысла.
А стало быть, в первую очередь, старайся понять
существо дела, потом обдумай ещё и ещё раз.
Эразм Роттердамский. Разговоры запросто
Из определения понятия "указатель" следует, что указатель существенно связан с типом объекта, на который он ссылается.
Если перед обозначением объекта в описании поставить символ "*", то будет описан указатель на объект того же типа и класса памяти, которые соответствуют данному обозначению без звёздочки.
Синтаксис определения указателя имеет вид:
<Спецификация_типа> *<Идентификатор>;
Символ "*" в этом контексте означает "указатель на".
Например, следующее описание типа переменных pa и pb: int *pa,*pb; говорит о том, что описаны два указателя на объекты типа int.


Компилятор по подобному описанию резервирует некоторые участки памяти и называет их pa и pb, что графически можно изобразить так:
Указатели можно инициализировать совместно с описанием их типа.
Примеры (прямое указание при инициализации).
1. Опишем переменную dec типа int с инициализацией (значением переменной должно стать целое число 10) и опишем указатель ptr на переменную типа int с инициализацией (значением указателя должен стал адрес &dec переменной dec): int dec=10; int *ptr=&dec;
2. Программный фрагмент char a; char *pa=&a; позволяет описать символьную переменную a и указатель pa на объект типа char, а также инициализируют pa так, чтобы он указывал на a.
Пример (косвенное указание при инициализации).
Опишем переменную dec типа int с инициализацией (значением переменной должно стать целое число 10), опишем указатель ptr на переменную типа int с инициализацией (значением указателя должен стал адрес &dec переменной dec) и, наконец, опишем указатель ref на указатель на переменную типа int с инициализацией (значением указателя должен стать адрес &ptr переменной ptr): int dec=10; int *ptr=&dec; int **ref=&ptr;
Пример (адресное указание при инициализации).
Рассмотрим следующий программный фрагмент, в котором использован указатель специального вида, называемый пустым указателем (или указателем на
пустое место), который указывает на определённый тип, неизвестный компилятору (никакой серьёзной работы выполнить с этим указателем невозможно: он является лишь маркером места в памяти и, причём, весьма полезным; а для того, чтобы сделать что-либо "полезное" с памятью, на которую мы указываем, необходимо использовать указатель "правильного" типа):
int dec=10,*u; void *ptr=&dec; printf("По адресу %u хранится %d\n",u=ptr,*(u=ptr));
Итак, указатель любого типа может быть присвоен указателю на void (т.е. типа void *), и void-указатель может быть присвоен указателю любого типа. В обоих случаях приведения типа не требуется.
Указатель на void не может быть разыменован. Например, при разыменовании указателя на целое компилятор "знает", что тот ссылается на четыре байта памяти
(на компьютере с целыми числами размером в 4 байта), но void-указатель содержит адрес памяти для неизвестного типа данных, размер которого неизвестен компилятору. Компилятор должен знать тип данных и, тем самым, размер элемента данных в байтах, чтобы правильно разыменовать указатель. В случае указателя на void размер элемента в байтах не может быть определён компилятором.
Типичные ошибки программирования.
1. Присвоение значения указателя одного типа указателю другого типа, когда ни один из них не является указателем на void *, приводит к синтаксической ошибке.
2. Разыменование void-указателя.
Работа с демонстрационными примерами
См. Пример 1, Пример 6.
Операции над указателями
То, что вы были вынуждены открыть сами, оставляет в
вашем уме дорожку, которой вы можете снова
воспользоваться, когда в этом возникнет необходимость.
Г.Лихтенберг
1. Операция нахождения адреса.
2. Операция косвенной адресации (операция раскрытия ссылки).
Напомним, что: (а) выражение *px в правой части операции присваивания означает "извлечь значение из области памяти, на которую указывает значение
переменной px"; (б) операция присваивания *px=10 читается как "присвоить 10 по
адресу, содержащемуся в переменной px".
3. "Операция" инициализации указателя.
4. Операция присваивания значения указателя другому указателю того же
типа.
5. Операция присваивания указателю нуля (NULL).
Указатели и целые не являются взаимозаменяемыми объектами. Константа 0
- единственное исключение из этого правила: её можно присвоить указателю и указатель можно сравнить с нулевой константой. Чтобы показать, что 0 — это специальное значение для указателя, вместо числа 0, как правило, записывают
NULL - константу, определенную в файле .
Заметим, что ни один "правильный" указатель не может иметь значения 0, поэтому равенство нулю значения указателя может служить сигналом о ненормальном завершении выполнения функции.
6. Операции сравнения указателей одного и того же типа:


<, >=, >, <=, !=, ==.
Например, если p и q - указатели на объекты одного типа, то отношение p!=q истинно, если p и q указывают на разные объекты, а отношение p==q истинно, если p и q указывают на один и тот же объект.
7. Операция сравнение указателя с нулем (NULL).
Например, p!=NULL истинно, если указатель p отличен от NULL.
Это очень часто используемая операция.
8. Операция сложения и вычитания указателей одного и того же типа.
9. Операции сложения и вычитания указателя и целого.
Если к значению указателя прибавить 1, то компилятор языка C добавит
единицу памяти, т.е. если переменная имеет тип int, то значение указателя на эту переменную увеличится на два (переменная типа int занимает два байта), если переменная - типа float, то значение указателя увеличится на четыре (переменная типа float занимает четыре байта), и если переменная - типа char, то значение указателя увеличится на единицу (переменная типа char занимает один байт). Вот почему необходимо специально оговаривать тип объекта, на который ссылается указатель; одного адреса здесь недостаточно, так как компилятор должен знать, сколько байтов потребуется для запоминания объекта.
Приведём несколько примеров: (а) пусть p - указатель на объект любого типа; тогда оператор p++; увеличивает p так, что он указывает на следующий объект того же типа; (б) пусть i - переменная целого типа; тогда оператор p+=i; увеличивает указатель p так, чтобы он указывал на объект, отстоящий на i "единиц" памяти, занимаемых объектом данного типа, от объекта, на который указывает p.
Замечание [Дейтел,Дейтел,2002,с.293].
Большинство компьютеров сегодня поддерживают 2-байтовые или 4- байтовые целые, а некоторые из новейших машин имеют ещё и 8-байтовые целые.
Поскольку результат арифметики указателей зависит от размера объектов, на которые ссылается указатель, то результат арифметических выражений с указателями является машинно-зависимым.
Работа с демонстрационными примерами
См. Пример 2.
Операция sizeof
В языке C имеется специальная унарная операция sizeof, при помощи которой можно определить размеры любого типа данных в байтах во время компиляции программы.
Операция sizeof может применяться к любой переменной, типу данных или константе. при определении размера переменной или константы возвращается число байтов, отводимых под тип переменной или константы. Скобки с sizeof обязательны, если в качестве операнда используется имя типа. Отсутствие скобок в этом случае приводит к сообщению об ошибке. Скобки не требуются, если операнд является именем переменной.
Замечание [Дейтел,Дейтел,2002,с.291].
Размер в байтах одного и того же типа данных может быть разным на различных системах. При написании программ, которые будут работать на различных платформах и при этом небезразличны к представлению данных, используйте операцию sizeof для определения размера типа данных.


Работа с демонстрационными примерами
См. Пример 3 1
Важнейшие стандартные библиотечные функции
Дело было в Год спокойного солнца. Как обычно в такое время,
устраивали большую околосолнечную уборку, подметали и выметали
массу железок, которые кружат на уровне орбиты Меркурия; за
шесть лет, что ушли на строительство в его перигелии большой
космической станции, в пустоте побросали целую кучу старых,
ненужных ракет, потому что работы велись тогда по системе Ле
Манса и, вместо того, чтобы сдавать ракетные трупы на слом, их
использовали в качестве строительных лесов.
С.Лем. Рассказ Пиркса
Определение [Жешке,1994].
Heap-область (куча) — это память, которая может быть динамически выделена и освобождена из программы пользователя посредством библиотечных функций calloc, free, malloc и realloc.
1. Функция calloc().
Определение функции имеет вид: void *calloc(unsigned size,unsigned n);
Функция возвращает в качестве своего значения указатель на область памяти, размер которой достаточен для помещения туда n объектов, размер каждого из которых равен size. Если произошла ошибка, то функция возвращает значение
NULL. Выделенный объём памяти заполняется нулями.
В случае, когда отсутствует достаточный объём памяти, эта функция возвращает NULL (0).
В дальнейшем часто будем выделять блок памяти в динамической области при помощи следующего оператора присваивания:
2. Функция free().
Определение функции имеет вид: void free(char *p);
Функция освобождает по указателю p блок основной памяти, которая была предварительно выделена функцией calloc(). Память, находящаяся вне пределов области, распределённой с помощью calloc(), функцией free() не анализируется.
3. Функция malloc().

Определение функции имеет вид: void *malloc(unsigned n);
Функция возвращает в качестве своего значения указатель на область памяти, размер которой достаточен для помещения туда n объектов, размер каждого из которых равен unsigned int.
Если произошла ошибка, то функция возвращает значение NULL.
В случае, когда отсутствует достаточный объём памяти, эта функция возвращает NULL (0).
Пример (применения функции malloc).
Выделим память для 50 целых чисел с помощью функции malloc(): int *p=(int *) malloc(50*sizeof(int));
Работа с демонстрационными примерами
См. Пример 3, Пример 4, Пример 5, Пример 7, Пример 8.
Операции динамического распределения
памяти в языке C++
Так как резервирование и освобождение блоков памяти является очень распространенной операцией, в языке С++ введены две операции new и delete, освобождающие программиста от необходимости явно использовать библиотечные функции calloc(), malloc() и free().
Операция new распределяет память для одного объекта или массива объектов любого типа (в том числе и определённого пользователем).
Операция new имеет следующий синтаксис:
<Тип> *<Идентификатор>=new <Тип>;
Результатом операции new будет указатель на участок памяти, соответствующий типу <Тип>, или нулевой указатель в случае ошибки.
Кроме этого, язык C++ допускает инициализацию динамического объекта, созданного с помощью операции new.
Например, следующий фрагмент инициализирует объект типа double значением 3.14159: double *Ptr=new double(3.14159);
Замечание (для знатоков).
Для расположения массива в динамической памяти необходимо указать размерность массива после имени типа (в качестве размерности может быть использовано выражение). Например: int *arrayPtr=new int[10];
Динамическая память, выделенная с помощью операции new, будет распределённой до тех пор, пока она не будет освобождена операцией delete. Это очень важный момент, т.к. при выходе из блока локальный указатель, содержащий адрес памяти, полученной операцией new, уничтожается, тогда как память остается распределённой, т.е. автоматически память не освобождается. В результате к этой памяти невозможно будет обратиться. Чтобы этого не случилось, перед выходом из блока такая память должна быть явно освобождена операцией delete.
Операция delete имеет следующий синтаксис: delete <Указатель>;
С помощью операции delete может быть освобождена только память, ранее распределённая операцией new.


Замечания (для знатоков).
1. Массивы нужно удалять с помощью операции delete[].
2. С помощью операции new можно определить ссылку на динамическую память. Для этого используется конструкция вида
<Тип> &<Идентификатор>=*new <Тип>;
Знак операции * перед операцией new необходим, потому что результатом операции new является указатель, а ссылка должна инициализироваться переменной.
Работа с демонстрационными примерами
См. Пример 9.
ДЕМОНСТРАЦИОННЫЕ ПРИМЕРЫ
Они красноречивы, ибо придерживаются правил; они
лишены красноречия, ибо придерживаются правил.
"Блаженный" Августин (354-430)
Пример 1.
/* Демонстрация простейшей работы с указателями и адресами */
/* ------------------------------------------------------- */
#include
#include int main()
{ int a,b,c,max; int *pa=&a, /* Переменная pa содержит адрес переменной a */
*pb=&b, /* Переменная pb содержит адрес переменной b */
*pc=&c, /* Переменная pc содержит адрес переменной c */
*min;
/* ----------------------------- */ printf("Введите значения a,b,c: "); scanf("%d %d %d",&a,&b,&c); printf("Посмотрим, по каким адресам лежат эти значения:\n"); printf(" в ячейке с адресом %p лежит %d\n",pa,*pa); printf(" в ячейке с адресом %p лежит %d\n",pb,*pb); printf(" в ячейке с адресом %p лежит %d\n",pc,*pc);
/* ------------------------------------------------ */ printf("Найдем большее из чисел a, b, c и положим\n"); printf("в ячейку с меньшим адресом.\n");
/* Поиск наименьшего адреса */ if (pa<=pb) min=pa; else min=pb; if (min<=pc)
; else min=pc;
/* Поиск наибольшего значения */
if (*pa<=*pb) max=*pb; else max=*pa; if (max<=*pc) max=*pc;
/* Требуемое размещение */
*min=max; printf("Значение max=%d расположено по адресу %p.\n\n",
*min,min); getch(); return 0;
}
Результат работы программы:
Введите значения a,b,c: 1 3 2
Посмотрим, по каким адресам лежат эти значения: в ячейке с адресом FFF4 лежит 1 в ячейке с адресом FFF2 лежит 3 в ячейке с адресом FFF0 лежит 2
Найдём большее из трех чисел и положим в ячейку с меньшим адресом.
Итак, max=3 расположен по адресу FFF0.
Пример 1
1
/* Демонстрация использования указателей для */
/* моделирования следующих структур памяти: */
/*
*/
/* ----------------------------------------- */
#include
#include
#include
/* ------------- */ int main()
{ int **p,**q=&p,
**r1,**q1=&r1,**p1=&q1; p=&q; printf("Проверка: %d %d\n",p==&q,q==&p); getch(); r1=&q1; printf("Проверка: %d %d %d\n",p1==&q1,q1==&r1,r1==&q1); getch();
}
Результат работы программы:
Проверка: 1 1
Проверка: 1 1 1