search

Введение в классы

Класс — это фундамент, на котором построена С++-поддержка объектно-ориентированного программирования, а также ядро многих более сложных программных средств. Класс — это базовая единица инкапсуляции, которая обеспечивает механизм создания объектов.

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

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

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

Объявление класса начинается с ключевого слова class. Класс создается с помощью ключевого слова class. Объявление класса синтаксически подобно объявлению структуры. Рассмотрим пример. Следующий класс определяет тип queue, который предназначен для реализации очереди. (Под очередью понимается список с дисциплиной обслуживания в порядке поступления, т.е. "первым прибыл — первым обслужен".)


// Создание класса queue.
class queue {
int q[100];
int sloc, rloc;
public:
void init();
void qput(int i);
int qget();
};	

Рассмотрим подробно объявление этого класса. По умолчанию члены класса являются закрытыми (private-членами). Все члены класса queue объявлены в теле инструкции class. Членами данных класса queue являются переменные q, sloc и rloc. Кроме того, здесь определено три функции-члена: init(), qput() и qget().

Любой класс может содержать как закрытые, так и открытые члены. По умолчанию все элементы, определенные в классе, являются закрытыми. Например, переменные q, sloc и rloc являются закрытыми. Это означает, что к ним могут получить доступ только другие члены класса queue; никакие другие части программы этого сделать не могут. В этом состоит одно из проявлений инкапсуляции: программист в полной мере может управлять доступом к определенным элементам данных. Закрытыми можно объявить и функции (в этом примере таких нет), и тогда их смогут вызывать только другие члены этого класса.

Ключевое слово public используется для объявления открытых членов класса. Чтобы сделать части класса открытыми (т.е. доступными для других частей программы), необходимо объявить их после ключевого слова public. Все переменные или функции, определенные после спецификатора public, доступны для всех других функций программы. Итак, в классе queue функции init(), qput() и qget() являются открытыми. Обычно в программе организуется доступ к закрытым членам класса через его открытые функции. Обратите внимание на то, что после ключевого слова public стоит двоеточие.

Следует иметь в виду, что объект образует своего рода связку между кодом и данными. Так, любая функция-член имеет доступ к закрытым элементам класса. Это означает, что функции init(), qput() и qget() имеют доступ к переменным q, sloc и rloc. Чтобы добавить функцию-член в класс, определите ее прототип в объявлении этого класса.

Определив класс, можно создать объект этого "классового" типа, используя имя класса. Таким образом, имя класса становится спецификатором нового типа. Например, при выполнении следующей инструкции создается два объекта Q1 и Q2 типа queue,


queue Q1, Q2;

После создания объект класса будет иметь собственную копию членов данных, которые составляют класс. Это означает, что каждый из объектов Q1 и Q2 будет иметь собственные отдельные копии переменных q, sloc и rloc. Следовательно, данные, связанные с объектом Q1, отделены (изолированы) от данных, связанных с объектом Q2.

Чтобы получить доступ к открытому члену класса через объект этого класса, используйте оператор "точка" (именно так это делается и при работе со структурами). Например, чтобы вывести на экран значение переменной sloc, принадлежащей объекту Q1, используйте следующую инструкцию.


cout << Q1.sloc;

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

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

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


void queue::qput(int i)
{
if(sloc==100) {
cout << "Очередь заполнена.\n";
return;
}
sloc++;
q[sloc] = i;
}

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

Оператор "::" называется оператором разрешения области видимости. По сути, он сообщает компилятору, что данная версия функции qput() принадлежит классу queue. Другими словами, оператор "::" заявляет о том, что функция qput() находится в области видимости класса queue. Различные классы могут использовать одинаковые имена функций. Компилятор же определит, к какому классу принадлежит функция, с помощью оператора разрешения области видимости и имени класса.

Функции-члены можно вызывать только относительно заданного объекта. Чтобы вызвать функцию-член из части программы, которая находится вне класса, необходимо использовать имя объекта и оператор "точка". Например, при выполнении этого кода будет вызвана функция init() для объекта ob1.


queue ob1, ob2;

ob1.init();

При вызове функции ob1.init() действия, определенные в функции init(), будут направлены на копии данных, относящиеся к объекту ob1. Следует иметь в виду, что ob1 и ob2 — это два отдельных объекта. Это означает, что, например, инициализация объекта ob1 отнюдь не приводит к инициализации объекта ob2. Единственное, что связывает объекты ob1 и ob2, состоит в том, что они имеют один и тот же тип.

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

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


#include <iostream>
using namespace std;
// Создание класса queue.
class queue {
int q[100];
int sloc, rloc;
public:
void init();
void qput(int i);
int qget();
};
// Инициализация класса queue.
void queue::init()
{
rloc = sloc = 0;
}
// Занесение в очередь целочисленного значения.
void queue::qput(int i)
{
if(sloc==100) {
cout << "Очередь заполнена.\n";
return;
}
sloc++;
q[sloc] = i;
}
// Извлечение из очереди целочисленного значения.
int queue::qget()
{
if(rloc == sloc) {
cout << "Очередь пуста.\n";
return 0;
}
rloc++;
return q[rloc];
}
int main()
{
queue a, b; // Создание двух объектов класса queue.
а.init();
b.init();
a.qput(10);
b.qput(19);
a.qput(20);
b.qput(1);
cout << "Содержимое очереди a: ";
cout << a.qget() << " ";
cout << a.qget() << "\n";
cout << "Содержимое очереди b: ";
cout << b.qget() << " ";
cout << b.qget() << "\n";
return 0;
}

При выполнении эта программа генерирует такие результаты.
Содержимое очереди а: 10 20
Содержимое очереди b: 19 1

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


а.rloc = 0;
нельзя включить в функцию main() нашей программы

Общий формат объявления класса

Все классы объявляются подобно приведенному выше классу queue. Общий формат объявления класса имеет следующий вид.


class имя_класса {
закрытые данные и функции
public:
открытые данные и функции
} список_объектов;

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

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

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


// Демонстрация доступа к членам класса.
#include <iostream>
using namespace std;
class myclass {
int a; // закрытые данные
public:
int b; // открытые данные
void setab(int i); // открытые функции
int geta();
void reset();
};
void myclass::setab(int i)
{
a = i; // прямое обращение к переменной а
b = i*i; // прямое обращение к переменной b
}
int myclass::geta()
{
return a; // прямое обращение к переменной а
}
void myclass::reset()
{
// Прямой вызов функции setab()
setab(0); // для уже известного объекта.
}
int main()
{
myclass ob;
ob.setab(5); // Устанавливаем члены данных ob.a и ob.b.
cout << "Объект ob после вызова функции setab(5): ";
cout << ob.geta() << ' ';
cout << ob.b; // К члену b можно получить прямой
доступ, поскольку он является public-членом.
cout << '\n';
ob.b = 20; // Член b можно установить напрямую, поскольку он
является public-членом.
cout << "Объект ob после установки члена ob.b=20: ";
cout << ob.geta() <<' ';
cout << ob.b;
cout << '\n';
ob.reset();
cout << "Объект ob после вызова функции ob.reset(): ";
cout << ob.geta() << ' ';
cout << ob.b;
cout << '\n';
return 0;
}
При выполнении этой программы получаем следующие результаты.
Объект ob после вызова функции setab(5): 5 25
Объект ob после установки члена ob.b=20: 5 20
Объект ob после вызова функции ob.reset(): 0 0

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


а = i; // прямое обращение к переменной а
b = i*i; // прямое обращение к переменной b

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

Теперь обратите внимание на то, что переменная b — открытый (public) член класса myclass. Это означает, что к b можно получить доступ из кода, определенного вне тела класса myclass. Следующая строка кода из функции main(), при выполнении которой переменной b присваивается число 20, демонстрирует реализацию такого прямого доступа


ob.b = 20; // К члену b можно получить прямой доступ.
// поскольку он является public-членом.

Поскольку эта инструкция не принадлежит телу класса myclass, то к переменной b возможен доступ только с использованием конкретного объекта (в данном случае объекта ob) и оператора "точка"

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

ob.reset();

Поскольку функция reset() является открытым членом класса, ее также можно вызвать из кода, определенного вне тела класса myclass, и посредством конкретного объекта (в данном случае объекта ob).

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

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

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