Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Объектные типы данных: структуры, объединения, классы (обзор)
Объектные типы данных - это агрегатные типы, полностью определяемые программистом, описание объектного типа должно содержать компоненты-данные, определяющие область возможных значений переменных этого типа, и описание операций, допустимых над переменными этого типа и компонентами-данными, составляющими переменную. Для сохранения совместимости с программами на Си синтаксис описания объектного типа в Си++ выбран подобным описанию структурного типа или типа объединения в Си. В сущности структуры и объединения в Си++ рассматриваются как варианты объектных типов. Имеются три варианта объектных типов: структура (struct), объединение (union) и класс (class), различающиеся возможностями доступа к компонентам типа. Структура - конструкция большинства языков программирования, позволяющая содержать в себе набор переменныхи функций. С++, а также многие другие языки поддерживающие модель объектно-ориентированного программирования позволяют использовать структуру. Структура очень похожа на классы и интерфейсы с некоторыми отличиями. Пример объявления: struct str_name { private int member_1; private float member_2; public string member_3; }
Объединение представляет собой структуру, члены которой расположены по одному и тому же адресу. Поэтому размер объединения равен размеру его наибольшего члена. В любой момент времени объединение хранит значение только одного из членов. Пример на С++: union Some { int i; double a; }; Это объединение хранит либо целое число (переменная i), либо число с плавающей точкой (переменная a). Поскольку объединение это вид структуры, то в Си мы будем обращаться к ней также как и к структуре т.е. через символ " -> ", при использовании указателя, или "." при использовании обычной переменной. Класс является важным понятием объектно-ориентированного подхода в программировании. Под классом подразумевается некая сущность, которая задает некоторое общее поведение для объектов. Таким образом, любой объект может принадлежать или не принадлежать определенному классу, то есть обладать или не обладать поведением, которое данный класс подразумевает. Класс определяет для объекта контракт, то есть правила, с помощью которых с объектом могут работать другие объекты (обычно это делается с помощью определения методов класса). Кроме того классы могут находиться друг с другом в различных отношениях, таких как, например, наследование. Фактически объектно-ориентированное программирование чаще всего сводится к созданию некоторого количества классов, описанию связей между этими классами и их свойств, и дальнейшей реализации полученных классов. В современных объектно-ориентированных языков программирования (в том числе в С++) создание класса сводится к написанию некоторой структуры, содержащей набор полей и методов (среди последних особую роль играют конструкторы, деструкторы, финализаторы). Практически класс может пониматься как некий шаблон, по которому создаются объекты — экземпляры данного класса. Экземпляры одного класса созданы по одному шаблону, поэтому имеют один и тот же набор полей и методов. Конструкторы и деструкторы При объявлении любой переменной в языке C++ ее, как правило, сразу же инициализируют, то есть присваивают данной переменной начальное значение. В языке C++ есть встроенная возможность использования специального метода, который будет автоматически вызываться при создании любого объекта нового типа. Такой метод называется конструктором. Конструктор определяет, как будет создаваться новый объект, когда это необходимо, может распределить под него память и инициализировать ее. Он может включать в себя код для распределения памяти, присваивание значений элементам, преобразование из одного типа в другой и многое другое. Конструкторы в языке C++ имеют имена, совпадающие с именем класса. Конструктор может быть определен пользователем, или компилятор сам сгенерирует конструктор по умолчанию. Конструктор может вызываться явно или неявно. Компилятор сам автоматически вызывает соответствующий конструктор там, где Вы определяете новый объект класса. Конструктор не возвращает никакое значение, и при описании конструктора не используется ключевое слово void. Функцией, обратной конструктору, является деструктор. Эта функция обычно вызывается при удалении объекта. Например, если при создании объекта для него динамически выделялась память, то при удалении объекта ее нужно освободить. Локальные объекты удаляются тогда, когда они выходят из области видимости. Глобальные объекты удаляются при завершении программы. В языке C++ деструкторы имеют имена: « ~имя_класса». Как и конструктор, деструктор не возвращает никакого значения, но, в отличие от конструктора, не может быть вызван явно. Конструктор и деструктор не могут быть описаны в закрытой части класса. Конструкторы с параметрами и перегрузка конструкторов Конструкторы могут иметь параметры. Д ля этого просто нужно добавить необходимые параметры в объявление и определение конструктора. Затем, при создании объекта, задать параметры в качестве аргументов. class _3d { double x, y, z; public: _3d (); _3d (double initX, double initY, double initZ); ... }; _3d:: _3d(double initX, double initY, double initZ) // конструктор класса _3d с параметрами { x = initX; y = initY; z = initZ; cout < < 'Работа конструктора _3d \n'; } main() { _3d A; // создается объект A и происходит // инициализация его элементов // A.x = A.y = A.z = 0; A.set (3, 4, 0); // Теперь A.x = 3.0, A.y = 4.0, // A.z = 0.0 _3d B (3, 4, 0); // создается объект B и происходит // инициализация его элементов // B.x = 3.0, B.y = 4.0, B.z = 0.0 }
Такой способ вызова конструктора является сокращенной формой записи выражения _3d B = _3d (3, 4, 0); В отличие от конструктора, деструктор не может иметь параметров. Это естественно, поскольку отсутствует механизм передачи параметров удаляемому объекту. В этом примере сознательно вместе с новым объектом B оставлен объект A, чтобы продемонстрировать различные способы создания объектов. Каждому способу объявления объекта класса должна соответствовать своя версия конструктора класса. Если это не будет обеспечено, то при компиляции программы обнаружится ошибка. На этом примере легко понять, чем может быть вызвана необходимость перегрузки конструкторов. Именно перегрузки, поскольку речь здесь идет о функциях, имеющих одинаковые имена, но различные списки параме тров. Г лавный смысл перегрузки конструкторов состоит в том, чтобы предоставить программисту наиболее подходящий метод инициализации объекта. В нашем примере представлен наиболее распространенный вариант перегрузки конструкторов, т.е. конструктор без параметров и конструктор с параметрами. Как правило, в программе используются об а эти вида, поскольку конструктор с параметрами более удобен при работе с одиночными объектами, но не может использоваться при инициализации объектов-элементов динамического массива (для статического массива может). Хотя конструктор можно перегружать сколь угодно раз, все же не стоит этим злоупотреблять. Конструктор стоит перегружать лишь для наиболее часто встречающихся ситуаций. Производные классы Наследование, являясь одной из наиболее интересных составляющих концепции объектно-ориентированного программирования, представляет собой механизм, посредством которого одни классы могут порождать другие, «делегируя» последним часть своих свойств и методов. В результате наследование позволяет строить иерархию классов, переходя от более общих к более специальным. Когда один класс наследуется другим, класс, который наследуется, называется базовым классом. Наследующий класс называют производным классом. Новый класс строится на базе уже существующего с помощью конструкции следующего вида. class Parent {....}; class Child: [модификатор наследования] Parent {....};
При определении класса-потомка за его именем следует разделитель-двоеточие («: »), затем - необязательный модификатор наследования и имя родительского класса. Модификатор наследования определяет видимость наследуемых переменных и методов для пользователей и возможных потом ков самого класса-потомка. Другим и словами, он определяет, какие права доступа к переменным и методам класса-родителя будут переданы классу-потомку. При реализации наследования область видимости принадлежащих классу данных и методов можно определять выбором ключевого слова private (собственный), public (общедоступный) или protected (защищенный), которые могут произвольно чередоваться внутри описания класса. С двумя первыми модификаторами доступа мы уже знакомы - private описывает закрытые члены класса, доступ к которым имеют только методы-члены этого класса, public предназначен для описания общедоступных элементов, доступ к которым возможен из любого места в программе. Особый интерес представляют элементы, обладающие модификатором доступа protected. Модификатор protected используется тогда, когда необходимо, чтобы некоторые ч лены базового класса оставались закрытыми, но были бы доступны для производного класса. Модификатор protected эквивалентен private с единственным исключением: защищенные ч лены базового класса доступны для ч ленов всех производных классов этого базового класса. То, как изменяется доступ к элементам базового класса из методов производного класса в зависимости от значения модификаторов наследования, можно видеть в следующей таблице. Встраиваемые функции Рассмотрим пример: struct _3d { double x, y, z; double mod() {return sqrt (x*x + y*y +z*z); } double projection(_3d r) {return (x*r.x + y*r.y + z*r.z) / mod(); } _3d operator + (_3d b); }; _3d _3d:: operator + (_3d b) { _3d c; c.x = x + b.x; c.y = y + b.y; c.z = z + b.z; return c; }
Обратим внимание на то, в каком месте описывается тело того или иного метода. Методы mod() и projection() описаны вместе со своими телами непосредственно внутри структуры. Однако можно поступить иначе: поместить прототипы метода внутрь структуры, а определение тела функции – вне структуры, как мы поступили с оператором « +». Первый способ используется для простых и коротких методов, которые в дальнейшем не предполагается изменять. Так поступают отчасти из-за того, что описания классов помещают обычно в файлы заголовков, включаемые затем в прикладную программу с помощью директивы #include. Кроме того, при этом способе машинные инструкции, генерируемые компилятором при обращении к этим функциям, непосредственно вставляются в оттранслированный текст. Это снижает затраты на их исполнение, поскольку выполнение таких методов не связано с вызовом функций и механизмом возврата, увеличивая в свою очередь размер исполняемого кода (то есть такие методы становятся встраиваемыми). Второй способ предпочтительнее для сложных методов. Объявленные таким об разом функции автоматически заменяются компилятором на вызовы подпрограмм, хотя при добавлении ключевого слова inline могут подставляться в текст, как и в первом случае. Кроме представленного выше способа создания встраиваемых функций (записать тело метода непосредственно в структуре), есть еще один способ: вставить спецификатор inline перед определением метода. inline _3d _3d:: operator + (_3d b) { _3d c; c.x = x + b.x; c.y = y + b.y; c.z = z + b.z; return c; } Теперь оператор « +» станет встраиваемой функцией. Встраиваемые функции действуют почти так же, как и макроопределения с параметрами, однако имеют ряд преимуществ. Во-первых, inline методы обеспечивают более стройный способ встраивания в программу короткой функции. Во-вторых, компилятор C++ гораздо лучше работает со встраиваемыми функциями, чем с макроопределениями. Важно понимать, что inline не является командой для компилятора, это, скорее, просьба сделать метод встраиваемым. Если по каким-то причинам (например, при наличии в теле функции операторов цикла, switch или goto) компилятор не выполнит запрос, то функция будет откомпилирована как не встраиваемая. Присваивание объектов Один объект можно присвоить другому, если оба объекта имеют одинаковый тип. По умолчанию, когда объект A присваивается объекту B, осуществляется побитовое копирование всех элементов-данных A в соответствующие элементы-данные B. Если объекты имеют разные типы, то компилятор выдаст сообщение об ошибке. Одинаковыми должны быть имена типов, а не их физическое содержание. Особенно внимательным нужно быть при присваивании объектов, в описании типа которых содержатся указатели. Чтобы избежать недоразумений, используют перегруженный оператор присваивания, в котором явным образом описывается (т.е. контролируется) процесс присваивания элементов-данных одного объекта соответствующим элементам-данным другого объекта. class Pair { int a, *b; public: Pair operator = (Pair p) { a = p.a; *b = *(p.b); return *this; } ... }; пример с трехмерным вектором; class _3d { double x, y, z; public: _3d (); _3d (double initX, double initY, double initZ); double mod() {return sqrt (x*x + y*y +z*z); } double projection(_3d r) {return (x*r.x + y*r.y + z*r.z) / mod(); } _3d operator + (_3d b); _3d operator = (_3d b); }; _3d _3d:: operator = (_3d b) { x = b.x; y = b.y; z = b.z; return *this; }
Каждая функция представлена в единственном экземпляре и в момент вызова получает один скрытый параметр - указатель на экземпляр переменной, для которого она вызвана. Этот указатель имеет имя this. Если используемая переменная не описана внутри функции, не является глобальной, то считается, что она является членом структуры и принадлежит рабочей переменной this. Поэтому при реализации функций операторов мы опускали путь доступа к полям структуры, для которой этот оператор будет вызываться. В качестве аргументов функций-операторов выступают операнды, а возвращаемое значение - результат применения оператора. В частности, для оператора « =» это необходимо, чтобы обеспечить возможность последовательного присваивания (a=b=c). Бинарные операторы имеют один аргумент - второй передается через указатель this. Унарные, соответственно, один - this.
Популярное:
|
Последнее изменение этой страницы: 2016-07-14; Просмотров: 1108; Нарушение авторского права страницы