вернуться в оглавление предыдущая глава предыдущий параграф следующий параграф следующая глава


4.2. Классы

В языке С/С++ концепция классов стала расширением понятия структуры, но в отличие от структуры, классы предназначены для выражения не только структуры объекта, но и его поведения. То есть благодаря классам мы можем описать объект (его атрибуты и поведение) на языке программирования и заставить его "жить" в программе. Атрибуты будут описаны на языке в виде переменных-членов класса, а поведение (операции, действия) - в виде функций-членов.

Благодаря этому, при моделировании оптических явлений и процессов, можно в программировании пользоваться теми же понятиями: оптическая поверхность, оптическая среда, линза, диафрагма, предмет, изображение.

На языке С/С++ реализация класса мало чем отличается от реализации структуры, но принято для описания поведения объекта использовать класс, а для хранения разнородных данных (если функции-члены не используются) использовать структуры.

Также как для идентификаторов (переменных) и функций существуют понятия определения, объявления, реализации класса.

Объявление класса - резервирует имя (лексему) как имя класса, нового типа данных.

 class Lens; 
 class Beam;
 class Surface; 

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

Определение класса - языковая конструкция, которая определяет переменные и функции члены класса (их имена, набор аргументов).

Реализация класса – реализация всех функций членов класса.

4.2.1. Пример 4.3. Класс Линза

Рассмотрим пример класса Линза.

/////////////////////////////////////////////////////////////////////////////
// Прикладное программирование
// Пример 4.3. Класс Линза
// lens.h
// 
// Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru
// Университет ИТМО
/////////////////////////////////////////////////////////////////////////////
// проверка на повторное подключение файла
#if !defined LENS_H
#define LENS_H 

#include <iostream>
#include "paraxial.h"
/////////////////////////////////////////////////////////////////////////////
// класс ЛИНЗА
class Lens
{
private:
    // радиусы кривизны линзы
    double m_r1, m_r2;
    // диаметр линзы
    double m_D;
    // осевое расстояние
    double m_d;
    // показатель преломления
    double m_n;
    // параксиальные характеристики
    Paraxial m_paraxial;

public: 
    // конструкторы и деструктор
    Lens();     
    Lens(double r1, double r2, double D, double d=0., double n=1.); 
    Lens(double r1, double r2);
    Lens(const Lens& one); 
    ~Lens(); 

    // установка показателя преломления 
    void Set_n(double n); 
    // получение показателя преломления 
    double Get_n() const; 
    // установка осевого расстояния 
    void Set_d(double d); 
    // получение осевого расстояния
    double Get_d() const; 
    // ...

    // получение параксиальных характеристик
    Paraxial GetParaxial() const;

    // запись и чтение
    void write(std::ostream& out) const;
    void read(std::istream& in);

private:
    // вычисление параксиальных характеристик
    void CalculateParaxial();
}; 
/////////////////////////////////////////////////////////////////////////////
// установка показателя преломления 
inline void Lens::Set_n(double n)
{
    m_n=n; 
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// получение показателя преломления
inline double Lens::Get_n() const
{
    return m_n; 
} 
/////////////////////////////////////////////////////////////////////////////
// установка осевого расстояния
inline void Lens::Set_d(double d)
{
    m_d=d; 
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// получение осевого расстояния
inline double Lens::Get_d() const
{
    return m_d; 
} 
/////////////////////////////////////////////////////////////////////////////
// получение параксиальных характеристик
inline Paraxial Lens::GetParaxial() const
{
    return m_paraxial;
}
/////////////////////////////////////////////////////////////////////////////
#endif //defined LENS_H

 

/////////////////////////////////////////////////////////////////////////////
// Прикладное программирование
// Пример 4.3. Класс Линза
// paraxial.h
// 
// Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru
// Университет ИТМО
/////////////////////////////////////////////////////////////////////////////
// проверка на повторное подключение файла
#if !defined PARAXIAL_H
#define PARAXIAL_H 

#include <iostream>
/////////////////////////////////////////////////////////////////////////////
// структура параксиальных характеристик
struct Paraxial
{
    double F, F_;   // фокусные расстояния
    double SF, SF_; // фокальные отрезки
    double SH, SH_; // положения главных плоскостей

    Paraxial();
    void write(std::ostream& out) const;
};
/////////////////////////////////////////////////////////////////////////////
inline Paraxial::Paraxial()
    : F(0)
    , F_(0)
    , SF(0)
    , SF_(0)
    , SH(0)
    , SH_(0)
{
}
/////////////////////////////////////////////////////////////////////////////
inline void Paraxial::write(std::ostream& out) const
{
    out<<"f="<<F<<" f\'="<<F_<<" Sf="<<SF<<" Sf\'="<<SF_<<" SH="<<SH<<" SH\'="<<SH_<<endl;
}
/////////////////////////////////////////////////////////////////////////////
#endif //defined LENS_H

 

/////////////////////////////////////////////////////////////////////////////
// Прикладное программирование
// Пример 4.3. Класс Линза
// lens.cpp
// 
// Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru
// Университет ИТМО
/////////////////////////////////////////////////////////////////////////////
using namespace std;

// подключение описания класса
#include "lens.h"
/////////////////////////////////////////////////////////////////////////////
// конструктор по умолчанию
Lens::Lens()
: m_r1(0)
, m_r2(0)
, m_d(0)
, m_D(0)
, m_n(1.)
{
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// полный конструктор
Lens::Lens(double r1,double r2,double D,double d,double n)
: m_r1(r1)
, m_r2(r2)
, m_d(d)
, m_D(D)
, m_n(n)
{
    if(n<1)
        throw exception("Index of refraction should be greater than 1.");
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// неполный конструктор
Lens::Lens(double r1, double r2)
: m_r1(r1), m_r2(r2), m_d(2.), m_D(5.), m_n(1.5)
{
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// конструктор копирования
Lens::Lens(const Lens& l)
: m_r1(l.m_r1)
, m_r2(l.m_r2)
, m_d(l.m_d)
, m_D(l.m_D)
, m_n(l.m_n)
{
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// деструктор
Lens::~Lens()
{
}
/////////////////////////////////////////////////////////////////////////////
// запись
void Lens::write(ostream& out) const
{
    out<<m_r1<<" "<<m_r2<<" "<<m_d<<" "<<m_D<<" "<<m_n<<endl;
} 
/////////////////////////////////////////////////////////////////////////////
// чтение
void Lens::read(istream& in)
{
    in>>m_r1>>m_r2>>m_d>>m_D>>m_n;
    CalculateParaxial();
} 
/////////////////////////////////////////////////////////////////////////////
// вычисление параксиальных характеристик
void Lens::CalculateParaxial()
{
    // вычисление параксиальных характеристик
    // для проверки задаются значения 100, -100
    m_paraxial.F=100.;
    m_paraxial.F_=-100.;
    //m_paraxial.SF=...
    //m_paraxial.SF_=...
    //m_paraxial.SH=...
    //m_paraxial.SH_=...
}
/////////////////////////////////////////////////////////////////////////////

 

/////////////////////////////////////////////////////////////////////////////
// Прикладное программирование
// Пример 4.3. Класс Линза
// test_lens.cpp
// 
// Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru
// Университет ИТМО
/////////////////////////////////////////////////////////////////////////////
#include <iostream>
using namespace std;

// подключение описания класса
#include "lens.h"
/////////////////////////////////////////////////////////////////////
void main()
{
    //----------------------------------------------------------------
    // тестирование констуркторов класса
    // в момент создания экземпляров класса вызывается конструктор по умолчанию
    Lens lens1; 
    Lens lens3[10]; 

    // вызывается полный конструктор
    Lens lens4(200., -200., 2., 5., 1.5); 
    // вызывается неполный конструктор
    Lens lens5(100,-100, 66.);

    //----------------------------------------------------------------
    // тестирование доступа к членам класса
    // доступ к членам класса осуществляется по оператору "." 
    lens5.Set_n(1.6); 

    // создание указателя на экзепляр класса
    Lens* lens6=&lens5;
    // если объявлен указатель на экземпляр класса, 
    // доступ к членам класса осуществляется по оператору "->"
    double n=lens6->Get_n(); 

    // вывод на экран значений показателя преломления
    // обращение к private членам осуществляется при помощи функций-селекторов
    cout<<n<<" "<<lens5.Get_n()<<" "<<lens6->Get_n()<<endl;
    //  cout<<lens5.m_n;     // m_n - private-член (ошибка)
    //  cout<<lens6->m_n;    // m_n - private-член (ошибка)
    //  cout<<lens6.Get_d(); // lens6 - указатель (ошибка)

    // вывод всех параметров линзы на экран
    lens5.write(cout);

    // вывод на экран параксиальных характеристик линзы
    Paraxial parax;  // объявление экземпляра параксиальных характеристик
    parax=lens4.GetParaxial();
    parax.write(cout);

    //----------------------------------------------------------------
    // в случае возникновения исключительной ситуации внутри блока try 
    // управление переходит к блоку catch
    try 
    {
        Lens lens7(100., -100., 50., 5., 0.); 
        parax=lens7.GetParaxial();
        parax.write(cout);
    }
    // блок catch - обработка ошибки
    catch(exception& error) 
    {
        // вывод на экран сообщения об ошибке
        cout<<error.what()<<endl;
    }
}
/////////////////////////////////////////////////////////////////////

Разберем подробнее этот пример.

4.2.2. Директивы препроцессору # if ! defined, # endif (проверка на повторное подключение)

Обратите внимание, что определение класса заключено в следующую конструкцию, представляющую собой директиву препроцессору:

// проверка на повторное подключение файла
#if !defined LENS_H
#define LENS_H 

// ...

#endif //defined LENS_H

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

Данная конструкция предотвращает включение кода, находящегося между # if ! defined и # endif. Если заголовок ранее не включался в исходный файл, директива #define определяет имя LENS _ H и далее подключит остальное содержимое заголовочного файла. Если этот файл уже подключался ранее, LENS _ H уже определено и содержимое заголовочного файла повторно не подключается.

Обычно имя константы в этих препроцессорных директивах является именем заголовочного файла в верхнем регистре с заменой точки на символ подчеркивания.

4.2.3. Тип доступа к членам класса

class Lens
{
private: // доступны только внутри класса
// ... 

protected: // доступны внутри и для наследников
// ... 

public: // доступны везде
// ...

} 

При описании класса для описания доступа к переменным и функциям-членам класса используются ключевые слова private, protected, public. Переменные и функции, описанные как private будут доступны только функциям-членам класса, к ним невозможно обратиться извне класса через оператор "." или "->".

Переменные и функции, описанные как public, будут доступны и внутри класса, и снаружи. Класс видимости protected играет важную роль при наследовании: если к некоторым private-членам класса необходимо получать доступ из классов наследников, то они должны быть помечены как protected. Подробно о наследовании см.главу 5.

Таким образом, public-функции и переменные называются интерфейсом и позволяют управлять состоянием и поведением класса, а то, что относится к внутренней реализации, должно быть скрыто посредством private. Посредством сокрытия части информации об объекте в нём самом, выполняется один из принципов ООП – инкапсуляция. Обычно скрывается его внутреннее устройство (набор атрибутов и реализация методов). Но внешнее поведение (public-члены) сразу прорабатываются настолько подробно, чтобы интерфейс объекта не изменялся.

4.2.4. Принципы объектно-ориентированного проектирования

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

Принципы ООП:

  • Абстракция - формирование представления о свойствах и поведении предмета путем выделения существенных характеристик, отличающих его от других видов объектов. Анализируется внешнее поведение объекта (интерфейс и интерфейсные функции).
    Абстрагирование - один из основных методов, используемых для решения сложных задач.
  • Инкапсуляция - никакая часть сложной системы не должна зависеть от внутреннего устройства какой-либо другой части. Инкапсуляция позволит не только скрыть детали реализации, но и позволит коллегам независимо друг от друга улучшать реализацию своих объектов (эффективность, скорость, требуемый объём памяти и т.п.).
  • Сохраняемость - возможность объекта сохранить информацию о своём состоянии, а затем при необходимости восстановить его.
  • Наследование - отношение между классами, когда один объект заимствует структурную и функциональную часть другого. Подробно о наследовании см.главу 5.
  • Полиморфизм (типизация) - возможность принимать множество форм, изменять поведение в зависимости от ситуации. Подробно о полиморфизме см.главу 6.

4.2.5. Типы функций-членов класса

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

Можно выделить следующие группы действий (функций-членов класса):

  • конструктор - операция создания объекта и инициализации его атрибутов
  • деструктор - операция, освобождающая используемые объектом ресурсы и разрушающая его
  • модификатор - операция, которая изменяет состояние объекта (clear, set_ n, set_ d, …)
  • селектор - операция, считывающая состояние объекта, но не меняющая ( get_ n, get_ r1, … )
  • утилита - операция высокого уровня, выполняющая те или иные действия в зависимости от сущности объекта ( Ray Trace, CalcParaxial).

При реализации функций-членов класса, оператор :: показывает принадлежность функции к классу. При реализации всех функций-членов класса к имени функции добавляет <имя класса::>, в нашем случае Lens::. Например:

void Lens::CalculateParaxial() 
{ 
 
}