Введение в объектно-ориентированное программирование на C++

№57-4,

Педагогические науки

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

Похожие материалы

В объектно-ориентированном программировании базовым понятием выступает понятие объекта, который имеет определенные свойства [3-5]. Состояние объекта задается перечислением значений его признаков (полей). Кроме того, обычно объект располагает набором методов, призванных решать разнообразные задачи. При написании программы, содержащей объекты, можно организовать взаимодействие объектов между собой.

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

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

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

  • class имя класса { список членов };
  • struct имя класса { список членов };
  • union имя класса { список членов };

Различия между тремя объявлениями класса заключаются, во-первых, в разных правах доступа, присваиваемых компонентам класса по умолчанию, а во-вторых, в способе расположения компонент класса в памяти. Если класс объявлен с использованием ключевых слов struct или union, компоненты класса по умолчанию являются доступными вне этого класса. Если же класс объявлен через class, его компоненты по умолчанию недоступны вне класса. Когда класс определен, его имя может быть использовано для объявления объекта этого класса.

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

Если метод описан в определении класса, то он автоматически встраивается inline; если же метод описан вне определения класса, это можно сделать путем явного использования ключевого слова inline.

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

#include "stdafx.h"
#include <iostream>
#include <math.h>
using namespace std;
class TComplex
  { public:
      int a,b;
      TComplex(int,int);    //Конструктор класса,
      void Out();                // Вывод комплексного числа
      double ABS(); };       //Подсчет модуля числа
  TComplex::TComplex(int a0, int b0)
  {a=a0; b=b0; }
  void TComplex::Out()
  {if (b>=0) cout<<a<<"+"<<b<<"*i"<<endl;
    else cout<<a<<b<<"*i"<<endl; }
  double TComplex::ABS()
  {return  pow(a*a+b*b,0.5); }
int main()
{setlocale(LC_ALL,"Rus");
  TComplex A(1,1),B(3,4);
  A.Out();
  cout<<"Модуль числа: "<<A.ABS()<<endl;
  B.Out();
  system("pause"); return 0;}

Здесь метка public (открытая) является одним из спецификаторов доступа к элементам класса, о которых будет сказано ниже. Функция-элемент с тем же именем, что и класс, называется конструктором этого класса. Конструктор представляет собой специальный метод, который инициализирует поля объекта этого класса. Конструктор класса вызывается автоматически при создании объекта этого класса, а в случае, если он явно не указан в описании класса, – создается автоматически. Класс может иметь несколько конструкторов.

Спецификаторы доступа к элементу используются для управления доступом к данным-элементам (полям) класса и функциям-элементам (методам) класса. Различают следующие спецификаторы доступа: public (открытый), private (закрытый), protected (защищенный). Спецификаторы доступа к элементам всегда заканчиваются двоеточием и могут быть использованы в определении класса сколько угодно раз и в любом порядке. После каждого спецификатора определенный им режим доступа действует до следующего спецификатора (или до конца определения класса). По умолчанию режим доступа для классов – private, поэтому все элементы после заголовка класса и до первого спецификатора доступа являются закрытыми.

Закрытые элементы класса могут быть доступны только для методов и дружественных функций этого класса [1, 2]. Открытые элементы класса могут быть доступны для любых функций в программе. Защищенные элементы класса могут использоваться только в функциях-членах и друзьях класса, а также в классах, производных от него. Смысл такого подхода заключается в том, что клиентов класса не должно волновать истинное представление данных внутри класса (клиентам не обязательно знать, каким образом класс выполняет их задачи). Другими словами, реализация класса скрыта от клиента – это облегчает восприятие класса клиентами и способствует модифицируемости программ. Клиентом может выступать, например, или метод другого класса, или некоторая глобальная функция.

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

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

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

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

Вне класса к элементам класса можно обращаться либо через имя объекта, либо через указатель на объект, либо ссылкой на элемент. Рассмотрим на примере простого класса TObject, каким образом это достигается. Создадим три экземпляра переменных типа TObject: Object, ObjectRef (ссылка на объект типа TObject), ObjectPtr (указатель на объект типа TObject).

#include "stdafx.h"
#include <iostream>
using namespace std;
class TObject
  { public:
       int x;
       void Out() {cout<<x<<endl;} };
int main()
{ setlocale(LC_ALL,"Rus");
  TObject Object,              //создаем объект Object
  &ObjectRef=Object,       //ссылка на Object
  *ObjectPtr=&Object;     //указатель на Object
  cout<<"Присваивание значения и вывод по имени объекта:"<<endl;
  Object.x=10;
  Object.Out();
  cout<<"Присваивание значения и вывод по ссылке:"<<endl;
  ObjectRef.x=15;
  ObjectRef.Out();
  cout<<"Присваивание значения и вывод по указателю:"<<endl;
  ObjectPtr->x=75;
  ObjectPtr->Out();
  system("pause"); return 0; }

Возвращение ссылки на закрытый элемент данных в некоторых случаях может привести к нежелательным последствиям, т.к. нарушает инкапсуляцию класса. Поэтому нужно стараться избегать возвращения из открытой функции-элемента неконстантной ссылки (или указателя) на закрытый элемент данных. Приведем здесь простой пример, поясняющий только что сказанное. Рассмотрим упрощенный класс работы с дробями. Пусть он включает закрытые поля (данные-элементы) – числитель и знаменатель дроби (x, y), закрытый метод – проверка на равенство нулю указанного значения Error_OK(), и открытые методы: установка значений числителя и знаменателя дроби Set(), вывод дроби Out(), метод, возвращающий значение знаменателя дроби по ссылке Get_Y().

#include "stdafx.h"
#include <iostream>
using namespace std;
class Drob {
  public:
    Drob(int=1,int=1);
    bool Set(int,int);
    void Out();
    int &Get_Y();         //нежелательное возвращение ссылки!!!
  private:
    int x,y;
    bool Error_OK(int); };
Drob::Drob(int a, int b)
  {x=a;
   if (!Error_OK(b)) y=b; else y=1;}
bool Drob::Error_OK(int a)
  {if (a==0) return true; else return false; }
bool Drob::Set (int a, int b)
  {if (!Error_OK(b)) { x=a; y=b; return true; }
    else return false; }
void Drob::Out()
  {cout<<x<<"/"<<y<<endl; }
int &Drob::Get_Y()
  {return y; }             //нежелательное возвращение ссылки!!!
int main()
{ setlocale(LC_ALL,"Rus");
  Drob A(5,0);
  A.Out();
  A.Set(2,3);
  A.Out();
  cout<<"Знаменатель дроби: "<<A.Get_Y()<<endl;
  A.Get_Y()=0;  //установит в ноль знаменатель дроби!!!
  A.Out();
  system("pause"); return 0;}

Здесь в конструкторе и методе Set() предусмотрена обработка такой ситуации, когда в качестве знаменателя дроби задается ноль. Конструктор в этом случае присваивает знаменателю значение 1, а метод Set() – просто возвращает false как результат выполнения операции (при этом значение дроби не изменяется). Метод же Get_Y() возвращает ссылку на закрытый элемент класса – знаменатель дроби, и используется для получения его значения. Однако, например, строка A.Get_Y()=0 приводит к опасному изменению закрытого члена класса – устанавливает в ноль знаменатель дроби, что является недопустимым. Чтобы исключить возможность такой случайной модификации закрытого элемента класса, нужно возвращать константную ссылку, т.е. в описании класса строку

int &Get_Y();

заменить на строку

const int &Get_Y();

Описание метода Get_Y() вне класса также изменится:

const int &Drob::Get_Y()

После таких изменений любая попытка модификации знаменателя дроби путем использования метода Get_Y() приведет к сообщению компилятора об ошибке.

Список литературы

  1. Дмитриев В.Л. Учебный пример реализации класса «Многочлен» на языке программирования С++ // NovaInfo.Ru. 2014. № 29. – С. 37-45.
  2. Дмитриев В.Л. Примеры использования дружественных функций и дружественных классов в языке программирования С++ // NovaInfo.Ru. 2014. № 29. – С. 45-50.
  3. Прата С. Язык программирования С++. Лекции и упражнения, 5-е изд.: Пер. с англ. – М.: Вильямс, 2007. – 1184 с.
  4. Страуструп Б. Язык программирования С++. Специальное издание. – М.: Бином, 2004. – 1054 с.
  5. Stroustrup Bjarne. The C++ programming language / Bjarne Stroustrup. – Fourth edition. – Boston: Addison-Wesley, 2013. – 1368 p.