Главная страница

Язык программирования C++. Вводный курс. С для начинающих


Скачать 5.41 Mb.
НазваниеС для начинающих
Дата24.08.2022
Размер5.41 Mb.
Формат файлаpdf
Имя файлаЯзык программирования C++. Вводный курс.pdf
ТипДокументы
#652350
страница5 из 93
1   2   3   4   5   6   7   8   9   ...   93

40
Поскольку мы поместили член _size в закрытую секцию, пользователь класса не имеет возможности обратиться к нему напрямую. Чтобы позволить внешней программе узнать размер массива, мы написали функцию-член size(), которая возвращает значение члена
_size
. Нам пришлось добавить символ подчеркивания к имени нашего скрытого члена
_size
, поскольку функция-член с именем size() уже определена. Члены класса – функции и данные – не могут иметь одинаковые имена.
Может показаться, что реализуя подобным образом доступ к скрытым данным класса, мы очень сильно проигрываем в эффективности. Сравним два выражения (предположим, что мы изменили спецификатор доступа члена _size на public): int array_size = array.size(); array_size = array._size;
Действительно, вызов функции гораздо менее эффективен, чем прямой доступ к памяти, как во втором операторе. Так что же, принцип сокрытия информации заставляет нас жертвовать эффективностью?
На самом деле, нет. С++ имеет механизм встроенных (inline) функций. Текст встроенной функции подставляется компилятором в то место, где записано обращение к ней. (Это напоминает механизм макросов, реализованный во многих языках, в том числе и в С++. Однако есть определенные отличия, о которых мы сейчас говорить не будем.)
Вот пример. Если у нас есть следующий фрагмент кода:
// ... то функция size() не будет вызываться _size раз во время исполнения. Вместо вызова компилятор подставит ее текст, и результат компиляции предыдущего кода будет в точности таким же, как если бы мы написали: for (int index=0; index // ...
Если функция определена внутри тела класса (как в нашем случае), она автоматически считается встроенной. Существует также ключевое слово inline, позволяющее объявить встроенной любую функцию3.
Мы до сих пор ничего не сказали о том, как будем инициализировать наш массив.
Одна из самых распространенных ошибок при программировании (на любом языке) состоит в том, что объект используется без предварительной инициализации. Чтобы помочь избежать этой ошибки, С++ обеспечивает механизм автоматической инициализации для определяемых пользователем классов – конструктор класса.
3 Объявление функции inline – это всего лишь подсказка компилятору. Однако компилятор не всегда может сделать функцию встроенной, существуют некоторые ограничения. Подробнее об этом сказано в разделе 7.6.
IntArray array; for (int index=0; index

С++ для начинающих
41
Конструктор – это специальная функция-член, которая вызывается автоматически при создании объекта типа класса. Конструктор пишется разработчиком класса, причем у одного класса может быть несколько конструкторов.
Функция-член класса, носящее то же имя, что и сам класс, считается конструктором. (Нет никаких специальных ключевых слов, позволяющих определить конструктор как-то по- другому.) Мы уже сказали, что конструкторов может быть несколько. Как же так: разные функции с одинаковыми именами?
В С++ это возможно. Разные функции могут иметь одно и то же имя, если у этих функций различны количество и/или типы параметров. Это называется перегрузкой
функции. Обрабатывая вызов перегруженной функции, компилятор смотрит не только на ее имя, но и на список параметров. По количеству и типам передаваемых параметров компилятор может определить, какую же из одноименных функций нужно вызывать в данном случае. Рассмотрим пример. Мы можем определить следующий набор перегруженных функций min(). (Перегружаться могут как обычные функции, так и функции-члены.) string min (string,string);
Поведение перегруженных функций во время выполнения ничем не отличается от поведения обычных. Компилятор определяет нужную функцию и помещает в объектный код именно ее вызов. (В главе 9 подробно обсуждается механизм перегрузки.)
Итак, вернемся к нашему классу IntArray. Давайте определим для него три конструктора:
}
Первый из перечисленных конструкторов
IntArray (int sz = DefaultArraySize); называется конструктором по умолчанию, потому что он может быть вызван без параметров. (Пока не будем объяснять ключевое слово explicit.) Если при создании объекта ему задается параметр типа int, например
// список перегруженных функций min()
// каждая функция отличается от других списком параметров
#include int min (const int *pia,int size); int min (int, int); int min (const char *str); char min (string); class IntArray { public: explicit IntArray (int sz = DefaultArraySize);
IntArray (int *array, int array_size);
IntArray (const IntArray &rhs);
// ... private: static const int DefaultArraySize = 12;

С++ для начинающих
42
IntArray array1(1024); то значение 1024 будет передано в конструктор. Если же размер не задан, допустим:
IntArray array2; то в качестве значения отсутствующего параметра конструктор принимает величину
DefaultArraySize
. (Не будем пока обсуждать использование ключевого слова static в определении члена DefaultArraySize: об этом говорится в разделе 13.5. Скажем лишь, что такой член данных существует в единственном экземпляре и принадлежит одновременно всем объектам данного класса.)
Вот как может выглядеть определение нашего конструктора по умолчанию:
}
Это определение содержит несколько упрощенный вариант реализации. Мы не позаботились о том, чтобы попытаться избежать возможных ошибок во время выполнения. Какие ошибки возможны? Во-первых, оператор new может потерпеть неудачу при выделении нужной памяти: в реальной жизни память не бесконечна. (В разделе 2.6 мы увидим, как обрабатываются подобные ситуации.) А во-вторых, параметр sz из-за небрежности программиста может иметь некорректное значение, например нуль или отрицательное.
Что необычного мы видим в таком определении конструктора? Сразу бросается в глаза первая строчка, в которой использована операция разрешения области видимости (::):
IntArray::IntArray(int sz);
Дело в том, что мы определяем нашу функцию-член (в данном случае конструктор) вне тела класса. Для того чтобы показать, что эта функция на самом деле является членом класса IntArray, мы должны явно предварить имя функции именем класса и двойным двоеточием. (Подробно области видимости разбираются в главе 8; области видимости применительно к классам рассматриваются в разделе 13.9.)
Второй конструктор класса IntArray инициализирует объект IntArray значениями элементов массива встроенного типа. Он требует двух параметров: массива встроенного типа со значениями для инициализации и размера этого массива. Вот как может выглядеть создание объекта IntArray с использованием данного конструктора:
IntArray iA3(ia,10);
IntArray::IntArray (int sz)
{
// инициализация членов данных
_size = sz; ia = new int[_size];
// инициализация элементов массива for (int ix=0; ix<_size; ++ix) ia[ix] = 0; int ia[10] = {0,1,2,3,4,5,6,7,8,9};

С++ для начинающих
43
Реализация второго конструктора очень мало отличается от реализации конструктора по умолчанию. (Как и в первом случае, мы пока опустили обработку ошибочных ситуаций.)
}
Третий конструктор называется копирующим конструктором. Он инициализирует один объект типа IntArray значением другого объекта IntArray. Такой конструктор вызывается автоматически при выполнении следующих инструкций:
IntArray array;
// следующие два объявления совершенно эквивалентны:
IntArray ia1 = array;
IntArray ia2 (array);
Вот как выглядит реализация копирующего конструктора для IntArray, опять-таки без обработки ошибок:
{
// инициализация членов данных
_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():
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 (const IntArray &rhs )

С++ для начинающих
44
{ init (rhs._size,rhs.ia); }
Имеется еще одна специальная функция-член – деструктор, который автоматически вызывается в тот момент, когда объект прекращает существование. Имя деструктора совпадает с именем класса, только в начале идет символ тильды (

). Основное назначение данной функции – освободить ресурсы, отведенные объекту во время его создания и использования. Применение деструкторов помогает бороться с трудно обнаруживаемыми ошибками, ведущими к утечке памяти и других ресурсов. В случае класса IntArray эта функция-член должна освободить память, выделенную в момент создания объекта.
(Подробно конструкторы и деструкторы описаны в главе 14.) Вот как выглядит деструктор для IntArray: public:
// конструкторы explicit IntArray (int sz = DefaultArraySize);
IntArray (int *array, int array_size);
IntArray (const IntArray &rhs);
// деструктор
IntArray() { delete[] ia; }
// ... private:
// ...
}; 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) class IntArray {

С++ для начинающих
45
Теперь нам нужно определить операции доступа к элементам массива IntArray. Мы хотим, чтобы обращение к элементам IntArray выглядело точно так же, как к элементам массива встроенного типа, с использованием оператора взятия индекса: int last_pos = array.size()-1; int temp = array[0]; array[0] = array[last_pos]; array[last_pos] = temp;
Для реализации доступа мы используем возможность перегрузки операций. Вот как выглядит функция, реализующая операцию взятия индекса:
}
Обычно для проектируемого класса перегружают операции присваивания, операцию сравнения на равенство, возможно, операции сравнения по величине и операции ввода/вывода. Как и перегруженных функций, перегруженных операторов, отличающихся типами операндов, может быть несколько. К примеру, можно создать несколько операций присваивания объекту значения другого объекта того же самого или иного типа. Конечно, эти объекты должны быть более или менее “похожи”. (Подробно о перегрузке операций мы расскажем в главе 15, а в разделе 3.15 приведем еще несколько примеров.)
Определения класса, различных относящихся к нему констант и, быть может, каких-то еще переменных и макросов по принятым соглашениям помещаются в заголовочный файл, имя которого совпадает с именем класса. Для класса IntArray мы должны создать заголовочный файл IntArray.h. Любая программа, в которой будет использоваться класс IntArray, должна включать этот заголовочный файл директивой препроцессора
#include
По тому же самому соглашению функции-члены класса, определенные вне его описания, помещаются в файл с именем класса и расширением, обозначающим исходный текст С++ программы. Мы будем использовать расширение .С (напомним, что в разных системах вы можете встретиться с разными расширениями исходных текстов С++ программ) и назовем наш файл IntArray.C.
Упражнение 2.5
Ключевой особенностью класса С++ является разделение интерфейса и реализации.
Интерфейс представляет собой набор операций (функций), выполняемых объектом; он определяет имя функции, возвращаемое значение и список параметров. Обычно пользователь не должен знать об объекте ничего, кроме его интерфейса. Реализация скрывает алгоритмы и данные, нужные объекту, и может меняться при развитии объекта,
IntArray array;
#include int& IntArray::operator[] (int index)
{ assert (index >= 0 && index < _size); return ia[index];

С++ для начинающих
46
никак не затрагивая интерфейс. Попробуйте определить интерфейсы для одного из следующих классов (выберите любой):
(a) матрица
(b) булевское значение
(c) паспортные данные человека
(d) дата
(e) указатель
(f) точка
Упражнение 2.6
Попробуйте определить набор конструкторов, необходимых для класса, выбранного вами в предыдущем упражнении. Нужен ли деструктор для вашего класса? Помните, что на самом деле конструктор не создает объект: память под объект отводится до начала работы данной функции, и конструктор только производит определенные действия по инициализации объекта. Аналогично деструктор уничтожает не сам объект, а только те дополнительные ресурсы, которые могли быть выделены в результате работы конструктора или других функций-членов класса.
Упражнение 2.7
В предыдущих упражнениях вы практически полностью определили интерфейс выбранного вами класса. Попробуйте теперь написать программу, использующую ваш класс. Удобно ли пользоваться вашим интерфейсом? Не хочется ли Вам пересмотреть спецификацию? Сможете ли вы сделать это и одновременно сохранить совместимость со старой версией?
2.4.
Объектно-ориентированный подход
Вспомним спецификацию нашего массива в предыдущем разделе. Мы говорили о том, что некоторым пользователям может понадобиться упорядоченный массив, в то время как большинство, скорее всего, удовлетворится и неупорядоченным. Если представить себе, что наш массив IntArray упорядочен, то реализация таких функций, как min(), max(), find()
, должна отличаться от их реализации для массива неупорядоченного большей эффективностью. Вместе с тем, для поддержания массива в упорядоченном состоянии все прочие функции должны быть сильно усложнены.
Мы выбрали наиболее общий случай – неупорядоченный массив. Но как же быть с теми немногочисленными пользователями, которым обязательно нужна функциональность массива упорядоченного? Мы должны специально для них создать другой вариант массива?
А вот и еще одна категория недовольных пользователей: их не удовлетворяют накладные расходы на проверку правильности индекса. Мы исходили из того, что корректность работы нашего класса превыше всего, и старались обезопасить себя от ошибочных ситуаций. Но возьмем, к примеру, разработчиков систем виртуальной реальности.
Трехмерные изображения должны строиться с максимально возможной скоростью, быть может, за счет точности.
Да, мы можем удовлетворить и тех и других, создав для каждой группы пользователей свой, немного модернизированный, вариант IntArray. Более того, его даже не слишком трудно сделать, поскольку мы старались создать хорошую реализацию и необходимые

С++ для начинающих
47
изменения затронут совсем небольшие участки кода. Итак, копируем исходный текст, вносим необходимые изменения в нужные места и получаем три класса: class IntSortedArray { ... };
Подобное решение имеет следующие недостатки:

нам необходимо сопровождать три копии кода, различающиеся весьма незначительно. Хорошо бы выделить общие участки кода. Кроме упрощения сопровождения, это позволит использовать их впоследствии, если мы захотим создать еще один вариант массива, например упорядоченный с проверкой границ индекса;

если понадобится какая-то общая функция для обработки всех наших массивов, то нам придется написать три копии, поскольку типы ее параметров будут различаться: void process_array (IntSortedArray&); хотя реализация этих функций может быть совершенно идентичной. Было бы лучше написать единственную функцию, которая могла бы работать не только со всеми нашими массивами, но и с теми их вариациями, какие мы, возможно, реализуем впоследствии.
Парадигма объектно-ориентированного программирования позволяет осуществить все эти пожелания. Механизм наследования обеспечивает пожелания из первого пункта. Если один класс является потомком другого (например, IntArrayRC потомок класса
IntArray
), то наследник имеет возможность пользоваться всеми данными и функциями- членами, определенными в классе-предке. То есть класс IntArrayRC может просто использовать всю основную функциональность, предоставляемую классом IntArray, и добавить только то, что нужно ему для обеспечения проверки границ индекса.
В С++ класс, свойства которого наследуются, называют также базовым классом, а класс- наследник – производным классом, или подклассом базового. Класс и подкласс имеют общий интерфейс, предоставляемый базовым классом (т.к. подкласс имеет все функции- члены базового класса). Значит, программу, использующую только функции из этого общего интерфейса, не должен интересовать фактический тип объекта, с которым она работает, – базового ли типа этот объект или производного. В этом смысле общий интерфейс скрывает специфичные для подкласса детали. Отношения между классами и подклассами называются иерархией наследования классов. Вот как может выглядеть реализация функции swap(), которая меняет местами два указанных элемента массива.
Первым параметром функции является ссылка на базовый класс IntArray:
// неупорядоченный массив без проверки границ индекса class IntArray { ... };
// неупорядоченный массив с проверкой границ индекса class IntArrayRC { ... };
// упорядоченный массив без проверки границ индекса void process_array (IntArray&); void process_array (IntArrayRC&);

С++ для начинающих
1   2   3   4   5   6   7   8   9   ...   93


написать администратору сайта