Лекция 10. Классы

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. Доступ к членам класса

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

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

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

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

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

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

Преимущества инкапсуляции

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

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

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

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

Описание функции-члена класса относиться к области действия класса. Это означает, что функция-член класса может непосредственно использовать имена членов своего класса. class X // Объявление класса Х { private: int n; public: void f(); }; void X::f() // Определение функции f из класса Х { n++; } 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 { return n++; } // Ошибка – попытка изменить значение члена класса в константной функции

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

4.2. Изменяемые данные-члены класса

Кроме ключевого слова const существует ключевое слово mutable, которое в некотором смысле противоположно ключевому слову const. Модификатор mutable можно применить к данным-членам класса, которые не являются статическими или константными. Такой член класса можно изменить даже в константной функции-члене класса. class String { private: char str[50]; public: int Cashe() const; }; int String::Cashe() const { // Вычисляем кэш каждый раз ... return cashe; }

Если мы захотим сохранить кэш в отдельном поле, чтобы не вычислять его каждый раз, возникнет проблема – функция Cashe по сути является константной, т.к. не меняет саму строку, поэтому снятие модификатора const было бы нелогичным, но будучи константной функция Cashe не может менять члены класса. Использование модификатора mutable для поля, хранящего кэш, решает эту проблему. class String { private: char str[50]; mutable int cashe = -1; public: int Cashe() const; void ResetCashe() { cashe = -1; } }; int String::Cashe() const { if (cashe != -1) return cashe; // Вычисляем кэш при необходимости ... return cashe; }

4.3. Указатель 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& g(); }; X& X::f() { ... return *this; } X& X::g() { ... return *this; } X a; a.f().g(); // К объету a последовательно применяются функции f и g

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

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

Пример. В обоих случаях функция Size является встраиваемой. class Vector { private: int size; ... public: int Size() { return size; } ... } class Vector { private: int size; ... public: int Size(); ... } inline int Vector::GetSize() { return size; }