Функции классаобъекта


Содержание

Функции Класса/Объекта

Введение

Эти функции позволяют получать информацию о классах и инстанциировать (создавать экземпляры) объекты. Вы можете получить имя класса, к которому принадлежит объект, а также свойства и методы члена класса. Используя эти функции, вы можете определить не только членство объекта в классе, но и наследование (т.е. — какой класс расширяется классом объекта).

Примеры

В первом примере мы сначала определяем базовый класс и расширение этого класса. Базовый класс описывает овощ/vegetable в целом: съедобен ли он и какого цвета. Подкласс Spinach добавляет метод для приготовления его и метод для определения, готов ли он.

Пример 1. >

Затем мы инстанциируем 2 объекта этих классов и печатаем информацию о них, включая их иерархию классов. Мы также определяем некоторые вспомогательные функции, в основном для нормальной печати переменных.

Пример 2. test_script.php

В этом примере важно отметить, что объект $leafy является экземпляром класса Spinach, который является подклассом от Vegetable, следовательно, последняя часть вышеприведённого скрипта выведет:

Содержание call_user_method_array — вызывает пользовательский метод, заданный с массивом параметров [не рекомендуется применять] call_user_method — вызывает пользовательский метод в специфическом объекте [не рекомендуется применять] class_exists — проверяет, определён ли данный класс get_class_methods — возвращает массив имён методов класса get_class_vars — возвращает массив свойств по умолчанию данного класса get_class — возвращает имя класса объекта get_declared_classes — возвращает массив с именем определённого класса get_object_vars — возвращает ассоциативный массив свойств объекта get_parent_class — запрашивает имя родительского класса для объекта или класса is_a — возвращает TRUE, если это объект данного класса или если он имеет этот класс как один из его родительских классов is_subclass_of — возвращает TRUE, если данный объект имеет этот класс как один из его родительских классов method_exists — проверяет, существует ли метод класса Оглавление

Электронная библиотека

Классом называется набор типизированных данных и функций, объединенных в новый тип данных. В отличие от структуры, типизированные данные и функции имеют различные уровни доступа. Переменная, объявленная в программе, имеющая определенный классом тип данных, называется объектом этого класса. Таким образом, класс – это структура, состоящая из некоторых переменных (и констант) и функций, а объект – это область памяти, которую занимает структура при ее объявлении. Для создания объектов предусмотрены принадлежащие классу функции, заполняющие поля объектов. Эти функции называются конструкторами. При удалении объектов вызываются функции, принадлежащие классу и предназначенные для освобождения памяти, — деструкторы.

2.1. Класс как обобщение структуры

Выше мы дали определение класса, как конструкции, состоящей из полей и функций. В частности, этому определению удовлетворяет структура. В действительности, структура в Си++ реализована как класс, все поля которой, по умолчанию, общедоступны в том смысле, что доступ к ним осуществляется через имена имя_структуры.поле, имя_структуры.функция(аргументы), или указатели указатель->поле, указатель->функция(аргументы).

Простейшим образом класс можно определить с помощью конструкции:

где первая пара фигурных скобок обозначает альтернативный выбор одного из ключевых слов, а вторая пара включает в себя поля и имена функций, которые принадлежат классу. Такие функции называются составными функциями класса. Заключенный в фигурные скобки список компонент называется телом класса. Определение тела класса заканчивается точкой с запятой.

Пример 1. Будем использовать ключевое слово struct для определения класса двумерного вектора, для которого определены функции ввода и вывода данных, составляющих объект.

double x, y; // Координаты вектора

// Функция вывода на экран координат вектора

Классы, объекты, методы

Java является объектно-ориентированным языком, поэтому такие понятия как «класс» и «объект» играют в нем ключевую роль. Любую программу на Java можно представить как набор взаимодействующих между собой объектов.

Шаблоном или описанием объекта является класс (class), а объект представляет экземпляр класса. Можно провести следующую аналогию. У нас у всех есть некоторое представление о машине — наличие двигателя, шасси, кузова и т.д. Есть некоторый шаблон auto — этот шаблон можно назвать классом. Реально же существующий автомобиль auto_solaris (фактически экземпляр данного класса) является объектом этого класса.

Определение класса

Класс определяется с помощью ключевого слова сlass. Вся функциональность класса представлена его членами — полями (полями называются переменные класса) и методами. Например, класс Book мог бы иметь следующее описание :

Таким образом, в классе Book определены три переменных и один метод Info, который выводит значения этих переменных.

Кроме обычных методов в классах используются также и специальные методы, которые называются конструкторами. Конструкторы нужны для создания нового объекта данного класса и, как правило, выполняют начальную инициализацию объекта. Название конструктора должно совпадать с названием класса :

Класс Book имеет два конструктора. Первый конструктор без параметров присваивает «неопределенные» начальные значения полям. Второй конструктор присваивает полям класса значения, которые передаются через его параметры.

Так как имена параметров и имена полей класса в данном случае у нас совпадают — name, author, year, то мы используем ключевое слово this. Это ключевое слово представляет ссылку на текущий объект. Поэтому в выражении this.name = name; первая часть this.name означает, что name — это поле текущего класса, а не название параметра name. Если бы у нас параметры и поля назывались по-разному, то использовать слово this было бы необязательно.

Мы можем определить несколько конструкторов для установки разного количества параметров и затем вызывать один конструктор класса из другого :

Вызов конструктора класса с двумя параметрами производится с помощью ключевого слова this, после которого в скобках указывается список параметров.

Создание объекта

Чтобы непосредственно использовать класс в программе, надо создать его объект. Процесс создания объекта двухступенчатый: вначале объявляется переменная данного класса, а затем с помощью ключевого слова new и конструктора непосредственно создается объект, на который и будет указывать объявленная переменная :

После объявления переменной Book b; эта переменная еще не ссылается ни на какой объект и имеет значение null. Затем создаем непосредственно объект класса Book с помощью одного из конструкторов и ключевого слова new.

Инициализаторы

Кроме конструктора начальную инициализацию полей объекта можно проводить с помощью инициализатора объекта. Так можно заменить конструктор без параметров следующим блоком :

Методы класса

Метод класса в объектно-ориентированном программировании — это функция или процедура, принадлежащая какому-либо классу или объекту.

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

Различают простые методы и статические методы :

  • простые методы имеют доступ к данным объекта конкретного экземпляра (данного класса);
  • статические методы не имеют доступа к данным объекта, и для их использования не нужно создавать экземпляры (данного класса).

Методы предоставляют интерфейс, при помощи которого осуществляется доступ к данным объекта некоторого класса, тем самым, обеспечивая инкапсуляцию данных.

Кроме имени и тела (кода) у метода есть ряд других характеристик:

  • набор модификаторов;
  • тип возвращаемого значения;
  • набор аргументов (параметров).

Модификаторы метода — public, protected, private

Модификаторы метода определяют уровень доступа. В зависимости от того, какой уровень доступа предоставляет тот или иной метод, выделяют:

  • public : открытый — общий интерфейс для всех пользователей данного класса;
  • protected : защищённый — внутренний интерфейс для всех наследников данного класса;
  • private : закрытый — интерфейс, доступный только изнутри данного класса.

Такое разделение интерфейсов позволяет сохранять неизменным открытый интерфейс, но изменять внутреннюю реализацию.

Для того чтобы создать статический метод, перед его именем надо указать модификатор static. Если этого не сделать, то метод можно будет вызывать только в приложении к конкретному объекту данного класса (будет нестатическим).

Класс может включать метод main, который должен иметь уровень доступа public; к нему обращается виртуальная машина Java, не являющаяся частью какого-либо пакета.

Абстрактный класс, abstract class

Абстрактный класс в объектно-ориентированном программировании — базовый класс, который не предполагает создания экземпляров. Абстрактные классы реализуют на практике один из принципов ООП — полиморфизм. Абстрактный класс может содержать (и не содержать) абстрактные методы. Абстрактный метод не реализуется для класса, в котором описан, однако должен быть реализован для его неабстрактных потомков. Пример абстрактного класса, включающего две абстрактные функции.

Переопределение метода, Override

В реализации ReleasePrice, наследующего свойства класса Price, «реализуем» абстрактные методы и «переопределяем» метод с использованием аннотации @Override :

Теперь, если в родительском класса Price метод bonusPrice будет удален или переименован, то среда разработки должна выдать соответствующее сообщение. Компилятор также выдаст сообщение об ошибке.

Перегрузка методов, overload

Совокупность имени метода и набора формальных параметров называется сигнатурой метода. Java позволяет создавать несколько методов с одинаковыми именами, но разными сигнатурами. Создание метода с тем же именем, но с другим набором параметров называется перегрузкой. Какой из перегруженных методов должен выполняться при вызове, Java определяет на основе фактических параметров, передаваемых методу.

Пример класса Test с тремя перегруженными методами test :

Пример использования класса Test:

Java рекурсия

Рекурсией называется метод (функция), которая внутри своего тела вызывает сама себя.

Рассмотрим пример рекурсивного метода вычисления факториала. Для того чтобы вычислить n!, достаточно знать и перемножить между собой (n-1)! и n. Создадим метод, реализующий описанный способ.

Указанный рекурсивный метод вычисляет факториал натурального числа.

Рассмотрим пример, вычисляющий через рекурсию n-ое число Фибоначчи. Напомним, как выглядят первые элементы этого ряда: 1 1 2 3 5 8 13 …

Суперкласс Object

В Java есть специальный суперкласс Object и все классы являются его подклассами. Поэтому ссылочная переменная класса Object может ссылаться на объект любого другого класса. Так как массивы являются тоже классами, то переменная класса Object может ссылаться и на любой массив.

У класса Object есть несколько важных методов:

Метод Описание
Object clone() Функция создания нового объекта, не отличающий от клонируемого
boolean equals(Object object) Функция определения равенства текущего объекта другому
void finalize() Процедура завершения работы объекта; вызывается перед удалением неиспользуемого объекта
Class getClass() Функция определения класса объекта во время выполнения
int hashCode() Функция получения хэш-кода объекта
void notify() Процедура возобновления выполнения потока, который ожидает вызывающего объекта
void notifyAll() Процедура возобновления выполнения всех потоков, которые ожидают вызывающего объекта
String toString() Функция возвращает строку описания объекта
void wait() Ожидание другого потока выполнения
void wait(long ms) Ожидание другого потока выполнения
void wait(long ms, int nano) Ожидание другого потока выполнения

Методы getClass(), notify(), notifyAll(), wait() являются «финальными» (final) и их нельзя переопределять.

Проверка принадлежности класса instanceof

Для проверки принадлежности класса какому-либо объекту необходимо использовать ключевого слова instanceof. Иногда требуется проверить, к какому классу принадлежит объект. Это можно сделать при помощи ключевого слова instanceof. Это логический оператор, и выражение foo instanceof Foo истинно, если объект foo принадлежит классу Foo или его наследнику, или реализует интерфейс Foo (или, в общем виде, наследует класс, который реализует интерфейс, который наследует Foo).

Пример с рыбками. Допустим имеется родительский класс Fish и у него есть унаследованные подклассы SaltwaterFish и FreshwaterFish. Необходимо протестировать, относится ли заданный объект к классу или подклассу по имени

Данная проверка удобна во многих случаях. Иногда приходится проверять принадлежность класса при помощи instanceof, чтобы можно было бы разделить логику кода:

Импорт класса import

Оператор import сообщает компилятору Java, где найти классы, на которые ссылается код. Любой сложный объект использует другие объекты для выполнения тех или иных функций, и оператор импорта позволяет сообщить о них компилятору Java. Оператор импорта обычно выглядит так:

За ключевым словом следуют класс, который нужно импортировать. Имя класса должно быть полным, то есть включать свой пакет. Чтобы импортировать все классы из пакета, после имени пакета можно поместить ‘.*;’

IDE Eclipse упрощает импорт. При написании кода в редакторе Eclipse можно ввести имя класса, а затем нажать Ctrl+Shift+O. Eclipse определяет, какие классы нужно импортировать, и добавляет их автоматически. Если Eclipse находит два класса с одним и тем же именем, он выводит диалоговое окно с запросом, какой именно класс вы хотите добавить.

Статический импорт

Существует ещё статический импорт, применяемый для импорта статических членов класса или интерфейса. Например, есть статические методы Math.pow(), Math.sqrt(). Для вычислений сложных формул с использованием математических методов, код становится перегружен. К примеру, вычислим гипотенузу.

В данном случае без указания класса не обойтись, так как методы статические. Чтобы не набирать имена классов, их можно импортировать следующим образом:

После импорта уже нет необходимости указывать имя класса.

Второй допустимый вариант, позволяющий сделать видимыми все статические методы класса:

В этом случае не нужно импортировать отдельные методы. Но данный подход в Android не рекомендуется, так как требует больше памяти.

Функции класса/объекта

В языке C++ можно разделять объявление и определение функций в том числе по отношению к функциям, которые создаются в классах. Для этого используется выражение имя_класса::имя_функции(параметры) < тело_функции>.

Например, возьмем следующий класс Person:

Разобъем класс, вынеся реализацию его методов во вне:

Теперь функции класса Person (в данном случае конструктор и функция move) в самом классе имеют только объявления. Реализации функций размещены вне класса Person.

Классы и объекты Classes and objects

Классы являются основным типом в языке C#. Classes are the most fundamental of C#’s types. Класс представляет собой структуру данных, которая объединяет в себе значения (поля) и действия (методы и другие функции-члены). A class is a data structure that combines state (fields) and actions (methods and other function members) in a single unit. Класс предоставляет определение для динамически создаваемых экземпляров класса, которые также именуются объектами. A class provides a definition for dynamically created instances of the class, also known as objects. Классы поддерживают механизмы наследования и полиморфизма, которые позволяют создавать производные классы, расширяющие и уточняющие определения базовых классов. Classes support inheritance and polymorphism, mechanisms whereby derived classes can extend and specialize base classes.

Новые классы создаются с помощью объявлений классов. New classes are created using class declarations. Объявление класса начинается с заголовка, в котором указаны атрибуты и модификаторы класса, имя класса, базовый класс (если есть) и интерфейсы, реализуемые этим классом. A class declaration starts with a header that specifies the attributes and modifiers of the class, the name of the class, the base class (if given), and the interfaces implemented by the class. За заголовком между разделителями < и >следует тело класса, в котором последовательно объявляются все члены класса. The header is followed by the class body, which consists of a list of member declarations written between the delimiters < and >.

Вот простой пример объявления класса с именем Point : The following is a declaration of a simple class named Point :

Экземпляры классов создаются с помощью оператора new , который выделяет память для нового экземпляра, вызывает конструктор для инициализации этого экземпляра и возвращает ссылку на экземпляр. Instances of classes are created using the new operator, which allocates memory for a new instance, invokes a constructor to initialize the instance, and returns a reference to the instance. Следующие инструкции создают два объекта Point и сохраняют ссылки на них в две переменные: The following statements create two Point objects and store references to those objects in two variables:

Занимаемая объектом память автоматически освобождается, когда объект становится недоступен. The memory occupied by an object is automatically reclaimed when the object is no longer reachable. В C# нет ни необходимости, ни возможности освобождать память объектов явным образом. It is neither necessary nor possible to explicitly deallocate objects in C#.

Участники Members

Члены класса могут быть статическими членами или членами экземпляра. The members of a class are either static members or instance members. Статические члены принадлежат классу в целом, а члены экземпляра принадлежат конкретным объектам (экземплярам классов). Static members belong to classes, and instance members belong to objects (instances of classes).

Ниже перечислены виды членов, которые могут содержаться в классе. The following provides an overview of the kinds of members a class can contain.

  • Константы Constants
    • Константные значения, связанные с классом. Constant values associated with the class
  • Поля Fields
    • Переменные класса. Variables of the class
  • Методы Methods
    • Вычисления и действия, которые может выполнять класс. Computations and actions that can be performed by the class
  • Свойства Properties
    • Действия, связанные с чтением и записью именованных свойств класса. Actions associated with reading and writing named properties of the class
  • Индексаторы Indexers
    • Действия, реализующие индексирование экземпляров класса, чтобы обращаться к ним как к массиву. Actions associated with indexing instances of the class like an array
  • События Events
    • Уведомления, которые могут быть созданы этим классом. Notifications that can be generated by the class
  • Операторы Operators
    • Поддерживаемые классом операторы преобразования и выражения. Conversions and expression operators supported by the class
  • Конструкторы Constructors
    • Действия, необходимые для инициализации экземпляров класса или класса в целом. Actions required to initialize instances of the class or the class itself
  • Методы завершения Finalizers
    • Действия, выполняемые перед окончательным удалением экземпляров класса. Actions to perform before instances of the class are permanently discarded
  • Типы Types
    • Вложенные типы, объявленные в классе. Nested types declared by the class

Специальные возможности Accessibility

Каждый член класса имеет определенный уровень доступности. Он определяет, из какой области программы можно обращаться к этому члену. Each member of a class has an associated accessibility, which controls the regions of program text that are able to access the member. Существует шесть уровней доступности. There are six possible forms of accessibility. Они кратко описаны ниже. These are summarized below.

  • public
    • Доступ не ограничен. Access not limited
  • protected
    • Доступ возможен из этого класса и из классов, унаследованных от него. Access limited to this class or classes derived from this class
  • internal
    • Доступ ограничен только текущей сборкой (.exe, .dll и т. д.). Access limited to the current assembly (.exe, .dll, etc.)
  • protected internal
    • Доступ ограничен содержащим классом, классами, которые являются производными от содержащего класса, либо классами в той же сборке Access limited to the containing class, classes derived from the containing class, or classes within the same assembly
  • private
    • Доступ возможен только из этого класса. Access limited to this class
  • private protected
    • Доступ ограничен содержащим классом или классами, которые являются производными от содержащего типа в той же сборке Access limited to the containing class or classes derived from the containing type within the same assembly

Параметры типа Type parameters

Определение класса может задать набор параметров типа. Список имен параметров типа указывается в угловых скобках после имени класса. A class definition may specify a set of type parameters by following the class name with angle brackets enclosing a list of type parameter names. Параметры типа можно использовать в теле класса в определениях, описывающих члены класса. The type parameters can then be used in the body of the class declarations to define the members of the class. В следующем примере для класса Pair заданы параметры типа TFirst и TSecond : In the following example, the type parameters of Pair are TFirst and TSecond :

Тип класса, для которого объявлены параметры типа, называется универсальным типом класса. A class type that is declared to take type parameters is called a generic class type. Типы структуры, интерфейса и делегата также могут быть универсальными. Struct, interface and delegate types can also be generic. Если вы используете универсальный класс, необходимо указать аргумент типа для каждого параметра типа, вот так: When the generic class is used, type arguments must be provided for each of the type parameters:

Универсальный тип, для которого указаны аргументы типа, как Pair в примере выше, называется сконструированным типом. A generic type with type arguments provided, like Pair above, is called a constructed type.

базовых классов; Base classes

В объявлении класса можно указать базовый класс, включив имя базового класса после имени класса и параметров типа, и отделив его двоеточием. A class declaration may specify a base class by following the class name and type parameters with a colon and the name of the base class. Если спецификация базового класса не указана, класс наследуется от типа object . Omitting a base class specification is the same as deriving from type object . В следующем примере Point3D имеет базовый класс Point , а Point — базовый класс object : In the following example, the base class of Point3D is Point , and the base class of Point is object :


Класс наследует члены базового класса. A class inherits the members of its base class. Наследование означает, что класс неявно содержит все члены своего базового класса, за исключением конструкторов экземпляра, статических конструкторов и методов завершения базового класса. Inheritance means that a class implicitly contains all members of its base class, except for the instance and static constructors, and the finalizers of the base class. Производный класс может дополнить наследуемые элементы новыми элементами, но он не может удалить определение для наследуемого члена. A derived class can add new members to those it inherits, but it cannot remove the definition of an inherited member. В предыдущем примере Point3D наследует поля x и y из Point , и каждый экземпляр Point3D содержит три поля: x , y и z . In the previous example, Point3D inherits the x and y fields from Point , and every Point3D instance contains three fields, x , y , and z .

Используется неявное преобразование из типа класса к любому из типов соответствующего базового класса. An implicit conversion exists from a class type to any of its base class types. Это означает, что переменная типа класса может ссылаться как на экземпляр этого класса, так и на экземпляры любого производного класса. Therefore, a variable of a class type can reference an instance of that class or an instance of any derived class. Например, если мы используем описанные выше объявления классов, то переменная типа Point может ссылаться на Point или Point3D : For example, given the previous class declarations, a variable of type Point can reference either a Point or a Point3D :

Поля Fields

Поле является переменной, связанной с определенным классом или экземпляром класса. A field is a variable that is associated with a class or with an instance of a class.

Поле, объявленное с модификатором static, является статическим. A field declared with the static modifier defines a static field. Статическое поле определяет строго одно место хранения. A static field identifies exactly one storage location. Независимо от того, сколько будет создано экземпляров этого класса, существует только одна копия статического поля. No matter how many instances of a class are created, there is only ever one copy of a static field.

Поле, объявленное без модификатора static, является полем экземпляра. A field declared without the static modifier defines an instance field. Каждый экземпляр класса содержит отдельные копии всех полей экземпляра, определенных для этого класса. Every instance of a class contains a separate copy of all the instance fields of that class.

В следующем примере каждый экземпляр класса Color содержит отдельную копию полей экземпляра r , g и b , но для каждого из статических полей Black , White , Red , Green и Blue существует только одна копия: In the following example, each instance of the Color class has a separate copy of the r , g , and b instance fields, but there is only one copy of the Black , White , Red , Green , and Blue static fields:

Как показано в предыдущем примере, можно объявить поля только для чтения, используя модификатор readonly . As shown in the previous example, read-only fields may be declared with a readonly modifier. Присвоение значения полю readonly может происходить только при объявлении этого поля или в конструкторе этого класса. Assignment to a readonly field can only occur as part of the field’s declaration or in a constructor in the same class.

Методы Methods

Метод — это член, реализующий вычисление или действие, которое может выполнять объект или класс. A method is a member that implements a computation or action that can be performed by an object or class. Доступ к статическим методам осуществляется через класс. Static methods are accessed through the class. Доступ к методам экземпляра осуществляется через экземпляр класса. Instance methods are accessed through instances of the class.

Для метода можно определить список параметров, которые представляют переданные методу значения или ссылки на переменные, а также возвращаемый тип, который задает тип значения, вычисляемого и возвращаемого методом. Methods may have a list of parameters, which represent values or variable references passed to the method, and a return type, which specifies the type of the value computed and returned by the method. Если метод не возвращает значений, для него устанавливается возвращаемый тип void . A method’s return type is void if it does not return a value.

Как и типы, методы могут иметь набор параметров типа, для которых при вызове метода необходимо указывать аргументы типа. Like types, methods may also have a set of type parameters, for which type arguments must be specified when the method is called. В отличие от типов, аргументы типа зачастую могут выводиться из аргументов вызова метода, и тогда их не обязательно задавать явным образом. Unlike types, the type arguments can often be inferred from the arguments of a method call and need not be explicitly given.

Сигнатура метода должна быть уникальной в пределах класса, в котором объявлен этот метод. The signature of a method must be unique in the class in which the method is declared. Сигнатура метода включает имя метода, количество параметров типа, а также количество, модификаторы и типы параметров метода. The signature of a method consists of the name of the method, the number of type parameters and the number, modifiers, and types of its parameters. Сигнатура метода не включает возвращаемый тип. The signature of a method does not include the return type.

Параметры Parameters

Параметры позволяют передать в метод значения или ссылки на переменные. Parameters are used to pass values or variable references to methods. Фактические значения параметрам метода присваиваются на основе аргументов, заданных при вызове метода. The parameters of a method get their actual values from the arguments that are specified when the method is invoked. Существует четыре типа параметров: параметры значения, ссылочные параметры, параметры вывода и массивы параметров. There are four kinds of parameters: value parameters, reference parameters, output parameters, and parameter arrays.

Параметр значения используется для передачи входных аргументов. A value parameter is used for passing input arguments. Параметр значения сопоставляется с локальной переменной, которая получит начальное значение из значения аргумента, переданного в этом параметре. A value parameter corresponds to a local variable that gets its initial value from the argument that was passed for the parameter. Изменения параметра значения не влияют на аргумент, переданный для этого параметра. Modifications to a value parameter do not affect the argument that was passed for the parameter.

Параметры значения можно сделать необязательными, указав для них значения по умолчанию. Тогда соответствующие аргументы можно не указывать. Value parameters can be optional, by specifying a default value so that corresponding arguments can be omitted.

Ссылочный параметр используется для передачи аргументов по ссылке. A reference parameter is used for passing arguments by reference. Аргумент, передаваемый в качестве ссылочного параметра, должен представлять собой переменную с определенным значением. При выполнении метода ссылочный параметр указывает на то же место хранения, в котором размещена переменная аргумента. The argument passed for a reference parameter must be a variable with a definite value, and during execution of the method, the reference parameter represents the same storage location as the argument variable. Чтобы объявить ссылочный параметр, используйте модификатор ref . A reference parameter is declared with the ref modifier. Следующий пример кода демонстрирует использование параметров ref . The following example shows the use of ref parameters.

Параметр вывода используется для передачи аргументов по ссылке. An output parameter is used for passing arguments by reference. Он похож на ссылочный параметр, однако не требует явно присваивать значение аргумента, предоставляемого вызывающим объектом. It’s similar to a reference parameter, except that it doesn’t require that you explicitly assign a value to the caller-provided argument. Чтобы объявить параметр вывода, используйте модификатор out . An output parameter is declared with the out modifier. В следующем примере показано использование параметров out с помощью синтаксиса, появившегося в C# 7. The following example shows the use of out parameters using the syntax introduced in C# 7.

Массив параметров позволяет передавать в метод переменное число аргументов. A parameter array permits a variable number of arguments to be passed to a method. Чтобы объявить массив параметров, используйте модификатор params . A parameter array is declared with the params modifier. Массив параметров может быть только последним параметром в методе. Для него можно использовать только тип одномерного массива. Only the last parameter of a method can be a parameter array, and the type of a parameter array must be a single-dimensional array type. В качестве примера правильного использования массива параметров можно назвать методы Write и WriteLine, реализованные в классе System.Console. The Write and WriteLine methods of the System.Console class are good examples of parameter array usage. Ниже представлены объявления этих методов. They are declared as follows.

Внутри метода массив параметров полностью идентичен обычному параметру типа массив. Within a method that uses a parameter array, the parameter array behaves exactly like a regular parameter of an array type. Но зато при вызове метода, использующего массив параметров, ему можно передать либо один аргумент типа массив, либо любое количество аргументов типа элемент для массива параметров. However, in an invocation of a method with a parameter array, it is possible to pass either a single argument of the parameter array type or any number of arguments of the element type of the parameter array. В последнем случае экземпляр массива автоматически создается и инициализируется с заданными аргументами. In the latter case, an array instance is automatically created and initialized with the given arguments. Код из этого примера. This example

. эквивалентен следующей конструкции: is equivalent to writing the following.

Тело метода и локальные переменные Method body and local variables

Тело метода содержит инструкции, которые будут выполнены при вызове метода. A method’s body specifies the statements to execute when the method is invoked.

В теле метода можно объявлять переменные, относящиеся к выполнению этого метода. A method body can declare variables that are specific to the invocation of the method. Такие переменные называются локальными переменными. Such variables are called local variables. В объявлении локальной переменной нужно указать имя типа и имя переменной. Также можно задать ее начальное значение. A local variable declaration specifies a type name, a variable name, and possibly an initial value. Следующий пример кода объявляет локальную переменную i с нулевым начальным значением, и еще одну локальную переменную j без начального значения. The following example declares a local variable i with an initial value of zero and a local variable j with no initial value.

C# требует, чтобы локальной переменной было явно присвоено значение, прежде чем можно будет получить это значение. C# requires a local variable to be definitely assigned before its value can be obtained. Например, если в предложенное выше объявление i не включить начальное значение, компилятор сообщит об ошибке при последующем использовании i , поскольку для i нет явно присвоенного значения. For example, if the declaration of the previous i did not include an initial value, the compiler would report an error for the subsequent usages of i because i would not be definitely assigned at those points in the program.

Метод может использовать инструкцию return , чтобы вернуть управление вызывающему объекту. A method can use return statements to return control to its caller. Если метод возвращает void , в нем нельзя использовать инструкцию return с выражением. In a method returning void , return statements cannot specify an expression. В методе, выходное значение которого имеет любой другой тип, инструкции return должны содержать выражение, которое вычисляет возвращаемое значение. In a method returning non-void, return statements must include an expression that computes the return value.

Статические методы и методы экземпляра Static and instance methods

Метод, объявленный с модификатором static, является статическим методом. A method declared with a static modifier is a static method. Статический метод не работает с конкретным экземпляром и может напрямую обращаться только к статическим членам. A static method does not operate on a specific instance and can only directly access static members.

Метод, объявленный без модификатора static, является методом экземпляра. A method declared without a static modifier is an instance method. Метод экземпляра работает в определенном экземпляре и может обращаться как к статическим методам, так и к методам этого экземпляра. An instance method operates on a specific instance and can access both static and instance members. В методе можно напрямую обратиться к экземпляру, для которого этот метод был вызван, используя дескриптор this . The instance on which an instance method was invoked can be explicitly accessed as this . Использование this в статическом методе приводит к ошибке. It is an error to refer to this in a static method.

Следующий класс Entity содержит статические члены и члены экземпляра. The following Entity class has both static and instance members.

Каждый экземпляр Entity содержит серийный номер (и может содержать другие данные, которые здесь не показаны). Each Entity instance contains a serial number (and presumably some other information that is not shown here). Конструктор объекта Entity (который рассматривается как метод экземпляра) задает для нового экземпляра следующий доступный серийный номер. The Entity constructor (which is like an instance method) initializes the new instance with the next available serial number. Поскольку конструктор является членом экземпляра, он может обращаться как к полю экземпляра serialNo , так и к статическому полю nextSerialNo . Because the constructor is an instance member, it is permitted to access both the serialNo instance field and the nextSerialNo static field.

Статические методы GetNextSerialNo и SetNextSerialNo могут обращаться к статическому полю nextSerialNo , но прямое обращение из них к полю экземпляра serialNo приводит к ошибке. The GetNextSerialNo and SetNextSerialNo static methods can access the nextSerialNo static field, but it would be an error for them to directly access the serialNo instance field.

В следующем примере показано использование класса Entity. The following example shows the use of the Entity class.

Обратите внимание, что статические методы SetNextSerialNo и GetNextSerialNo вызываются для класса, а метод экземпляра GetSerialNo вызывается для экземпляров класса. Note that the SetNextSerialNo and GetNextSerialNo static methods are invoked on the class whereas the GetSerialNo instance method is invoked on instances of the class.

Виртуальные, переопределяющие и абстрактные методы Virtual, override, and abstract methods

Если объявление метода экземпляра включает модификатор virtual , такой метод называется виртуальным методом. When an instance method declaration includes a virtual modifier, the method is said to be a virtual method. Если модификатор virtual отсутствует, метод считается невиртуальным. When no virtual modifier is present, the method is said to be a nonvirtual method.

При вызове виртуального метода могут быть вызваны разные его реализации в зависимости от того, какой тип среды выполнения имеет экземпляр, для которого вызван этот метод. When a virtual method is invoked, the run-time type of the instance for which that invocation takes place determines the actual method implementation to invoke. При вызове невиртуального метода решающим фактором является тип во время компиляции для этого экземпляра. In a nonvirtual method invocation, the compile-time type of the instance is the determining factor.

Виртуальный метод можно переопределить в производном классе. A virtual method can be overridden in a derived class. Если объявление метода экземпляра содержит модификатор override, этот метод переопределяет унаследованный виртуальный метод с такой же сигнатурой. When an instance method declaration includes an override modifier, the method overrides an inherited virtual method with the same signature. Изначальное объявление виртуального метода создает новый метод, а переопределение этого метода создает специализированный виртуальный метод с новой реализацией взамен унаследованного виртуального метода. Whereas a virtual method declaration introduces a new method, an override method declaration specializes an existing inherited virtual method by providing a new implementation of that method.

Абстрактным методом называется виртуальный метод без реализации. An abstract method is a virtual method with no implementation. Абстрактный метод объявляется с модификатором abstract. Его можно объявить только в классе, который также объявлен абстрактным. An abstract method is declared with the abstract modifier and is permitted only in a class that is also declared abstract. Абстрактный метод должен обязательно переопределяться в каждом производном классе, не являющемся абстрактным. An abstract method must be overridden in every non-abstract derived class.

Следующий пример кода объявляет абстрактный класс Expression , который представляет узел дерева выражений, а также три производных класса: Constant , VariableReference и Operation , которые реализуют узлы дерева выражений для констант, ссылок на переменные и арифметических операций. The following example declares an abstract class, Expression , which represents an expression tree node, and three derived classes, Constant , VariableReference , and Operation , which implement expression tree nodes for constants, variable references, and arithmetic operations. (Они похожи на типы дерева выражений, но их нельзя путать.) (This is similar to, but not to be confused with the expression tree types).

Четыре приведенных выше класса можно использовать для моделирования арифметических выражений. The previous four classes can be used to model arithmetic expressions. Например, с помощью экземпляров этих классов выражение x + 3 можно представить следующим образом. For example, using instances of these classes, the expression x + 3 can be represented as follows.

Метод Evaluate экземпляра Expression вызывается для вычисления данного выражения и создает значение double . The Evaluate method of an Expression instance is invoked to evaluate the given expression and produce a double value. Этот метод принимает аргумент Dictionary , который содержит имена переменных (в качестве ключей записей) и значения переменных (в качестве значений записей). The method takes a Dictionary argument that contains variable names (as keys of the entries) and values (as values of the entries). Так как Evaluate — абстрактный метод, то в неабстрактных классах, производных от Expression , необходимо переопределить Evaluate . Because Evaluate is an abstract method, non-abstract classes derived from Expression must override Evaluate .

В Constant реализация метода Evaluate просто возвращает хранимую константу. A Constant ‘s implementation of Evaluate simply returns the stored constant. В VariableReference реализация этого метода выполняет поиск имени переменной в словаре и возвращает полученное значение. A VariableReference ‘s implementation looks up the variable name in the dictionary and returns the resulting value. В Operation реализация этого метода сначала вычисляет левый и правый операнды (рекурсивно вызывая их методы Evaluate ), а затем выполняет предоставленную арифметическую операцию. An Operation ‘s implementation first evaluates the left and right operands (by recursively invoking their Evaluate methods) and then performs the given arithmetic operation.

В следующей программе классы Expression используются для вычисления выражения x * (y + 2) с различными значениями x и y . The following program uses the Expression classes to evaluate the expression x * (y + 2) for different values of x and y .

Перегрузка методов Method overloading

Перегрузка метода позволяет использовать в одном классе несколько методов с одинаковыми именами, если они имеют уникальные сигнатуры. Method overloading permits multiple methods in the same class to have the same name as long as they have unique signatures. Когда при компиляции встречается вызов перегруженного метода, компилятор использует принцип разрешения перегрузки, чтобы определить, какой из методов следует вызвать. When compiling an invocation of an overloaded method, the compiler uses overload resolution to determine the specific method to invoke. Разрешение перегрузки выбирает из методов тот, который лучше всего соответствует предоставленным аргументам, или возвращает ошибку, если не удается выбрать конкретный подходящий метод. Overload resolution finds the one method that best matches the arguments or reports an error if no single best match can be found. В следующем примере показано, как работает разрешение перегрузки. The following example shows overload resolution in effect. Комментарий к каждому вызову метода UsageExample подсказывает, какой из методов будет вызван для этой строки. The comment for each invocation in the UsageExample method shows which method is actually invoked.

Как видно из этого примера, вы всегда можете выбрать конкретный метод, явным образом приведя типы аргументов к соответствующим типам параметров, и (или) явно предоставив аргументы нужного типа. As shown by the example, a particular method can always be selected by explicitly casting the arguments to the exact parameter types and/or explicitly supplying type arguments.

Другие функции-члены Other function members

Все члены класса, содержащие исполняемый код, совокупно называются функции-члены. Members that contain executable code are collectively known as the function members of a class. В предыдущем разделе мы рассмотрели основные варианты методов, используемых как функции-члены. The preceding section describes methods, which are the primary kind of function members. В этом разделе описываются другие типы функций-членов, поддерживаемые в языке C#: конструкторы, свойства, индексаторы, события, операторы и методы завершения. This section describes the other kinds of function members supported by C#: constructors, properties, indexers, events, operators, and finalizers.

Ниже приведен универсальный класс с именем MyList , который реализует расширяемый список объектов. The following shows a generic class called MyList , which implements a growable list of objects. Этот класс содержит несколько наиболее распространенных типов функций-членов. The class contains several examples of the most common kinds of function members.

В этом примере создается класс MyList , который отличается от стандартного System.Collections.Generic.List в .NET. This example creates a MyList class, which is not the same as the .NET standard System.Collections.Generic.List . Здесь показаны основные понятия, необходимые для этого руководства, но они не заменят собой этот класс. It does illustrate the concepts needed for this tour, but is not a replacement for that class.

Конструкторы Constructors

C# поддерживает конструкторы экземпляров и статические конструкторы. C# supports both instance and static constructors. Конструктор экземпляра является членом, который реализует действия для инициализации нового экземпляра класса. An instance constructor is a member that implements the actions required to initialize an instance of a class. Статический конструктор является членом, который реализует действия для инициализации самого класса при первоначальной его загрузке. A static constructor is a member that implements the actions required to initialize a class itself when it is first loaded.

Конструктор объявляется в виде метода без возвращаемого типа, имя которого совпадает с именем класса, в котором он определен. A constructor is declared like a method with no return type and the same name as the containing class. Если объявление конструктора содержит модификатор static, создается статический конструктор. If a constructor declaration includes a static modifier, it declares a static constructor. В противном случае это объявление считается конструктором экземпляра. Otherwise, it declares an instance constructor.

Конструкторы экземпляров можно перегружать, и для них можно указать необязательные параметры. Instance constructors can be overloaded and can have optional parameters. Например, класс MyList объявляет один конструктор экземпляра с одним необязательным параметром int . For example, the MyList class declares one instance constructor with a single optional int parameter. Конструкторы экземпляров вызываются с помощью оператора new . Instance constructors are invoked using the new operator. Следующий пример кода выделяет два экземпляра MyList с помощью конструкторов класса MyList : один с необязательным аргументом, а второй — без. The following statements allocate two MyList instances using the constructor of the MyList class with and without the optional argument.

В отличие от других членов конструкторы экземпляров не наследуются, и класс не имеет конструкторов экземпляров, кроме объявленных в самом этом классе. Unlike other members, instance constructors are not inherited, and a class has no instance constructors other than those actually declared in the class. Если в классе не объявлен конструктор экземпляра, для него автоматически создается пустой конструктор без параметров. If no instance constructor is supplied for a class, then an empty one with no parameters is automatically provided.

Свойства Properties

Свойства естественным образом дополняют поля. Properties are a natural extension of fields. И те, и другие являются именованными членами со связанными типами, и для доступа к ним используется одинаковый синтаксис. Both are named members with associated types, and the syntax for accessing fields and properties is the same. Однако свойства, в отличие от полей, не указывают места хранения. However, unlike fields, properties do not denote storage locations. Вместо этого свойства содержат методы доступа, в которых описаны инструкции для выполнения при чтении или записи значений. Instead, properties have accessors that specify the statements to be executed when their values are read or written.

Свойство объявляется так же, как поле, за исключением того, что объявление заканчивается не точкой с запятой, а парой разделителей < и >, между которыми указаны акцессоры get для чтения и (или) set для записи. A property is declared like a field, except that the declaration ends with a get accessor and/or a set accessor written between the delimiters < and >instead of ending in a semicolon. Свойство, для которого определены акцессоры get и set, является свойством для чтения и записи. Если в свойстве есть только акцессор get, оно является свойством только для чтения, и если только акцессор set — свойством только для записи. A property that has both a get accessor and a set accessor is a read-write property, a property that has only a get accessor is a read-only property, and a property that has only a set accessor is a write-only property.

Акцессор get оформляется как метод без параметров, у которого тип возвращаемого значения совпадает с типом, установленным для этого свойства. A get accessor corresponds to a parameterless method with a return value of the property type. Во всех ситуациях, кроме использования в качестве назначения в операторе присваивания, для вычисления значения свойства вызывается акцессор get. Except as the target of an assignment, when a property is referenced in an expression, the get accessor of the property is invoked to compute the value of the property.

Илон Маск рекомендует:  Что такое код gmp_add

Метод доступа set соответствует методу с одним именованным значением параметра и без возвращаемого типа. A set accessor corresponds to a method with a single parameter named value and no return type. При ссылке на свойство в качестве назначения в операторе присваивания или в качестве операнда для ++ или — вызывается метод доступа set с аргументом, предоставляющим новое значение. When a property is referenced as the target of an assignment or as the operand of ++ or —, the set accessor is invoked with an argument that provides the new value.

Класс MyList объявляет два свойства: Count (только для чтения) и Capacity (только для записи). The MyList class declares two properties, Count and Capacity , which are read-only and read-write, respectively. Пример использования этих свойств приведен ниже. The following is an example of use of these properties:

Как и в отношении полей и методов, C# поддерживает свойства экземпляра и статические свойства. Similar to fields and methods, C# supports both instance properties and static properties. Статические свойства объявляются с модификатором static, а свойства экземпляра — без него. Static properties are declared with the static modifier, and instance properties are declared without it.

Акцессоры свойства могут быть виртуальными. The accessor(s) of a property can be virtual. Если объявление свойства содержит модификатор virtual , abstract или override , этот модификатор применяется к акцессорам свойства. When a property declaration includes a virtual , abstract , or override modifier, it applies to the accessor(s) of the property.

Индексаторы Indexers

Индексатор является членом, позволяющим индексировать объекты так, как будто они включены в массив. An indexer is a member that enables objects to be indexed in the same way as an array. Индексатор объявляется так же, как свойство, за исключением того, что именем элемента является this , а за этим именем следует список параметров, находящийся между разделителями [ и ] . An indexer is declared like a property except that the name of the member is this followed by a parameter list written between the delimiters [ and ] . Эти параметры доступны в акцессорах индексатора. The parameters are available in the accessor(s) of the indexer. Как и свойства, можно объявить индексаторы для чтения и записи, только для чтения или только для записи. Кроме того, поддерживаются виртуальные акцессоры индексатора. Similar to properties, indexers can be read-write, read-only, and write-only, and the accessor(s) of an indexer can be virtual.

Класс MyList объявляет один индексатор для чтения и записи, который принимает параметр int . The MyList class declares a single read-write indexer that takes an int parameter. Индексатор позволяет индексировать экземпляры MyList значениями с типом int . The indexer makes it possible to index MyList instances with int values. Например: For example:

Индексаторы можно перегружать, то есть в одном классе можно объявить несколько индексаторов, если у них различаются количество или типы параметров. Indexers can be overloaded, meaning that a class can declare multiple indexers as long as the number or types of their parameters differ.

События Events

Событие — это член, с помощью которого класс или объект предоставляют уведомления. An event is a member that enables a class or object to provide notifications. Объявление события выглядит так же, как объявление поля, но содержит ключевое слово event и обязано иметь тип делегата. An event is declared like a field except that the declaration includes an event keyword and the type must be a delegate type.

В классе, который объявляет член события, это событие действует, как обычное поле с типом делегата (если это событие не является абстрактным и не объявляет акцессоры). Within a class that declares an event member, the event behaves just like a field of a delegate type (provided the event is not abstract and does not declare accessors). Это поле хранит ссылку на делегат, который представляет добавленные к событию обработчики событий. The field stores a reference to a delegate that represents the event handlers that have been added to the event. Если обработчики событий отсутствуют, это поле имеет значение null . If no event handlers are present, the field is null .

Класс MyList объявляет один член события с именем Changed , который обрабатывает добавление нового элемента. The MyList class declares a single event member called Changed , which indicates that a new item has been added to the list. Событие Changed вызывается виртуальным методом OnChanged , который сначала проверяет, не имеет ли это событие значение null (это означает, что обработчики отсутствуют). The Changed event is raised by the OnChanged virtual method, which first checks whether the event is null (meaning that no handlers are present). Концепция создания события в точности соответствует вызову делегата, представленного этим событием. Это позволяет обойтись без особой языковой конструкции для создания событий. The notion of raising an event is precisely equivalent to invoking the delegate represented by the event—thus, there are no special language constructs for raising events.

Клиенты реагируют на события посредством обработчиков событий. Clients react to events through event handlers. Обработчики событий можно подключать с помощью оператора += и удалять с помощью оператора -= . Event handlers are attached using the += operator and removed using the -= operator. Следующий пример кода подключает обработчик события Changed к событию MyList . The following example attaches an event handler to the Changed event of a MyList .

Для более сложных сценариев, требующих контроля над базовым хранилищем события, в объявлении события можно явным образом предоставить акцессоры add и remove . Они будут действовать примерно так же, как акцессор set для свойства. For advanced scenarios where control of the underlying storage of an event is desired, an event declaration can explicitly provide add and remove accessors, which are somewhat similar to the set accessor of a property.

Операторы Operators

Оператор является членом, который определяет правила применения определенного выражения к экземплярам класса. An operator is a member that defines the meaning of applying a particular expression operator to instances of a class. Вы можете определить операторы трех типов: унарные операторы, двоичные операторы и операторы преобразования. Three kinds of operators can be defined: unary operators, binary operators, and conversion operators. Все операторы объявляются с модификаторами public и static . All operators must be declared as public and static .

Класс MyList объявляет два оператора: operator == и operator != . Это позволяет определить новое значение для выражений, которые применяют эти операторы к экземплярам MyList . The MyList class declares two operators, operator == and operator != , and thus gives new meaning to expressions that apply those operators to MyList instances. В частности, эти операторы определяют, что равенство двух экземпляров MyList проверяется путем сравнения всех содержащихся в них объектов с помощью определенных для них методов Equals. Specifically, the operators define equality of two MyList instances as comparing each of the contained objects using their Equals methods. Следующий пример кода использует оператор == для сравнения двух экземпляров MyList . The following example uses the == operator to compare two MyList instances.

Первый Console.WriteLine выводит True , поскольку два списка содержат одинаковое число объектов с одинаковыми значениями в том же порядке. The first Console.WriteLine outputs True because the two lists contain the same number of objects with the same values in the same order. Если бы в MyList не было определения operator == , первый Console.WriteLine возвращал бы False , поскольку a и b указывают на различные экземпляры MyList . Had MyList not defined operator == , the first Console.WriteLine would have output False because a and b reference different MyList instances.

Методы завершения Finalizers

Метод завершения является членом, который реализует действия для завершения существования экземпляра класса. A finalizer is a member that implements the actions required to finalize an instance of a class. Методы завершения не могут иметь параметры, не могут содержать модификаторы доступа и их нельзя вызвать явным образом. Finalizers cannot have parameters, they cannot have accessibility modifiers, and they cannot be invoked explicitly. Метод завершения для экземпляра вызывается автоматически в процессе сборки мусора. The finalizer for an instance is invoked automatically during garbage collection.

Сборщик мусора имеет широкую степень свободы в выборе времени уничтожения объектов и вызова методов завершения. The garbage collector is allowed wide latitude in deciding when to collect objects and run finalizers. В частности, время вызова методов завершения не является детерминированным, и эти методы могут выполняться в любом потоке. Specifically, the timing of finalizer invocations is not deterministic, and finalizers may be executed on any thread. По этим и некоторым другим причинам методы завершения следует использовать в классах только в крайнем случае, когда невозможны другие решения. For these and other reasons, classes should implement finalizers only when no other solutions are feasible.

Уничтожение объектов лучше контролировать с помощью инструкции using . The using statement provides a better approach to object destruction.

1. Понятие класса

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

Тип является конкретным представлением некоторой концепции. Например, встроенный тип float вместе с операциями +, –, * и т.д. представляет конкретное воплощение математической концепции вещественного числа.

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

Основной смысл введения новых типов состоит в разделении малозначащих деталей реализации (например, расположения в памяти составных частей объектов данного типа) от свойств, имеющих определяющее значение для правильного использования сущности (например, полный набор функций доступа к данным). Подобное разделение лучше всего выражается в терминах ограничения доступа к данным извне и использования для этой цели специальных процедур в рамках четко определённого интерфейса.

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

Определение класса выглядит следующим образом:
class < >;

Объявление класса является объявлением некоторого типа. Для дальнейшей работы необходимо объявлять соответствующие переменные или объекты класса.

class X < . >; // Объявление типа X
X x; // Объявляем переменную х – объект класса (типа) Х

Объекты класса можно присваивать, передавать в качестве параметров функции и возвращать как её результат. Другие естественные операции, вроде проверки на равенство, также могут быть определены пользователем.

2. Члены класса

В списке членов класса можно объявлять переменные, функции, классы, перечисления, а также дружественные функции и классы. Член класса не может объявляться в списке членов класса дважды. Это относиться и к функциям (хотя могут быть функции с одним именем, но разным набором формальных параметров). Кроме того, нельзя объявить в классе переменную и функцию с одним именем. Список членов класса определяет полный набор членов этого класса. Нельзя добавлять к классу члены ещё в каком-то месте.

class X
< int i;
int i; // Ошибка – повторное объявление
>;
int X::k; // Ошибка – попытка объявить член класса вне объявления класса
class Y
< int f();
int f(); // Ошибка – повторное объявление функции
int f(int x); // Ошибок нет
>;
class Z
< int f();
int f; // Ошибка – есть функция с таким же именем
>;

Член класса не может иметь инициализатора. Член класса не может быть объявлен со спецификациями класса памяти auto, extern и register. Инициализация объектов класса осуществляется с помощью конструкторов. Объект класса не может содержать объект того же класса, но может содержать указатель или ссылку на объект того же класса.

Для доступа к членам класса (после объявления некоторой переменной этого класса или указателя на объект данного класса) используется следующий синтаксис:
. ->

3. Доступ к членам класса

Управление доступом применяется единообразно к функциям-членам класса и данным-членам класса.

Член класса может быть:

  • приватным ( private ) – это значит, что его имя может употребляться лишь внутри функций-членов класса и друзей класса, в котором этот член класса объявлен;
  • защищённым ( protected ) – это значит, что его имя может употребляться лишь внутри функций-членов класса, друзей этого класса и производных от него классов;
  • публичным ( public ) – это значит, что его имя может употребляться внутри любой функции (а также и вне функций в инициализаторах).

Модификаторы доступа можно использовать несколько раз в одном и том же объявлении класса.

Механизмы управления доступом в C++ обеспечивают защиту от случайного, а не от преднамеренного доступа. Однако это относится к проблемам дисциплины программирования, а не к проблемам языка.

Члены класса без спецификатора доступа по умолчанию являются приватными. Члены структур и объединений по умолчанию являются публичными.

4. Функции-члены класса

Функция, объявленная в классе без спецификатора friend, называется функцией-членом класса. Её вызов имеет соответствующий синтаксис.

Описание функции-члена класса относиться к области действия класса. Это означает, что функция-член класса может непосредственно использовать имена членов своего класса.

class X < private : int n; public : void f(); >; // Объявление класса Х
void X::f() // Определение функции f из класса Х
X a, b; // Объявление переменных класса Х
a.f(); // Вызов функции f применяется к переменной а. Таким образом, // изменяется член n переменной a. Переменная b остаётся без изменений.

4.1. Константные функции-члены класса

В объявлении функции после списка параметров можно добавить модификатор const. Это будет означать, что функция не меняет состояние объекта, к которому она применяется. Суффикс const является частью типа функции и должен записываться, когда функция определяется вне класса.

class X < private : int n; public : int f() const ; >; int X::f() const
// Ошибка – попытка изменить значение члена класса в константной функции

Константную функцию-член класса можно вызвать как для константного, так и для неконстантного объекта, в то время как неконстантную функцию можно вызвать только для объекта, не являющегося константой.

4.2. Указатель this

В нестатической функции-члене класса ключевое слово this обозначает указатель на объект, для которого вызвана данная функция, т.е. внутри функции-члена класса член того же класса с именем х можно обозначать как x , и как this -> x . Указатель на объект, для которого вызвана функция, является неявным параметром этой функции.

class X < private : int n; public :
void f( int n) < this ->n = n; > >; // Члену класса n присваивается значение параметра n

Указатель this в функции-члене класса Х имеет тип X * const . Однако, это не обычная переменная, невозможно получить её адрес или присвоить ей что-нибудь. В константной функции-члене класса Х this имеет тип const X * const для предотвращения модификации самого объекта.

В большинстве случаев использование this является неявным. В частности, каждое обращение к нестатическому члену класса неявно использует this для доступа к члену соответствующего объекта.

Функции-члены класса могут также возвращать с помощью указателя this ссылку на объект класса для того, чтобы можно было использовать вызов функции как параметр другой функции.

class X < . public : X& f(); >; X& X::f()

4.3. Встраиваемые функции-члены класса

Функция-член класса может быть описана внутри объявления класса. В этом случае она считается встраиваемой (inline) функцией.

Часть IV

В части 4 мы сосредоточимся на объектном программировании, т.е. на применении классов C++ для определения новых типов, манипулировать которыми так же просто, как и встроенными. Создавая новые типы для описания предметной области, C++ помогает программисту писать более легкие для понимания приложения. Классы позволяют отделить детали, касающиеся реализации нового типа, от определения интерфейса и операций, предоставляемых пользователю. При этом уделяется меньше внимания мелочам, из-за чего программирование становится таким утомительным занятием. Значимые для приложения типы можно реализовать всего один раз, после чего использовать повторно. Средства, обеспечивающие инкапсуляцию данных и функций, необходимых для реализации типа, помогают значительно упростить последующее сопровождение и развитие приложения.

В главе 13 мы рассмотрим общий механизм классов: порядок их определения, концепцию сокрытия информации (т.е. отделение открытого интерфейса от закрытой реализации), способы определения и манипулирования объектами класса, область видимости, вложенные классы и классы как члены пространства имен.

В главе 14 изучаются предоставляемые C++ средства инициализации и уничтожения объектов класса, а также присваивания им значений путем применения таких специальных функций-членов класса, как конструкторы, деструкторы и копирующие конструкторы. Мы рассмотрим вопрос о почленной инициализации и копировании, когда объект класса инициализируется или ему присваивается значение другого объекта того же класса.

В главе 15 мы расскажем о перегрузке операторов, которая позволяет использовать операнды типа класса со встроенными операторами, описанными в главе 4. Таким образом, работа с объектами типа класса может быть сделана столь же понятной, как и работа со встроенными типами. В начале главы 15 представлены общие концепции и соображения, касающиеся проектирования перегрузки операторов, а затем рассмотрены конкретные операторы, такие, как присваивание, взятие индекса, вызов, а также специфичные для классов операторы new и delete. Иногда необходимо объявить перегруженный оператор, как друга класса, наделив его специальными правами доступа, в данной главе объясняется, зачем это нужно. Здесь же представлен еще один специальный вид функций-членов – конвертеры, которые позволяют программисту определить стандартные преобразования. Конвертеры неявно применяются компилятором, когда объекты класса используются в качестве фактических аргументов функции или операндов встроенного либо перегруженного оператора. Завершается глава изложением правил разрешения перегрузки функций с учетом аргументов типа класса, функций-членов и перегруженных операторов.

Тема главы 16 – шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов параметризованы. Например, vector может быть параметризован типом элементов, хранящихся в нем, а buffer – типом элементов в буфере или его размером. В этой главе объясняется, как определить и конкретизировать шаблон. Поддержка классов в C++ теперь рассматривается иначе – в свете наличия шаблонов, и снова обсуждаются функции-члены, объявления друзей и вложенные типы. Здесь мы еще раз вернемся к модели компиляции шаблонов, описанной в главе 10, чтобы показать, какое влияние оказывают на нее шаблоны классов.

13. Классы

Механизм классов в C++ позволяет пользователям определять собственные типы данных. По этой причине их часто называют пользовательскими типами. Класс может наделять дополнительной функциональностью уже существующий тип. Так, например, IntArray, введенный в главе 2, предоставляет больше возможностей, чем тип «массив int». С помощью классов можно создавать абсолютно новые типы, например Screen (экран) или Account (расчетный счет). Как правило, классы используются для абстракций, не отражаемых встроенными типами адекватно.

В этой главе мы узнаем, как определять типы и использовать объекты классов; увидим, что определение класса вводит как данные-члены, описывающие его, так и функции-члены, составляющие набор операций, применимых к объектам класса. Мы покажем, как можно обеспечить сокрытие информации, объявив внутреннее представление и реализацию закрытыми, но открыв операции над объектами. Говорят, что закрытое внутреннее представление инкапсулировано, а открытую часть класса называют его интерфейсом.

Далее в этой главе мы познакомимся с особым видом членов класса – статическими членами. Мы расскажем также, как можно использовать указатели на члены и функции-члены класса, и рассмотрим объединения, представляющие собой специализированный вид класса для хранения объектов разных типов в одной области памяти. Завершается глава обсуждением области видимости класса и описанием правил разрешения имен в этой области; затрагиваются такие понятия, как вложенные классы, классы-члены пространства имен и локальные классы.

13.1. Определение класса

Определение класса состоит из двух частей: заголовка, включающего ключевое слово class, за которым следует имя класса, и тела, заключенного в фигурные скобки. После такого определения должны стоять точка с запятой или список объявлений:

Внутри тела объявляются данные-члены и функции-члены и указываются уровни доступа к ним. Таким образом, тело класса определяет список его членов.

Каждое определение вводит новый тип данных. Даже если два класса имеют одинаковые списки членов, они все равно считаются разными типами:

Тело класса определяет отдельную область видимости. Объявление членов внутри тела помещает их имена в область видимости класса. Наличие в двух разных классах членов с одинаковыми именами – не ошибка, эти имена относятся к разным объектам. (Подробнее об областях видимости классов мы поговорим в разделе 13.9.)

После того как тип класса определен, на него можно ссылаться двумя способами:

  • написать ключевое слово class, а после него – имя класса. В предыдущем примере объект obj1 класса First объявлен именно таким образом;
  • указать только имя класса. Так объявлен объект obj2 класса Second из приведенного примера.

Оба способа сослаться на тип класса эквивалентны. Первый заимствован из языка C и остается корректным методом задания типа класса; второй способ введен в C++ для упрощения объявлений.

13.1.1. Данные-члены

Данные-члены класса объявляются так же, как переменные. Например, у класса Screen могут быть следующие данные-члены:

Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor – это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)

Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:

Член класса может иметь любой тип:

Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)

Объявления данных-членов очень похожи на объявления переменных в области видимости блока или пространства имен. Однако их, за исключением статических членов, нельзя явно инициализировать в теле класса:

Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)


13.1.2. Функции-члены

Пользователям, по-видимому, понадобится широкий набор операций над объектами типа Screen: возможность перемещать курсор, проверять и устанавливать области экрана и рассчитывать его реальные размеры во время выполнения, а также копировать один объект в другой. Все эти операции можно реализовать с помощью функций-членов.

Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:

Определение функции-члена также можно поместить внутрь тела класса:

home() перемещает курсор в левый верхний угол экрана; get() возвращает символ, находящийся в текущей позиции курсора.

Функции-члены отличаются от обычных функций следующим:

    функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (->):

(в разделе 13.9 область видимости класса обсуждается более детально);

  • функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
  • Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:

    (Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)

    13.1.3. Доступ к членам

    Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen показал, что для его объектов всегда задается размер экрана 80 ? 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:

    Прежняя реализация функций-членов (то, как они манипулируют данными-членами класса) больше не годится, ее нужно переписать. Но это не означает, что должен измениться и интерфейс функций-членов (список формальных параметров и тип возвращаемого значения).

    Если бы данные-члены класса Screen были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?

    • все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;
    • так как интерфейс не изменился, то коды, манипулировавшие объектами класса Screen только через функции-члены, не пришлось бы модифицировать. Но поскольку сами функции-члены все же изменились, программу пришлось бы откомпилировать заново.

    Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected соответственно закрытыми или защищенными.

    • открытый член доступен из любого места программы. Класс, скрывающий информацию, оставляет открытыми только функции-члены, определяющие операции, с помощью которых внешняя программа может манипулировать его объектами;
    • закрытый член доступен только функциям-членам и друзьям класса. Класс, который хочет скрыть информацию, объявляет свои данные-члены закрытыми;
    • защищенный член ведет себя как открытый по отношению к производному классу и как закрытый по отношению к остальной части программы. (В главе 2 мы видели пример использования защищенных членов в классе IntArray. Детально они рассматриваются в главе 17, где вводится понятие наследования.)

    В следующем определении класса Screen указаны секции public и private:

    Согласно принятому соглашению, сначала объявляются открытые члены класса. (Обсуждение того, почему в старых программах C++ сначала шли закрытые члены и почему этот стиль еще кое-где сохранился, см. в книге [LIPPMAN96a].) В теле класса может быть несколько секций public, protected и private. Каждая секция продолжается либо до метки следующей секции, либо до закрывающей фигурной скобки. Если спецификатор доступа не указан, то секция, непосредственно следующая за открывающей скобкой, по умолчанию считается private.

    13.1.4. Друзья

    Иногда удобно разрешить некоторым функциям доступ к закрытым членам класса. Механизм друзей позволяет классу разрешать доступ к своим неоткрытым членам.

    Объявление друга начинается с ключевого слова friend и может встречаться только внутри определения класса. Так как друзья не являются членами класса, то не имеет значения, в какой секции они объявлены. В примере ниже мы сгруппировали все подобные объявления сразу после заголовка класса:

    Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen. Простая реализация оператора вывода выглядит следующим образом:

    Другом может быть функция из пространства имен, функция-член другого класса или даже целый класс. В последнем случае всем его функциям-членам предоставляется доступ к неоткрытым членам класса, объявляющего дружественные отношения. (В разделе 15.2 друзья обсуждаются более подробно.)

    13.1.5. Объявление и определение класса

    О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.

    Можно объявить класс, не определяя его. Например:

    Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.

    Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.

    Однако указатель или ссылку на объект такого класса объявлять можно, так как они имеют фиксированный размер, не зависящий от типа. Но, поскольку размеры класса и его членов неизвестны, применять оператор разыменования (*) к такому указателю, а также использовать указатель или ссылку для обращения к члену не разрешается, пока класс не будет полностью определен.

    Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:

    Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например:

    Пусть дан класс Person со следующими двумя членами:

    и такие функции-члены:

    Какие члены вы объявили бы в секции public, а какие – в секции private? Поясните свой выбор.

    Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?

    13.2. Объекты классов

    Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:

    выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.

    Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса:

    Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().

    Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.)

    Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например:

    Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)

    По умолчанию объект класса передается по значению, если он выступает в роли аргумента функции или ее возвращаемого значения. Можно объявить формальный параметр функции или возвращаемое ею значение как указатель или ссылку на тип класса. (В разделе 7.3 были представлены параметры, являющиеся указателями или ссылками на типы классов, и объяснялось, когда их следует использовать. В разделе 7.4 с этой точки зрения рассматривались типы возвращаемых значений.)

    Для доступа к данным или функциям-членам объекта класса следует пользоваться соответствующими операторами. Оператор «точка» (.) применяется, когда операндом является сам объект или ссылка на него; а «стрелка»(->) – когда операндом служит указатель на объект:

    isEqual() – это не являющаяся членом функция, которая сравнивает два объекта Screen. У нее нет права доступа к закрытым членам Screen, поэтому напрямую обращаться к ним она не может. Сравнение проводится с помощью открытых функций-членов данного класса.

    Для получения высоты и ширины экрана isEqual() должна пользоваться функциями-членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна:

    Применение оператора доступа к указателю на объект класса эквивалентно последовательному выполнению двух операций: применению оператора разыменования (*) к указателю, чтобы получить адресуемый объект, и последующему применению оператора «точка» для доступа к нужному члену класса. Например, выражение

    можно переписать так:

    Результат будет одним и тем же.

    13.3. Функции-члены класса

    Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen такой набор состоит из следующих объявленных в нем функций-членов:

    Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре:

    При вызове функции home() для объекта myScreen происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.

    13.3.1. Когда использовать встроенные функции-члены

    Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.)

    Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline:

    Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций-членов, определенных в теле класса.

    Функции-члены, состоящие из двух или более строк, лучше определять вне тела. Для идентификации функции как члена некоторого класса требуется специальный синтаксис объявления: имя функции должно быть квалифицировано именем ее класса. Вот как выглядит определение функции checkRange(), квалифицированное именем Screen:

    Прежде чем определять функцию-член вне тела класса, необходимо объявить ее внутри тела, обеспечив ее видимость. Например, если бы перед определением функции checkRange() не был включен заголовочный файл Screen.h, то компилятор выдал бы сообщение об ошибке. Тело класса определяет полный список его членов. Этот список не может быть расширен после закрытия тела.

    Обычно функции-члены, определенные вне тела класса, не делают встроенными. Но объявить такую функцию встроенной можно, если явно добавить слово inline в объявление функции внутри тела класса или в ее определение вне тела, либо сделав то и другое одновременно. В следующем примере move() определена как встроенная функция-член класса Screen:

    Функция get(int, int) объявляется встроенной с помощью слова inline:

    Определение функции следует после объявления класса. При этом слово inline можно опустить:

    Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen.

    13.3.2. Доступ к членам класса

    Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода:

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

    Хотя _screen, _height, _width и _cursor являются закрытыми членами класса Screen, функция-член copy() работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом:

    то параметр sobj внутри определения copy() соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.

    13.3.3. Закрытые и открытые функции-члены

    Функцию-член можно объявить в любой из секций public, private или protected тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов составляет интерфейс класса. Например, функции-члены home(), move() и get() класса Screen определяют операции, с помощью которых программа манипулирует объектами этого типа.,/p>

    Поскольку мы прячем от пользователей внутреннее представление класса, объявляя его члены закрытыми, то для манипуляции объектами типа Screen необходимо предоставить открытые функции-члены. Такой прием – сокрытие информации – защищает написанный пользователем код от изменений во внутреннем представлении.

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

    До сих пор мы встречались лишь с функциями, поддерживающими доступ к закрытым членам только для чтения. Ниже приведены две функции set(), позволяющие пользователю модифицировать объект Screen. Добавим их объявления в тело класса:

    Далее следуют определения функций:

    В реализации класса Screen мы предполагаем, что объект Screen не содержит двоичных нулей. По этой причине set() не позволяет записать на экран нуль.

    Представленные до сих пор функции-члены были открытыми, их можно вызывать из любого места программы, а закрытые вызываются только из других функций-членов (или друзей) класса, но не из программы, обеспечивая поддержку другим операциям в реализации абстракции класса. Примером может служить функция-член remainingSpace класса Screen(), использованная в set(const string&).

    (Детально защищенные функции-члены будут рассмотрены в главе 17.)

    Следующая программа предназначена для тестирования описанных к настоящему моменту функций-членов:

    Откомпилировав и запустив эту программу, мы получим следующее:

    13.3.4. Специальные функции-члены

    Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground:

    Определение конструктора класса Screen выглядит так:

    Каждый объявленный объект класса Screen автоматически инициализируется конструктором:

    (В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.)

    13.3.5. Функции-члены со спецификаторами const и volatile

    Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:

    Однако объект класса, как правило, не модифицируется программой напрямую. Вместо этого вызывается та или иная открытая функция-член. Чтобы не было «покушений» на константность объекта, компилятор должен различать безопасные (те, которые не изменяют объект) и небезопасные (те, которые пытаются это сделать) функции-члены:

    Проектировщик класса может указать, какие функции-члены не модифицируют объект, объявив их константными с помощью спецификатора const:

    Для класса, объявленного как const, могут быть вызваны только те функции-члены, которые также объявлены со спецификатором const. Ключевое слово const помещается между списком параметров и телом функции-члена. Для константной функции-члена, определенной вне тела класса, это слово должно присутствовать как в объявлении, так и в определении:

    Запрещено объявлять константную функцию-член, которая модифицирует члены класса. Например, в следующем упрощенном определении:

    определение функции-члена ok() корректно, так как она не изменяет значения _cursor. В определении же error() значение _cursor изменяется, поэтому такая функция-член не может быть объявлена константной и компилятор выдает сообщение об ошибке:

    Если класс будет интенсивно использоваться, лучше объявить его функции-члены, не модифицирующие данных, константными. Однако наличие спецификатора const в объявлении функции-члена не предотвращает все возможные изменения. Такое объявление гарантирует лишь, что функции-члены не смогут изменять данные-члены, но если класс содержит указатели, то адресуемые ими объекты могут быть модифицированы константной функцией, не вызывая ошибки компиляции. Это часто приводит в недоумение начинающих программистов. Например:

    Модифицировать _text нельзя, но это объект типа char*, и символы, на которые он указывает, можно изменить внутри константной функции-члена класса Text. Функция-член bad() демонстрирует плохой стиль программирования. Константность функции-члена не гарантирует, что объекты внутри класса останутся неизменными после ее вызова, причем компилятор не поможет обнаружить такую ситуацию.

    Константную функцию-член можно перегружать неконстантной функцией с тем же списком параметров:

    В этом случае наличие спецификатора const у объекта класса определяет, какая из двух функций будет вызвана:

    Хотя конструкторы и деструкторы не являются константными функциями-членами, они все же могут вызываться для константных объектов. Объект становится константным после того, как конструктор проинициализирует его, и перестает быть таковым, как только вызывается деструктор. Таким образом, объект со спецификатором const трактуется как константный с момента завершения работы конструктора и до вызова деструктора.

    Функцию-член можно также объявить со спецификатором volatile (он был введен в разделе 3.13). Объект класса объявляется как volatile, если его значение изменяется способом, который не обнаруживается компилятором (например, если это структура данных, представляющая порт ввода/вывода). Для таких объектов вызываются только функции-члены с тем же спецификатором, конструкторы и деструкторы:

    13.3.6. Объявление mutable

    При объявлении объекта класса Screen константным возникают некоторые проблемы. Предполагается, что после инициализации объекта Screen, его содержимое уже нельзя изменять. Но это не должно мешать нам читать содержимое экрана. Рассмотрим следующий константный объект класса Screen:

    Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так:

    // прочитать содержимое экрана в позиции (3,4)

    // Увы! Это не работает

    Но такая конструкция не работает: move() – это не константная функция-член, и сделать ее таковой непросто. Определение move() выглядит следующим образом:

    Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.

    Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const.

    Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена класса должно предшествовать ключевое слово mutable:

    Теперь любая константная функция способна модифицировать _cursor, и move() может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой.

    // move() — константная функция-член

    Показанные в начале этого подраздела операции позиционирования внутри экрана теперь можно выполнить без сообщения об ошибке.

    Отметим, что изменчивым объявлен только член _cursor, тогда как _screen, _height и _width не имеют спецификатора mutable, поскольку их значения в константном объекте класса Screen изменять нельзя.

    Объясните, как будет вести себя copy() при следующих вызовах:

    К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward().

    Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом ‘007’.

    Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение.

    13.4. Неявный указатель this

    У каждого объекта класса есть собственная копия данных-членов. Например:

    У объекта myScreen есть свои члены _width, _height, _cursor и _screen, а у объекта bufScreen – свои. Однако каждая функция-член класса существует в единственном экземпляре. Их и вызывают myScreen и bufScreen.

    В предыдущем разделе мы видели, что функция-член может обращаться к членам своего класса, не используя операторы доступа. Так, определение функции move() выглядит следующим образом:

    Если функция move() вызывается для объекта myScreen, то члены _width и _height, к которым внутри нее имеются обращения, – это члены объекта myScreen. Если же она вызывается для объекта bufScreen, то и обращения производятся к членам данного объекта. Каким же образом _cursor, которым манипулирует move(), оказывается членом то myScreen, то bufScreen? Дело в указателе this.

    Каждой функции-члену передается указатель на объект, для которого она вызвана, – this. В неконстантной функции-члене это указатель на тип класса, в константной – константный указатель на тот же тип, а в функции со спецификатором volatile указатель с тем же спецификатором. Например, внутри функции-члена move() класса Screen указатель this имеет тип Screen*, а в неконстантной функции-члене List – тип List*.

    Поскольку this адресует объект, для которого вызвана функция-член, то при вызове move() для myScreen он указывает на объект myScreen, а при вызове для bufScreen – на объект bufScreen. Таким образом, член _cursor, с которым работает функция move(), в первом случае принадлежит объекту myScreen, а во втором – bufScreen.

    Понять все это можно, если представить себе, как компилятор реализует объект this. Для его поддержки необходимо две трансформации:

      Изменить определение функции-члена класса, добавив дополнительный параметр:

    В этом определении использование указателя this для доступа к членам _width и _cursor сделано явным.

  • Изменение каждого вызова функции-члена класса с целью передачи одного дополнительного аргумента – адреса объекта, для которого она вызвана:
  • Программист может явно обращаться к указателю this внутри функции. Так, вполне корректно, хотя и излишне, определить функцию-член home() следующим образом:

    Однако бывают случаи, когда без такого обращения не обойтись, как мы видели на примере функции-члена copy() класса Screen. В следующем подразделе мы рассмотрим и другие примеры.

    13.4.1. Когда использовать указатель this

    Наша функция main() вызывает функции-члены класса Screen для объектов myScreen и bufScreen таким образом, что каждое действие – это отдельная инструкция. У нас есть возможность определить функции-члены так, чтобы конкатенировать их вызовы при обращении к одному и тому же объекту. Например, все вызовы внутри main() будут выглядеть так:

    Именно так интуитивно представляется последовательность операций с экраном: очистить экран myScreen, переместить курсор в позицию (2,2), записать в эту позицию символ ‘*’ и вывести результат.

    Операторы доступа «точка» и «стрелка» левоассоциативны, т.е. их последовательность выполняется слева направо. Например, сначала вызывается myScreen.clear(), затем myScreen.move() и т.д. Чтобы myScreen.move() можно было вызвать после myScreen.clear(), функция clear() должна возвращать объект myScreen, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear():


    // объявление clear() находится в теле класса

    // в нем задан аргумент по умолчанию bkground = ‘#’

    Обратите внимание, что возвращаемый тип этой функции-члена – Screen& – ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this.

    Аналогично функцию-член display() можно написать так:

    А вот реализация reSize():

    // объявление reSize() находится в теле класса

    // в нем задан аргумент по умолчанию bkground = ‘#’

    Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования:

    Указатель this хранит адрес объекта, для которого была вызвана функция-член. Если адрес, на который ссылается sobj, совпадает со значением this, то sobj и this относятся к одному и тому же объекту, так что операция копирования не нужна. (Мы еще встретимся с этой конструкцией, когда будем рассматривать копирующий оператор присваивания в разделе 14.7.)

    Указатель this можно использовать для модификации адресуемого объекта, а также для его замены другим объектом того же типа. Например, функция-член assign() класса classType выглядит так. Можете ли вы объяснить, что она делает?

    classType – это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4.

    Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему?

    13.5. Статические члены класса

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

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

    По сравнению с глобальным объектом у статического члена есть следующие преимущества:

    • статический член не находится в глобальном пространстве имен программы, следовательно, уменьшается вероятность случайного конфликта имен с другими глобальными объектами;
    • остается возможность сокрытия информации, так как статический член может быть закрытым, а глобальный объект – никогда.

    Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член _interestRate объявлен как закрытый и статический типа double:

    Почему _interestRate сделан статическим, а _amount и _owner нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account.

    Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок.

    В общем случае статический член инициализируется вне определения класса. Его имя во внешнем определении должно быть специфицировано именем класса. Вот так можно инициализировать _interestRate:

    // явная инициализация статического члена класса

    В программе может быть только одно определение статического члена. Это означает, что инициализацию таких членов следует помещать не в заголовочные файлы, а туда, где находятся определения невстроенных функций-членов класса.

    В объявлении статического члена можно указать любой тип. Это могут быть константные объекты, массивы, объекты классов и т.д. Например:

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

    Отметим, что константный статический член целого типа, инициализированный константой, – это константное выражение. Проектировщик может объявить такой статический член, если внутри тела класса возникает необходимость в именованной константе. Например, поскольку константный статический член nameSize является константным выражением, проектировщик использует его для задания размера члена-массива с именем name.

    Даже если такой член инициализируется в теле класса, его все равно необходимо задать вне определения класса. Однако поскольку начальное значение уже задано в объявлении, то при определении оно не указывается.

    Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции:

    Член name должен быть инициализирован вне определения класса.

    Обратите внимание, что член nameSize задает размер массива name в определении, находящемся вне тела класса:

    nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.)

    Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов:

    Что же касается функций, не являющихся членами класса, то они могут обращаться к статическому члену двумя способами. Во-первых, посредством операторов доступа:

    // мы используем ссылочный и указательный параметры,

    // чтобы проиллюстрировать оба оператора доступа

    Как ac1._interestRate, так и ac2->_interestRate относятся к статическому члену Account::_interestRate.

    Поскольку есть лишь одна копия статического члена класса, до нее необязательно добираться через объект или указатель. Другой способ заключается в том, чтобы обратиться к статическому члену напрямую, квалифицировав его имя именем класса:

    // доступ к статическому члену с указанием квалифицированного имени

    Если обращение к статическому члену производится без помощи оператора доступа, то его имя следует квалифицировать именем класса, за которым следует оператор разрешения области видимости:

    Это необходимо, поскольку такой член не является глобальным объектом, а значит, в глобальной области видимости отсутствует. Следующее определение дружественной функции compareRevenue эквивалентно приведенному выше:

    Уникальная особенность статического члена – то, что он существует независимо от объектов класса, – позволяет использовать его такими способами, которые для нестатических членов недопустимы.

    • статический член может принадлежать к типу того же класса, членом которого он является. Нестатические объявляются лишь как указатели или ссылки на объект своего класса:
    • статический член может выступать в роли аргумента по умолчанию для функции-члена класса, а для нестатического это запрещено:

    13.5.1. Статические функции-члены

    Функции-члены raiseInterest() и interest() обращаются к глобальному статическому члену _interestRate:

    Проблема в том, что любая функция-член должна вызываться с помощью оператора доступа к конкретному объекту класса. Поскольку приведенные выше функции обращаются только к статическому _interestRate, то совершенно безразлично, для какого объекта они вызываются. Нестатические члены при вызове этих функций не читаются и не модифицируются.

    Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом:

    Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile запрещены. В ее определении, находящемся вне тела класса, слова static быть не должно.

    Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.

    Статическую функцию-член можно вызвать для объекта класса, пользуясь одним из операторов доступа. Ее также можно вызвать непосредственно, квалифицировав ее имя, даже если никаких объектов класса не объявлено. Вот небольшая программа, иллюстрирующая их применение:

    Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами:

    Инициализируйте _xval значением 20, а _callsXval значением 0.

    Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval().

    Какие из следующих объявлений и определений статических членов ошибочны? Почему?

    13.6. Указатель на член класса

    Предположим, что в нашем классе Screen определены четыре новых функции-члена: forward(), back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса:

    Функции-члены forward() и back() перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол.

    end() перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home():

    Функции up() и down() перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал:

    row() – это закрытая функция-член, которая используется в функциях up() и down(), возвращая номер строки, где находится курсор:

    Пользователи класса Screen попросили нас добавить функцию repeat(), которая повторяет указанное действие n раз. Ее реализация могла бы выглядеть так:

    Такая реализация имеет ряд недостатков. В частности, предполагается, что функции-члены класса Screen останутся неизменными, поэтому при добавлении или удалении функции-члена repeat() необходимо модифицировать. Вторая проблема – размер функции. Поскольку приходится проверять все возможные функции-члены, то исходный текст становится громоздким и неоправданно сложным.

    В более общей реализации параметр op заменяется параметром типа указателя на функцию-член класса Screen. Теперь repeat() не должна сама устанавливать, какую операцию следует выполнить, и всю инструкцию switch можно удалить. Определение и использование указателей на члены класса – тема последующих подразделов.

    13.6.1. Тип члена класса

    Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:

    Если имеются глобальные функции HeightIs() и WidthIs() вида:

    то допустимо присваивание pfi адреса любой из этих переменных:

    В классе Screen также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:

    Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:

    // неверное присваивание: нарушение типизации

    В чем нарушение? У функций-членов есть дополнительный атрибут типа, отсутствующий у функций, не являющихся членами, – класс. Указатель на функцию-член должен соответствовать типу присваиваемой ему функции не в двух, а в трех отношениях: по типу и количеству формальных параметров; типу возвращаемого значения; типу класса, членом которого является функция.

    Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член должен быть сначала привязан к объекту или указателю на объект, чтобы получить this, и только после этого он применяется для вызова функции-члена. (В следующем подразделе мы покажем, как осуществить такую привязку.) Хотя для указателя на обычную функцию и для указателя на функцию-член используется один и тот же термин, их природа различна.

    Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height класса Screen. Его полный тип таков: член класса Screen типа short. Следовательно, полный тип указателя на _height – это указатель на член класса Screen типа short:

    Определение указателя на член класса Screen типа short выглядит следующим образом:

    Переменную ps_Screen можно инициализировать адресом _height:

    или присвоить ей адрес _width:

    Переменной ps_Screen разрешается присваивать указатель на _width или _height, так как они являются членами класса Screen типа short.

    Несоответствие типов указателя на данные-члены и обычного указателя также связано с различием в их представлении. Обычный указатель содержит всю информацию, необходимую для обращения к объекту. Указатель на данные-члены следует сначала привязать к объекту или указателю на него, а лишь затем использовать для доступа к члену этого объекта. (В книге “Inside the C++ Object Model” ([LIPPMAN96a]) также описывается представление указателей на члены.)

    Указатель на функцию-член определяется путем задания типа возвращаемого функцией значения, списка ее параметров и класса. Например, следующий указатель, с помощью которого можно вызвать функции height() и width(), имеет тип указателя на функцию-член класса Screen без параметров, которая возвращает значение типа int:

    Указатели на функции-члены можно объявлять, инициализировать и присваивать:

    Использование typedef может облегчить чтение объявлений указателей на члены. Например, для типа “указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е.

    Следующий typedef определяет Action как альтернативное имя:

    Тип «указатель на функцию-член» можно использовать для объявления формальных параметров и типа возвращаемого значения функции. Для параметра того же типа можно также указать значение аргумента по умолчанию:

    action() объявлена как принимающая два параметра: ссылку на объект класса Screen и указатель на функцию-член Screen без параметров, которая возвращает ссылку на его объект. Вызвать action() можно любым из следующих способов:

    В следующем подразделе обсуждается вызов функции-члена посредством указателя.

    13.6.2. Работа с указателями на члены класса

    К указателям на члены класса можно обращаться только с помощью конкретного объекта или указателя на объект типа класса. Для этого применяется любой из двух операторов доступа (.* для объектов класса и ссылок на них или ->* для указателей). Например, так вызывается функция-член через указатель на нее:

    требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок

    Это означает вызов функции pmfi() и привязку возвращенного ей значения к оператору (.*). Разумеется, тип pmfi не поддерживает такого использования, так что компилятор выдаст сообщение об ошибке.

    Указатели на данные-члены используются аналогично:

    Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член:

    Параметр op – это указатель на функцию-член, которая должна вызываться times раз.

    Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:

    А ее вызовы так:

    Определим таблицу указателей. В следующем примере Menu – это таблица указателей на функции-члены класса Screen, которые реализуют перемещение курсора. CursorMovements – перечисление, элементами которого являются номера в таблице Menu.

    Можно определить перегруженную функцию-член move(), которая принимает параметр CursorMovements и использует таблицу Menu для вызова указанной функции-члена. Вот ее реализация:

    У оператора взятия индекса ([]) приоритет выше, чем у оператора указателя на функцию-член (->*). Первая инструкция в move() сначала по индексу выбирает из таблицы Menu нужную функцию-член, которая и вызывается с помощью указателя this и оператора указателя на функцию-член. move() можно применять в интерактивной программе, где пользователь выбирает вид перемещения курсора из отображаемого на экране меню.

    13.6.3. Указатели на статические члены класса

    Между указателями на статические и нестатические члены класса есть разница. Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу. Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)

    Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:

    Тип &_interestRate – это double*:

    Определение указателя на &_interestRate имеет вид:

    Этот указатель разыменовывается так же, как и обычный, объект класса для этого не требуется:

    Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().

    Указатель на interest() – это обычный указатель на функцию:

    Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей:

    К какому типу принадлежат члены _screen и _cursor класса Screen?

    Определите указатель на член и инициализируйте его значением Screen::_screen; присвойте ему значение Screen::_cursor.

    Определите typedef для каждой из функций-членов класса Screen.

    Указатели на члены можно также объявлять как данные-члены класса. Модифицируйте определение класса Screen так, чтобы оно содержало указатель на его функцию-член того же типа, что home() и end().

    Модифицируйте имеющийся конструктор класса Screen (или напишите новый) так, чтобы он принимал параметр типа указателя на функцию-член класса Screen, для которой список формальных параметров и тип возвращаемого значения такие же, как у home() и end(). Реализуйте для этого параметра значение по умолчанию и используйте параметр для инициализации члена класса, описанного в упражнении 13.14. Напишите функцию-член Screen, позволяющую пользователю задать ее значение.

    Определите перегруженный вариант repeat(), который принимает параметр типа cursorMovements.

    13.7. Объединение – класс, экономящий память

    Объединение – это специальный вид класса. Данные-члены хранятся в нем таким образом, что перекрывают друг друга. Все члены размещаются, начиная с одного и того же адреса. Для объединения отводится столько памяти, сколько необходимо для хранения самого большого его члена. В любой момент времени можно присвоить значение лишь одному такому члену.

    Рассмотрим пример, иллюстрирующий использование объединения. Лексический анализатор, входящий в состав компилятора, разбивает программу на последовательность лексем. Так, инструкция

    преобразуется в последовательность из пяти лексем:

    1. Ключевое слово int.
    2. Идентификатор i.
    3. Оператор =
    4. Константа 0 типа int.
    5. Точка с запятой.

    Лексический анализатор передает эти лексемы синтаксическому анализатору, парсеру, который идентифицирует полученную последовательность. Полученная информация должна дать парсеру возможность распознать эту последовательность лексем как объявление. Для этого с каждой лексемой ассоциируется информация, позволяющая парсеру увидеть следующее:

    (Тип ИД Присваивание Константа Точка с запятой)

    Далее парсер анализирует значения каждой лексемы. В данном случае он видит:

    Для Assign и Semicolon дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;

    Таким образом, в представлении лексемы могло бы быть два члена – token и value. token – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign, Constant или Semicolon, например 85 для ID и 72 для Semicolon.value содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value будет содержать строку «i», а для лексемы Type – некоторое представление типа int.

    Представление члена value несколько проблематично. Хотя для любой отдельной лексемы в нем хранится всего одно значение, их типы для разных лексем могут различаться. Для лексемы ID в value хранится строка символов, а для Constant – целое число.

    Конечно, для хранения данных нескольких типов можно использовать класс. Разработчик компилятора может объявить, что value принадлежит к типу класса, в котором для каждого типа данных есть отдельный член.

    Применение класса решает проблему представления value. Однако для любой данной лексемы value имеет лишь один из множества возможных типов и, следовательно, будет задействован только один член класса, хотя памяти выделяется столько, сколько нужно для хранения всех членов. Чтобы память резервировалась только для нужного в данный момент члена, применяется объединение. Вот как оно определяется:

    Если самым большим типом среди всех членов TokenValue является dval, то размер TokenValue будет равен размеру объекта типа double. По умолчанию члены объединения открыты. Имя объединения можно использовать в программе всюду, где допустимо имя класса:

    Обращение к членам объединения, как и к членам класса, производится с помощью операторов доступа:

    Члены объединения можно объявлять открытыми, закрытыми или защищенными:

    У объединения не бывает статических членов или членов, являющихся ссылками. Его членом не может быть класс, имеющий конструктор, деструктор или копирующий оператор присваивания. Например:

    Для объединения разрешается определять функции-члены, включая конструкторы и деструкторы:

    Вот пример работы объединения TokenValue:

    Объект типа Token можно использовать так:

    Опасность, связанная с применением объединения, заключается в том, что можно случайно извлечь хранящееся в нем значение, пользуясь не тем членом. Например, если в последний раз значение присваивалось _ival, то вряд ли понадобится значение, оказавшееся в _sval. Это, по всей вероятности, приведет к ошибке в программе.

    Чтобы защититься от подобного рода ошибок, следует создать дополнительный объект, дискриминант объединения, определяющий тип значения, которое в данный момент хранится в объединении. В классе Token роль такого объекта играет член tok:

    При работе с объединением, являющимся членом класса, полезно иметь набор функций для каждого хранящегося в объединении типа данных:

    Имя в определении объединения задавать необязательно. Если оно не используется в программе как имя типа для объявления других объектов, его можно опустить. Например, следующее определение объединения Token эквивалентно приведенному выше, но без указания имени:

    Существует анонимное объединение – объединение без имени, за которым не следует определение объекта. Вот, например, определение класса Token, содержащее анонимное объединение:

    К данным-членам анонимного объединения можно напрямую обращаться в той области видимости, в которой оно определено. Перепишем функцию lex(), используя предыдущее определение:

    Анонимное объединение позволяет убрать один уровень доступа, поскольку обращение к его членам идет как к членам класса Token. У него не может быть закрытых или защищенных членов, а также функций-членов. Такое объединение, определенное в глобальной области видимости, должно быть объявлено в безымянном пространстве имен или иметь модификатор static.

    13.8. Битовое поле – член, экономящий память


    Для хранения заданного числа битов можно объявить член класса специального вида, называемый битовым полем. Он должен иметь целый тип данных, со знаком или без знака:

    После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.

    Битовые поля, определенные в теле класса подряд, по возможности упаковываются в соседние биты одного целого числа, делая хранение объекта более компактным. Так, в следующем объявлении пять битовых полей будут содержаться в одном числе типа unsigned int, ассоциированном с первым полем mode:

    Доступ к битовому полю осуществляется так же, как к прочим членам класса. Скажем, к битовому полю, являющемуся закрытым членом класса, можно обратиться лишь из функций-членов и друзей этого класса:

    Вот простой пример использования битового поля длиной больше 1 (примененные здесь побитовые операции рассматривались в разделе 4.11):

    Обычно для проверки значения битового поля-члена определяются встроенные функции-члены. Допустим, в классе File можно ввести члены isRead() и isWrite():

    С помощью таких функций-членов битовые поля можно сделать закрытыми членами класса File.

    К битовому полю нельзя применять оператор взятия адреса (&), поэтому не может быть и указателя на подобные поля-члены. Кроме того, полю запрещено быть статическим членом.

    В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)

    Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и прямого манипулирования битовыми полями использовался класс bitset и его операторы.

    13.9. Область видимости класса A

    Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.

    Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)

    Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:

    Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:

    Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.

    Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]() определен как встроенный внутри тела класса:

    На первом этапе просматриваются имена, использованные в объявлении operator[](), чтобы найти имя типа параметра index_type. Поскольку первый шаг выполняется тогда, когда в теле класса встретилось определение функции-члена, то имя index_type должно быть объявлено до определения operator[]().

    Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.

    Аргументы по умолчанию также разрешаются на втором шаге. Например, в объявлении функции-члена clear() используется имя статического члена bkground, который определен позже:

    Хотя такие аргументы в объявлениях функций-членов разрешаются во всей области видимости класса, программа будет считаться ошибочной, если он ссылается на нестатический член. Нестатический член должен быть привязан к объекту своего класса или к указателю на такой объект, иначе использовать его нельзя. Употребление подобных членов в качестве аргументов по умолчанию нарушает это ограничение. Если переписать предыдущий пример так:

    то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.

    Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?

    Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора operator[]() из класса String:

    Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String. Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.

    Определения статических данных-членов также появляются вне определения класса. В них часть программы, следующая за именем статического члена вплоть до конца определения, считается находящейся в области видимости класса. Например, инициализатор статического члена может непосредственно, без соответствующих операторов, ссылаться на члены класса:

    Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.

    Не только инициализатор, но и все, что следует за именем статического члена _interestRate до завершающей точки с запятой, находится в области видимости класса Account. Поэтому в определении статического члена name может быть обращение к члену класса nameSize:

    Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.

    В определении члена, которое появляется вне тела, часть программы перед определяемым именем не находится в области видимости класса. При обращении к члену в этой части следует пользоваться оператором разрешения области видимости. Например, если типом статического члена является typedef Money, определенный в классе Account, то имя Money должно быть квалифицировано, когда статический член данных определяется вне тела класса:

    // Money должно быть квалифицировано именем класса Account::

    С каждым классом ассоциируется отдельная область видимости, причем у разных классов эти области различны. К членам одного класса нельзя напрямую обращаться в определениях членов другого класса, если только один из них не является для второго базовым. (Наследование и базовые классы рассматриваются в главах 17 и 18.)

    13.9.1. Разрешение имен в области видимости класса

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

    Имя, использованное внутри определения класса (за исключением определений встроенных функций-членов и аргументов по умолчанию), разрешается следующим образом:

    1. Просматриваются объявления членов класса, появляющиеся перед употреблением имени.
    2. Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости – это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)

    Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().

    Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:

    1. Сначала просматриваются объявления в локальных областях видимости функции-члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)
    2. Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.
    3. Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.

    Имена, встречающиеся в теле встроенной функции-члена, разрешаются так:

    В поисках объявления имени _height, которое встретилось в определении конструктора Screen, компилятор просматривает локальную область видимости функции и находит его там. Следовательно, это имя относится к объявлению параметра.

    Если бы такое объявление не было найдено, компилятор начал бы поиск в области видимости класса Screen, просматривая все объявления его членов, пока не встретится объявление члена _height. Говорят, что имя члена _height скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если квалифицировать имя члена именем его класса или явно использовать указатель this:

    Если бы не были найдены ни объявление параметра, ни объявление члена, компилятор стал бы искать их в объемлющих областях видимости пространств имен. В нашем примере в глобальной области видимости просматриваются объявления, которые расположены перед определением класса Screen. В результате было бы найдено объявление глобального объекта _height. Говорят, что такой объект скрыт за объявлением члена класса, однако его можно использовать в теле конструктора, если квалифицировать оператором разрешения глобальной области видимости:

    Если конструктор объявлен вне определения класса, то на третьем шаге разрешения имени просматриваются объявления в глобальной области видимости, которые встретились перед определением класса Screen, а также перед определением функции-члена:

    Обратите внимание, что объявление глобальной функции verify() невидимо до определения класса Screen. Однако на третьем шаге разрешения имени просматриваются объявления в областях видимости пространств имен, видимые перед определением члена, поэтому нужное объявление обнаруживается.

    Имя, встретившееся в определении статического члена класса, разрешается следующим образом:

    1. Просматриваются объявления всех членов класса.
    2. Если шаг 1 не привел к успеху, то просматриваются объявления, расположенные в областях видимости пространств имен перед определением статического члена, а не только предшествующие определению класса.

    Назовите те части программы, которые находятся в области видимости класса.

    Назовите те части программы, которые находятся в области видимости класса и для которых при разрешении имен просматривается полная область (т.е. принимаются во внимание все члены, объявленные в теле класса).

    К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при употреблении в определении функции-члена setVal()?

    Определение функции-члена setVal() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal().

    13.10. Вложенные классы A

    Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, а его определение может находиться в любой из секций public, private или protected объемлющего класса.

    Имя вложенного класса известно в области видимости объемлющего класса, но ни в каких других областях. Это означает, что оно не конфликтует с таким же именем, объявленным в объемлющей области видимости. Например:

    Для вложенного класса допустимы такие же виды членов, как и для невложенного:

    Закрытым называется член, который доступен только в определениях членов и друзей класса. У объемлющего класса нет права доступа к закрытым членам вложенного. Чтобы в определениях членов List можно было обращаться к закрытым членам ListItem, класс ListItem объявляет List как друга. Равно и вложенный класс не имеет никаких специальных прав доступа к закрытым членам объемлющего класса. Если бы нужно было разрешить ListItem доступ к закрытым членам класса List, то в объемлющем классе List следовало бы объявить вложенный класс как друга. В приведенном выше примере этого не сделано, поэтому ListItem не может обращаться к закрытым членам List.

    Объявление ListItem открытым членом класса List означает, что вложенный класс можно использовать как тип во всей программе, в том числе и за пределами определений членов и друзей класса. Например:

    Это дает более широкую область видимости, чем мы планировали. Вложенный ListItem поддерживает абстракцию класса List и не должен быть доступен во всей программе. Поэтому лучше объявить вложенный класс ListItem закрытым членом List:

    Теперь тип ListItem доступен только из определений членов и друзей класса List, поэтому все члены класса ListItem можно сделать открытыми. При таком подходе объявление List как друга ListItem становится ненужным. Вот новое определение класса List:

    Конструктор ListItem не задан как встроенный внутри определения класса и, следовательно, должен быть определен вне него. Но где именно? Конструктор класса ListItem не является членом List и, значит, не может быть определен в теле последнего; его нужно определить в глобальной области видимости – той, которая содержит определение объемлющего класса. Когда функция-член вложенного класса не определяется как встроенная в теле, она должна быть определена вне самого внешнего из объемлющих классов.

    Вот как могло бы выглядеть определение конструктора ListItem. Однако показанный ниже синтаксис в глобальной области видимости некорректен:

    Проблема в том, что имя ListItem отсутствует в глобальной области видимости. При использовании его таким образом следует указывать, что ListItem – вложенный класс в области видимости List. Это делается путем квалификации имени ListItem именем объемлющего класса. Следующая конструкция синтаксически правильна:

    Заметим, что квалифицировано только имя вложенного класса. Первый квалификатор List:: именует объемлющий класс и квалифицирует следующее за ним имя вложенного ListItem. Второе вхождение ListItem – это имя конструктора, а не вложенного класса. В данном определении имя члена некорректно:

    Если бы внутри ListItem был объявлен статический член, то его определение также следовало бы поместить в глобальную область видимости. Имя этого члена могло бы выглядеть так:

    Обратите внимание, что функции-члены и статические данные-члены не обязаны быть открытыми членами вложенного класса для того, чтобы их можно было определить вне его тела. Закрытые члены ListItem также определяются в глобальной области видимости.

    Вложенный класс разрешается определять вне тела объемлющего. Например, определение ListItem могло бы находиться и в глобальной области видимости:

    В глобальном определении имя вложенного ListItem должно быть квалифицировано именем объемлющего класса List. Заметьте, что объявление ListItem в теле List опустить нельзя. Определение вложенного класса не может быть задано в глобальной области видимости, если предварительно оно не было объявлено членом объемлющего класса. Но при этом вложенный класс не обязательно должен быть открытым членом объемлющего.

    Пока компилятор не увидел определения вложенного класса, разрешается объявлять лишь указатели и ссылки на него. Объявления членов list и at_end класса List правильны несмотря на то, что ListItem определен в глобальной области видимости, поскольку оба члена – указатели. Если бы один из них был объектом, то его объявление в классе List привело бы к ошибке компиляции:

    Зачем определять вложенный класс вне тела объемлющего? Возможно, он поддерживает некоторые детали реализации ListItem, а нам нужно скрыть их от пользователей класса List. Поэтому мы помещаем определение вложенного класса в заголовочный файл, содержащий интерфейс List. Таким образом, определение ListItem может находиться лишь внутри исходного файла, включающего реализацию класса List и его членов.

    Вложенный класс можно сначала объявить, а затем определить в теле объемлющего. Это позволяет иметь во вложенных классах члены, ссылающиеся друг на друга:

    Если бы ListItem не был объявлен перед определением класса Ref, то объявление члена pli было бы ошибкой.

    Вложенный класс не может напрямую обращаться к нестатическим членам объемлющего, даже если они открыты. Любое такое обращение должно производиться через указатель, ссылку или объект объемлющего класса. Например:

    При использовании нестатических членов класса компилятор должен иметь возможность идентифицировать объект, которому принадлежит такой член. Внутри функции-члена класса ListItem указатель this неявно применяется лишь к его членам. Благодаря неявному this мы знаем, что член value относится к объекту, для которого вызван конструктор. Внутри конструктора ListItem указатель this имеет тип ListItem*. Для доступа же к функции-члену init() нужен объект типа List или указатель типа List*.

    Следующая функция-член mf() обращается к init() с помощью параметра-ссылки. Таким образом, init() вызывается для объекта, переданного в аргументе функции:

    Хотя для доступа к нестатическим членам объемлющего класса нужен объект, указатель или ссылка, к статическим его членам, именам типов и элементам перечисления вложенный класс может обращаться напрямую (если, конечно, эти члены открыты). Имя типа – это либо имя typedef, либо имя перечисления, либо имя класса. Например:

    pFunc, ListStatus и ListItem – все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно обращаться в области видимости класса ListItem даже без квалификации:

    Вне области видимости ListItem и List при обращении к статическим членам, именам типов и элементам перечисления объемлющего класса требуется оператор разрешения области видимости:

    При обращении к элементам перечисления мы не пишем:

    поскольку они доступны непосредственно в той области видимости, в которой определено само перечисление. Почему? Потому что с ним, в отличие от класса, не связана отдельная область.

    13.10.1. Разрешение имен в области видимости вложенного класса

    Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.

    Имя, встречающееся в определении вложенного класса (кроме тех, которые употребляются во встроенных функциях-членах и аргументах по умолчанию) разрешается следующим образом:

    1. Просматриваются члены вложенного класса, расположенные перед употреблением имени.
    2. Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени.
    3. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.

    Сначала компилятор ищет объявление ListStatus в области видимости класса ListItem. Поскольку его там нет, поиск продолжается в области видимости List, а затем в глобальной. При этом во всех трех областях просматриваются только объявления, предшествующие использованию ListStatus. В конце концов находится глобальное объявление перечисления ListStatus – оно и будет типом, использованным в объявлении status.

    Если вложенный класс ListItem определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены:

    При разрешении имени ListStatus сначала просматривается область видимости класса ListItem. Поскольку там его нет, поиск продолжается в области видимости List. Так как полное определение класса List уже встречалось, просматриваются все члены этого класса. Вложенное перечисление ListStatus найдено несмотря даже на то, что оно объявлено после объявления ListItem. Таким образом, status объявляется как указатель на данное перечисление в классе List. Если бы в List не было члена с таким именем, поиск был бы продолжен в глобальной области видимости среди тех объявлений, которые предшествуют определению класса ListItem.

    Имя, встретившееся в определении функции-члена вложенного класса, разрешается следующим образом:

    1. Сначала просматриваются локальные области видимости функции-члена.
    2. Если шаг 1 не привел к успеху, то просматриваются объявления всех членов вложенного класса.
    3. Если имя еще не найдено, то просматриваются объявления всех членов объемлющего класса.
    4. Если и этого недостаточно, то просматриваются объявления, появляющиеся в области видимости пространства имен перед определением функции-члена.

    Какое объявление относится к имени list в определении функции-члена check_status() в следующем фрагменте кода:

    Весьма вероятно, что при использовании list внутри check_status() программист имел в виду глобальный объект:

    • и value, и глобальный объект list имеют тип int. Член List::list объявлен как указатель и не может быть присвоен value без явного приведения типа;
    • ListItem не имеет прав доступа к закрытым членам объемлющего класса, в частности list;
    • list – это нестатический член, и обращение к нему в функциях-членах ListItem должно производиться через объект, указатель или ссылку.

    Однако, несмотря на все это, имя list, встречающееся в функции-члене check_status(), разрешается в пользу члена list класса List. Напоминаем, что если имя не найдено в области видимости вложенного ListItem, то далее просматривается область видимости объемлющего класса, а не глобальная. Член list в List скрывает глобальный объект. А так как использование указателя list в check_status() недопустимо, то выводится сообщение об ошибке.

    Права доступа и совместимость типов проверяются только после того, как имя разрешено. Если при этом обнаруживается ошибка, то выдается сообщение о ней и дальнейший поиск объявления, которое было бы лучше согласовано с именем, уже не производится. Для доступа к глобальному объекту list следует использовать оператор разрешения области видимости:

    Если бы функция-член check_status() была определена как встроенная в теле класса ListItem, то последнее объявление привело бы к выдаче сообщения об ошибке из-за того, что имя list не объявлено в глобальной области видимости:

    Глобальный объект list объявлен после определения класса List. Во встроенной функции-члене, определенной внутри тела класса, рассматриваются только те глобальные объявления, которые были видны перед определением объемлющего класса. Если же определение check_status() следует за определением List, то рассматриваются глобальные объявления, расположенные перед ним, поэтому будет найдено глобальное определение объекта list.

    В главе 11 был приведен пример программы, использующей класс iStack. Измените его, объявив классы исключений pushOnFull и popOnEmpty открытыми вложенными в iStack. Модифицируйте соответствующим образом определение класса iStack и его функций-членов, а также определение main().

    13.11. Классы как члены пространства имен A

    Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например:

    Как было показано в двух предыдущих разделах, член класса (функция-член, статический член или вложенный класс) может быть определен вне его тела. Если мы реализуем библиотеку и помещаем определения наших классов в объявленное пользователем пространство имен, то где расположить определения членов, находящиеся вне тел своих классов? Их можно разместить либо в пространстве имен, которое содержит определение самого внешнего класса, либо в одном из объемлющих его пространств. Это дает возможность организовать код библиотеки следующим образом:

    Члены вложенного класса ListItem можно определить в пространстве имен cplusplus_primer, которое содержит определение List, или в глобальном пространстве, включающем определение cplusplus_primer. В любом случае имя члена в определении должно быть квалифицировано именами объемлющих классов и объявленных пользователем пространств, вне которых находится объявление члена.

    Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal:

    Сначала просматриваются локальные области видимости в определении функции-члена, затем поиск продолжается в области видимости ListItem, затем – в области видимости List. До этого момента все происходит так же, как в процессе разрешения имен, описанном в разделе 13.10. Далее просматриваются объявления из пространства cplusplus_primer и наконец объявления в глобальной области видимости, причем во внимание принимаются только те, которые расположены до определения функции-члена action():

    Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() – часть интерфейса cplusplus_primer, ее следовало бы объявить в той части данного пространства, которая находится в заголовочном файле:

    Если же calc() используется только в action() и не является частью интерфейса пространства имен, то ее нужно объявить перед action(), чтобы можно было ссылаться на нее внутри определения action().

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

    Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя action() в предыдущем примере квалифицируется так:

    Квалификаторы cplusplus_primer::List::ListItem:: записаны в порядке, обратном тому, в котором просматриваются имена областей видимости классов и пространств имен. Сначала поиск ведется в области ListItem, затем продолжается в объемлющем классе List и наконец в пространстве cplusplus_primer, предшествующем той области, в которой находится определение action(). Во время поиска в любой области видимости класса просматриваются все объявления членов, а в любом пространстве имен – только те объявления, которые встречались перед определением члена.

    Класс, определенный в области видимости пространства имен, потенциально виден во всей программе. Если заголовочный файл primer.h включен в несколько исходных файлов, то имя cplusplus_primer::List везде относится к одному и тому же классу. Класс – это сущность, для которой в программе может быть более одного определения. Определение класса должно присутствовать один раз в каждом исходном файле, где определяются или используются сам класс или его члены. Однако оно должно быть одинаковым во всех файлах, где встречается, поэтому его следует помещать в заголовочный файл, например primer.h. Затем такой файл можно включать в любой исходный, где определяются или используются члены класса. Это предотвратит несоответствия в случае, когда определение класса записывается более одного раза.

    Невстроенные функции-члены и статические данные-члены класса в пространстве имен – это также программные сущности. Однако они могут быть определены лишь один раз во всей программе. Поэтому их определения помещаются не в заголовочный, а в отдельный исходный файл типа primer.C.

    Используя класс iStack, определенный в упражнении 13.21, объявите классы исключений pushOnFull и popOnEmpty как члены пространства имен LibException:

    а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main().

    13.12. Локальные классы A

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

    Поскольку невозможно определить член локального класса в области видимости пространства имен, то в таком классе не бывает статических членов.

    Класс, вложенный в локальный, может быть определен вне определения объемлющего класса, но только в локальной области видимости, содержащей это определение. Имя вложенного класса в таком определении должно быть квалифицировано именем объемлющего класса. Объявление вложенного класса в объемлющем нельзя опускать:

    У объемлющей функции нет никаких специальных прав доступа к закрытым членам локального класса. Разумеется, это можно обойти, объявив ее другом данного класса. Однако необходимость делать его члены закрытыми вообще сомнительна, поскольку часть программы, из которой разрешается обратиться к нему, весьма ограничена. Локальный класс инкапсулирован в своей локальной области видимости. Дальнейшая инкапсуляция путем сокрытия информации не требуется: вряд ли на практике найдется причина, по которой не все члены локального класса должны быть открыты.

    У локального класса, как и у вложенного, ограничен доступ к именам из объемлющей области видимости. Он может обратиться только к именам типов, статических переменных и элементов перечислений, определенных в объемлющих локальных областях. Например:

    Имена в теле локального класса разрешаются лексически путем поиска в объемлющих областях видимости объявлений, предшествующих определению такого класса. При разрешении имен, встречающихся в телах его функций-членов, сначала просматривается область видимости класса, а только потом – объемлющие области,

    Как всегда, если первое найденное объявление таково, что употребление имени оказывается некорректным, поиск других объявлений не производится. Несмотря на то что использование val в fooBar() выше является ошибкой, глобальная переменная val не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости.

    Спасибо за материал

    Отличный текст и всё нужно в одном месте. Часто пользуюсь! Спасибо.

    Функции класса/объекта

    Эта статья даст базовое понимание терминов «класс», «метод», «наследование», «перегрузка метода»

    Содержание

    Методы

    Методы — это функции, объявление которых размещено внутри определения класса или структуры. В список переменных, доступных для метода, неявно попадают все поля структуры или класса, в котором он объявлен. Другими словами, в список областей видимости метода попадает область видимости структуры.

    Взгляните на пример:

    Методу Vec2f::getLength доступны все символы (т.е. переменные, функции, типы данных), которые были объявлены в одной из трёх областей видимости. При наличии символов с одинаковыми идентификаторами один символ перекрывает другой, т.к. поиск происходит от внутренней области видимости к внешней.

    Понять идею проще на схеме. В ней область видимости названа по-английски: scope.

    Поднимаясь по схеме от внутренней области видимости к внешней, легко понять, какие имена символов доступны в методе getLength:

    1. локальная переменная “lengthSquare”
    2. поля Vec2f под именами “x” и “y”
    3. всё, что есть в глобальной области видимости

    К слову, в других методах структуры Vec2f переменная “lengthSquare” будет недоступна, а поля “x” и “y” будут доступны.

    Конструкторы

    Конструктор — это специальный метод, который вызывается автоматически при выполнении инструкции объявления переменной. При этом память под переменную уже выделена заранее, т.к. память под все локальные переменные выделяется на стеке программы в момент вызова функции. Конструктор позволяет выполнить сложный код для инициализации переменной.

    Посмотрите на простой пример. В нём есть проблема: и поля, и параметры конструктора названы одинаково. В результате в области видимости конструктора доступны только параметры, и своими именами они перекрывают поля!

    Язык C++ предлагает два решения. Первый способ — использовать косвенное обращение к полям через привязанный к методу объект. Указатель на него доступен по ключевому слову this :


    Второй путь считается более правильным: мы используем специальную возможность конструкторов — “списки инициализации конструктора” (англ. constructor initializer lists). Списки инициализации — это список, разделённый запятыми и начинающийся с “:”. Элемент списка инициализации выглядит как field(expression) , т.е. для каждого выбранного программистом поля можно указать выражение, инициализирующее его. Имя переменной является выражением. Поэтому мы инициализируем поле его параметром:

    Объявление и определение методов

    C++ требует, чтобы каждый метод структуры или класса был упомянут в определении этой структуры или класса. Но допускается писать лишь объявление метода, о определение размещать где-нибудь в другом месте:

    Классы и структуры

    В C++ есть ключевое слово class — это практически аналог ключевого слова struct . Оба ключевых слова объявляют тип данных, и разница между ними есть только на стыке наследования и инкапсуляции. Других различий class и struct не существует.

    Основы инкапсуляции

    В C++ можно блокировать доступ к полям извне, но сохранять доступ для методов. Для этого введены три области доступа

    1. public — символ в этой области доступен извне
    2. private — символ из этой области доступен лишь собственных в методах
    3. protected — используется редко, о нём можете прочитать в документации

    Давайте сделаем поля типа Vec2f недоступными извне. Также мы заменим ключевое слово struct на class — это не меняет смысла программы, но считается хорошим тоном использовать struct только если все поля доступны публично.

    Запомните несколько хороших правил:

    • Используйте struct, если все поля публичные и не зависят друг от друга; используйте class, если между полями должны соблюдаться закономерности (например, поле “площадь” круга должно быть)

    Основы наследования

    В C++ новый тип может наследовать все поля и методы другого типа. Для этого достаточно указать структуру или класс в списке базовых типов. Такой приём используется в SFML при объявлении классов фигур:

    Что означает public перед именем базового типа? Во-первых внешний код может передать RectangleShape в функцию, принимающую ссылку на Shape, то есть возможен так называемы upcast от более низкого (и более конкретного) типа RectangleShape к более высокому (и более абстрактному) типу Shape:

    Во-вторых из-за public наследования все унаследованные поля и методы сохраняют свой уровень доступ: приватные остаются приватными, публичные остаются публичными. А если бы мы наследовали Shape с ключевым словом private, то уровень доступа стал бы ниже: все методы и поля стали бы приватными:

    Контроль уровня доступа полей и методов — хитрый механизм, пройдёт немало времени, прежде чем вы научитесь пользоваться им правильно. В начале просто старайтесь сделать правильный выбор между private и public. Скорее всего поля будут private, а конструктор и все методы будут public. Это позволяет сохранять инвариант класса, то есть держать поля объекта в согласованном состоянии независимо от того, какие методы вызывают извне.

    Основы полиморфизма: виртуальные методы и их перегрузка

    SFML использует ещё одну идиому C++: виртуальные методы. Ключевые слова virtual , final , override относятся именно к этой идиоме. Например, в SFML определяется класс Drawable, который обозначает “сущность, которую можно нарисовать”. Все рисуемые классы SFML, включая sf::Sprite , sf::RectangleShape , sf::Text , прямо или косвенно наследуются от sf::Drawable .

    Зачем это надо? Дело в том, что метод draw класса RenderWindow принимает параметр типа Drawable . Тем не менее, этот метод успешно рисует любые типы объектов: спрайты, фигуры, тексты. Он не выполняет проверок — он просто настраивает состояние рисования (RenderStates) и вызывает метод draw у сущности, которая является Drawable .

    Виртуальный метод вызывается косвенно: если класс Shape , унаследованный от Drawable , переопределил метод, а потом был передан как параметр типа Drawable , то вызов метода draw всё равно приведёт к вызову переопределённого метода Shape::draw , а не метода Drawable::draw ! С обычными (не виртуальными) методами такого не происходит: если бы мы убрали слово virtual из объявления draw , то вызов метода draw у параметра типа Drawable всегда приводил бы к вызову Drawable::draw , даже если реальный тип объекта, скрытого за этим параметром, совсем другой.

    Возможность по-разному реагировать на одинаковый вызов метода извне называется полиморфизмом. Точнее, это одна из разновидностей полиморфизма объектов.

    Другими словами, RenderWindow и RectangleShape не знают, что они работают друг с другом, но тем не менее каждый вызывает правильный метод другого класса!

    Когда вы просто вызываете window.draw(shape) , повышение класса происходит дважды: сначала конкретный класс фигуры повышается до более ограниченного класса Drawable, затем конкретный класс RenderWindow повышается до абстрактного RenderTarget. Всё это не требует времени при выполнении: просто компилятор выполняет проверки типов данных ещё при компиляции, не более того.

    Как унаследовать Drawable: практический пример

    Мы создадим класс, который рисует флаг России. Он будет унаследован от Drawable, чтобы использовать для рисования обычный метод draw у объекта окна.

    Теперь мы можем реализовать конструктор и метод draw. В конструкторе мы должны вычислить и установить позиции и размеры трёх полос на флаге, а в методе draw мы должны их последовательно нарисовать.

    Теперь использовать класс RussianFlag извне очень легко!

    Разработка интерфейсных классов на С++

    Интерфейсные классы весьма широко используются в программах на C++. Но, к сожалению, при реализации решений на основе интерфейсных классов часто допускаются ошибки. В статье описано, как правильно проектировать интерфейсные классы, рассмотрено несколько вариантов. Подробно описано использование интеллектуальных указателей. Приведен пример реализации класса исключения и шаблона класса коллекции на основе интерфейсных классов.

    Оглавление

    Введение

    Интерфейсным классом называется класс, не имеющий данных и состоящий в основном из чисто виртуальных функций. Такое решение позволяет полностью отделить реализацию от интерфейса — клиент использует интерфейсный класс, — в другом месте создается производный класс, в котором переопределяются чисто виртуальные функции и определяется функция-фабрика. Детали реализации полностью скрыты от клиента. Таким образом реализуется истинная инкапсуляция, невозможная при использовании обычного класса. Про интерфейсные классы можно почитать у Скотта Мейерса [Meyers2]. Интерфейсные классы также называют классами-протоколами.

    Использование интерфейсных классов позволяет ослабить зависимости между разными частями проекта, что упрощает командную разработку, снижается время компиляции/сборки. Интерфейсные классы делают более простой реализацию гибких, динамических решений, когда модули подгружаются выборочно во время исполнения. Использование интерфейсных классов в качестве интерфейса (API) библиотек (SDK) упрощает решение проблем двоичной совместимости.

    Интерфейсные классы используются достаточно широко, с их помощью реализуют интерфейс (API) библиотек (SDK), интерфейс подключаемых модулей (plugin’ов) и многое другое. Многие паттерны Банды Четырех [GoF] естественным образом реализуются с помощью интерфейсных классов. К интерфейсным классам можно отнести COM-интерфейсы. Но, к сожалению, при реализации решений на основе интерфейсныx классов часто допускаются ошибки. Попробуем навести ясность в этом вопросе.

    1. Специальные функции-члены, создание и удаление объектов

    В этом разделе кратко описывается ряд особенностей C++, которые надо знать, чтобы полностью понимать решения, предлагаемые для интерфейсных классов.

    1.1. Специальные функции-члены

    Если программист не определил функции-члены класса из следующего списка — конструктор по умолчанию, копирующий конструктор, оператор копирующего присваивания, деструктор, — то компилятор может сделать это за него. С++11 добавил к этому списку перемещающий конструктор и оператор перемещающего присваивания. Эти функции-члены называются специальные функции-члены. Они генерируются, только если они используются, и выполняются дополнительные условия, специфичные для каждой функции. Обратим внимание, на то, что это использование может оказаться достаточно скрытым (например, при реализации наследования). Если требуемая функция не может быть сгенерирована, выдается ошибка. (За исключением перемещающих операций, они заменяются на копирующие.) Генерируемые компилятором функции-члены являются открытыми и встраиваемыми.

    Специальные функции-члены не наследуются, если в производном классе требуется специальная функция-член, то компилятор всегда будет пытаться ее генерировать, наличие определенной программистом соответствующей функции-члена в базовом классе на это не влияет.

    Программист может запретить генерацию специальных функций-членов, в С++11 надо применить при объявлении конструкцию «=delete» , в С++98 объявить соответствующую функцию-член закрытой и не определять. При наследовании классов, запрет генерации специальной функции-члена, сделанный в базовом классе, распространяется на все производные классы.

    Если программиста устраивает функции-члены, генерируемые компилятором, то в С++11 он может обозначить это явно, а не просто опустив объявление. Для этого при объявлении надо использовать конструкцию «=default» , код при этом лучше читается и появляется дополнительные возможности, связанные с управлением уровнем доступа.

    Подробности о специальных функциях-членах можно найти в [Meyers3].

    1.2. Создание и удаление объектов — основные подробности

    Создание и удаление объектов с помощью операторов new/delete — это типичная операция «два в одном». При вызове new сначала выделяется память для объекта. Если выделение прошло успешно, то вызывается конструктор. Если конструктор выбрасывает исключение, то выделенная память освобождается. При вызове оператора delete все происходит в обратном порядке: сначала вызывается деструктор, потом освобождается память. Деструктор не должен выбрасывать исключений.

    Если оператор new используется для создания массива объектов, то сначала выделяется память для всего массива. Если выделение прошло успешно, то вызывается конструктор по умолчанию для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Для удаления массива надо вызвать оператор delete[] (называется оператор delete для массивов), при этом для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается.

    Внимание! Необходимо вызывать правильную форму оператора delete в зависимости от того, удаляется одиночный объект или массив. Это правило надо соблюдать неукоснительно, иначе можно получить неопределенное поведение, то есть может случиться все, что угодно: утечки памяти, аварийное завершение и т.д. Подробнее см. [Meyers2].

    Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc .

    Любую форму оператора delete безопасно применять к нулевому указателю.

    В приведенном выше описании необходимо сделать одно уточнение. Для так называемых тривиальных типов (встроенные типы, структуры в стиле С), конструктор может не вызываться, а деструктор в любом случае ничего не делает. См. также раздел 1.6.

    1.3. Уровень доступа деструктора

    Когда оператор delete применяется к указателю на класс, деструктор этого класса должен быть доступен в точке вызова delete . (Есть некоторое исключение из этого правила, рассмотренное в разделе 1.6.) Таким образом, делая деструктор защищенным или закрытым, программист запрещает использование оператора delete там, где деструктор недоступен. Напомним, что если в классе не определен деструктор, компилятор это сделает самостоятельно, и этот деструктор будет открытым (см. раздел 1.1).

    1.4. Создание и удаление в одном модуле

    Если оператор new создал объект, то вызов оператора delete для его удаления должен быть в том же модуле. Образно говоря, «положи туда, где взял». Это правило хорошо известно, см., например [Sutter/Alexandrescu]. При нарушении этого правила может произойти «нестыковка» функций выделения и освобождения памяти, что, как правило, приводит к аварийному завершению программы.

    1.5. Полиморфное удаление

    Если проектируется полиморфная иерархия классов, экземпляры которых удаляются с помощью оператора delete , то в базовом классе должен быть открытый виртуальный деструктор, это гарантирует вызов деструктора фактического типа объекта при применении оператора delete к указателю на базовый класс. При нарушении этого правила может произойти вызов деструктора базового класса, из-за чего возможна утечка ресурсов.

    1.6. Удаление при неполном объявлении класса

    Определенные проблемы может создать «всеядность» оператора delete , его можно применить к указателю типа void* или к указателю на класс, который имеет неполное (упреждающее) объявление. В этом случае ошибки не происходит, просто вызов деструктора пропускается, вызывается только функция освобождения памяти. Рассмотрим пример:

    Этот код компилируется, даже если в точке вызова delete не доступно полное объявление класса X . Правда, при компиляции (Visual Studio) выдается предупреждение:

    warning C4150: deletion of pointer to incomplete type ‘X’; no destructor called

    Если есть реализация X и CreateX() , то код компонуется, если CreateX() возвращает указатель на объект, созданный оператором new , то вызов Foo() успешно выполняется, деструктор при этом не вызывается. Понятно, что это может привести к утечке ресурсов, так что еще раз о необходимости внимательно относится к предупреждениям.

    Ситуация эта не надумана, она легко может возникнуть при использовании классов типа интеллектуального указателя или классов-дескрипторов. Скотт Мейерс разбирается с этой проблемой в [Meyers3].

    2. Чисто виртуальные функции и абстрактные классы

    Концепция интерфейсных классов базируется на таких понятиях С++ как чисто виртуальные функции и абстрактные классы.

    2.1. Чисто виртуальные функции

    Виртуальная функция, объявленная с использованием конструкции «=0» , называется чисто виртуальной.

    В отличии от обычной виртуальной функции, чисто виртуальную функцию можно не определять (за исключением деструктора, см. раздел 2.3), но она должна быть переопределена в одном из производных классов.

    Чисто виртуальные функции могут быть определены. Герб Саттер предлагает несколько полезных применений для этой возможности [Shutter].

    2.2. Абстрактные классы

    Абстрактным классом называется класс, имеющий хотя бы одну чисто виртуальную функцию. Абстрактным будет также класс, производный от абстрактного класса и не переопределяющий хотя бы одну чисто виртуальную функцию. Стандарт С++ запрещает создавать экземпляры абстрактного класса, можно создавать только экземпляры производных не абстрактных классов. Таким образом, абстрактный класс создается, чтобы использоваться в качестве базового класса. Соответственно, если в абстрактном классе определяется конструктор, то его не имеет смысла делать открытым, он должен быть защищенным.

    2.3. Чисто виртуальный деструктор

    В ряде случаев чисто виртуальным целесообразно сделать деструктор. Но такое решение имеет две особенности.

    1. Чисто виртуальный деструктор должен быть обязательно определен. (Обычно используется определение по умолчанию, то есть с использованием конструкции «=default» .) Деструктор производного класса вызывает деструкторы базовых классов по всей цепочке наследования и, следовательно, очередь гарантировано дойдет до корня — чисто виртуального деструктора.
    2. Если программист не переопределил чисто виртуальный деструктор в производном классе, компилятор сделает это за него (см. раздел 1.1). Таким образом, класс, производный от абстрактного класса с чисто виртуальным деструктором, может потерять абстрактность и без явного переопределения деструктора.

    Пример использования чисто виртуального деструктора можно найти в разделе 4.4.

    3. Интерфейсные классы

    Интерфейсным классом называется абстрактный класс, не имеющий данных и состоящий в основном из чисто виртуальных функций. Такой класс может иметь обычные виртуальные функции (не чисто виртуальные), например деструктор. Также могут быть статические функции-члены, например функции-фабрики.

    3.1. Реализации

    Реализацией интерфейсного класса будем называть производный класс, в котором переопределены чисто виртуальные функции. Реализаций одного и того же интерфейсного класса может быть несколько, причем возможны две схемы: горизонтальная, когда несколько разных классов наследуют один и тот же интерфейсный класс, и вертикальная, когда интерфейсный класс является корнем полиморфной иерархии. Конечно, могут быть и гибриды.

    Ключевым моментом концепции интерфейсных классов является полное отделение интерфейса от реализации — клиент работает только с интерфейсным классом, реализация ему не доступна.

    3.2. Создание объекта

    Недоступность класса реализации вызывает определенные проблемы при создании объектов. Клиент должен создать экземпляр класса реализации и получить указатель на интерфейсный класс, через который и будет осуществляться доступ к объекту. Так как класс реализации не доступен, то использовать конструктор нельзя, поэтому используется функция-фабрика, определяемая на стороне реализации. Эта функция обычно создает объект с помощью оператора new и возвращает указатель на созданный объект, приведенный к указателю на интерфейсный класс. Функция-фабрика может быть статическим членом интерфейсного класса, но это не обязательно, она, например, может быть членом специального класса-фабрики (который, в свою очередь, сам может быть интерфейсным) или свободной функцией. Функция-фабрика может возвращать не сырой указатель на интерфейсный класс, а интеллектуальный. Этот вариант рассмотрен в разделах 3.3.4 и 4.3.2.

    3.3. Удаление объекта

    Удаление объекта является чрезвычайно ответственной операцией. При ошибке возникает либо утечка памяти, либо двойное удаление, которое обычно приводит к аварийному завершению программы. Ниже этот вопрос рассматривается максимально подробно, причем много внимания уделяется предупреждению ошибочных действий клиента.

    Существуют четыре основных варианта:

    1. Использование оператора delete .
    2. Использование специальной виртуальной функции.
    3. Использование внешней функции.
    4. Автоматическое удаление с помощью интеллектуального указателя.

    3.3.1. Использование оператора delete

    Для этого в интерфейсном классе необходимо иметь открытый виртуальный деструктор. В этом случае оператор delete , вызванный для указателя на интерфейсный класс на стороне клиента обеспечивает вызов деструктора класса-реализации. Этот вариант может работать, но удачным его признать трудно. Мы получаем вызовы операторов new и delete по разные стороны «барьера», new на стороне реализации, delete на стороне клиента. А если реализация интерфейсного класса сделана в отдельном модуле (что достаточно обычное дело), то получаем нарушение правила из раздела 1.4.

    3.3.2. Использование специальной виртуальной функции

    Более прогрессивным является другой вариант: интерфейсный класс должен иметь специальную виртуальную функцию, которая и удаляет объект. Такая функция, в конце концов, сводится к вызову delete this , но это происходит уже на стороне реализации. Называться такая функция может по-разному, например Delete() , но используются и другие варианты: Release() , Destroy() , Dispose() , Free() , Close() , etc. Кроме соблюдения правила из раздела 1.4, этот вариант имеет несколько дополнительных преимуществ.

    1. Позволяет использовать для класса реализации пользовательские функции выделения/освобождения памяти.
    2. Позволяет реализовать более сложную схему управления временем жизни объекта реализации, например с использованием счетчика ссылок.

    В этом варианте попытка удаления объекта с помощью оператора delete может компилироваться и даже выполняться, но это является ошибкой. Для ее предотвращения в интерфейсном классе достаточно иметь пустой или чисто виртуальный защищенный деструктор (см. раздел 1.3). Отметим, что использование оператора delete может оказаться достаточно сильно замаскированным, например, стандартные интеллектуальные указатели для удаления объекта по умолчанию используют оператор delete и соответствующий код глубоко «зарыт» в их реализации. Защищенный деструктор позволяет обнаружить все такие попытки на этапе компиляции.

    3.3.3. Использование внешней функции

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

    3.3.4. Автоматическое удаление с помощью интеллектуального указателя

    В этом случае функция-фабрика возвращает не сырой указатель на интерфейсный класс, а соответствующий интеллектуальный указатель. Этот интеллектуальный указатель создается на стороне реализации и инкапсулирует объект-удалитель, который автоматически удаляет объект реализации, когда интеллектуальный указатель (или последняя его копия) выходит из области видимости на стороне клиента. В этом случае специальная виртуальная функция для удаления объекта реализации может не потребоваться, но защищенный деструктор по-прежнему нужен, необходимо предотвратить ошибочное использование оператора delete . (Правда, надо отметить, что вероятность такой ошибки заметно снижается.) Более подробно этот вариант рассмотрен в разделе 4.3.2.

    3.4. Другие варианты управления временем жизни экземпляра класса реализации

    В ряде случаев клиент может получать указатель на интерфейсный класс, но не владеть им. Управления временем жизни объекта реализации находится полностью на стороне реализации. Например, объект может быть статическим объектом-синглтоном (такое решение характерно для фабрик). Другой пример связан с двунаправленным взаимодействием, см. раздел 3.7. Удалять такой объект клиент не должен, но защищенный деструктор для такого интерфейсного класса нужен, необходимо предотвратить ошибочное использование оператора delete .

    3.5. Семантика копирования

    Для интерфейсного класса создание копии объекта реализации с помощью копирующего конструктора невозможно, поэтому если требуется копирование, то в классе должна быть виртуальная функция, создающая копию объекта реализации и возвращающая указатель на интерфейсный класс. Такую функцию часто называют виртуальным конструктором, и традиционное имя для нее Clone() или Duplicate() .

    Использование оператора копирующего присваивания не запрещено, но нельзя признать удачной идеей. Оператор копирующего присваивания всегда является парным, он должен идти в паре с копирующим конструктором. Оператор, генерируемый компилятором по умолчанию, бессмыслен, он ничего не делает. Теоретически можно объявить оператор присваивания чисто виртуальным с последующим переопределением, но виртуальное присваивание является не рекомендуемой практикой, подробности можно найти в [Meyers1]. К тому же присваивание выглядит весьма неестественно: доступ к объектам класса реализации обычно осуществляется через указатель на интерфейсный класс, поэтому присваивание будет выглядеть так:

    Оператор присваивания лучше всего запретить, а при необходимости подобной семантики иметь в интерфейсном классе соответствующую виртуальную функцию.

    Запретить присваивание можно двумя способами.

    1. Объявить оператор присваивания удаленным ( =delete ). Если интерфейсные классы образуют иерархию, то это достаточно сделать в базовом классе. Недостаток этого способа заключается в том, что это влияет на класс реализации, запрет распространяется и на него.
    2. Объявить защищенный оператор присваивания с определением по умолчанию ( =default ). Это не влияет на класс реализации, но в случае иерархии интерфейсных классов такое объявление нужно делать в каждом классе.

    3.6. Конструктор интерфейсного класса

    Часто конструктор интерфейсного класса не объявляется. В этом случае компилятор генерирует конструктор по умолчанию, необходимый для реализации наследования (см. раздел 1.1). Этот конструктор открытый, хотя достаточно, чтобы он был защищенным. Если в интерфейсном классе копирующий конструктор объявлен удаленным ( =delete ), то генерация компилятором конструктора по умолчанию подавляется, и необходимо явно объявить такой конструктор. Естественно его сделать защищенным с определением по умолчанию ( =default ). В принципе, объявление такого защищенного конструктора можно делать всегда. Пример находится в разделе 4.4.

    3.7. Двунаправленное взаимодействие

    Интерфейсные классы удобно использовать для организации двунаправленного взаимодействия. Если некоторый модуль доступен через интерфейсные классы, то клиент также может создать реализации некоторых интерфейсных классов и передать указатели на них в модуль. Через эти указатели модуль может получать сервисы от клиента а также передавать клиенту данные или нотификации.

    3.8. Интеллектуальные указатели

    Так как доступ к объектам класса реализации обычно осуществляется через указатель, то для управления их временем жизни естественно воспользоваться интеллектуальными указателями. Но следует иметь в виду, что если используется второй вариант удаления объектов, то стандартным интеллектуальным указателем необходимо передать пользовательский удалитель (тип) или экземпляр этого типа. Если этого не сделать, то для удаления объекта интеллектуальный указатель будет использовать оператор delete , и код просто не будет компилироваться (благодаря защищенному деструктору). Стандартные интеллектуальные указатели (включая использование пользовательских удалителей) подробно рассмотрены в [Josuttis], [Meyers3]. Пример использования пользовательского удалителя можно найти в разделе 4.3.1.

    Если интерфейсный класс поддерживает счетчик ссылок, то целесообразно использовать не стандартные интеллектуальные указатели, а специально написанный для такого случая, это сделать достаточно легко.

    3.9. Константные функции-члены

    Следует с осторожностью объявлять функции-члены интерфейсных классов как const. Одним из важных достоинств интерфейсных классов является возможность максимально полного отделения интерфейса от реализации, но ограничения, связанные с константностью функции-члена, могут создать проблемы при разработке класса реализации.

    3.10. COM-интерфейсы

    COM-интерфейсы являются примером интерфейсных классов, но следует иметь в виду, что COM — это независимый от языка программирования стандарт, и COM-интерфейсы можно реализовывать на разных языках, например на C, где нет ни деструкторов, ни защищенных членов. Разработка COM-интерфейсов на C++ должна вестись в соответствии с правилами, определяемыми технологией COM.

    3.11. Интерфейсные классы и библиотеки

    Достаточно часто интерфейсные классы используются в качестве интерфейса (API) для целых библиотек (SDK). В этом случае целесообразно следовать следующей схеме. Библиотека имеет доступную функцию-фабрику, которая возвращает указатель на интерфейсный класс-фабрику, с помощью которого и создаются экземпляры классов реализации других интерфейсных классов. В этом случае для библиотек, поддерживающих явную спецификацию экспорта (Windows DLL), требуется всего одна точка экспорта: вышеупомянутая функция-фабрика. Весь остальной интерфейс библиотеки становится доступным через таблицы виртуальных функций. Именно такая схема позволяет максимально просто реализовывать гибкие, динамические решения, когда модули подгружаются выборочно во время исполнения. Модуль загружается с помощью LoadLibrary() или ее аналогом на других платформах, далее получается адрес функции-фабрики, и после этого библиотека становится полностью доступной.

    4. Пример интерфейсного класса и его реализации

    4.1. Интерфейсный класс

    Так как интерфейсный класс редко бывает один, то обычно целесообразно создать базовый класс.

    Вот демонстрационный интерфейсный класс.

    Отметим, что защищенный деструктор должен быть как в базовом классе, так и в интерфейсном классе. В базовом классе он нужен, потому что в некоторых сценариях клиент может использовать указатель на IBase . В интерфейсном классе он нужен, потому что при его отсутствии компилятор сгенерирует открытый деструктор по умолчанию (см. раздел 1.3). Запрет присваивания достаточно сделать в базовом классе, он будет распространяться на все производные классы.

    4.2. Класс реализации

    В классе реализации деструктор является защищенным, конструктор, а также наследование от интерфейсного класса являются закрытыми, а функция-фабрика объявлена другом, это обеспечивает максимальную инкапсуляцию процедуры создания и удаления объекта.

    4.3. Стандартные интеллектуальные указатели

    4.3.1. Создание на стороне клиента

    При создании интеллектуального указателя на стороне клиента необходимо использовать пользовательский удалитель. Класс-удалитель очень простой (он может быть вложен в IBase ):

    Для std::unique_ptr<> класс-удалитель является шаблонным параметром:

    Отметим, что благодаря тому, что класс-удалитель не содержит данных, размер UniquePtr равен размеру сырого указателя.

    Вот шаблон функции-фабрики:

    Вот шаблон преобразования из сырого указателя в интеллектуальный:

    Экземпляры std::shared_ptr<> можно инициализировать экземплярами std::unique_ptr<> , поэтому специальные функции, возвращающие std::shared_ptr<> определять не нужно. Вот пример создания объектов типа Activator .

    А этот ошибочный код благодаря защищенному деструктору не компилируется (конструктор должен принимать второй аргумент — объект-удалитель):

    Также нельзя использовать шаблон std::make_shared<>() , он не поддерживает пользовательские удалители (соответствующий код не будет компилироваться).

    Описанная схема имеет недостаток: через интеллектуальный указатель можно вызвать виртуальную функцию удаления объекта реализации, что приведет к двойному удалению. Эту проблему можно решить так: сделать виртуальную функцию удаления защищенной, а класс-удалитель другом. Пример находится в разделе 4.4.

    4.3.2. Создание на стороне реализации

    Интеллектуальный указатель можно создавать на стороне реализации. В этом случае клиент получает его в качестве возвращаемого значения функциии-фабрики. Если использовать std::shared_ptr<> и в его конструктор передать указатель на класс реализации, который имеет открытый деструктор, то пользовательский удалитель не нужен (и не требуется специальная виртуальная функция для удаления объекта реализации). В этом случае конструктор std::shared_ptr<> (а это шаблон) создает объект-удалитель по умолчанию, который базируется на типе аргумента и при удалении применяет оператор delete к указателю на объект реализации. Для std::shared_ptr<> объект-удалитель входит в состав экземпляра интеллектуального указателя (точнее его управляющего блока) и тип объекта-удалителя не влияет на тип интеллектуального указателя. В этом варианте предыдущий пример можно переписать так.

    Для функции-фабрики более оптимальным является вариант с использованием шаблона std::make_shared<>() :


    В описанном сценарии нельзя использовать std::unique_ptr<> , так как у него несколько иная стратегия удаления, класс-удалитель является шаблонным параметром, то есть является составной частью типа интеллектуального указателя.

    4.4. Альтернативная реализация базового класса

    В отличие от C# или Java в C++ нет специального понятия «интерфейс», необходимое поведение моделируется с помощью виртуальных функций. Это дает дополнительную гибкость при реализации интерфейсного класса. Рассмотрим еще один вариант реализации IBase .

    Чисто виртуальный деструктор нужно определить, Delete() не чисто виртуальная функция, поэтому ее также нужно определить.

    Остальные интерфейсные классы наследуются от IBase . Теперь при реализации интерфейсного класса не требуется переопределять Delete() , она определена в базовом классе и благодаря виртуальному деструктору обеспечивает вызов деструктора класса реализации. Класс-удалитель также естественно сделать вложенным в IBase . Delete() объявлена защищенной, класс-удалитель другом. Это запрещает непосредственный вызов Delete() на стороне клиента и тем самым снижает вероятность ошибок, связанных с удалением объекта. Рассмотренный вариант ориентирован на использование интеллектуальных указателей, описанное в разделе 4.3.1.

    5. Исключения и коллекции, реализованные с помощью интерфейсных классов

    5.1 Исключения

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

    В заголовочном файле, доступном клиенту, объявляется интерфейсный класс IException и обычный класс Exception .

    При возникновении исключительной ситуации модуль выбрасывает исключение типа Exception , клиент перехватывает это исключение и получает информацию через доступный указатель на IException . При необходимости клиент может пробросить исключение дальше, путем вызова оператора throw , или сохранить исключение. Первый конструктор класса Exception используется только в точке выброса исключения, его экспортировать из модуля не надо. Остальные функции-члены являются встраиваемыми и доступны как модулю, так и клиенту.

    Реализовать Exception можно, например, следующим образом.

    Класс реализации IException :

    Определение конструктора Exception :

    Обратим внимание на то, что при программировании в смешанных решениях — .NET — родные модули, — такое исключение корректно проходит границу между родным и управляемым модулем, если он написан на C++/CLI. Таким образом, это исключение может быть выброшено в родном модуле, а перехвачено в управляемом классе, написанном на C++/CLI.

    5.2 Коллекции

    Шаблон интерфейсного класса-коллекции может выглядеть следующим образом:

    С такой коллекцией уже можно достаточно комфортно работать, но при желании указатель на такой шаблонный класс можно обернуть в шаблон класса-контейнера, который предоставляет интерфейс в стиле контейнеров стандартной библиотеки.

    Такой контейнер реализовать совсем не сложно. Он владеет коллекцией, то есть выполняет ее освобождение в деструкторе. Возможно, это контейнер не полностью удовлетворяет требованиям, предъявляемым к стандартным контейнерам, но это не особенно нужно, главное он имеет функции-члены begin() и end() , которые возвращают итератор. А вот если итератор определен в соответствии со стандартом итератора (см. [Josuttis]), то с этим контейнером можно использовать диапазонный цикл for и стандартные алгоритмы. Определение итератора в соответствии с правилами стандартной библиотеки является достаточно объемным и поэтому здесь не приводится. Определения шаблонов классов контейнера и итератора полностью находится в заголовочных файлах и, следовательно, никаких функций дополнительно экспортировать не надо.

    6. Интерфейсные классы и классы-обертки

    Интерфейсные классы являются достаточно низкоуровневыми средствами программирования. Для более комфортной работы их желательно обернуть в классы-обертки, обеспечивающие автоматическое управление временем жизни объектов. Также обычно желательно иметь стандартные решения типа исключений и контейнеров. Выше было показано, как это можно сделать для программирования в среде С++. Но интерфейсные классы могут служить функциональной основой для реализации решений и на других платформах, таких как .NET, Java или Pyton. На этих платформах используются другие механизмы управления временем жизни объектов и другие стандартные интерфейсы. В этом случае надо создавать обертку, используя технологию, обеспечивающую интеграцию с целевой платформой и учитывающую особенности платформы. Например для .NET Framework такая обертка пишется на C++/CLI и она будет отличаться от предложенной выше обертки для C++. Пример можно посмотреть здесь.

    7. Итоги

    Объект реализации интерфейсного класса создается функцией-фабрикой, которая возвращает указатель или интеллектуальный указатель на интерфейсный класс.

    Для удаления объекта реализации интерфейсного класса существуют три варианта.

    1. Использование оператора delete .
    2. Использование специальной виртуальной функции.
    3. Автоматическое удаление с помощью интеллектуального указателя.

    В первом варианте интерфейсный класс должен иметь открытый виртуальный деструктор.

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

    В третьем варианте функция-фабрика возвращает интеллектуальный указатель, который создается на стороне реализации и инкапсулирует процедуру удаления объекта реализации. В этом случае специальная виртуальная функция для удаления объекта реализации может не потребоваться, но защищенный деструктор нужен, необходимо предотвратить ошибочное использование оператора delete .

    Семантика копирования для объектов реализации интерфейсного класса реализуется с помощью специальных виртуальных функций.

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

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

    [GoF]
    Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования.: Пер. с англ. — СПб.: Питер, 2001.

    [Josuttis]
    Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.

    [Dewhurst]
    Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.

    [Meyers1]
    Мейерс, Скотт. Наиболее эффективное использование C++. 35 новых рекомендаций по улучшению ваших программ и проектов.: Пер. с англ. — М.: ДМК Пресс, 2000.

    [Meyers2]
    Мейерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.

    [Meyers3]
    Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2020.

    [Sutter]
    Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.

    [Sutter/Alexandrescu]
    Саттер, Герб. Александреску, Андрей. Стандарты программирования на С++.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2015.

    Функции класса/объекта

    Недостатком рассмотренных ранее классов является отсутствие автоматической инициализации создаваемых объектов. Для каждого вновь создаваемого объекта необходимо было вызвать функцию типа set (как для класса complex ), либо явным образом присваивать значения данным объекта. Однако для инициализации объектов класса в его определение можно явно включить специальную компонентную функцию, называемую конструктором. Формат определения конструктора следующий

    Имя этой компонентной функции по правилам языка С++ должно совпадать с именем класса. Такая функция автоматически вызывается при определении или размещении в памяти с помощью оператора new каждого объекта класса.

    Конструктор выделяет память для объекта и инициализирует данные-члены класса.

    Конструктор имеет ряд особенностей:

    • Для конструктора не определяется тип возвращаемого значения. Даже тип void не допустим.
    • Указатель на конструктор не может быть определен и соответственно нельзя получить адрес конструктора.
    • Конструкторы не наследуются.
    • Конструкторы не могут быть описаны с ключевыми словами virtual , static , const , mutable , volatile .

    Конструктор всегда существует для любого класса, причем, если он не определен явно, он создается автоматически. По умолчанию создается конструктор без параметров и конструктор копирования. Если конструктор описан явно, то конструктор по умолчанию не создается. По умолчанию конструкторы создаются общедоступными ( public ).

    В классе может быть несколько конструкторов, но только один с умалчиваемыми значениями параметров. Перегрузка чаще всего используется для передачи конструктору аргументов, предназначенных для инициализации данных- членов класса. Параметром конструктора не может быть его собственный класс, но может быть ссылка на него ( T& ). Без явного указания программиста конструктор всегда автоматически вызывается при определении (создании) объекта. В этом случае вызывается конструктор без параметров. Для явного вызова конструктора используются две формы:

    Первая форма допускается только при не пустом списке фактических параметров. Она предусматривает вызов конструктора при определении нового объекта данного класса:

    Complex ss(5.9, 0.15);

    Вторая форма вызова приводит к созданию объекта без имени:

    Complex ss = Complex(5.9, 0.15);

    Существует два способа инициализации данных объекта с помощью конструктора. Ранее мы рассматривали первый способ, а именно передача значений параметров в тело конструктора. Второй способ предусматривает применение списка инициализаторов данного класса. Этот список помещается между списком параметров и телом конструктора. Каждый инициализатор списка относится к конкретному компоненту и имеет вид

    Здесь у класса S tring два конструктора – перегружаемые функции.

    По умолчанию создается также конструктор копирования вида T::T(const T &) , где T — имя класса. Конструктор копирования вызывается всякий раз, когда выполняется копирование объектов, принадлежащих классу. В частности он вызывается:

    • когда объект передается функции по значению;
    • при построении временного объекта как возвращаемого значения функции;
    • при использовании объекта для инициализации другого объекта.

    Если класс не содержит явным образом определенного конструктора копирования, то при возникновении одной из этих трех ситуаций, производится побитовое копирование объекта. Побитовое копирование не во всех случаях является адекватным. Именно для таких случаев и необходимо определить собственный конструктор копирования. Например, создадим два объекта типа S tring .

    Здесь объект s2 инициализируется объектом s1 путем вызова конструктора копирования, созданного компилятором по умолчанию. В результате эти объекты имеют одинаковое значение в полях ch , то есть эти поля указывают на одну и ту же область памяти. В результате при удалении объекта s1 будет освобождаться и область, занятая строкой, но она еще нужна объекту s2 . Чтобы не возникало подобных ошибок, определим собственный конструктор копирования.

    Конструктор с одним аргументом может выполнять неявное преобразование типа своего аргумента в тип класса конструктора.

    Этот конструктор реализует представление вещественной оси в комплексной плоскости.

    Вызвать этот конструктор можно традиционным способом

    Но можно вызвать его и так

    Здесь необходимо преобразование скалярной величины (типа аргумента конструктора) в тип C omplex . Это осуществляется вызовом конструктора с одним параметром. Поэтому конструктор, имеющий один аргумент не нужно вызывать явно, а можно просто записать C omplex b = 5 , что означает C omplex b = Complex (5) .

    Преобразование, определяемое пользователем, неявно применяется в том случае, если оно уникально. Например,

    Здесь в D emo a = 3 неоднозначность: вызов D emo(char) или D emo(long) ?

    А в D emo a = 0; также неоднозначность: вызов D emo(char *) , D emo(int *) , D emo(char) или D emo(long) ?

    В некоторых случаях необходимо задать конструктор, который можно вызвать только явно. Например,

    В этом случае неявное преобразование может привести к ошибке. В случае S tring s = ‘a’ ; создается строка длиной int(‘a’) . Вряд ли это то, что мы хотели.

    Неявное преобразование можно подавить, объявив конструктор с модификатором explicit . В этом случае конструктор будет вызываться только явно. В частности, там где конструктор копирования в принципе необходим, explicit -конструктор не будет вызываться неявно. Например:

    Можно создавать массив объектов, однако при этом соответствующий класс должен иметь конструктор по умолчанию (без параметров).

    Это связано с тем, что при объявлении массива объектов невоuможно определить параметры для констрaкторов этих объектов и единственная возможность zызова конструкторов — это передача им параметров, заданных по умолчанию.

    Массив объектов может инициализироваться либо автоматически конструктором по умолчанию, либо явным присваиванием значений каждому элементу массива.

    При объявлении массива объектов невозможно определить параметры для конструкторов этих объектов и единственная возможность вызова конструкторов — это передача им параметров, заданных по умолчанию. Таким образом, для того чтобы создавать массив объектов соответствующий класс должен иметь заданный по умолчанию конструктор. Можно уменьшить количество конструкторов, если задать конструктор с аргументами по умолчанию. Он же будет конструкторов по умолчанию.

    Динамическое выделение памяти для объекта создает необходимость освобождения этой памяти при уничтожении объекта. Например, если объект формируется как локальный внутри блока, то целесообразно, чтобы при выходе из блока, когда уже объект перестает существовать, выделенная для него память была возвращена. Желательно чтобы освобождение памяти происходило автоматически. Такую возможность обеспечивает специальный компонент класса – деструктор класса. Его формат:

    Имя деструктора совпадает с именем его класса, но предваряется символом «

    Деструктор не имеет параметров и возвращаемого значения. Вызов деструктора выполняется не явно (автоматически), как только объект класса уничтожается.

    Например, при выходе за область определения или при вызове оператора delete для указателя на объект.

    Если в классе деструктор не определен явно, то компилятор генерирует деструктор по умолчанию, который просто освобождает память занятую данными объекта. В тех случаях, когда требуется выполнить освобождение и других объектов памяти, например область, на которую указывает ch в объекте string , необходимо определить деструктор явно:

    Также как и для конструктора, не может быть определен указатель на деструктор.

    Компоненты-данные и компоненты-функции

    Данные — члены класса.

    Определение данных класса внешне аналогично описанию переменных базовых или производных типов. Однако при описании данных класса не допускается их инициализация. Для их инициализации должен использоваться автоматический или явно вызываемый конструктор. Принадлежащие классу функции имеют полный доступ к его данным. Для доступа к элементам-данным из операторов, выполняемых вне определения класса нужно использовать операции выбора компонентов класса ( » . » или » –> «). Данные класса не обязательно должны быть определены или описаны до их первого использования в принадлежащих классу функциях. Все компоненты класса «видны» во всех операторах его тела. Область доступа к компонентам-данным регулируется модификатором доступа.

    Компоненты-данные могут быть описаны как const . В этом случае после инициализации они не могут быть изменены.

    Компоненты-данные могут быть описаны как mutable . В этом случае они являются изменяемыми, даже если объект, содержащий их, описан как const .

    Функции — члены класса.

    Компонентная функция должна быть обязательно описана в теле класса. При определении классов их компонентные функции могут быть специфицированы как подставляемые ( inline ). Кроме явного использования слова inline для этого используются следующие соглашения. Если определение функции полностью размещено в теле класса, то эта функция по умолчанию считается подставляемой. Если в теле класса помещен только прототип функции, а ее определение – вне класса, то для того, чтобы функция была подставляемой, ее надо явно специфицировать словом inline . При внешнем определении функции в теле класса помещается прототип функции

    Вне тела класса функция определяется так

    Константные компоненты-функции.

    Функции-члены класса могут быть описаны как const . В этом случае они не не могут изменять значения данных-членов класса и могут возвращать указатель или ссылку только на данные-члены класса, описанные как const . Они являются единственными функциями, которые могут вызываться для объекта-константы.

    Например, в классе Complex

    Объявление функций real() и imag() как const гарантирует, что они не изменяют состояние объекта Complex . Компилятор обнаружит случайные попытки нарушить это условие.

    Когда константная функция определяется вне класса указывать const надо обязательно:

    Константную функцию-член можно вызвать как для константного, так и для неконстантного объекта, в то время как неконстантную функцию-член можно вызвать только для объекта, не являющегося константой.

    Статические члены класса

    Каждый объект одного и того же класса имеет собственную копию данных класса. Это не всегда соответствует требованиям решаемой задачи. Например, счетчик объектов, указатели на первый и последний объект в списке объектов одного класса. Эти данные должны быть компонентами класса, но иметь их нужно только в единственном числе. Такие компоненты должны быть определены в классе как статические ( static ). Статические данные классов не дублируются при создании объектов, т.е. каждый статический компонент существует в единственном экземпляре. Доступ к статическому компоненту возможен только после его инициализации. Для инициализации используется конструкция

    Например, int goods::percent = 12;

    Это предложение должно быть размещено в глобальной области после определения класса. Только при инициализации статическое данное класса получает память и становится доступным. Обращаться к статическому данному класса можно обычным образом через имя объекта

    Но к статическим компонентам можно обращаться и тогда, когда объект класса еще не существует. Доступ к статическим компонентам возможен не только через имя объекта, но и через имя класса

    Однако так можно обращаться только к public компонентам.

    А как обратиться к private статической компоненте извне определения объекта? С помощью компонента-функции этого класса. Однако при вызове такой функции необходимо указывать имя объекта, а объект может еще не существовать. Эту проблему решают статические компоненты-функции. Эти функции можно вызвать через имя класса.

    Пример. Моделирование списка.

    Указатели на компоненты класса.

    Указатели на компоненты-данные.

    Можно определить указатель на компоненты-данные.

    В определении указателя можно включить его инициализатор

    Естественно, что в этом случае данные-члены должны иметь статус открытых ( public ).

    После инициализации указателя его можно использовать для доступа к данным объекта.

    Указатель на компонент класса можно использовать в качестве фактического параметра при вызове функции.

    Если определены указатели на объект и на компонент, то доступ к компоненту с помощью операции » –>* «.

    Можно определить тип указателя на компоненты-данные класса:

    Указатели на компоненты-функции.

    Можно определить указатель на компоненты-функции.

    Можно определить также тип указателя на функцию

    а затем определить и сам указатель

    Указатель this .

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

    Указатель this является дополнительным скрытым параметром каждой нестатической компонентной функции. При входе в тело принадлежащей классу функции this инициализируется значением адреса того объекта, для которого вызвана функция. В результате этого объект становится доступным внутри этой функции.

    В большинстве случаев использование this является неявным. В частности, каждое обращение к нестатической функции-члену класса неявно использует this для доступа к члену соответствующего объекта. Например, функцию add в классе Complex можно определить эквивалентным, хотя и более пространным способом:

    Если функция возвращает объект, который ее вызвал, используется указатель this .

    Например, пусть функция add возвращает ссылку на объект. Тогда

    Примером широко распространенного использования this являются операции со связанными списками.

    Пример. Связанный список.

    Дружественная функция

    Дружественная функция – это функция, которая, не являясь компонентом класса, имеет доступ к его защищенным и собственным компонентам. Такая функция должна быть описана в теле класса со спецификатором friend .

    Функция set описана в классе MyClass как дружественная и определена как обычная глобальная функция (вне класса, без указания его имени, без операции ‘ :: ‘ и без спецификатора friend ).

    Дружественная функция при вызове не получает указатель this . Объекты класса должны передаваться дружественной функции только через параметр.

    Итак, дружественная функция:

    • не может быть компонентной функцией того класса, по отношению к которому определяется как дружественная;
    • может быть глобальной функцией;
    • может быть компонентной функцией другого ранее определенного класса.
      Например В этом примере класс Class1 с помощью своей компонентной функции f() получает доступ к компонентам класса Class2 .
    • может быть дружественной по отношению к нескольким классам;
      Например, В этом примере функция f имеет доступ к компонентам классов CL1 и CL2 .

    Дружественный класс

    Класс может быть дружественным другому классу. Это означает, что все компонентные функции класса являются дружественными для другого класса. Дружественный класс должен быть определен вне тела класса «предоставляющего дружбу».

    В этом примере функции f1 и f2 класса X1 являются друзьями класса X2 , хотя они описываются без спецификатора friend .

    Рассмотрим класс Point – точка в n-мерном пространстве и дружественный ему класс Vector – радиус-вектор точки («вектор с началом в начале координат n-мерного пространства»). В классе Vector определим функцию для определения нормы вектора, который вычисляется как сумма квадратов координат его конца.

    Недостатком предложенного класса Point является то, что значения всех координат точки x[i] одинаковы. Чтобы они были произвольными и разными, необходимо определить конструктор как функцию с переменным числом параметров, например так:

    Определение классов и методов классов.

    Определение классов обычно помещают в заголовочный файл.

    Т.к. описание класса Point в дальнейшем планируется включать в другие классы, то для предотвращения недопустимого дублирования описаний в текст включена условная препроцессорная директива #ifndef __POINT_H__ . Тем самым текст описания класса Point может появляться в компилируемом файле только однократно, несмотря на возможность неоднократного появления директив #include «point.h» .

    Определить методы можно следующим образом

    Программа, использующая объекты класса

    К проекту должен быть подключен файл point.cpp.

    Внешнее определение методов класса дает возможность, не меняя интерфейс объектов класса с другими частями программ, по-разному реализовать компонентные функции.

    Илон Маск рекомендует:  Примеры манипулирования окнами
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL