<< Пред.           стр. 3 (из 121)           След. >>

Список литературы по разделу

 
  for (index=9; index>=0; --index)
  cout << ia[index] << " ";
 
  cout << endl;
 }
 Оба цикла выполняются по 10 раз. Все управление циклом for осуществляется инструкциями в круглых скобках за ключевым словом for. Первая присваивает начальное значение переменной index. Это производится один раз перед началом цикла:
 index = 0;
 Вторая инструкция:
 index < 10;
 представляет собой условие окончания цикла. Оно проверяется в самом начале каждой итерации цикла. Если результатом этой инструкции является true, то выполнение цикла продолжается; если же результатом является false, цикл заканчивается. В нашем примере цикл продолжается до тех пор, пока значение переменной index меньше 10. На каждой итерации цикла выполняется некоторая инструкция или группа инструкций, составляющих тело цикла. В нашем случае это инструкция
 ia[index] = index;
 Третья управляющая инструкция цикла
 ++index
 выполняется в конце каждой итерации, по завершении тела цикла. В нашем примере это увеличение переменной index на единицу. Мы могли бы записать то же действие как
 index = index + 1
 но С++ дает возможность использовать более короткую (и более наглядную) форму записи. Этой инструкцией завершается итерация цикла. Описанные действия повторяются до тех пор, пока условие цикла не станет ложным.
 Вторая инструкция for в нашем примере печатает элементы массива. Она отличается от первой только тем, что в ней переменная index уменьшается от 9 до 0. (Подробнее инструкция for рассматривается в главе 5.)
 Несмотря на то, что в С++ встроена поддержка для типа данных “массив”, она весьма ограничена. Фактически мы имеем лишь возможность доступа к отдельным элементам массива. С++ не поддерживает абстракцию массива, не существует операций над массивами в целом, таких, например, как присвоение одного массива другому или сравнение двух массивов на равенство, и даже такой простой, на первый взгляд, операции, как получение размера массива. Мы не можем скопировать один массив в другой, используя простой оператор присваивания:
 int array0[10]; array1[10];
 ...
 array0 = array1; // ошибка
 Вместо этого мы должны программировать такую операцию с помощью цикла:
 for (int index=0; index<10; ++index)
  array0[index] = array1[index];
 Массив “не знает” собственный размер. Поэтому мы должны сами следить за тем, чтобы случайно не обратиться к несуществующему элементу массива. Это становится особенно утомительным в таких ситуациях, как передача массива функции в качестве параметра. Можно сказать, что этот встроенный тип достался языку С++ в наследство от С и процедурно-ориентированной парадигмы программирования. В оставшейся части главы мы исследуем разные возможности “улучшить” массив.
 Упражнение 2.1
 Как вы думаете, почему для встроенных массивов не поддерживается операция присваивания? Какая информация нужна для того, чтобы поддержать эту операцию?
 Упражнение 2.2
 Какие операции должен поддерживать “полноценный” массив?
 2.2. Динамическое выделение памяти и указатели
 Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать сколько-нибудь сложную программу, не умея выделять память во время выполнения и обращаться к ней.
 В С++ объекты могут быть размещены либо статически – во время компиляции, либо динамически – во время выполнения программы, путем вызова функций из стандартной библиотеки. Основная разница в использовании этих методов – в их эффективности и гибкости. Статическое размещение более эффективно, так как выделение памяти происходит до выполнения программы, однако оно гораздо менее гибко, потому что мы должны заранее знать тип и размер размещаемого объекта. К примеру, совсем не просто разместить содержимое некоторого текстового файла в статическом массиве строк: нам нужно заранее знать его размер. Задачи, в которых нужно хранить и обрабатывать заранее неизвестное число элементов, обычно требуют динамического выделения памяти.
 До сих пор во всех наших примерах использовалось статическое выделение памяти. Скажем, определение переменной ival
 int ival = 1024;
 заставляет компилятор выделить в памяти область, достаточную для хранения переменной типа int, связать с этой областью имя ival и поместить туда значение 1024. Все это делается на этапе компиляции, до выполнения программы.
 С объектом ival ассоциируются две величины: собственно значение переменной, 1024 в данном случае, и адрес той области памяти, где хранится это значение. Мы можем обращаться к любой из этих двух величин. Когда мы пишем:
 int ival2 = ival + 1;
 то обращаемся к значению, содержащемуся в переменной ival: прибавляем к нему 1 и инициализируем переменную ival2 этим новым значением, 1025. Каким же образом обратиться к адресу, по которому размещена переменная?
 С++ имеет встроенный тип “указатель”, который используется для хранения адресов объектов. Чтобы объявить указатель, содержащий адрес переменной ival, мы должны написать:
 int *pint; // указатель на объект типа int
 Существует также специальная операция взятия адреса, обозначаемая символом &. Ее результатом является адрес объекта. Следующий оператор присваивает указателю pint адрес переменной ival:
 int *pint;
 pint = &ival; // pint получает значение адреса ival
 Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем случае), используя операцию разыменования, называемую также косвенной адресацией. Эта операция обозначается символом *. Вот как можно косвенно прибавить единицу к ival, используя ее адрес:
 *pint = *pint + 1; // неявно увеличивает ival
 Это выражение производит в точности те же действия, что и
 ival = ival + 1; // явно увеличивает ival
 В этом примере нет никакого реального смысла: использование указателя для косвенной манипуляции переменной ival менее эффективно и менее наглядно. Мы привели этот пример только для того, чтобы дать самое начальное представление об указателях. В реальности указатели используют чаще всего для манипуляций с динамически размещенными объектами.
 Основные отличия между статическим и динамическим выделением памяти таковы:
 статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей;
 выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы 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:
  // дальше идут закрытые члены,
  // обеспечивающие реализацию класса
  ...
 }
 Номера, указанные в комментариях при объявлениях методов, ссылаются на спецификацию класса, которую мы составили в начале данного раздела. Сейчас мы не будем объяснять смысл ключевого слова const, он не так уж важен для понимания того, что мы хотим продемонстрировать на данном примере. Будем считать, что это ключевое слово необходимо для правильной компиляции программы.
 Именованная функция-член (например, min()) может быть вызвана с использованием одной из двух операций доступа к члену класса. Первая операция доступа, обозначаемая точкой (.), применяется к объектам класса, вторая – стрелка (->) – к указателям на объекты. Так, чтобы найти минимальный элемент в объекте, имеющем тип IntArray, мы должны написать:
 // инициализация переменной min_val
 // минимальным элементом myArray
 int min_val = myArray.min();
 Чтобы найти минимальный элемент в динамически созданном объекте типа IntArray, мы должны написать:
 int min_val = pArray->min();
 (Да, мы еще ничего не сказали о том, как же проинициализировать наш объект – задать его размер и наполнить элементами. Для этого служит специальная функция-член, называемая конструктором. Мы поговорим об этом чуть ниже.)
 Операции применяются к объектам класса точно так же, как и к встроенным типам данных. Пусть мы имеем два объекта типа IntArray:
 IntArray myАrray0, myArray1;
 Инструкции присваивания и сравнения с этими объектами выглядят совершенно обычным образом:
 // инструкция присваивания -
 // вызывает функцию-член myArray0.operator=(myArray1)
 myArray0 = myArray1;
 
 // инструкция сравнения -
 // вызывает функцию-член myArray0.operator==(myArray1)
 if (myArray0 == myArray1)
  cout << "Ура! Оператор присваивания сработал!\n";
 Спецификаторы доступа public и private определяют уровень доступа к членам класса. К тем членам, которые перечислены после public, можно обращаться из любого места программы, а к тем, которые объявлены после private, могут обращаться только функции-члены данного класса. (Помимо функций-членов, существуют еще функции-друзья класса, но мы не будем говорить о них вплоть до раздела 15.2.)
 В общем случае открытые члены класса составляют его открытый интерфейс, то есть набор операций, которые определяют поведение класса. Закрытые члены класса обеспечивают его скрытую реализацию.
 Такое деление на открытый интерфейс и скрытую реализацию называют сокрытием информации, или инкапсуляцией. Это очень важная концепция программирования, мы еще поговорим о ней в следующих главах. В двух словах, эта концепция помогает решить следующие проблемы:
 если мы меняем или расширяем реализацию класса, то изменения можно выполнить так, что большинство пользовательских программ, использующих наш класс, их “не заметят”: модификации коснутся лишь скрытых членов (мы поговорим об этом в разделе 6.18);
 если в реализации класса обнаруживается ошибка, то обычно для ее исправления достаточно проверить код, составляющий именно скрытую реализацию, а не весь код программы, где данный класс используется.
 Какие же внутренние данные потребуются для реализации класса IntArray? Необходимо где-то сохранить размер массива и сами его элементы. Мы будем хранить их в массиве встроенного типа, память для которого выделяется динамически. Так что нам потребуется указатель на этот массив. Вот как будут выглядеть определения этих данных-членов:
 class IntArray {
 public:
  // ...
  int size() const { return _size; }
 private:
  // внутренние данные-члены
  int _size;
  int *ia;
 };
 Поскольку мы поместили член _size в закрытую секцию, пользователь класса не имеет возможности обратиться к нему напрямую. Чтобы позволить внешней программе узнать размер массива, мы написали функцию-член size(), которая возвращает значение члена _size. Нам пришлось добавить символ подчеркивания к имени нашего скрытого члена _size, поскольку функция-член с именем size() уже определена. Члены класса – функции и данные – не могут иметь одинаковые имена.
 Может показаться, что реализуя подобным образом доступ к скрытым данным класса, мы очень сильно проигрываем в эффективности. Сравним два выражения (предположим, что мы изменили спецификатор доступа члена _size на public):
 IntArray array;
 int array_size = array.size();
 array_size = array._size;
 Действительно, вызов функции гораздо менее эффективен, чем прямой доступ к памяти, как во втором операторе. Так что же, принцип сокрытия информации заставляет нас жертвовать эффективностью?
 На самом деле, нет. С++ имеет механизм встроенных (inline) функций. Текст встроенной функции подставляется компилятором в то место, где записано обращение к ней. (Это напоминает механизм макросов, реализованный во многих языках, в том числе и в С++. Однако есть определенные отличия, о которых мы сейчас говорить не будем.) Вот пример. Если у нас есть следующий фрагмент кода:
 for (int index=0; index   // ...
 то функция size() не будет вызываться _size раз во время исполнения. Вместо вызова компилятор подставит ее текст, и результат компиляции предыдущего кода будет в точности таким же, как если бы мы написали:
 for (int index=0; index   // ...
 Если функция определена внутри тела класса (как в нашем случае), она автоматически считается встроенной. Существует также ключевое слово inline, позволяющее объявить встроенной любую функцию.
 Мы до сих пор ничего не сказали о том, как будем инициализировать наш массив.
 Одна из самых распространенных ошибок при программировании (на любом языке) состоит в том, что объект используется без предварительной инициализации. Чтобы помочь избежать этой ошибки, С++ обеспечивает механизм автоматической инициализации для определяемых пользователем классов – конструктор класса.
 Конструктор – это специальная функция-член, которая вызывается автоматически при создании объекта типа класса. Конструктор пишется разработчиком класса, причем у одного класса может быть несколько конструкторов.
 Функция-член класса, носящее то же имя, что и сам класс, считается конструктором. (Нет никаких специальных ключевых слов, позволяющих определить конструктор как-то по-другому.) Мы уже сказали, что конструкторов может быть несколько. Как же так: разные функции с одинаковыми именами?
 В С++ это возможно. Разные функции могут иметь одно и то же имя, если у этих функций различны количество и/или типы параметров. Это называется перегрузкой функции. Обрабатывая вызов перегруженной функции, компилятор смотрит не только на ее имя, но и на список параметров. По количеству и типам передаваемых параметров компилятор может определить, какую же из одноименных функций нужно вызывать в данном случае. Рассмотрим пример. Мы можем определить следующий набор перегруженных функций min(). (Перегружаться могут как обычные функции, так и функции-члены.)
 // список перегруженных функций min()
 // каждая функция отличается от других списком параметров
 #include
 
 int min (const int *pia,int size);
 int min (int, int);
 int min (const char *str);
 char min (string);
 string min (string,string);
 Поведение перегруженных функций во время выполнения ничем не отличается от поведения обычных. Компилятор определяет нужную функцию и помещает в объектный код именно ее вызов. (В главе 9 подробно обсуждается механизм перегрузки.)
 Итак, вернемся к нашему классу IntArray. Давайте определим для него три конструктора:
 class IntArray {
 public:
  explicit IntArray (int sz = DefaultArraySize);
  IntArray (int *array, int array_size);
  IntArray (const IntArray &rhs);
  // ...
 private:
  static const int DefaultArraySize = 12;
 }
 Первый из перечисленных конструкторов
 IntArray (int sz = DefaultArraySize);
 называется конструктором по умолчанию, потому что он может быть вызван без параметров. (Пока не будем объяснять ключевое слово explicit.) Если при создании объекта ему задается параметр типа int, например
 IntArray array1(1024);
 то значение 1024 будет передано в конструктор. Если же размер не задан, допустим:
 IntArray array2;
 то в качестве значения отсутствующего параметра конструктор принимает величину DefaultArraySize. (Не будем пока обсуждать использование ключевого слова static в определении члена DefaultArraySize: об этом говорится в разделе 13.5. Скажем лишь, что такой член данных существует в единственном экземпляре и принадлежит одновременно всем объектам данного класса.)
 Вот как может выглядеть определение нашего конструктора по умолчанию:
 IntArray::IntArray (int sz)
 {
  // инициализация членов данных
  _size = sz;
  ia = new int[_size];
 
  // инициализация элементов массива
  for (int ix=0; ix<_size; ++ix)
  ia[ix] = 0;
 }
 Это определение содержит несколько упрощенный вариант реализации. Мы не позаботились о том, чтобы попытаться избежать возможных ошибок во время выполнения. Какие ошибки возможны? Во-первых, оператор new может потерпеть неудачу при выделении нужной памяти: в реальной жизни память не бесконечна. (В разделе 2.6 мы увидим, как обрабатываются подобные ситуации.) А во-вторых, параметр sz из-за небрежности программиста может иметь некорректное значение, например нуль или отрицательное.
 Что необычного мы видим в таком определении конструктора? Сразу бросается в глаза первая строчка, в которой использована операция разрешения области видимости (::):
 IntArray::IntArray(int sz);
 Дело в том, что мы определяем нашу функцию-член (в данном случае конструктор) вне тела класса. Для того чтобы показать, что эта функция на самом деле является членом класса IntArray, мы должны явно предварить имя функции именем класса и двойным двоеточием. (Подробно области видимости разбираются в главе 8; области видимости применительно к классам рассматриваются в разделе 13.9.)
 Второй конструктор класса IntArray инициализирует объект IntArray значениями элементов массива встроенного типа. Он требует двух параметров: массива встроенного типа со значениями для инициализации и размера этого массива. Вот как может выглядеть создание объекта IntArray с использованием данного конструктора:
 int ia[10] = {0,1,2,3,4,5,6,7,8,9};
 IntArray iA3(ia,10);
 Реализация второго конструктора очень мало отличается от реализации конструктора по умолчанию. (Как и в первом случае, мы пока опустили обработку ошибочных ситуаций.)
 IntArray::IntArray (int *array, int sz)
 {
  // инициализация членов данных
  _size = sz;
  ia = new int[_size];
 
  // инициализация элементов массива
  for (int ix=0; ix<_size; ++ix)
  ia[ix] = array[ix];
 }
 Третий конструктор называется копирующим конструктором. Он инициализирует один объект типа IntArray значением другого объекта IntArray. Такой конструктор вызывается автоматически при выполнении следующих инструкций:
 IntArray array;
 
 // следующие два объявления совершенно эквивалентны:
 IntArray ia1 = array;
 IntArray ia2 (array);
 Вот как выглядит реализация копирующего конструктора для IntArray, опять-таки без обработки ошибок:
 IntArray::IntArray (const IntArray &rhs )
 {
  // инициализация членов данных
  _size = rhs._size;
  ia = new int[_size];
 
  // инициализация элементов массива
  for (int ix=0; ix<_size; ++ix)
  ia[ix] = rhs.ia[ix];
 }
 В этом примере мы видим еще один составной тип данных – ссылку на объект, которая обозначается символом &. Ссылку можно рассматривать как разновидность указателя: она также позволяет косвенно обращаться к объекту. Однако синтаксис их использования различается: для доступа к члену объекта, на который у нас есть ссылка, следует использовать точку, а не стрелку; следовательно, мы пишем rhs._size, а не rhs->_size. (Ссылки рассматриваются в разделе 3.6.)
 Заметим, что реализация всех трех конструкторов очень похожа. Если один и тот же код повторяется в разных местах, желательно вынести его в отдельную функцию. Это облегчает и дальнейшую модификацию кода, и чтение программы. Вот как можно модернизировать наши конструкторы, если выделить повторяющийся код в отдельную функцию init():
 class IntArray {
 public:
  explicit IntArray (int sz = DefaultArraySize);
  IntArray (int *array, int array_size);
  IntArray (const IntArray &rhs);
  // ...
 private:
  void init (int sz,int *array);
  // ...
 };
 
 // функция, используемая всеми конструкторами
 void IntArray::init (int sz,int *array)
 {
  _size = sz;
  ia = new int[_size];
 
  for (int ix=0; ix<_size; ++ix)
  if ( !array )
  ia[ix] = 0;
  else
  ix[ix] = array[ix];
 }
 
 // модифицированные конструкторы
 IntArray::IntArray (int sz) { init(sz,0); }
 IntArray::IntArray (int *array, int array_size)
  { init (array_size,array); }
 IntArray::IntArray (const IntArray &rhs)
  { init (rhs._size,rhs.ia); }
 Имеется еще одна специальная функция-член – деструктор, который автоматически вызывается в тот момент, когда объект прекращает существование. Имя деструктора совпадает с именем класса, только в начале идет символ тильды (~). Основное назначение данной функции – освободить ресурсы, отведенные объекту во время его создания и использования. Применение деструкторов помогает бороться с трудно обнаруживаемыми ошибками, ведущими к утечке памяти и других ресурсов. В случае класса IntArray эта функция-член должна освободить память, выделенную в момент создания объекта. (Подробно конструкторы и деструкторы описаны в главе 14.) Вот как выглядит деструктор для IntArray:
 class IntArray {
 public:
  // конструкторы

<< Пред.           стр. 3 (из 121)           След. >>

Список литературы по разделу