Управление памятью в С++
Инициализация массива, элементы которого содержат количество дней в каждом месяце года. Управление выделением памяти в С++. Перегрузка операторов new и delete. Создание производного класса с подсчетом ссылок. Алгоритм сборки мусора на уровне поколений.
Рубрика | Программирование, компьютеры и кибернетика |
Вид | учебное пособие |
Язык | русский |
Дата добавления | 13.09.2015 |
Размер файла | 56,8 K |
Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже
Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.
Размещено на http://www.allbest.ru//
Раздел 1. Элементы работы с памятью
C++ предоставляет разностороннее управление памятью, но лишь немногие программисты в полной мере знакомы с имеющимися возможностями. Самые разнообразные языковые средства (перегрузка, сокрытие имен, конструкторы и деструкторы, исключения, статические и виртуальные функции) используются согласованно для того, чтобы обеспечить максимальную гибкость и возможности настройки управления памятью [1].
1.1 Массивы, указатели и ссылки
Массив. Инициализация массива
Массивы позволяют удобным образом организовать размещение и обработку больших объемов информации. Массив представляет собой набор однотипных объектов, имеющих общее имя и различающихся местоположением в этом наборе (или индексом, присвоенным каждому элементу массива). Элементы массива занимают один непрерывный участок памяти компьютера и располагаются последовательно друг за другом.
Пример 1.1 Описание массивов.
int mas1[492]; //внешний массив из 492 элем
void main(void)
{
double mas2[250]; //массив 250 эл. типа double
static char mas3[20]; //статическая строка из 20 символов
extern mas1[]; //внешний массив, размер указан выше
int mas4[2][4]; //двумерный массив из числе типа int
}
В примере 1.1 квадратные скобки [] обозначают, что все идентификаторы, после которых они следуют, являются именами массивов. Число, заключенное в скобки, определяет количество элементов массива. Доступ к отдельному элементу массива организует с использованием номера этого элемента, или индекса. Нумерация элементов массива начинается с нуля и заканчивается n-1, где n - число элементов массива.
В примере 1.2 производится инициализация массива.
Пример 1.2 Описание массива и присваивание начальных значений его элементам.
int mas[2]; //объявление массива
int a=10,b=5; //объявление переменных
mas[0]=a;
mas[1]=b;
Инициализация массива означает присвоение начальных значений его элементам при объявлении. Массивы можно инициализировать списком значений или выражений, отделенных запятой, заключенным в квадратные скобки.
В примере 1.3 массив инициализируется кол-вом дней в каждом месяце года.
Пример 1.3 Инициализация массива, элементы которого содержат количество дней в каждом месяце года:
int days[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
Если список инициализируемых значений короче длины массива, то инициализации подвергаются первые элементы массива, а остальные инициализируются нулем.
Массив также можно инициализировать списком без указания в скобках длины массива. При этом, массиву присваивается длина по количеству инициализаторов.
Пример 1.4 Определение длины массива при инициализации.
сhar code[] = {'a', 'b','c'};
В примере 1.4 массив code будет иметь длину 3.
Если же массив явно не проинициализирован, то внешние и статические массивы инициализируются нулями. Автоматические же массивы после объявления ничем не инициализируются и содержат неизвестную информацию.
1.2 Указатели и ссылки
Указатель - это символическое представление адреса. Он используется для косвенной адресации переменных и объектов. Указатели тесным образом связаны с обработкой строк и массивов.
В С++ имеется операция определения адреса - &, с помощью которой определяется адрес ячейки памяти, содержащей заданную переменную. Например, если vr - имя переменной, то &vr - адрес этой переменной. В данном случае запись &vr означает: “указатель на переменную vr”. Указатель на переменную - это число, которое представляет собой содержимое регистров CS:IP процессора. Символическое представление адреса &vr является константой типа указатель.
В С++ также существуют и переменные типа указатель. Например, значением переменной типа char является целое число длинной 1 байт, переменной типа int - целое число, а значением переменной типа указатель служит адрес переменной или объекта. Пусть переменная типа указатель имеет имя ptr, тогда в качестве значения ей можно присвоить адрес с помощью следующего оператора:
ptr=&vr;
В языке С++ при работе с указателями важное значение имеет операция косвенной адресации - *. Операция * позволяет обратиться к переменной не напрямую, а через указатель, содержащий адрес этой переменной. Данная операция является одноместной и имеет ассоциативность слева направо. Эту операцию не следует путать с бинарной операцией умножения. Пусть ptr - указатель, тогда *ptr - это значение переменной, на которую указывает ptr.
Описание переменных типа указатель выполняется с помощью операторов следующей формы:
<тип> *<имя указателя на переменную заданного типа>;
В примере 1.5 производится описание указателей.
Пример 1.5 Описание указателей.
int *ptri; //указатель на переменную целого типа
char *ptrc; //указатель на переменную символьного типа
float *ptrf; //указатель на переменную с плавающей точкой
Такой способ объявления указателей возник вследствие того, что переменные разных типов занимают различное число ячеек памяти. При этом для некоторых операций с указателями требуется знать объем отведенной памяти.
Операция * в некотором смысле является обратной операции &. Использование указателей в примере 1.6.
Пример 1.6 Использование указателей.
int a=10, b;
int *ptr=&a; //инициализация указателя адресом переменной а
cout<<"указатель = "<<ptr<<"\nПеременная = "<<*ptr;
ptr=&b; //теперь ptr указывает на переменную b
Обработка объектов без указателей невозможна. Например, передать объект некоторой функции можно только через указатель.
Ссылка представляет собой видоизмененную форму указателя, которая используется в качестве псевдонима (другого имени) переменной. В связи с этим для ссылок не требуется дополнительной памяти. Для определения ссылки используется символ &, указываемый перед переменной-ссылкой.
Переменные ссылочного типа могут использоваться в следующих целях:
*вместо передачи в функцию объекта по значению;
*для определения конструктора копии;
*для перегрузки унарных операций;
*для предотвращения неявного вызова конструктора копии при передаче в функцию по значению объекта определенного пользователем класса.
Пример 1.7 Использование ссылок.
#include <iostream.h> //cout
int main()
{
int
t = 13,
&r = t; //инициализация ссылки на t
//теперь r синоним имени t
cout << "Начальное значение t: " << t; //выводит 13
r += 10; //изменение значения t через ссылку
cout << "\nКонечное значение t: " << t; //выводит 23
return 0;
}
В примере 1.7 была использована ссылка в качестве псевдонима переменной. В этой ситуации она называется независимой ссылкой и должна быть инициализирована при объявлении. Такой способ использования ссылок может привести к фатальным и трудно обнаруживаемым ошибкам по причине возникновения путаницы в использовании переменных.
Другое значение ссылок - возможность создания параметров функции, передаваемых по ссылке, при этом перед этим параметром ставится знак &.
1.3 Динамический массив
Динамическим называется массив, размерность которого становится известной в процессе выполнения программы.
В С++ для работы с динамическими объектами применяются специальные операции new и delete. С помощью операции new выделяется память под динамический объект (создаваемый в процессе выполнения программы), а с помощью операции delete созданный объект удаляется из памяти.
Пример 1.8 Выделение памяти под динамический массив.
Пусть размерность динамического массива вводится с клавиатуры. Необходимо выделить память под этот массив, а затем созданный динамический массив надо удалить. Соответствующий фрагмент программы имеет вид:
int n;
cin>>n; //n - размерность массива
int *mas=new int[n]; //выделение памяти под массив
delete mas; //освобождение памяти
В примере 1.8 mas является указателем на массив из n элементов. Оператор int *mas=new int[n]; производит два действия: объявляется переменная типа указатель, а затем указателю присваивается адрес выделенной области памяти в соответствии с заданным типом объекта. Для лучшего понимания работы с динамическими массивами эти две операции можно разделить.
Иногда при программировании возникает необходимость создания много мерных динамических объектов. Начинающие программисты по аналогии с описанным способом создания одномерных массивов для двумерного динамического массива размерности n*k могут пытаться выделить память с помощью следующей записи new в примере 1.9:
Пример 1.9 Неправильное выделение памяти под двумерный массив.
mas=new int[n][k]; //Неверно
Такой способ выделения памяти не дает верного результата. Будет выдаваться ошибка о несоответствии типов указателей. Попытка устранить подобную ошибку с помощью операции явного преобразования приводит к ошибкам при обращении к элементам массива из-за неверного типа выделенной области памяти. В примере 1.10 показан метод выделения памяти под двумерный массив.
Пример 1.10 Выделение памяти под двумерные массивы.
Пусть требуется создать двумерный динамический массив целых чисел размерностью n*k:
int n, k, i, * *mas;
cin>>n; //n - число строк массива
cin>>k; //k - число столбцов массива
mas=new * int[n]; //выделение памяти под n указателей на строку
for(i=0;i<n;i++)
mas[i]=new int[k]; /*выделение память для каждой строки по числу
столбцов k*/
for(i=0;i<n;i++)
delete mas[i]; //освобождение памяти
delete [] mas;
Сначала необходимо с помощью операции new выделить память под n указателей. Выделенная память будет представлять собой вектор, элементом которого является указатель. При этом все указатели располагаются в памяти последовательно друг за другом. После этого необходимо в цикле каждому указателю присвоить адрес выделенной области памяти размером, равным второй границе массива [2].
1.4 Управление выделением памяти
Существуют три основные стратегии для определения того, где объект будет находиться в памяти и как занимаемая им память в конечном счете возвратится в систему:
1. Глобальное управление.
2. Управление в классах.
3. Управление под руководством клиента.
Это не окончательная классификация: клиентский код может определять, а может и не определять место размещения объектов. Эти решения могут приниматься, а могут и не приниматься объектами классов. Вся тяжелая работа может выполняться ведущими указателями, а может и не выполняться.
1.5 Глобальное управление
По умолчанию объекты создаются глобальным оператором new и уничтожаются глобальным оператором delete. Перегрузка этих операторов позволяет реализовать нестандартную схему управления памятью, но это не эффективно.
*Трудно объединить раздельно написанные библиотеки, каждая из которых перегружает заданные по умолчанию операторы new и delete.
*Перегрузки влияют не только на ваш код, но и на код, написанный другими (включая библиотеки, для которых нет исходных текстов).
*Все перегрузки, принадлежащие конкретному классу, перегружают ваши глобальные версии.
*Пользователи могут изменить вашу, предположительно, глобальную стратегию, перегружая операторы new и delete в конкретных классах.
1.6 Выделение и освобождение памяти в классах
Перегрузка операторов new и delete как функций класса несколько повышает контроль над происходящим. Изменения относятся только к данному классу и его производным классам, так что побочные эффекты оказываются минимальными. Такой вариант работает лучше всего при выделении нестандартной схемы управления памятью в отдельный класс и его последующем подключении средствами множественного наследования. Для некоторых схем управления памятью такая возможность исключается. Если управление памятью реализовано в классе и можно создать от него производный класс, деструктор следует сделать виртуальным, чтобы тот же класс мог и освобождать память. Производные классы не перегружают перегруженные версии.
Раздел 2. Управление памятью в С++
2.1 Пространства памяти
В простом случае вся доступная память рассматривается как один большой блок, из которого выделяются блоки меньшего размера.
Для этого можно либо напрямую обратиться к операционной системе с требованием выделить большой блок памяти при запуске, либо косвенно, в конечном счете перепоручая работу операторным функциям ::operator new и ::operator delete.
В реализации пространств памяти могут быть использованы любые методики:
*Глобальная перегрузка операторов new и delete.
*Перегрузка операторов new и delete на уровне класса.
*Использование оператора new с аргументами под руководством клиента.
*Использование оператора new с аргументами на базе ведущих указателей.
Существуют причины для деления памяти на пространства.
Далее описаны некоторые распространенные стратегии выбора объектов, которые должны находиться в одном пространстве памяти.
2.1.1 Деление по классам и размеру
Для определения класса объекта для имеющегося объекта, нужно добавить переменную, которая ссылается на объект класса.
Это связано с заметными затратами как по памяти, так и кода конструктора.
Но существует еще два других варианта:
1. Хранить указатель на объект класса в ведущем указателе.
2. Выделить все экземпляры некоторого класса в одно пространство памяти и хранить указатель на объект класса в начале пространства (рис.2.1).
Рисунок 2.1 Пространство памяти
Второй вариант существенно снижает затраты при условии, что по адресу объекта можно эффективно определить начало адресного пространства памяти, которому он принадлежит.
Существует минимум две причины, почему нельзя добавлять дополнительную переменную, ссылающуюся на объект класса:
1. Класс, с которым идет работа, может входить в коммерческую библиотеку, для которой у нет исходных текстов, или его модификация нежелательна по другим причинам.
2. Класс может представлять собой тривиальную объектно-ориентированную оболочку для примитивного типа данных (например, int). Лишние байты для указателя на объект класса (также v-таблица, которая неизбежно потребует для виртуальных функций доступ к нему) могут оказаться существенными.
Нужно объединить все объекты одинакового размера (или принадлежащие одному диапазону размеров) в одно пространство памяти для оптимизации создания и уничтожения. Многие стратегии управления памятью работают для одних диапазонов лучше, чем для других. Все пространство памяти может заполняться объектами одинакового размера. Такие пространства обладают чрезвычайно эффективным представлением и легко управляются.
2.1.2 Деление по способу использования
Еще один возможный вариант -- делить объекты в соответствии со способом их использования. Например, для многопоточного приложения-сервера объекты можно разделить по клиентам. Редко используемые объекты могут находиться в одном пространстве, а часто используемые -- в другом.
Объекты, кэшируемые на диске, могут отделяться от объектов, постоянно находящихся в памяти. Деление может осуществляться как на уровне классов, так и на уровне отдельных объектов.
2.2 Перегрузка операторов new и delete
2.2.1 Простой список свободной памяти
Рассмотрим пример 2.1, где оператор delete включает освобождаемые блоки в список свободной памяти. Оператор new сначала пытается выделить блок из списка и обращается к глобальному оператору new лишь в том случае, если список свободной памяти пуст.
Пример 2.1 Оператор delete включает освобождаемые блоки в список свободной памяти.
class Foo {
private:
struct FreeNode {
FreeNode* next;
};
static FreeNode* fdFreeList;
public:
void* operator new(size_t bytes)
{
if (fgFreeList == NULL)
return ::operator new(bytes);
FreeNode* node = fgFreeList;
FgFreeList = fgFreeList->next;
return node;
}
void operator delete(void* space)
{
((FreeNode*)space->next = fgFreeList;
fgFreeList = (FreeNode*)space;
}
};
Пример 2.1 демонстрирует общие принципы перегрузки операторов new и delete для конкретного класса. Оператор new получает один аргумент, объем выделяемого блока, и возвращает адрес выделенного блока. Аргументом оператора delete является адрес освобождаемой области. Объявлять их виртуальными нельзя.
При вызове оператора new, компилятор знает, с каким классом он имеет дело, поэтому v-таблица ему нужна. При вызове оператора delete деструктор определяет, какому классу этот оператор должен принадлежать. Если нужно гарантировать, что будет вызываться оператор delete производного класса, то виртуальным нужно сделать деструктор, а не оператор delete.
Перегрузки будут унаследованы производными класса Foo, поэтому это повлияет и на процесс выделения/освобождения памяти в них. На практике нередко создается абстрактный базовый класс, который не делает почти ничего (как и в приведенном примере) и используется только для создания классов с данной схемой управления памятью.
Пример 2.2 Наследование Bar
class Bar : public Baseclass, public Foo { ... };
В примере 2.2, Bar наследует все базовые характеристики типа Baseclass, а нестандартное управление памятью -- от Foo.
Предполагается, что экземпляр Foo содержит не меньше байт, чем Foo::FreeNode*. Для классов вроде этого, не имеющего переменных и виртуальных функций, этого гарантировать нельзя. Он будет иметь определенный размер (во многих компиляторах -- два байта), чтобы объекты обладали уникальным адресом, но по количеству байт он может быть меньше указателя на FreeNode. Нужно гарантировать, что размер Foo не меньше размера указателя -- для этого нужно включить в него v-таблицу или хотя бы переменные, дополняющие его до размера указателя.
Другая проблема заключается в том, что приведенный фрагмент не работает с производными классами, в которых добавляются новые переменные. Показана иерархия классов в примере 2.3.
Пример 2.3 Иерархия классов
class Bar : public Foo {
private:
int x;
};
Каждый экземпляр Bar по крайней мере на пару байт больше, чем экземпляр Foo. Если удалить экземпляр Foo, а затем попытаться немедленно выделить память для экземпляра Bar - выделенный блок оказывается на ту же на пару байт короче. Возможное решение в примере 2.4 -- сделать оператор new так, чтобы он перехватывал только попытки выделения правильного количества байт.
Пример 2.4 Перехват попытки выделения правильного количества байт
class Foo {
public:
void* operator new(size_t bytes)
if (bytes != sizeof(Foo) || fgFreeList == NULL)
return ::operator new(bytes);
FreeNode* node = fgFreeList;
FgFreeList = fgFreeList->next;
Return node;
}
};
Процесс освобождения необходимо изменить в соответствии с этой стратегией. Альтернативная форма оператора delete в примере 2.5 имеет второй аргумент -- количество освобождаемых байт:
Пример 2.5 Альтернативная форма оператора delete
class Foo {
public:
void* operator new(size_t bytes);
void operator delete(void* space, size_t bytes)
{
if (bytes != sizeof(Foo))
::operator delete(space);
((FreeNode*)space)->next = fgFreeList;
fgFreeList = (FreeNode*)space;
}
};
Теперь в список будут заноситься только настоящие Foo и производные классы, совпадающие по размеру.
Foo* foo = new Bar;
delete foo;
Bar больше Foo, поэтому Foo::operator new перепоручает работу глобальному оператору new. Но когда подходит время освобождать память, происходит путаница. Размер, передаваемый Foo::operator delete, основан на предположении компилятора относительно настоящего типа, а оно может оказаться неверным. В данном случае было указано компилятору, что это Foo, а не Bar. Чтобы справиться с затруднениями, необходимо знать точную последовательность уничтожения, возникающую в операторах вида delete foo;. Сначала вызываются деструкторы, начиная с производного класса, и далее вверх по цепочке. Затем оператор delete вызывается кодом, окружающим деструктор производного класса. Это означает, что проблема возникает только для невиртуальных деструкторов. Если деструктор является виртуальным, аргумент размера в операторе delete всегда будет правильным.
Фрагмент, в примере 2.6, всегда будет правильно работать на компиляторах, использующих v-таблицы.
Пример 2.6 Фрагмент, работающий с использование v-таблицы
class Foo {
private:
struct FreeNode {
FreeNode* next;
};
static FreeNode* fdFreeList;
public:
virtual ~Foo() {}
void* operator new(size_t bytes)
{
if (bytes != sizeof(Foo) || fgFreeList == NULL)
return ::operator new(bytes);
FreeNode* node = fgFreeList;
FgFreeList = fgFreeList->next;
return node;
}
void operator delete(void* space, size_t bytes)
{
if (bytes != sizeof(Foo))
return ::operator delete(space);
((FreeNode*)space)->next = fgFreeList;
fgFreeList = (FreeNode*)space;
}
};
Указатель v-таблицы гарантирует, что каждый Foo по крайней мере не меньше указателя на следующий элемент списка (FreeNode*), а виртуальный деструктор обеспечивает правильность размера, передаваемого оператору delete.
2.2.2 Наследование операторов new и delete. Аргументы оператора new
Если перегрузить операторы new и delete для некоторого класса, перегруженные версии будут унаследованы производными классами, как показано в примере 2.7. Можно снова перегрузить new и/или delete в одном из этих производных классов.
Пример 2.7 Унаследование производными классами
class Bar : public Foo {
public:
virtual ~Bar(); //Foo::~Foo тоже должен быть виртуальным
void* operator new(size_t bytes);
void operator delete(void* space, size_t bytes);
};
С виртуальным деструктором все работает. Если деструктор не виртуальный, в следующем фрагменте будет вызван правильный оператор new и оператор delete базового класса:
Foo* foo = new Bar;
delete foo;
Подобное переопределение перегруженных операторов считается не эффективным. Когда производный класс начинает вмешиваться в управление памятью базового класса, во всей программе начинают возникать непредвиденные эффекты.
Оператор new можно перегрузить так, чтобы помимо размера он вызывался и с другими дополнительными аргументами. Перегрузка лишает стандартной сигнатуры void* operator new(size_t), и, если это не требуется, ее нужно будет включить в программу вручную.
Пример 2.8 Вызов оператора new с дополнительными аргументами
#define kPoolSize 4096
struct Pool {
unsigned char* next; // Следующий свободный байт
unsigned char space[kPoolSize];
Pool() : next(&space[0]) {}
};
class Foo {
public:
void* operator new(size_t bytes)
{ return ::operator new(bytes); }
void* operator new(size_t bytes, Pool* pool)
{
void* space = pool->next;
pool->next += bytes;
return space;
}
};
void f()
{
Pool localPool;
Foo* foo1 = new Foo; // Использует оператор new по умолчанию
Foo* foo2 = new(&localPool) Foo; // Использует перегрузку
}
В примере 2.8, клиент, а не класс указывает, где следует разместить объект. Основная идея: предоставить клиенту класса некоторую степень контроля над размещением экземпляров в памяти. Это означает, что способ выделения памяти может выбираться для конкретных объектов и не обязан совпадать для всех экземпляров класса.
Оператор new можно перегружать с любыми новыми сигнатурами при условии, что все они различаются, а первым аргументом каждой перегруженной версии является size_t -- количество нужных байт. Перегрузки могут быть как глобальными, так и принадлежать конкретным классам. Когда компилятор встречает аргументы между new и именем класса, он подставляет размер в начало списка аргументов и ищет подходящую сигнатуру.
Конструирование и уничтожение с разделением фаз
Идиома, в примере 2.9, предложена Джеймсом Коплином (James Coplien), который назвал ее «виртуальным конструктором».
Пример 2.9 Идиома Джеймса Коплина
class Foo {
public:
void* operator new(size_t, void* p) { return p; }
};
union U {
Foo foo;
Bar bar;
Banana banana;
};
U whatIsThis;
Компилятор С++ не может определить, какой конструктор следует вызывать для whatIsThis -- Foo::Foo(), Bar::Bar() или Banana::Banana().Больше одного конструктора вызывать нельзя, поскольку все члены занимают одно и то же место в памяти, но без инструкций от вас, он не может выбрать нужный конструктор. Как и во многих других ситуациях, компилятор сообщает об ошибке и отказывается принимать объединение, члены которого имеют конструкторы. Если требуется, чтобы одна область памяти могла инициализироваться несколькими различными способами, нужно найти способ, как обойти компилятор.
unsigned char space[4096];
Foo* whatIsThis = new(&space[0]) Foo;
Фактически происходит -- вызов конструктора. При этом память на выделяется и не освобождается, поскольку оператор new ничего не делает. Тем не менее, компилятор С++ сочтет, что это новый объект, и все равно вызовет конструктор. Если позднее нужно будет использовать ту же область памяти для другого объекта, то можно снова вызвать оператор new и инициализировать ее заново.
При создании объекта оператором new компилятор всегда использует двухшаговый процесс:
1. Выделение памяти под объект.
2. Вызов конструктора объекта.
Этот код находится в выполняемом коде, генерируемом компилятором, и в обычных ситуациях второй шаг не выполняется без первого. Идиома виртуального конструктора позволяет обойти это ограничение.
Если позднее нужно будет использовать ту же область для Banana, то при наличии у Banana того же перегруженного оператора new, можно быстро сменить тип объекта.
Banana* b = new(&space[0]) Banana;
Был Foo - стал Banana. Это и называется идиомой виртуального конструктора. Такое решение полностью соответствует спецификации языка.
Применяя эту идиому, необходимо помнить о двух обстоятельствах:
1. Область, передаваемая оператору new, должна быть достаточна для конструирования класса.
2. Об изменении должны знать все клиенты, хранящие адрес объекта.
Объект, переданный в качестве аргумента оператору delete, уничтожается компилятором в два этапа:
1. Вызов деструктора.
2. Вызов оператора delete для освобождения памяти.
Если нужно вызвать деструктор, но не трогать память.
Допустим, размещен объект в пуле, а теперь нужно, чтобы часть локально созданного пула вернулась в главное хранилище памяти. По аналогии с тем, как был разделен двухшаговый процесс конструирования, можно разделить и двухшаговый процесс уничтожения, напрямую вызывая деструкторы. Но в отличие от тех методов, которыми сопровождалось разделение процесса конструирования, при уничтожении -- достаточно вызвать деструктор так, как обычную функцию класса, показано в примере 2.10.
Пример 2.10 Вызов деструктора напрямую, как функцию класса
void f()
{
Pool localPool;
Foo* foo1 = new Foo; // Использует оператор new по умолчанию
Foo* foo2 = new(&localPool) Foo;
// Использует перегрузку
delete foo1; // Для оператора new по умолчанию
foo2->~Foo(); // Прямой вызов деструктора
}
localPool -- большой блок памяти, локальный по отношению к функции. Поскольку он создается в стеке, при завершении f(), он выталкивается из стека. Выделение происходит мгновенно, поскольку локальные объекты заполняют пул снизу вверх. Освобождение происходит еще быстрее, поскольку уничтожается сразу весь пул. Проблема заключается в том, что компилятор не будет автоматически вызывать деструкторы объектов, созданных внутри localPool.
Указатели и их поиск
Указатели применяются для работы с массивами, со свободной памятью и в качестве параметров функций.
Указатель - это тип данных, значением которого является адрес данных определенного типа. Бывают и бестиповые указатели, которые хранят просто адрес памяти, но в С++ они применяются редко.
Значение указателя можно получить:
а) определив, где расположена в памяти некоторая переменная;
б) выделив участок свободной памяти для хранения значений;
в) при помощи арифметической операции над целым числом и другим указателем [3].
Помимо подсчета ссылок и нестандартных операторов new и delete в большинстве стратегий управления памятью сочетаются две методики: определение момента, когда доступ к объекту становится невозможным, для его автоматического уничтожения (сборка мусора) и перемещение объектов в памяти (уплотнение).
Поиск указателей в программе на С++ чрезвычайно сложен. Более того, в С++ программа может получить адрес части объекта, поэтому некоторые указатели могут ссылаться на середину большого объекта.
В С++ существуют разнообразные способы получения указателей. Одни связаны с конкретным представлением объектов в памяти, другие -- с наследованием, третьи -- с переменными классов. Самый очевидный способ -- это нахождение адреса. Рассмотрим другие, не столь тривиальные способы.
Имея объект, можно получить адрес переменной класса, воспользоваться им или передать другому объекту. В примере 2.11 показано получение адреса переменной класса.
Пример 2.11 Получения адреса переменной класса
class Foo {
private:
int x;
String y;
public:
int& X() { return x; } // Ссылка на x
String* Name() { return &y; } // Адрес y
};
Каждый экземпляр Foo выглядит примерно так, как показано на (рис.2.2) (все зависит от компилятора, но в большинстве компиляторов дело обстоит именно так):
Как правило, несколько первых байт занимает указатель на v-таблицу для класса данного объекта. За ним следуют переменные класса в порядке их объявления. Если был получен адрес переменной класса в виде ссылки или указателя, возникает указатель на середину объекта.
Пример 2.12 Одиночное наследование
class A {...}; // Один базовый класс
class B {...}; // Другой базовый класс
class C : public A, public B {...}; // Множественное наследование
При одиночном наследовании, в примере 2.12, преобразование от derived* к base* (где base -- базовый, а derived -- производный класс) адрес остается прежним, даже если компилятор полагает, что тип изменился.
При множественном наследовании, в примере 2.13, дело обстоит несколько сложнее.
Пример 2.13 Множественное наследование
C* c = new C;
A* a = c; // Преобразование от производного к первому базовому классу
B* b = c; // Преобразование от производного ко второму базовому классу
cout << c << endl;
cout << a << endl;
cout << b << endl;
При преобразовании C* к A* (рис.2.3) указатель остается прежним. Однако при преобразовании C* к B* компилятор действительно изменяет адрес. Это связано с тем, как объект хранится в памяти.
Компилятор строит объект в порядке появления базовых классов, за которыми следует производный класс. Когда компилятор преобразует C* к A* (рис.2.4), он убеждает клиентский код, что тот работает с A.
Размещение v-таблицы в начале объекта приводит к тому, что принадлежащие C реализации виртуальных функций, объявленных в A, останутся доступными, но будут иметь те же смещения, что и для A. Работая с C*, компилятор знает полную структуру всего объекта и может обращаться к членам A, B и C на их законных местах. Но когда компилятор выполняет преобразование ко второму или одному из следующих классов (рис.2.5) в списке множественного наследования, адрес изменяется -- клиентский код будет считать, что он работает с B.
V-таблиц две. Одна находится в начале объекта и содержит все виртуальные функции, первоначально объявленные в A или C, а другая -- в начале компонента B и содержит виртуальные функции, объявленные в B. Это означает, что преобразование типа от производного к базовому классу в С++ может при некоторых обстоятельствах породить указатель на середину объекта.
C* anotherC = C*(void*(B*(c)));
anotherC->MemberOfC();
Преобразование B*(c) смещает указатель. Затем он преобразуется к типу void*. Далее следует обратное преобразование к C* -- и программа будет уверена, что C начинается с неверного адреса. Без преобразования к void* все работает, поскольку компилятор может определить смещение B* в C*. В сущности, преобразование от base* к derived* (где base -- базовый, а derived -- производный класс) выполняется каждый раз, когда клиент вызывает виртуальную функцию B, переопределенную в C. Но когда происходит преобразование от void* к C*, компилятор лишь полагает, что программист действует сознательно.
Если используются виртуальные базовые классы, то все схемы уплотнения и сборки мусора, требующими перемещения объектов в памяти, работать не будут. Ниже приведен фрагмент программы и показано, как объект представлен в памяти.
class Base {...};
class A : virtual public Base {...};
class B : virtual public Base {...};
class Foo : public A, public B {...};
Base приходится реализовывать как виртуальный базовый класс (рис.2.6). A и B содержат указатели на экземпляр Base. Не имея доступ к этим указателям, нельзя будет обновить их при перемещении объекта в памяти.
Идея указателя на переменную класса, в примере 2.14, заключается в том, что переменную можно однозначно идентифицировать не по ее непосредственному адресу, но по адресу содержащего ее объекта и смещению переменной внутри объекта.
Пример 2.14 Указатель на переменную класса
class Foo {
private:
int x;
public:
static int& Foo::*X() { return &Foo::x; }
};
Foo f = new Foo; //Создать экземпляр
int& Foo::*pm = Foo::X(); //Вычислить смещение int
int& i = f->*pm; //Применить смещение к экземпляру
Функция X() возвращает не ссылку на int, а смещение некоторого int в экземплярах класса Foo. Функция Foo::X() объявлена статической, поскольку относится не к конкретному экземпляру, а к классу в целом. Команда return &Foo::x; определяет смещение конкретной переменной, x. В строке int& Foo::*pm = Foo::X(); объявляется переменная pm, которая содержит смещение переменной int класса Foo. Она инициализируется смещением, полученным от Foo::X(). Наконец, в строке int& i = f->*pm; смещение применяется к конкретному экземпляру для вычисления адреса конкретного int. Значение pm само по себе бесполезно до тех пор, пока оно не будет применено к объекту. Все эти int& можно заменить на int*. В любом случае все завершается косвенным получением адреса некоторой части объекта так (рис.2.7).
Чтобы переместить объект, нужно обновить все указатели на него. Чтобы понять, доступен ли объект, требуется собрать все указатели.
Одно из «силовых» решений -- сложить все указатели в одно место, где их будет легко найти. В свою очередь, это подразумевает, что все указатели должны быть умными и храниться в специальных пространствах памяти. Эти пространства должны быть организованы так, чтобы можно было перебирать их содержимое (то есть создать итерацию для набора умных указателей). В классах все эти *-указатели заменяются дескрипторами или ссылками на умные указатели, поскольку сами указатели должны находиться в отдельном пространстве. В примере 2.15, P и H представляют собой стандартные указатели и дескрипторы соответственно, за исключением того, что P сохраняет экземпляры указателей в специальном пространстве памяти. Указатель P является ведущим, но это не обязательно.
Пример 2.15 Представление P и H стандартными указателями
template <class Type>
class P { // Указатель
private:
Type* pointee;
public:
void* operator new(size_t); /* Использует специальное пространство
памяти*/
void operator delete(void*); /* Использует специальное пространство
памяти*/
// Все для умных указателей
};
template <class Type>
class H { // Дескриптор
private:
P<Type>* ptr;
public:
// Все для дескрипторов
};
class Foo {
private:
P<Bar>& bar; // Ссылка на умный указатель на Bar
// ИЛИ
H<Bar>& bar; // Дескриптор Bar
};
В первом варианте хранится ссылка на умный указатель, причем сам указатель, вероятно, хранится где-то в другом месте. Во втором варианте используется идиома дескриптора -- умного указателя на умный указатель.
Сам дескриптор находится в объекте, но указатель, на который он ссылается, -- в специальном пространстве указателей, используемом операторами new и delete класса P. Если пространство указателей будет реализовано толково, все указатели можно будет перебирать прямо из него. В это случае задача перемещения объекта несколько упрощается, поскольку все указатели на него можно найти в пространстве указателей.
Другое возможное решение в примере 2.16 -- поддержать скрытые коллекции умных указателей.
Пример 2.16 Скрытые списки умных указателей
template <class Type>
class P {
private:
static P<Type>* head; // Начало списка MP
static P<Type>* tail; // Конец списка
P<Type>* next; // Следующий элемент списка
P<Type>* previous; // Предыдущий элемент списка
Type* pointee;
public:
P(); // Занести `this' в список
P(const P<Type>& p); // Занести `this' в список
~P(); // Удалить `this' из списка
P<Type>& operator=(const P<Type>& p); /* Не изменяя список,
скопировать p.pointee*/
// Все для умных указателей
};
Необходимо внимание при выполнении операций со списком в конструкторе копий и операторе =, но во всем остальном реализация тривиальная. Используя этот шаблон, в примере 2.17, класс обходится без хранения ссылок на умные указатели, он хранит их непосредственно.
Пример 2.17 Шаблон хранения ссылок на умные указатели непосредственно
class Foo {
private:
P<Bar> bar; /* Указатель автоматически заносится в скрытую
коллекцию*/
};
При конструировании Foo вызывается соответствующий конструктор P, который автоматически заносит bar в скрытую коллекцию. При уничтожении Foo вызывает деструктор P, который удаляет bar из коллекции. Вместо двусвязного списка можно воспользоваться другими структурами данных.
Кроме того, для всех этих специализированных указателей стоит создать общий базовый класс и сохранить их все в одной коллекции.
Более радикальный подход -- использовать самые обычные указатели и предусмотреть средства для перебора всех указателей, внедренных в экземпляр.
class Foo {
private:
Bar* bar;
};
Не все указатели являются членами объектов. Некоторые из них -- обычные переменные, находящиеся в стеке. Решение со специальными пространствами для стековых переменных не подойдет, если только они не являются дескрипторами или ссылками на умные указатели, хранящиеся в другом месте. Скрытые коллекции так же будут работать для указателей, хранящихся в стеке или куче -- при условии, что будет организована обработка исключений, которая будет правильно раскручивать стек. Особого обращения требует лишь одна переменная this, значение которой задается компилятором, а не вашим кодом, работающим с умными указателями. Стековые переменные значительно усложняют решение с анализом экземпляров, поскольку также придется разрабатывать отдельную схему обнаружения или хотя бы предотвращения коллизий.
Если удаляется указатель, который уже был удален, то есть риск разрушения всей программы. Для того, чтобы этого не произошло, нужно устанавливать все удаленные указатели, как показано в примере 2.18, в значение NULL. Удалять пустой указатель безопасно.
Пример 2.18 Безопасное удаление указателей
#include <iostream.h>
class myClass
{
public:
myClass(int val=0):myValue(val) { cout << "In myClass constructor\n"; }
myClass(const myClass & hs):myValue(rhs.myValue)
{
cout << "In myClass copy constructor\n" ;
}
~myClass() { cout << "In myClass Destructor\n"; }
int GetValue() const { return myValue; }
void SetValue(int theVal) { myValue = theVal; }
private:
int myValue;
};
int main()
{
myClass * pc = new myClass(5);
cout << "The value of the object is " << pc->GetValue () << endl;
delete pc;
рс = 0;
cout << "Here is other work, passing pointers around willy nilly.\n";
cout << "Now ready to delete again..." << endl;
delete pc;
cout << "No harm done" << endl;
return 0;
}
Ниже приведен выход из примера 2.18:
In myClass constructor
The value of the object is 5
In myClass Destructor
Here is other work, passing pointers around willy nilly.
Now ready to delete again...
No harm done
Когда указатели часто передаются в метод и из метода, то при этом создаются их копии. В сложной программе легко потерять их след и случайно удалить уже удаленный указатель. Установка в NULL уже удаленных указателей защитит от этой ошибки. Такой метод гарантирует, что если будет попытка использовать указатель, установленный в NULL, то ошибка произойдет немедленно, а не превратится в тонкую и трудно обнаруживаемую ошибку [4].
2.3 Ссылки и их подсчет
Ссылка - это псевдоним, который при создании инициализируется именем другого объекта, адресата (target). С этого момента ссылка выступает в роли альтернативного имени адресата, а, следовательно, все, что делается со ссылкой, происходит и с объектом.
Указатели - это переменные, которые содержат адрес другого объекта, а ссылки - это псевдонимы объектов [5].
Подсчет ссылок основан на простой идее -- идет слежка за количеством указателей, ссылающихся на объект. Когда счетчик становится равным 0, объект удаляется. Подсчет ссылок обладает довольно жесткими ограничениями, которые снижают его практическую ценность.
2.4 Базовый класс с подсчетом ссылок
Абстрактный базовый класс, от которого можно создать производный класс с подсчетом ссылок. Он содержит переменную, в которой хранится количество вызовов функции Grab() за вычетом количества вызовов функции Release(). Создание этого класса в примере 2.19.
Пример 2.19 Создание производного класса с подсчетом ссылок
class RefCount {
private:
unsigned long count; // Счетчик ссылок
public:
RefCount() : count(0) {}
RefCount(const RefCount&) : count(0) {}
RefCount& operator=(const RefCount&)
{ return *this; } // Не изменяет счетчик
virtual ~RefCount() {} // Заготовка
void Grab() { count++; }
void Release()
{
if (count > 0) count --;
if (count == 0) delete this;
}
};
Каждый раз, когда клиент получает или копирует адрес объекта, производного от RefCount, он вызывает Grab(). Когда клиент гарантирует, что адрес больше не используется, он вызывает Release(). Если счетчик принимает значение 0 -- объекта больше нет.
2.4.1 Указатели и ведущие указатели с подсчетом ссылок
В примере 2.20 показано усовершенствование базового класса RefCount и создание модифицированного шаблона умного указателя для любых классов, производных от RefCount.
Пример 2.20 Усовершенствование базового класса RefCount
template <class Type>
class CP { // “Указатель с подсчетом ссылок”
private:
Type* pointee;
public:
CP(Type* p) : pointee(p) { pointee->Grab(); }
CP(const CP<Type>& cp) : pointee(cp.pointee)
{ pointee->Grab(); }
~CP() { ponintee->Release(); }
CP<Type>& operator=(const CP<Type>& cp)
{
if (this == &cp) return *this;
pointee->Release();
pointee = cp.pointee;
pointee->Grab();
return *this;
}
Type* operator->() { return pointee; }
};
Если весь клиентский код будет обращаться к классам с подсчетом ссылок через этот или аналогичный шаблон, подсчет ссылок осуществляется автоматически. При каждом создании новой копии указателя происходит автоматический вызов Grab(). При каждом уничтожении указателя его деструктор уменьшает значение счетчика. Единственная опасность заключается в том, что клиент обойдет умный указатель. С этой проблемой можно справиться с помощью производящих функций целевого класса в примере 2.21.
Пример 2.21 Решение проблемы с обходом клиентом умного указателя
class Foo : public RefCount {
private:
Foo(); // Вместе с другими конструкторами
public:
static CP<Foo> make(); // Создаем экземпляр
// Далее следует интерфейс Foo
};
Тем самым гарантируется, что доступ к Foo будет осуществляться только через указатель с подсчетом ссылок. Это не ведущий, а самый обычный умный указатель.
Даже если не нужно модифицировать конкретный класс, чтобы сделать его производным от RefCount (например, если он имеет критические требования по быстродействию и объему или входит в коммерческую библиотеку классов), подсчет ссылок можно переместить в ведущий указатель, как показано в примере 2.22.
Пример 2.22 Перемещение подсчета ссылок в ведущий указатель
template <class Type>
class CMP { // “Ведущий указатель с подсчетом ссылок”
private:
Type* pointee;
unsigned long count;
public:
CMP() : pointee(new Type), count(0) {}
CMP(const CMP<Type>& cmp)
: pointee(new Type(*(cmp.pointee))), count(0) {}
~CMP() { delete pointee; } // Независимо от счетчика
CMP<Type>& operator=(const CMP<Type>& cmp)
{
if (this == &cmp) return *this;
delete pointee;
pointee = new Type(*(cmp.pointee));
return *this;
}
Type* operator->() const { return pointee; }
void Grab() { count++; }
void Release()
{
if (count > 0) count--;
if (count <= 0)
{
delete pointee;
delete this;
}
}
};
Это равносильно объединению старого шаблона ведущего указателя с базовым классом RefCount. Подсчет ссылок уже не выделяется в отдельный класс.
2.4 Дескрипторы
Одна из стратегий уплотнения и сборки мусора в С++ -- ссылаться на все объекты только через дескрипторы, как показано в примере 2.23.
Пример 2.23 Ссылка на объекты через дескрипторы
class Foo {
private:
H<Bar> bar; // Дескриптор Bar
public:
H<Bar> GetBar() { return bar; }
};
Здесь H -- шаблон дескриптора. Каждый H<Bar> представляет собой умный указатель на ведущий указатель на Bar. Функции, косвенно открывающие переменные класса (такие как GetBar()), возвращают копию дескриптора. Все ведущие указатели находятся в специальном пространстве памяти, поэтому найти их несложно.
2.4.1 Общее описание архитектуры
В общих чертах архитектура строится на следующих принципах:
* Поскольку различные типы объединяются в один набор ведущих указателей, будет использован абстрактный базовый класс VoidPtr для ведущих указателей. Конкретные ведущие указатели будут создаваться по шаблону, производимому от этого базового класса.
*Ведущие указатели находятся в специальном пространстве, обеспечивающем простой перебор указателей.
*Каждый ведущий указатель обеспечивает подсчет ссылок и удаляет себя, когда счетчик переходит от 1 к 0. В свою очередь, его деструктор вызывает деструктор указываемого объекта и, в зависимости от используемых алгоритмов, пытается (или не пытается) вернуть занимаемую объектом память.
*Во всех переменных классов и обычных переменных используются дескрипторы ведущих указателей вместо прямых указателей на другие объекты.
*Память возвращается лишь в процессе уплотнения управляемой части кучи. Иначе говоря, понадобится больше памяти, нужно спускать активные объекты в них по куче, чтобы освободить место наверху. Выделение памяти всегда происходит снизу вверх. Описана лишь одна из возможных архитектур уплотнения.
2.4.2 Ведущие указатели
Как и во многих других стратегиях управления памятью, придется хранить множество различных ведущих указателей в одной структуре с возможностью перебора. Требуется общий абстрактный базовый класс для всех ведущих указателей. К ведущим указателям предъявляются следующие требования:
1. Ведение счетчика ссылок.
2. Хранение их в специальном пространстве памяти с поддержкой перебора.
3. Вызов деструктора указываемого объекта в деструкторе указателя. В зависимости от используемого алгоритма сборки мусора, одновременно идет попытка вернуть занимаемую объектом память.
В примере 2.24, показан абстрактный базовый класс, удовлетворяющий этим требованиям.
Пример 2.24 Базовый класс, удовлетворяющий требованиям
class VoidPtrPool; // Используется для создания уничтожения
// и перебора VoidPtr
class VoidPtr {
friend class VoidPtrPool;
private:
unsigned long refcount; // Счетчик ссылок
protected:
void* address; // Адрес указываемого объекта
size_t size; // Размер указываемого объекта в байтах
VoidPtr() : address(NULL), size(0), refcount(0) {}
VoidPtr(void* adr, size_t s) : address(adr), size(s), refcount(0) {}
public:
static VoidPtrPool* pool;
virtual ~VoidPtr() { size = 0; address = NULL; }
void* operator new(size_t)
{
if (pool == NULL)
pool = new VoidPtrPool;
return pool->Allocate();
}
void operator delete(void* space)
{ pool->Deallocate((VoidPtr*)space); }
void Grab() { refcount++; }
void Release()
{
if (refcount > 0) refcount--;
if (refcount <= 0) delete this;
}
};
Ведущий указатель представляет собой шаблон, производный от VoidPtr. Он существует для того, чтобы реализовать оператор -> и виртуальный деструктор, который знает, какой деструктор должен вызываться для указываемого объекта. При копировании дескриптора, должен копироваться адрес ведущего указателя, а не сам ведущий указатель или указываемый объект. Следовательно, нет необходимости поддерживать копирование и присваивание для ведущих указателей. Данные операции запрещаются в примере 2.25.
Пример 2.25 Запрещение копирования и присваивания
template <class Type>
class MP : public VoidPtr {
private: // Чтобы запретить копирование и присваивание
MP(const MP<Type>&) {}
MP<Type>& operator=(const MP<Type>&) { return this; }
public:
MP() : VoidPtr(new Type, sizeof(Type)) {}
virtual ~MP() { ((Type*)address)->Type::~TypeOf(); }
Type* operator->() { return (Type*)address; }
};
В примере 2.26 показано шаблон дескриптора с подсчетом ссылок.
Пример 2.26 Шаблон дескриптора с подсчетом ссылок.
template <class Type>
class Handle {
private:
MP<Type>* pointer;
public:
Handle() : pointer(new MP<Type>) { pointer->Grab(); }
Handle(const Handle<Type>& h) : pointer(h.pointer)
{ pointer->Grab(); }
Handle<Type>& operator=(const Handle<Type>& h)
{
if (this == &h) return *this;
if (pointer == h.pointer) return *this;
pointer->Release();
h.pointer->Grab();
return *this;
}
MP<Type>& operator->() { return *pointer; }
};
Для перебора всех ведущих указателей, будет создан класс итератора с именем VoidPtrIterator в примере 2.27. VoidPtrPool возвращает итератор, перебирающий все активные указатели (то есть все указатели, не присутствующие в списке свободных). Он будет объявлен как абстрактный базовый класс.
Пример 2.27 Создание класса для перебора ведущих указателей
class VoidPtrIterator {
protected:
VoidPtrIterator() {}
public:
virtual bool More() = 0;
virtual VoidPtr* Next() = 0;
};
Сам итератор работает прямолинейно. Он перебирает блоки в цикле и ищет указатели с ненулевым значением переменной size, как показано в примере 2.28.
Пример 2.28 Поиск ненулевых указателей
class VoidPtrPoolIterator : public VoidPtrIterator {
private:
VoidPtrBlock* block;
int slot; // Номер позиции в текущем блоке
virtual void Advance() // Найти следующую используемую позицию
{
while (block != NULL)
{
if (slot >= BLOCKSIZE)
{
block = block->next;
slot = 0;
}
else if (block->slots[slot].size != 0)
break;
slot++;
}
}
public:
VoidPtrPoolIterator(VoidPtrBlock* vpb)
: block(vpb), slot(0), { Advance(); }
virtual bool More() { return block != NULL; }
virtual VoidPtr* Next()
{
VoidPtr* vp = &block->slots[slot];
Advance();
return vp;
}
};
Кроме того, добавим в VoidPtrPool следующую функцию:
VoidPtrIterator* iterator()
{ return new VoidPtrPoolIterator(this); }
Далее требовалось объявить VoidPtrPoolIterator, относящемуся к VoidPtr, чтобы в программе можно было обратиться к его переменной size. Если найденная позиция имеет нулевое значение size, идет пропуск. Во всем остальном работа сводится к простому перебору в массивах, образующих блоки указателей.
2.5 Строительные блоки
Поблочное освобождение памяти
Если выделение и освобождение памяти плохо влияет на быстродействие программы, то решение проблемы заключается в выполнении операций с блоками. Память выделяется снизу блока к его верху, а возвращается в систему сразу целым блоком (а не отдельными объектами).
Выделение памяти для новых объектов, в примере 2.29, оказывается самым серьезным фактором, снижающим быстродействие программ. Простая стратегия оптимизации заключается в том, что выделяется память под объекты снизу вверх большого блока и не удаляется.
...Подобные документы
Цели объектно-ориентированного программирования, абстрактные классы и адреса базовых классов, множественное и виртуальное наследование. Инициализация элементов производного класса, программный вызов конструкторов базового и производного классов.
реферат [21,8 K], добавлен 31.10.2011Улучшение параметров модулей памяти. Функционирование и взаимодействие операционной системы с оперативной памятью. Анализ основных типов, параметров оперативной памяти. Программная часть с обработкой выполнения команд и размещением в оперативной памяти.
курсовая работа [99,5 K], добавлен 02.12.2009Сравнение различных способов обхода данных. Заполнение массива для случайного обхода. Изучение понятия кэш-памяти, ее основных размеров и функций. Оптимальный и неоптимальный алгоритм умножения двух матриц с точки зрения порядка обхода данных в памяти.
презентация [94,7 K], добавлен 02.06.2013Инициализация элементов данных класса в программе С++ с использованием конструктора, который запускается для каждого объекта. Применение операции ссылки в языке С++ для взятия адреса объекта. Деструкция как освобождение заказанной памяти, закрытие файлов.
реферат [20,1 K], добавлен 30.10.2011Понятие перегрузки (доопределения) операций и её разновидности. Пример соответствующей программы перегрузки, понятие полиморфизма и правила isA. Использование классов операторов в программах языка С++, конструкций операторов и производных классов.
реферат [19,9 K], добавлен 30.10.2011Создание программного обеспечения, позволяющего сортировать элементы числового массива в порядке возрастания или убывания их значений. Выбор языка программирования, среды разработки и построение алгоритма. Руководство пользователя и программиста.
курсовая работа [295,4 K], добавлен 07.04.2011Изучение определения, описания и вызова функций, указателей и ссылок на них. Написание функции умножения произвольного столбца двумерного массива на const. Умножение 2 столбцов массива на константы. Составление блок-схемы алгоритма и текста программы.
лабораторная работа [182,3 K], добавлен 09.01.2012- Управление памятью. Страничная организация памяти. Сегментная организация памяти. Виртуальная память
Как осуществляется трансляция адресов при страничной организации. Что такое компактировка и как с ее помощью избавиться от внешней фрагментации. Что такое регистр таблицы страниц, сегментация. Методы распределения памяти в виде отдельных сегментов.
контрольная работа [236,2 K], добавлен 23.12.2016 Механизм классов в C++. Инициализация внутреннего объекта с помощью конструктора. Управление доступом к классу. Защищенные члены класса. Графические средства компилятора Borland C 3.1. Библиотека стандартных шаблонов. Реализация и использование класса.
курсовая работа [2,7 M], добавлен 16.05.2012Основные типы циклов программирования. Методы применения специальных функций break, continue и цикла while. Обработка массивов информации. Условия применения циклических алгоритмов на языке программирования С++. Инициализация одномерного массива.
курсовая работа [1,7 M], добавлен 06.01.2014Разработка и реализация типовых алгоритмов обработки одномерных массивов на языке Delphi. Максимальный и минимальный элемент массива. Значение и расположение элементов массива. Элементы массива, находящиеся перед максимальным или минимальным элементом.
лабораторная работа [12,8 K], добавлен 02.12.2014Управление основной и вторичной памятью компьютера. Доступ пользователей к различным общим сетевым ресурсам. Система поддержки командного интерпретатора. Распределение ресурсов между пользователями, программами и процессами, работающими одновременно.
презентация [1,4 M], добавлен 24.01.2014Главная задача компьютерной системы. Виртуальные адресные пространства нескольких программ. Классификация методов распределения памяти. Зависимость загрузки процессора от числа задач и интенсивности ввода-вывода. Схема функционирования кэш-памяти.
презентация [2,2 M], добавлен 14.11.2012Определение размерности исходного массива на листе электронной таблицы, адреса ячейки. Считывание исходного массива в программу. Создание фрагмента программы для выполнения задания с использованием операторов условного перехода, адресация диапазонов.
контрольная работа [791,6 K], добавлен 16.04.2010Организация памяти компьютера и простые схемы управления ею. Принципы связывания адресов. Динамическое распределение и свопинг. Сегментная и сегментно-страничная организация памяти. Выталкивание редко используемой страницы. Описание работы с программой.
курсовая работа [3,1 M], добавлен 19.01.2016Архитектура компьютеров и возможности операционной системы по управлению памятью. Суть концепции виртуальной памяти. Аппаратно-независимые и аппаратно-зависимые средства управления виртуальной памятью. Сегментно-страничная организации виртуальной памяти.
презентация [355,2 K], добавлен 27.12.2010Массив как пронумерованная последовательность величин одинакового типа, обозначаемая одним именем. Расположение в последовательных ячейках памяти, обозначение именем массива и индексом, инициализация. Передача одномерных и двумерных массивов в функцию.
лабораторная работа [32,6 K], добавлен 06.07.2009Разработка на языке ассемблера алгоритма контроля, на циклический CRC-код, массива данных хранящегося в некоторой области памяти. Сохранение кода для последующей периодической проверки массива данных. Сообщение об искажении данных. Описание алгоритма.
курсовая работа [453,0 K], добавлен 27.02.2009Сравнительный анализ статической и динамической памяти. Быстродействие и потребление энергии статической памятью. Объем памяти микросхем. Временные диаграммы чтения и записи памяти. Микросхемы синхронной и асинхронной памяти. Режимы модулей памяти.
презентация [114,2 K], добавлен 27.08.2013Разработка алгоритма работы и структуры контроллера кэш-памяти с полностью ассоциативным отображением основной памяти. Представление операционной и управляющей частей черного ящика устройства. Схема алгоритма контроллера кэш на уровне микроопераций.
курсовая работа [1,0 M], добавлен 19.03.2012