1 Классы и управление доступом

1.1 Понятие класса

Класс можно понимать как:

  1. Развитие структуры языка С.
  2. Представление некоторой концепции, понятия.

Преимущества классов

  • Класс - определяемый пользователем тип
  • Новые типы настолько же удобны в использовании, как и встроенные
  • Программы легче понимать и изменять
  • Проще анализировать код и обнаруживать ошибки
  • Отделение деталей реализации от интерфейса

Формула класса

Существует определённая терминология, связанная с классом

  • Член класса - любой элемент данных или функция, описанная в классе;
  • Метод - функция, описанная в классе;
  • Объект, или экземпляр класса - переменная, созданная на основе класса;

Устройство класса

Рассмотрим общий синтаксис класса

class имя_класса
{
   private:
     закрытые данные
     закрытые методы
   protected:
     защищенные данные
     защищенные методы
   public:
     открытые данные
     открытые методы
};

1.2 Класс Counter

Класс ''счетчик''

В качестве первого примера приведем класс Counter (''счетчик'')


Описание класса Counter

typedef unsigned short ushort; // псевдоним для краткости
class Counter { 
   private: 
     ushort value; // значение счётчика 
   public: 
      void set(ushort); // установить
      ushort get();     // прочитать
      void inc();       // увеличить на 1
      void dec();       // уменьшить на 1 
}; 

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

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


Переменная, определенная в классе, имеет область видимости от точки объявления до конца объявления класса. Класс может иметь произвольное количество переменных - членов.

В дополнению к описанию класса, носящему названию интерфейса, прилагается описание функций, прототипы (заголовки) которых приводятся в классе


Реализация методов класса

 void Counter::set(ushort val) { value=val;    } 
 ushort Counter::get()         { return value; } 
 void Counter::inc()           { value++;      } 
 void Counter::dec()           { value--;      } 

Это описание носит название реализации функций класса. Через оператор :: показывается принадлежность данных или методов конкретному классу.

Данный класс демонстрирует идею счетчика, который может изменять свое состояние (текущее значение) при обращении к ему посредством вызовов методов set(), inc(), dec(). В нашей реализации значение счетчика может принимать любые значения, допустимые для диапазона ushort. Если требуется ограничить значение числом N, то требуется изменить реализацию некоторых методов:


Модификация метода set

void Counter::set(ushort val)  { 
   if(val<=N)
      count=val;
   else
      count=0; 
}
void Counter::inc()   {
   if(count+1<=N)  count++;           
}                       

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


Хранение флага ошибки

class Counter { 
   private: 
     ushort value; 
     bool error; 
   public: 
      void set(ushort); 
      ushort get();
      void inc(); 
      void dec();          
      bool ifError(); 
      void reset();
};                                          

Добавление новых возможностей приводит к расширению интерфейса класса:


Обработка ошибок

  bool Counter::ifError()      { return error;   } 
  void Counter::reset()        { error=false;    } 

Метод инициализации необходим для переключения значения флага ошибки.

Использование класса

После описания класса и реализации его методов можно создавать объекты и использовать их


Пример 1. Статические переменные и массивы


Создание статических экземпляров

Counter people;       // статический одиночный экземпляр
Counter animals[100]; // статический массив объектов

Здесь создается объект people. Для доступа к методам используется операция "точка"


Использование статических экземпляров

people.set(10); 
people.inc(); 
animals[i].inc();

Пример 2. Динамические переменные и массивы


Создание статических экземпляров

Counter *cities = new Counter;    // динамический объект       
Counter *roads = new Counter[100];// динамический массив 

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


Использование динамических экземпляров

cities->set(1); 
roads[8].set(50); 
cities->inc(); 

Организация консольной программы

Проект консольного приложения С++ обычно содержит три файла:

  1. заголовочный файл с интерфейсом класса;
  2. исходный файл с реализацией методов класса;
  3. исходный файл с текстом программы (содержит main).

Рассмотрим пример создания демонстрационной программы для работы с классом Counter.


Создадим три файла, которые будут размещаться в одном каталоге проекта counter_demo

  1. Counter.h - заголовочный файл, в который мы помещаем описание класса Counter.
  2. Counter.cpp - файл с реализацией функций (методов) класса Counter.
  3. main.cpp - файл с демонстрацией работы класса (содержит функцию main.

Файлы Counter.cpp и main.cpp содержат включение заголовочного файла Counter.h


Построение программы counter_demo происходит в несколько этапов

  1. Благодаря препроцессору, в исходные файлы (.cpp) помещается содержимое заголовочного файла с описанием класса.
  2. Каждый из исходных файлов подвергается компиляции, в результате которой образуются объектные файлы (.obj).
  3. Компоновщик производит сборку объектных файлов с кодом из библиотек, необходимых для работы программы.
  4. Полученный exe-файл готов к использованию.

1.3 Члены класса и управление доступом


Stack.h

class Stack {
public:
  void  create_stack(int size);
  void  destroy_stack();
  void  push(char);
  char  pop();
  int   get_max_size();
  int   get_current_size();
private:
  int   maxSize;
  char* storage;
  int   top;
};


Date.h

class Date {
public:
  void init_date(int d, int m, int y);
  void next_date();
  int get_day();
  int get_month();
  int get_year();
private:
 int day, month, year;
 void set_day(int);
 void set_month(int);
 void set_year(int);
};


Stack.cpp

void  Stack::create_stack(int size)
{
   maxSize=size;
   storage=new char[maxSize];
   top=0;
}
void  Stack::push(char c)
{
   if (top<maxSize)
      storage[top++] = c;
}
char  Stack::pop()
{
   return top ? storage[--top] : 0;
}

{
  push(‘a’);     // !
  Stack stack;
  stack.create_stack(100);
  stack.push(‘a’);
  char c = stack.pop();
  stack.maxSize = 200;  // !
  stack.destroy_stack();

  Stack *p= new Stack;
  p->create_stack(300);
  p->push(‘c’);
  p->top = 0;           // !
}

{
  Date date;
  date.init_date(20, 08, 2007);
  date.set_day(35);    // !

  Date *d = &date;
  d->get_month();
  d->month = 13;       // !
  d->set_month(13);    // ! 
}

2 Специальные члены класса

Если вернуться к рассмотрению класса Counter, то можно обратить внимание на следующее обстоятельство: после создания экземпляра класса, значение счетчика не определено. Это опасно, так как если написать код

Counter c;
c.inc();

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

Counter c;
c.set(0);
c.inc();

Для повышения защищенности кода и расширения функциональности вводят специальные члены класса. К ним относятся:

  1. Конструктор - это функция, автоматически вызываемая программой при создании экземпляра класса. Конструкторы используются для присвоения значений данным объекта и выделения динамической памяти под внутренние массивы объекта. Имя конструктора должно совпадать с именем класса.
  2. Деструктор - это функция, автоматически вызываемая программой при уничтожении экземпляра класса. Деструкторы используются чаще всего для освобождения динамической памяти. Имя деструктора состоит из символа ''тильда'' и имени класса.
  3. Указатель this - Каждый экземпляр класса содержит указатель на свой код. Этот механизм реализуется через использование this. Благодаря этому указателю любой объект может узнать собственный адрес в оперативной памяти.

Пример класса с конструктором и деструктором:


Класс Vector

class Vector {
    int *data;
    int size;
  public:
    Vector(int num)  { data=new int[size=num];   }  // конструктор
    ~Vector()        { delete[] data;            }  // деструктор
 };

В данном классе конструктор Vector принимает в качестве параметра число элементов и выделяет динамическую память для хранения массива. Деструктор ~Vector выполняет обратное действие: освобождение памяти, занимаемой массивом.


В классе может быть сколько угодно конструкторов (они должны отличаться списком параметров) и только один деструктор.

А вот как можно воспользоваться классом Vector:


Использование Vector

void fun()
{
   Vector v1(10);    // статика, размер вектора 10 элементов
   Vector *v2=new Vector(20); // динамика, размер - 20.
   delete v2;        // здесь будет вызов деструктора для v2
}                    // здесь - для v1

Существуют особенные разновидности конструкторов

  • Конструктор без параметров - вызывается в том случае, если экземпляр класса создается без значения
  • Конструктор по-умолчанию - стандартный конструктор, вызывается в случае отсутствия описания конструкторов в классе
  • Конструктор копирования - конструктор, вызываемый при создании копии объекта. В качестве параметра должен содержать ссылку на класс

Работа этих конструкторов будет проиллюстрирована в дальнейших лекциях курса.

Указатель this

Скрытый указатель на экземпляр класса



void  Stack::create_stack(Stack* this, int size)
{
   this->maxSize=size;
   this->storage=new char[maxSize];
   this->top=0;
}
void  Stack::push(Stack* this, char c)
{
   if (this->top < this->maxSize)
     this->storage[this->top++] = c;
}
char  Stack::pop(Stack* this)
{
   return this->top ? this->storage[--this->top] : 0;
}

Конструкторы


Конструктор

Стандартный член класса, который автоматически вызывается при создании экземпляра (объекта)



class Stack {
public:
  Stack();
  Stack(unsigned int size);
  //...
};

Stack default_stack;
Stack short_stack(20);
Stack long_stack(500);



class Date
{
   Date (int dd=0, int mm=0, int yy=0);
   //...
};

Date today(26, 5, 2004);
Date any_day;



Stack::Stack(unsigned int size):
   maxSize(size),
   top(0)
{
  storage = new char[size];
}
Stack::Stack()
{
  storage = new char[maxSize=100];
  top=0;
}



Date::Date(int d, int m, int y)
{
  day=d?d:current_day();
  month=m?m:current_month();
  year=y?y:current_year();
}

Конструкторы по-умолчанию



class Date
{
public:
   void next_date();
   int get_date();
...
};
class Date
{
   Date ();
   Date (int dd, int mm, int yy);
   //...
};
Date today(26, 5, 2004);
Date any_day;



class Date
{
   Date (int dd, int mm, int yy);
   //...
};
Date today(26, 5, 2004);
Date any_day;  // ошибка!

Деструкторы

  • Освобождают ресурсы: файлы, память, блокировки
  • Вызываются неявно, когда переменная выходит из области видимости, удаляется объект и т.д.


class Stack
{
public:
  Stack();
  ~Stack();
  //...
};
Stack::~Stack() 
{
   if storage
   {
     delete[] storage;
   }
}

3 Разное

Константные функции-члены



class Date {
  int day, month, year;
public:
  //...
  int get_day();
  int get_month();
  int get_year();
}
int Date::get_day() {
  return day++; 
}



class Date {
  int day, month, year;
public:
  //...
  int get_day() const;
  int get_month() const;
  int get_year() const;
}
void f(Date& d, const Date& cd ) {
  int i = d.get_year();
  d.add_year(2);
  int j = cd.get_year();
  cd.add_year(3); // ошибка!
}

Статические члены



class Counter {
public:
  static int incr();
  int get_counter();

private:
  static int count;
};

Counter count;
count.incr();
Counter::incr();
int i = count.get_counter();
};



int Counter::count = 0;

int Counter::incr()
{
  return ++count;
}
int Counter::get_counter()
{
  return count;
}

3.1 Управление доступом

Управление доступом к классу

Главная забота класса - скрыть как можно больше информации. Существует 4 вида пользователей класса:

  1. сам класс;
  2. обычные пользователи (другие классы);
  3. производные классы;
  4. дружественные классы;

Каждый пользователь обладает привилегиями доступа к членам класса.

Рассмотрим правила, определяющие доступ к элементам класса

  • Сам класс имеет полный доступ ко всем своим элементам;
  • Обычные пользователи (другие классы) имеют полный доступ только к открытому (public) разделу;
  • Производные классы имеют доступ к public-разделу и к разделу protected;
  • Дружественные классы и функции имеют полный доступ ко всем разделам;

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

class MyClass1
{ 
  friend MyClass2; 
};

class MyClass2
{ 
};                     

Класс MyClass2 теперь имеет право обращаться к закрытым и защищенным членам класса MyClass1. Обратное, однако, не верно до тех пор, пока в описание класса MyClass2 мы не поместим строку friend MyClass1. Таким образом, дружественность не взаимна.


Отношение дружественности не наследуется!

Пример дружественности



class Vector {
  float V[4];
  //...
  friend Vector operator*(const Matrix&, const Vector&);
};

class Matrix {
  Vector M[4];
  //...
  friend Vector operator*(const Matrix&, const Vector&);
};

friend Vector operator*(const Matrix& rm, const Vector& rv)
{
   Vector tmp;
   //...
   tmp.V[i] += rm.M[i].V[j] * rv.V[j];
}

Полное и неполное описание класса

Класс должен быть описан до использования его членов. В этом случае мы используем неполное, или предварительное объявление.

// ПРЕДВАРИТЕЛЬНОЕ ОБЪЯВЛЕНИЕ
class MyClass;

MyClass *mc; // для этого мы добавили неполное объявление

// ПОЛНОЕ ОПИСАНИЕ
class MyClass
{
......
};

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


Нельзя создать экземпляр неполно объявленного класса.

3.2 Копирование объектов

Проблемы копирования


Проблема!

void h ()
{
   Stack s1(1000);
   Stack s2 = s1;
   Stack s3(100000000);
   s3 = s2;
}


По умолчанию – почленное копирование.


Возможные проблемы:

  • Утечка памяти;
  • Многократный вызов деструктора - попытка освобождения одних и тех же ресурсов;

Конструктор копирования

Конструктор копирования используется для создания копии объекта.


Пример с конструктором копирования

class Counter { 
   int count; 
 public: 
   Counter(const Counter& ref) { 
      count=ref.count;
   }
};
int main() { 
  Counter c1; 
  Counter c2=c1;
  return 0;
}



class Stack {
public:
  //...
  Stack(const Stack& rs);    // копирующий конструктор
  Stack& operator= (const Stack& rs);// коп. присваивание
}




Stack::Stack(const Stack& rs) :
  max_size(rs.max_size),
  top(rs.top)
{
  storage = new char[max_size];
  for(int i=0; i<top; i++) storage[i]=rs.storage[i];
}

Копирующее присваивание



Stack& Stack::operator= (const Stack& rs)
{
  if (&rs != this)
  {
    delete[] storage;
    storage = new char[max_size=rs.max_size];
    top = rs.top;
    for(int i=0; i<top; i++)
        storage[i]=rs.storage[i];
  }
  return *this;
}

Итог

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

3.3 Поля-объекты

Объекты в виде данных

Можно объявить член данных, который является объектом класса.


Если член данных класса должен быть инициализирован с особыми параметрами, то нужно использовать специальную запись. Конструктор члена данных нужно вызвать с аргументами путем передачи параметров непосредственно перед входом в тело конструктора включающего класса.


Класс First

class First   { 
  public: 
    int val; 
    First()      { val=0;      } 
    First(int i) { val=i;      } 
    int GetVal() { return val; }
 }; 


Класс Second

class Second   { 
    int id; 
  public: 
    First object; 
    Second()                 { id=0;      } 
    Second(int v):object(v)) { id=v;      } 
    int GetID()              { return id; }
};
int main()  { 
   First one; 
   Second two; 
   Second three(10);
   return 0;
} 

При создании экземпляра класса-контейнера Second перед выполнением его конструктора будет вызван конструктор класса First.

Инициализация

Class Schedule {
public:
  Schedule(Date dt1,
           Stack& s);
  Schedule(Date dt1);
  //...
private:
  Date  m_date;
  Stack m_stack;
}

Schedule::Schedule(Date dt1, 
                   Stack& s)
         : m_date(dt1), m_stack(s)
{
   m_stack.push(/*...*/);
}
...
Schedule::Schedule(Date dt1)
         :m_date(dt1),m_stack()
{ }
...
Schedule::Schedule(Date dt1, 
                  Stack& s)
                :m_date(dt1)
{
   m_stack = s;
}

Копирование



Class Schedule {
public:
 Schedule& operator=(const Schedule &sch);
 //...
private:
  Date  m_date;
  Stack m_stack;
}



Schedule& Schedule::operator=(const Schedule &sch) 
{
   m_date = sch.m_date;
   m_stack = sch.m_stack;
}

Инициализация в конструкторе

Class Schedule {
public:
 Schedule(int   ii,
          Date& dd,
          Time  tt);
  //…
private:
const int m_i;
      Date& m_rd;
      Time m_t;
};
Schedule::Schedule(int ii, Date& dd,
                   Time tt)
         :m_i(ii), m_rd(dd), m_t(tt)
 { }

Инициализация в конструкторе обязательна для:

  • членов без конструкторов по умолчанию,
  • константных членов,
  • членов, являющихся ссылками.

4 Класс Vector

Постановка задачи

В этом разделе мы проведем разработку полезного класса Vector для упрощения работы с динамическим массивом


Формулировка задачи

Разработать класс Vector для хранения массива целых чисел

Анализ задачи

Класс Vector должен иметь поле-указатель для хранения адреса динамического массива и переменную для хранения размера массива. В качестве функций необходимо задать:

  1. Конструктор с параметром (размер массива)
  2. Конструктор по-умолчанию
  3. Конструктор копирования
  4. Деструктор (для освобождения динамической памяти)
  5. Выбор элемента из массива
  6. Добавление элемента в массив

Описание класса


Vector.h

// Vector.h
class Vector { 
     int *data;  // данные
     int size;   // размер данных
   public: 
     Vector(int num);    // конструктор с параметром
     Vector();           // конструктор без параметров
     Vector(Vector&);    // конструктор копирования
    ~Vector();           // деструктор
     bool getValue(int,int&);  // прочитать элемент
     bool setValue(int,int);   // записать элемент
  };                                           

Реализация класса


Vector.cpp

// Vector.cpp
Vector::Vector(int num) {
   if(num>0) 
      data=new int[size=num];
   else 
      data=new int[size=10];
}
Vector::Vector() {     
      data=new int[size=10];
}
Vector::Vector(Vector & v) {
   data=new int[size=v.size];
   for(int i=0;i<size;i++)
     data[i]=v.data[i];
}


Vector.cpp (продолжение)

// Vector.cpp (продолжение)
Vector::~Vector() {
   delete[] data;
}
bool Vector::getValue(int index,int& value) {
   if(index>=size || index<0)
     return false;
   else   
   {
     value=data[index];
     return true;
   }
}
bool Vector::setValue(int index,int value)  {
   if(index>=size || index<0)
     return false;
   else
   {
     data[index]=value;
     return true;
   }
}

Пример использования Vector


main.cpp

// main.cpp
int main()
{
   int value,i;
   Vector vector(100);      // массив из ста значений
   Vector vector2(vector);  // копия существующего вектора

   for(i=0;i<100;i++)
      vector.setValue(i,i*i); // заполняем квадратами чисел

   bool result=vector.getValue(101,value);  // Ошибка! Вернется false
      
   return 0;
}