Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Структуры и алгоритмы обработки данных. Часть 1. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
51
счет понижения универсальности механизма наследования. Примером может
служить аппарат виртуальных функций в языке Си++.
4.5. Разновидности полиморфизма
Полиморфизм в языках программирования - это очень широкое и собира­тельное понятие, включающее разные аспекты. Выделяют две разновидности полиморфных операций:
1) одноименные операции одного или нескольких типов, различающиеся
сигнатурами;
2) операции с общей сигнатурой, определяемые и переопределяемые в
иерархии наследования типов.
Основной причиной появления полиморфизма первого типа является же-
лание использовать для обозначения операций абстрактных типов привычные
знаки операций, принятые во встроенных типах данных. Например, если опре-
деляется абстрактный тип данных "комплексные числа", то естественно было
бы использовать знак "+" для обозначения сложения, "-" - для обозначения вы-
читания и т.д. Естественно, для того же типа хотелось бы использовать один и
тот же знак "+" для обозначения операций сложения двух комплексных чисел,
сложения комплексного числа с вещественным и вещественного с комплекс-
ным.
Технику, обеспечивающую возможность ввести новую интерпретацию
уже используемых знаков операций и/или имен функций и процедур, принято
называть "перегрузкой". Перегрузка существует и в языках, не поддерживаю-
щих абстрактные типы данных.
Преимуществом полиморфизма на основе перегрузки является то, что для
его поддержки требуется не слишком существенное усложнение компилятора и
не возникают дополнительные накладные расходы во время выполнения про-
граммы. Обычно реализация основывается на использовании сигнатур функций
и процедур. При соблюдении обязательного требования различия сигнатур всех
52
одноименных функций и процедур достаточно просто придумать правила фор-
мирования расширенных имен на основе основного имени функции или проце-
дуры и имен типов ее параметров. Если язык достаточно строго типизирован, и
сигнатура любой доступной функции или процедуры находится в области ви-
димости компилятора, то по имени и составу фактических параметров вызова
можно определить расширенное имя требуемой подпрограммы.
Второй род полиморфизма операций типов и реализующих их функций и
процедур возникает при переопределении в подтипе реализации операций су­пертипа. В данном случае сигнатуры операций изменяться не должны, и полу­чаются разные реализации операции с одной сигнатурой. Такие возникающие
неоднозначности невозможно разрешить во время компиляции программы, и
приходится использовать метод позднего связывания.
53
5. Работа с динамической памятью
В общем случае оперативная память, с которой работает программа, под-
разделяется на три вида: статическую, автоматическую и динамическую.
Статическая память - это область памяти, выделяемая при запуске про­граммы до вызова функции основной части программы или основной функции
(например, main в Си) из свободной оперативной памяти для размещения гло-
бальных и статических объектов, а также объектов, определённых в простран-
ствах имён.
Объект называют глобальным, если он определён вне функции, класса
или пространства имён. В Си объект, определённый с использованием ключе-
вого слова static, называют статическим.
Глобальные объекты, а также объекты, определённые в пространствах
имён или статически в классах, размещаются в памяти (конструируются) до вы-
зова основной функции, а разрушаются после завершения работы этой функции.
Пространства имён - это способ разделения программы на логические составляющие; механизм ограничения области видимости имён.
Автоматическая память - это специальный регион памяти, резервируе- мый при запуске программы до вызова основной функции из свободной опера­тивной памяти и используемый в дальнейшем для размещения локальных объ­ектов, определяемых в теле функций и получаемых функциями через парамет­ры в момент вызова. Автоматическую память часто называют стеком.
Динамическая память - это совокупность блоков памяти, выделяемых из доступной свободной оперативной памяти непосредственно во время выполне­ния программы под размещение конкретных объектов.
Регионом, областью или блоком памяти называют непрерывную последо-
вательность ячеек памяти. Обычно регион больше области, а область больше
блока.
54
При работе с динамической памятью часто используется такое понятие
как "куча". Куча (англ. heap) - в информатике и программировании регион заре-
зервированного адресного пространства, условное название структуры данных,
поверх которой реализована динамическая память приложения. Куча использу-
ет память, выделенную статически или запрошенную динамически у операци-
онной системы. Эта память используется для размещения объектов, динамиче-
ски созданных программой.
В любой момент времени существования кучи вся память, на которой ра-
ботает куча, разделена на занятую и свободную. Занятая память использована
под размещение объектов, уже созданных и ещё не освобождённых к этому
моменту времени. Из объёма свободной памяти примитивы работы с кучей мо-
гут выделять память под новые объекты. Для хранения данных о принадлежно-
сти памяти к занятой или свободной обычно используется дополнительная об-
ласть памяти.
Для размещения и удаления динамических объектов используются при-
митивы (некоторые базовые функции) "создать объект" (например, malloc) и
"удалить объект" (например, free). Кроме того, перед началом работы програм-
мы выполняется инициализация кучи, в ходе которой вся изначально выделен-
ная под кучу память отмечается как свободная.
При размещении объекта реализация примитива "создать объект" про-
сматривает доступную куче свободную память в поисках возможности разме-
стить в ней объект требуемого размера. Многие реализации примитивов кучи
могут в случае нехватки собственной свободной памяти запросить дополни-
тельную память у операционной системы. В случае, если по тем или иным при-
чинам разместить объект невозможно, примитив "создать объект" сообщает об
ошибке (например, malloc возвращает 0). Выбранная область памяти отмечает-
ся как занятая. Примитив возвращает адрес начала выделенной области. При
удалении объекта реализация примитива "удалить объект" отмечает, что об-
ласть, ранее использованная удаляемым объектом, теперь свободна.
55
В промежутках между вызовами примитивов "создать объект" и "удалить
объект" выделенная под объект область памяти не может быть отдана ни под
какой другой объект. Поэтому приложение может свободно пользоваться выде-
ленной ему областью памяти. В то же время, после вызова примитива "удалить
объект" освобождённая область может быть использована повторно или отдана
операционной системе, поэтому использование указателя, полученного ранее
от примитива "создать объект" будет приводить в сбоям или непредсказуемой
работе программы.
С точки зрения разработчика программного обеспечения оперативная па-
мять представляет собой упорядоченную последовательность ячеек - байт,
предназначенных для размещения данных, которыми оперирует программа во
время своего выполнения.
Упорядоченность означает, что каждый элемент последовательности
(каждая ячейка памяти) имеет свой порядковый номер. Этот порядковый номер
называют адресом ячейки памяти - адресом байта. Непрерывный диапазон яче-
ек, доступный для адресации в конкретной операционной системе, называ-
ют адресным пространством.
Схематично память обычно представляют так, как показано на рис. 9.
Рис. 9. Адресное пространство
56
Байты упорядочивают снизу вверх, указанные в ячейках числа - это номе-
ра ячеек, являющиеся их адресами. Начальные адреса памяти часто называют
младшими или нижними, а конечные - старшими или верхними.
Общее количество доступных для адресации ячеек памяти определяется
разрядностью операционной системы, точнее - разрядностью типа данных, ис­пользуемого в конкретной операционной системе для хранения номера ячейки.
Современные операционные системы в своём большинстве представлены
32- и 64-разрядными версиями. В 32-разрядных операционных системах для работы с номерами ячеек памяти используются 32-разрядные (4-байтные) типы данных. Адресовать в таких системах можно до 4294967296 ячеек - максималь-
ное число, которое можно представить 32 разрядами, что в точности соответ-
ствует 4 Гб адресного пространства. В 64-разрядных операционных системах для работы с номерами ячеек памяти используются 64-разрядные (8-байтные)
типы данных. Количество ячеек, которое можно адресовать в таких системах
измеряется эксабайтами.
В итоге, с каждой ячейкой памяти связано две величины - порядковый
номер ячейки, называемый адресом, и данные, которые хранятся в ячейке по
этому адресу.
5.1. Представление объектов в оперативной памяти, указатели
Все объекты, которые каким-либо образом определяются в программе,
размещаются в оперативной памяти. Рассмотрим следующую небольшую про-
грамму на языке C++:
#include <iostream> using namespace std;
void main() { int i = 17; int j = 29;
57
int* p1 = &i; cout << p1 << endl; cout << *p1 << endl;
*p1 = 103; cout << i << endl;
int* p2 = p1; *p2 = 107; cout << i << endl << *p1 << endl;
i = 207; cout << *p1 << endl << *p2 << endl;
p2 = &j; cout << *p2 << endl; }
В первых двух строчках функции main
int i = 17;
int j = 29; определяются переменные i и j, являющиеся объектами целочисленного типа int, которые инициализируются значениями 17 и 29 соответственно. При вы-
полнении этих строк кода в памяти выделяются ячейки под хранение значений
переменных i и j или, иными словами, под размещение объектов i и j, в которые записываются начальные значения 17 и 29.
На рис. 10 показано состояние памяти после выполнения рассматривае-
мых строк кода.
Рис. 10. Размещение в памяти объектов i и j
Для определенности будем считать, что объект с именем i размещен в па-
мяти, начиная с 48-ой ячейки.
58
Количество ячеек памяти, необходимое для размещения объекта (необхо-
димое для хранения состояния объекта), определяется типом объекта. Объектам
целочисленного типа достаточно 4 ячеек - 4 байт. Таким образом, объект i зани­мает 4 ячейки - с 48-й по 51-ю включительно, в этих ячейках хранится число 17.
Другими словами данные, расположенные в памяти с 48-й по 51-ю ячей-
ку, известны в программе под именем i и интерпретируются как целое число.
Следом за объектом i располагается объект j. Он также является объектом типа int, а следовательно, тоже занимает 4 ячейки - с 52-й по 55-ю, в этих ячей­ках хранится число 29.
Номер первой из занимаемых объектом ячеек памяти называют адресом объекта. Так, адресом объекта i будет число 48, а объекта j - число 52.
Адрес объекта является той характеристикой, которая идентифицирует
объект, отличает один объект ото всех других объектов в системе.
Рассмотрим следующую строчку программы:
int* p1 = &i;
Здесь определяется переменная p1, являющаяся указателем (pointer) на объект типа int, которая инициализируется адресом объекта i. Указатели пред­ставляют собой особый вид объектов, предназначенных для хранения инфор­мации о местоположении в памяти других объектов.
Указатель - это переменная, значением которой является адрес некоторо-
го объекта (обычно другой переменной) в памяти компьютера. Подобно тому,
как переменная типа char имеет в качестве значения символ, а переменная ти­па int - целочисленное значение, переменная типа указателя имеет в качестве
значения адрес ячейки оперативной памяти. Допустимые значения для пере-
менной-указателя - множество адресов оперативной памяти компьютера.
Указатель является одной из важных концепций языков семейства Cи:
- указатели являются средством, при помощи которого функции могут
изменять значения передаваемых в нее аргументов.
- с помощью указателей выполняется динамическое распределение памяти.
59
- указатели позволяют повысить эффективность программирования.
- указатели обеспечивают поддержку динамических структур данных
(двоичные деревья, связные списки).
Объявление указателя состоит из имени базового типа, символа * и имени переменной.
Общая форма объявления указателя:
тип *имя;
Тип указателя определяет тип объекта, на который указатель будет ссы-
латься, например,
int *p1;
Фактически, указатель любого типа может ссылаться на любое место в
памяти, но выполняемые над указателем операции существенно зависят от его
типа. Так, если объявлен указатель типа int *, компилятор предполагает, что любой адрес, на который он ссылается, содержит переменную типа int, хотя это
может быть не так. Следовательно, объявляя указатель, необходимо убедиться
в том, что его тип совместим с типом объекта, на который он будет ссылаться.
Значением объекта-указателя является адрес другого объекта.
Понятие указателя тесно связано с понятием адреса объекта. В Cи есть специальная операция, позволяющая получить адрес любой переменной:
&p - получение адреса, где p - идентификатор переменной. Результатом операции является адрес переменной p.
Пример программы:
# include <stdio.h> int main() { int x=2,*p; p=&x; printf("\n x=%d address of x=%u",x,p); return 0; }
Понятие переменной типа указатель также связано с операцией косвен­ной адресации *, называемой еще операцией разыменования, которая имеет
60
структуру: *р - разыменование, где р - идентификатор переменной-указателя. Эта запись означает, что в ячейку с адресом, записанным в переменную р, по­мещено значение некоторой величины.
Операторы ptr=&a; и val=*ptr; равнозначны оператору val=a;
Например,
n=32; p=&n; /* p-адрес ячейки, куда записано n */ v=*p;
В результате выполнения этих действий в переменную v будет помеще­но число 32.
Над указателями определено 5 основных операций:
1) определение адреса указателя: &p, где p - указатель (&p - адрес ячейки,
в которой находится указатель).
2) присваивание. Указателю можно присвоить адрес переменной p=&q,
где p - указатель, q - идентификатор переменной.
3) определение значения, на которое ссылается указатель: *p (операция
косвенной адресации).
4) увеличение (уменьшение) указателя. Увеличение выполняется как с
помощью операции сложения (+), так и с помощью операции инкремента (++).
Уменьшение - с помощью операции вычитания (-) либо декремента (--).
Например, пусть p1 - указатель, тогда р1++ перемещает указатель на:
а. 1 байт, если *p1 имеет тип char;
б. 4 байта, если *p1 имеет тип int (в 32 разрядной операционной системе) или 2 байта (в 16 разрядной операционной системе);
в. 4 байта, если *p1 имеет тип float.
5) разность двух указателей. Пусть р1 и р2 - указатели одного и того же типа. Можно определить разность р1 и р2, чтобы найти, на каком расстоянии друг от друга находятся элементы массива.
Пример программы. Даны адреса переменных &a=63384,&b=64390,&c=64404.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]