Доброго времени суток, случайные и неслучайные читатели. Сегодня я хочу рассказать о любопытном эксперименте, который провёл в процессе изучения C++.
Содержание
- Постановка задачи
- «Царство Протозоа, класс Лобозные амёбы, отряд...»
- Реализация методов
- Разделять и властвовать
Постановка задачи
В один прекрасный день на курсах мы начали изучать классы в C++.
По условию одной из задач, заданных на практике в НИИТе, необходимо было создать класс-счётчик, который знал бы количество созданных экземпляров этого класса.
Эта задача легко решается с помощью статических (static) полей класса.
Так как в работе с классами я чувствовал себя ещё недостаточно уверенно, то решил пойти дальше и усложнить задачу. Чтобы понять, как всё это работает, я начал создавать класс, который мог бы "делиться", знал бы своих "потомков" и их количество, а так же знал "предков". Но обо всём по-порядку.
«Царство Протозоа, класс Лобозные амёбы, отряд...»
Поскольку я хочу понять работу с классами, с конструирования класса и начнём. Из-за сходства поведения (доступных методов) этого класса с амёбой, я решил назвать мой класс в честь этих микроскопических организмов - clsAmoeba (по-русски, я думаю, это будет что-то вроде "КАмёба", т.е. "Класс амёба"). Название класса само собой стало названием этого проекта.
Предупреждаю, что мои ручные КАмёбы совершенно безобидны (они не умеют стрелять лазерными лучами и так малы, что требуют совсем немного памяти). Поэтому, когда программа заработает, вы сможете создать их сколько угодно (насколько хватит памяти вашего компьютера) без риска для жизни.
Тем не менее, стандартная КАмёба должна уметь как минимум три вещи:
- Делиться;
- Сообщать количество уже существующих КАмёб;
- Знать всех своих потомков.
Каждая КАмёба имеет свой IDентификационный номер, который тоже умеет сообщать (иначе как мы узнаем, с какой КАмёбой мы имеем дело?).
Я объявил класс в заголовочном файле main.h и включил этот файл с помощью директивы include в другие файлы проекта.
Листинг 1: Класс clsAmoeba.
class clsAmoeba
{
public:
clsAmoeba();
clsAmoeba(const clsAmoeba& pt);
~clsAmoeba();
/* This methods works with the counter */
void incr() { counter++; };
void decr() { counter--; };
int getCount() { return counter; };
/* This method returns ID of an amoeba */
int getID() { return id; };
/* This method returns pointer to parent of an amoeba */
clsAmoeba& getParent() { return *parent; };
/* Get count of the descendants of an amoeba */
int getDCount() { return dCount; }
/* Get descendant of an amoeba */
clsAmoeba& getDescendant(int i) { return *descendants[i]; }
/* This method divide an amoeba */
void divide();
private:
/* ID of the amoeba */
int id;
/* Pointer to parent of an amoeba */
clsAmoeba* parent;
/* Descendants of an amoeba */
clsAmoeba** descendants;
int dCount;
/* Counter of amoebas */
static int counter;
};
Методами класса называются функции (некоторые действия), которые может выполнять данный класс.
Полями класса называются переменные этого класса.
Ключевые слова "public" и "private" являются спецификаторами области видимости всего того, что объявлено после них (до конца объявления класса или же до следующего спецификатора). Методы, как правило, представляют собой интерфейс доступа к классу. Прямого доступа к полям "извне" желательно избегать. Поля предназначены для "внутреннего использования" в классе, и доступ к ним должны иметь только особо посвящённые - а именно, методы этого класса. Поэтому обычно методы объявляются в секции "public" и доступны извне, а поля объявляются в секции "private" и скрыты от внешнего мира.
Доступ к потомкам КАмёбы обеспечен через метод getDescendant(). Этот метод принимает номер потомка (не ID, а именно номер потомка в массиве descendants), и возвращает указатель на класс-потомок. Метод getDCount() возвращает количество потомков.
Самый интересный метод в классе clsAmoeba - метод divide(). При вызове этого метода КАмёба (т.е. класс) делится две части: класс создаёт свою копию в массиве потомков.
Часть методов - например, простые методы вроде getID(), которые просто возвращают значение какого-то поля класса - объявлены прямо в теле класса. Такие классы называются "инлайновыми" (англ. Inline), т.е. в переводе на русский язык, встраиваемыми. И это действительно так - компилятор заменяет в коде вызовы этих методов на код внутри самих методов. Такие методы работают быстрее, так как при работе программы не тратится время на вызов метода. Встраиваемые методы не обязательно объявлять в теле класса - вместо этого, можно использовать ключевое слово inline перед объявлением метода.
Реализация методов
Все не-инлайновые методы КАмёбы я вынес в отдельный модуль amoeba.cpp.
Первым логично создать конструкторы и деструкторы класса - которые вызываются при создании экземпляра класса и при его уничтожении, соответственно.
Конструкторы и деструкторы
Конструкторы класса вызываются при создании каждого экземпляра класса и представляют собой специальные методы, имя которых должно совпадать с именем класса.
Первый конструктор производит инициализацию полей класса, увеличивает счётчик КАмёб на 1 и присваивает текущее значение счётчика в качестве ID созданной КАмёбе. Этот конструктор вызывается при создании первой КАмёбы.
Листинг 2: Конструктор 1.
clsAmoeba::clsAmoeba() :
parent(NULL),
descendants(NULL),
dCount(0)
{
incr();
id = counter;
}
Второй конструктор называется конструктором копирования и вызывается в случае деления КАмёбы.
Листинг 3: Конструктор 2.
clsAmoeba::clsAmoeba(const clsAmoeba& pt) :
descendants(NULL),
dCount(0)
{
if(&pt != this)
{
incr();
id = counter;
parent = (clsAmoeba*) &pt;
}
}
Эти два конструктора являются по сути перегруженными методами, т.е. методами с одним именем и разными списками принимаемых параметров (есть ещё несколько критериев для различия перегруженных методов, но об этом вы можете прочитать где-нибудь самостоятельно). В первом случае, метод не принимает никаких параметров, во втором - он принимает указатель на КАмёбу Компилятор сам решает, когда какую функцию (метод) вызывать.
Деструктор вызывается при уничтожении экземпляра класса, и участвует в его уничтожении. В данном случае, он уменьшает на 1 количество "живых" КАмёб и уничтожает своих потомков (что, кстати, не совсем логично).
Листинг 4: Деструктор.
clsAmoeba::~clsAmoeba()
{
decr();
parent = NULL;
/* I think here may be a memory leak
* because I don't call delete for the
* descendents of the every descendent...
*/
delete [] descendants;
}
Метод divide()
Представим, что наш первый экземпляр КАмёбы, шевеля ложноножками, выполз из воды на сушу...
Листинг 5: Создаю первый экземпляр КАмёбы (файл main.cpp).
#include <iostream>
#include "main.h"
using namespace std;
int clsAmoeba::counter = 0; // Инициализирую счётчик КАмёб
int main()
{
clsAmoeba amoeba1;
//...
return 0;
}
Вокруг - крайне недружелюбная атмосфера. Однако, других живых созданий крупнее нашей КАмёбы мы здесь вряд ли найдём. Поэтому хваталки и кусалки для нашего экземпляра не особенно и нужны (возможно, его потомки обзаведутся этими "фичами"). Что действительно важно, так это возможность создавать себе подобных. В случае с КАмёбой этот процесс достаточно прост - он сводится к делению оригинала надвое. Для этого предназначен метод divide().
Листинг 6: Метод divide() (файл amoeba.cpp)
void clsAmoeba::divide()
{
if(!dCount)
{
descendants = new clsAmoeba*;
descendants[dCount++] = new clsAmoeba(*this);
}
else
{
clsAmoeba** tmp = new clsAmoeba* [dCount];
for(int i = 0; i < dCount; i++)
tmp[i] = descendants[i];
delete [] descendants;
dCount++;
descendants = new clsAmoeba* [dCount];
for(int i = 0; i < dCount; i++)
descendants[i] = tmp[i];
descendants[dCount-1] = new clsAmoeba(*this);
delete [] tmp;
}
}
Если КАмёба делится первый раз, то потомков 0. Следовательно, выделяю память под первого потомка и создаю его. Если потомков на момент вызова метода больше 0, то создаю временное хранилище tmp под существующих потомков, освобождаю память, выделенную под массив descendants. Увеличиваю количество потомков на 1, снова выделяю память под массив descendants, но уже для нового количества потомков. Далее копирую существующих потомков из tmp в descendants. В завершении, создаю нового потомка в конце массива и освобождаю память, выделенную под временный массив tmp.
Разделять и властвовать
Для тестирования программы я создал 24 КАмёбы в main.cpp путём деления amoeba1 в цикле, а потом - её потомков, и потомков потомков...
Листинг 7: Делим, делим, делим...
// ...
/* I'm making heap of amoebas */
for(int i = 0; i < 10; i++)
amoeba1.divide();
clsAmoeba& amoebaN = amoeba1.getDescendant(2);
for(int i = 0; i < 5; i++)
amoebaN.divide();
clsAmoeba& amoebaN2 = amoebaN.getDescendant(4);
for(int i = 0; i < 4; i++)
amoebaN2.divide();
clsAmoeba& amoebaN3 = amoebaN2.getDescendant(2);
for(int i = 0; i < 4; i++)
amoebaN3.divide();
//...
В качестве интересных идей по улучшению программы можно предложить "сохранение в живых" потомков класса после его "гибели". Так же неплохо бы иметь возможность получить информацию по созданным экземплярам класса. Вывод информации по созданным КАмёбам удобно реализовать в виде рекурсивной функции, вариант которой я рассмотрю в следующем посте.