* выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete.
Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа:
int *pint = new int(1024);
Здесь оператор new выделяет память под безымянный объект типа int, инициализирует его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется для инициализации указателя pint. Все действия над таким безымянным объектом производятся путем разыменовывания данного указателя, т.к. явно манипулировать динамическим объектом невозможно.
Вторая форма оператора new выделяет память под массив заданного размера, состоящий из элементов определенного типа:
int *pia = new int[4];
В этом примере память выделяется под массив из четырех элементов типа int. К сожалению, данная форма оператора new не позволяет инициализировать элементы массива.
Некоторую путаницу вносит то, что обе формы оператора new возвращают одинаковый указатель, в нашем примере это указатель на целое. И pint, и pia объявлены совершенно одинаково, однако pint указывает на единственный объект типа int, а pia – на первый элемент массива из четырех объектов типа int.
Когда динамический объект больше не нужен, мы должны явным образом освободить отведенную под него память. Это делается с помощью оператора delete, имеющего, как и new, две формы – для единичного объекта и для массива:
// освобождение единичного объекта
delete pint;
// освобождение массива
delete[] pia;
Что случится, если мы забудем освободить выделенную память? Память будет расходоваться впустую, она окажется неиспользуемой, однако возвратить ее системе нельзя, поскольку у нас нет указателя на нее. Такое явление получило специальное название утечка памяти. В конце концов программа аварийно завершится из-за нехватки памяти (если, конечно, она будет работать достаточно долго). Небольшая утечка трудно поддается обнаружению, но существуют утилиты, помогающие это сделать.
Наш сжатый обзор динамического выделения памяти и использования указателей, наверное, больше породил вопросов, чем дал ответов. В разделе 8.4 затронутые проблемы будут освещены во всех подробностях. Однако мы не могли обойтись без этого отступления, так как класс Array, который мы собираемся спроектировать в последующих разделах, основан на использовании динамически выделяемой памяти.
Упражнение 2.3
Объясните разницу между четырьмя объектами:
(a) int ival = 1024;
(b) int *pi = ival;
(c) int *pi2 = new int(1024);
(d) int *pi3 = new int[1024];
Упражнение 2.4
Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим, что операция взятия индекса ([]) правильно применена к указателю pia. Объяснение этому факту можно найти в разделе 3.9.2.)
int *pi = new int(10);
int *pia = new int[10];
while ( *pi 10 ) {
pia[*pi] = *pi;
*pi = *pi + 1;
}
delete pi;
delete[] pia;
2.3. Объектный подход
В этом разделе мы спроектируем и реализуем абстракцию массива, используя механизм классов С++. Первоначальный вариант будет поддерживать только массив элементов типа int. Впоследствии при помощи шаблонов мы расширим наш массив для поддержки любых типов данных.
Первый шаг состоит в том, чтобы определить, какие операции будет поддерживать наш массив. Конечно, было бы заманчиво реализовать все мыслимые и немыслимые операции, но невозможно сделать сразу все на свете. Поэтому для начала определим то, что должен уметь наш массив:
* обладать некоторыми знаниями о самом себе. Пусть для начала это будет знание собственного размера;
* поддерживать операцию присваивания и операцию сравнения на равенство;
* отвечать на некоторые вопросы, например: какова величина минимального и максимального элемента; содержит ли массив элемент с определенным значением; если да, то каков индекс первого встречающегося элемента, имеющего это значение;
* сортировать сам себя. Пусть такая операция покажется излишней, все-таки реализуем ее в качестве дополнительного упражнения: ведь кому-то это может пригодиться.
* Конечно, мы должны реализовать и базовые операции работы с массивом, а именно:Возможность задать размер массива при его создании. (Речь не идет о том, чтобы знать эту величину на этапе компиляции.)
* Возможность проинициализировать массив некоторым набором значений.
* Возможность обращаться к элементу массива по индексу. Пусть эта возможность реализуется с помощью стандартной операции взятия индекса.
* Возможность обнаруживать обращения к несуществующим элементам массива и сигнализировать об ошибке. Не будем обращать внимание на тех потенциальных пользователей нашего класса, которые привыкли работать со встроенными массивами С и не считают данную возможность полезной – мы хотим создать такой массив, который был бы удобен в использовании даже самым неискушенным программистам на С++.
Кажется, мы перечислили достаточно потенциальных достоинств нашего будущего массива, чтобы загореться желанием немедленно приступить к его реализации. Как же это будет выглядеть на С++? В самом общем случае объявление класса выглядит следующим образом:
class classname {
public:
// набор открытых операций
private:
// закрытые функции, обеспечивающие реализацию
};
class, public и private – это ключевые слова С++, а classname – имя, которое программист дал своему классу. Назовем наш проектируемый класс IntArray: на первом этапе этот массив будет содержать только целые числа. Когда мы научим его обращаться с данными любого типа, можно будет переименовать его в Array.
Определяя класс, мы создаем новый тип данных. На имя класса можно ссылаться точно так же, как на любой встроенный описатель типа. Можно создавать объекты этого нового типа аналогично тому, как мы создаем объекты встроенных типов:
// статический объект типа IntArray
IntArray myArray;
// указатель на динамический объект типа IntArray
IntArray *pArray = new IntArray;
Определение класса состоит из двух частей: заголовка (имя, предваренное ключевым словом class) и тела, заключенного в фигурные скобки. Заголовок без тела может служить объявлением класса.
// объявление класса IntArray
// без определения его
class IntArray;
Тело класса состоит из определений членов и спецификаторов доступа – ключевых слов public, private и protected. (Пока мы ничего не будем говорить об уровне доступа protected.) Членами класса могут являться функции, которые определяют набор действий, выполняемых классом, и переменные, содержащие некие внутренние данные, необходимые для реализации класса. Функции, принадлежащие классу, называют функциями-членами или, по-другому, методами класса. Вот набор методов класса IntArray:
class IntArray {
public:
// операции сравнения: #2b
bool operator== (const IntArray) const;
bool operator!= (const IntArray) const;
// операция присваивания: #2a
IntArray operator= (const IntArray);
int size() const; // #1
void sort(); // #4
int min() const; // #3a
int max() const; // #3b
// функция find возвращает индекс первого
// найденного элемента массива
// или -1, если элементов не найдено
int find (int value) const; // #3c
private:
// дальше идут закрытые члены,
// обеспечивающие реализацию класса
...
}