Знакомство с С-классами

Знакомство с C-классами

Перевод оригинальной статьи "Introduction to C-classes" для сайта Symbian Developer Network.

Авторы: разработчики компании Penrillian. Перевод: Александр Смирнов.


1 Знакомство с C-классами

Стандарты Symbian OS определяют набор типовых классов, имеющих
разные определенные характеристики. Эти типы образуют группы классов со
схожими свойствами и поведением. Упомянутые типы представляют собой:

  • M-классы, похожие на интерфейсы языка программирования Ява.
    Эти классы могут иметь только виртуальные методы, т.е. методы, которые
    обязательно должны будут реализованы в наследующих классах. М-классы
    всегда имеют в названии начальную букву "М" как например, в классе MNotify.
  • Т-классы являются "простыми", т.е. они не содержат в себе
    никаких "внешних" данных, расположенных на "куче". Как правило, для
    таких классов не нужен деструктор. Т-классы, конечно же, могут хранить
    внутри указатели на внешние данные, однако они не должны отвечать за
    управление этими указателями (например, создавать, переопределять или
    удалять их). Т-классы представляют собою простые типы, и их имя всегда
    начинается с буквы "Т", как например, в классах TRect и TPoint.
  • L-классы представляют собой новую идиому в Symbian OS, и
    являются самоуправляющимися строковыми шаблонными классами,
    поддерживающими автоматическую очистку памяти.
  • R-классы имеют доступ к внешним ресурсам, например, к файлам. Название R-класса всегда начинается с буквы "R", как в классах RFile и RSocket.

Однако самым распространенным типом классов являются С-классы.
С-классы могут содержать внутри данные, расположенные на "куче", и
всегда наследуются от базового класса CBase. Любой класс, унаследованный от CBase, имеет в своем названии начальную букву "С", означающую слово "class" [1].

Как же нам выбрать подходящий тип класса? Тут всегда может
помочь одно простое правило: если вам нужен деструктор, значит, вам
нужен С-класс. Если вы собираетесь делать что-то необычное, или просто
нуждаетесь в классе, объекты которого будут располагаться на "куче", то
вам так же потребуется С-класс.


2 Анатомия класса, унаследованного от CBase

Класс, унаследованный от CBase, имеет следующие свойства:

  • Все данные внутри С-класса автоматически обнуляются при
    инициализации объекта этого класса. За обнуление данных отвечает
    перегруженный оператор new, переопределенный в классе CBase.
  • Все объекты С-классов размещаются на "куче". Одной из причин
    для такого размещения является нужда в одинаковом механизме
    инициализации С-классов. Так как объекты в стеке не создаются при
    помощи оператора new, их данные могут быть и не обнулены при создании
    объектов. Если бы разрешалось размещать С-классы в стеке, мы могли бы
    оказаться в ситуации, когда одни из только что созданных С-классов
    обнулили свои данные, а другие – нет.
  • В случае возникновения "сброса" (leave), С-классы
    должны самостоятельно очистить занимаемую ими память, т.к. они
    располагаются на "куче". Данное обстоятельство накладывает на нас
    обязательства сознательной и аккуратной очистки памяти при работе с
    С-классами (сбросы будут обсуждаться в главе 3.3).
  • С-классы передаются через указатель или по ссылке, и поэтому
    для них не нужен явный конструктор копирования или оператор
    присваивания, если, конечно, такое копирование явно не понадобится
    какому-нибудь С-классу.
  • Так как С-классы имеют необычное конструирование, и так как во
    время создания объекта С-класса может возникнуть сброс, для защиты от
    утечек памяти в С-классах используется двухфазное конструирование
    объектов. Первой фазой такого конструирования является обычный
    конструктор Си++, код которого не должен вызывать сброс. А второй фазой
    конструирования объекта С-класса является вызов метода ConstructL(), содержащий в себе код, способный привести к сбросу.

Некоторые из приведенных пунктов будут более детально раскрыты в дальнейших главах.

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


2.1 Что такое CBase?

Класс CBase является родителем всех остальных С-классов. Этот класс определен в заголовочном файле e32base.h, и имеет три основных свойства:

  • 1. Он обладает виртуальным деструктором, который используется
    стеком очистки для стандартного освобождения памяти, занимаемой всеми
    объектами, унаследованными от класса CBase. Виртуальный деструктор базового класса CBase позволяет больше не объявлять деструкторы наследующих классов виртуальными, так как это уже сделано в базовом классе.
  • 2. Класс CBase перегружает оператор new для обнуления данных С-классов при их инициализации. Удобства такого приема будут обсуждаться далее.
  • 3. Класс CBase имеет приватный конструктор
    копирования и оператор присваивания. Подобный прием позволяет
    предостеречь пользователей С-классов от случайного создания
    нежелательных копий объектов. Теперь, для создания копий объектов
    С-класса понадобится явно объявлять и реализовывать конструктор
    копирования и оператор присваивания.


2.2 Стек очистки

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

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

  • Check() – проверяет, находится ли ожидаемый элемент на самой вершине стека очистки, или нет.
  • Pop() – извлекает самый верхний элемент из стека очистки. Метод так же может использовать в качестве аргумента переменную TInt,
    указывающую на количество элементов, которые нужно извлечь из стека за
    один прием. Так же есть вариант этого метода, который использует в
    качестве аргумента указатель на последний элемент, который нужно будет
    извлечь из стека вместе с другими элементами, количество которых будет
    так же указываться при помощи TInt-аргумента aCount. При этом из стека сначала будет извлечено элементов в количестве aCount - 1,
    а затем будет произведена проверка корректности очередности при помощи
    переданного через аргументы указателя. Если последний элемент в стеке
    не будет равен элементу, переданному в качестве аргумента, то Symbian
    OS вызовет панику с кодом USER 90.
  • PopAndDestroy() извлекает элементы из стека, и освобождает занимаемую ими память. Этот метод может использовать те же аргументы, что и метод Pop().
  • PushL() – помещает указанный элемент в стек очистки.

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


2.3 Наследование класса CBase


2.3.1 Помните о главной причине наследования от CBase!

Что будет, если мы забудем произвести класс от CBase? Когда
указатель на объект такого класса помещается в стек очистки, стек
очистки будет интерпретировать его как указатель на объект класса TAny, а не как указатель на объект класса CBase. И когда стек очистки будет освобождать память, занимаемую таким объектом, он воспользуется методом User::Free(), в результате чего мы получим утечку памяти.


2.3.2 Вначале – CBase, потом – все остальное

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

class COurClass : public CBase, public MClass1

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

class CAnotherClass : public COurClass, public MClass2

Подобная практика не только улучшает иерархию наследования, но
еще так же исключает проблемы при приведении наследующего класса к
базовому. Если на первом месте в списке наследуемых классов окажется
класс, унаследованный не от класса CBase, у вас могут
возникнуть проблемы при помещении объектов такого класса в стек
очистки. Все дело в том, что когда объект, унаследованный от CBase, помещается в стек очистки при помощи одного из методов CleanupStack::PushL(), в дело пускается неявное приведение типов, и указатель на объект вашего класса конвертируется в указатель на объект класса CBase. При этом указатель, полученный таким образом, адресует в памяти объект типа CBase, а не объект класса COurClass, к примеру.

Когда объект извлекается из стека очистки, для него вызывается метод CleanupStack::Pop(TAny* aExpectedItem). Данный метод сравнивает адрес аргумента aExpectedItem
с адресом элемента, находящегося на самой вершине стека очистки. Если
эти адреса совпадают, то объект извлекается из стека очистки.

Что может случиться, если класс CBase не указан первым в списке наследуемых классов? Давайте рассмотрим такую ситуацию шаг за шагом:

1. У нас имеется следующий класс:

class CTestClass : public MCallback, public CBase

2. Как уже было сказано выше, когда указатель на объект класса CTestClass будет помещен в стек очистки, для него будет вызван метод PushL(CBase*). Для того, чтобы указатель, передаваемый в стек очистки, имел адрес объекта класса CBase, а не адрес объекта М-класса, его значение будет автоматически увеличено на 4 байта (см. рис. 1):


Image:Introduction-to-C-classes.png

Рис. 1: Размещение в памяти объекта класса CTestClass [2].

3. К сожалению, стек очистки не имеет соответствующего метода Pop(CBase*). Метод, при помощи которого указатели удаляется из стека, объявлен как Pop(TAny*). Наш M-класс может быть без проблем интерпретирован как TAny, и поэтому метод Pop(TAny*)
с удовольствием принимает переданный ему параметр без необходимого нам
увеличения значения указателя. В действительности же, метод Pop(TAny*)
сверяет адреса переданного ему в качестве аргумента указателя и
указателя, находящегося на самой вершине стека очистки. Данная проверка
выполняется при помощи метода Check(), и если адреса указателей окажутся неравными (в нашем случае разница будет равна 4 байтам), Symbian OS объявит панику с кодом E32USER-CBase 90, и остановит ваше приложение. (Обратитесь к источнику [2] за дополнительной информацией.)


2.3.3 Сравнивая CBase с другими...

Достаточно интересно сравнить класс CBase и его место в
иерархии классов с решениями в других языках программирования и
системах. Например, все классы языка Java неявным образом берут свое
начало от класса Object, и при объявлении Java-класса этот
факт даже не нужно описывать в коде. С другой стороны, стандарт ISO C++
не определяет какой-либо базовый класс, поэтому разработчики имеют
полную свободу в определении своих базовых классов. Однако библиотека
Си++ классов от Microsoft Foundation имеет класс CObject, являющийся базовым для всех остальных классов, и определяет следующие базовые параметры классов [3]:

  • Поддержка сериализации объекта
  • Информация о классе во время выполнения программы
  • Получение диагностической информации об объекте
  • Совместимость с классами массивов и коллекций

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


2.4 Виртуальный деструктор

Виртуальный деструктор, унаследованный нашим классом COurClass от CBase, позволяет создавать CBase-указатели на объекты класса COurClass. И при уничтожении такого CBase-указателя будет вызываться деструктор класса COurClass:

class COurClass : public CBase
{
// реализация нашего класса
}
 
void SomeOtherObject::SomeFunction()
{
CBase* anObject = new (ELeave) COurClass();
delete anObject;
}

В вышеприведенном примере указатель на объект класса COurClass, унаследованный от CBase, сохраняется в переменной указателя типа CBase. Когда будет вызываться оператор delete, код автоматически вызовет деструктор класса COurClass.

Обратите внимание на то, что в Symbian OS используется перегруженный оператор new, или new (ELeave). Этот оператор вызывает сброс, если для нового объекта не хватает места в памяти устройства.


3 Основные шаблоны использования С-классов


3.1 Очистка памяти

Деструктор C-класса должен освобождать все ресурсы, занимаемые
объектом. Конечно, есть вероятность, что очистка памяти будет
производиться частично инициализированным объектом, однако и в этом
случае не должно возникнуть никаких проблем, так как классы,
наследуемые от CBase, обнуляют свои члены, если при их создании был использован оператор new. А удаление NULL-объектов в Symbian OS никогда не приводит к ошибкам.

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

void CMyClass::SomeFunctionL()
{
delete iMemberData; // 1
iMemberData = NULL; // 2
iMemberData = CMyOtherClass::NewL(); // 3
}

После вызова оператора delete, переменная указателя iMemberData
все еще указывает на область памяти, которую раньше занимал удаленный
объект. Если перед новым использованием указателя ему не присвоить
значение NULL (строка 2), то дальнейшие попытки вызвать оператор delete для iMemberData, приведут к двойному удалению указателя, что, в свою очередь, вызовет панику системы. Если, опять же, вызов CMyOtherClass::NewL() в третьей строке приведет к сбросу, то системой тут же будет вызван деструктор класса CMyClass, который, в свою очередь, постарается удалить уже удаленный указатель iMemberData,
что опять приведет к двойному удалению указателя. Поэтому после того
как вы удалили указатель, всегда присваивайте ему значение NULL.

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

CMyClass::~CMyClass()
{
delete iMyDataMember;
// нет больше надобности присваивать указателю NULL!
}

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


3.2 Конструирование

С-классы в Symbian OS обычно конструируются в два шага:

1. Вначале создается объект при помощи вызова оператора new (ELeave) CMyTestClass(). Этот оператор, в свою очередь, вызывает стандартный конструктор класса, CMyTestClass().

2. Затем объект инициализируется при помощи метода