Раздел «Язык Си».OOP-Template:

Шаблоны

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

Например.

class Point{
int x,y;
 public:
  Point();
  Point(int, int);
  //... еще функции
  void print();
};

class MWeight{
   int kg; // килограммы
   int gr; // граммы.
public:
   MWeight(); // пустой мешок
   void getWeight(int p, int k);// получить вес риса
// Сложение мешка с мешком
   MWeight operator+(MWeight m1);
// Деление веса на целое число - получаем вес
   MWeight operator/(int n); 
// оператор > . Если "наш" мешок больше чем параметр a, возвращает 1
// если нет - 0
   int operator>(MWeight a);
// печать
   void print();  
};

Допустим имеются массивы объектов этих классов. Как правило, при работе с массивами возникает потребность поменять местами элементы этих массивов или просто поменять содержимое двух объектов.

Для решения этой задачи необходимо реализовать две различные функции: для работы с классом Point и для работы с классом MWeight.

void changePoint(Point& a, Point& b){
    Point c;
    c = a;
    a = b;
    b = c;
};
void changeMWeight(MWeight& a, MWeight& b){
    MWeight c;
    c = a;
    a = b;
    b = c;
};
Как видно, все операции в этих функциях абсолютно идентичны. Различны только типы объектов, с которыми они совершаются.

Возникает желание написать каким-то образом написать одну функцию вместо двух.

Чтобы иметь такую возможность, нужно сформулировать требования к классам объектов, с которыми такие функции могут работать:

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

Как видно, в этих функциях changeXXX используется только одна операция, это - присваивание. Значит во всех классах эта операция должна либо адекватно работать как операция присваивания "по умолчанию", либо переопределена для нужд класса.

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

Функции-шаблоны

Запишем функцию swap как шаблон.

#include <iostream>
#include <cstdlib>
#include <string>
using namespace std;
// Класс точек на плоскости
class Point{
  int x,y;
public:
    Point();
    Point(int x1, int y2);
// Функции класса ....
// .....
};

// Класс треугольников
class Triangle{
   Point ugol[3]; // массив точек - вершины
public:
   Triangle();
   Triangle(Point, Point, Point);
// Еще функции класса....
// .....
   void print();
};

// Описание функции-шаблона.
// слово template означает, что это шаблон.
// в угловых скобках указывается параметр шаблона и формальное имя параметра. 
// В данном случае параметр шаблона - имя класса, и формальное имя этого класса Т
// вместо T при вызове функции С++ выяснит определит какой класс будет использоваться
// T& a и T& b - обычные параметры функции. T c - локальна япеременная класса T
// при подстановке конкретных имен объектов в качестве параметров С++  выясняет тип этих объектов, 
// и формирует функцию уже для работы с конкретным классом.

template<class T> void change(T& a, T&b){
      T c ;
      c = a; // предполагается, что для всех объектов семейства class T
      a = b; // оператор = определенн
      b = c; // и работает правильно
};


// В классе Triangle есть нединамический массив
// С++ столь "любезен", что обеспечивает правильное копирование
// нединамических массивов  в объектах..
// Поэтому для класса Triangle тоже можно использовать стандартный оператор копирования

int main(){
   Point a(3,7), b(8,10), c(11,12); // точки
   Point d(44 ,1), f(0,0),z(-2,-1);
// Треугольники инициализируются точками
   Triangle tr1(a,b,c), tr2(d,f,z);

// меняем точки местами
// Как только функция-шаблон получает параметры, С++ сразу определяет класс или тип
// этих параметром. И функция начинает работу с переменными этого класса или типа
   change(a,d);
   a.print();
   d.print();
   cout<<endl;

//меняем местами треугольники
  change(tr1, tr2);
  tr1.print();
  tr2.print();
 
 int  k =3, m = 9; 
// меняем местами целые числа
  change(k,m);
  cout<<k<<' '<<m<<endl;
}

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

Одна полезная функция-шаблон.

Часто приходится сталкиваться с необходимостью вывода состояния объктов в поток вывода. Для того, чтобы это можно было делать удобно, попробуем переопределить для класса оператор вывода << .

Опишем общую схему:

class A{
  // здесь атрибуты класса
 public:
    A(<инициализация>);
// Здесь полезные функции класса

// Функция вывода в поток вывода
// Будем пользоваться "общим потоком" ostream.
// Он может "превращаться" в консольный поток, файловый поток и др.
// Механизм превращений будет ясен позже
 
// Функция получает в качестве параметра ссылку на объект ostream и
// возвращает ссылку нна этот же объект, чтобы им могли воспользоваться другие 
// функции вывода.
   ostream& put(ostream&);   
};
// Реализация функции put
ostream& A::put(ostream& s){
// Здесь выводим в s все что нужно
// например 
// s<<атрибут1<<' '<<атрибут2<<endl;

// возвращаем измененный поток
   return s;
};

// Чтобы пользоваться оператором вывода, его теперь нужно переопределить для класса А
// Это "свободная" функция, она не принадлежит никакому классу
ostream& operator<<(ostream& s, A obj){
     return obj.put(s);
};

// Теперь можно не беспокоится о выводе объектов класса А
int main(){
  A z(<значения атрибутов>);
// Вывод на экран
   cout<<z;

Если же у нас есть несколько классов, для которых требуется переопределение операторов вывода или ввода, то писать для каждого свободную функцию переопределения оператора хлопотно и долго.

Опишем такую функцию-шаблон

// Теперь, если в классе описана функция ostream& put(ostream&), то эта функция-шаблон будет работать с таким классом
template<class T> ostream& operator<<(ostream& s, T& a){
        return a.put(s);
}

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

#include <iostream>
#include <cstdlib>
#include <string>
using namespace std;
class Point{
  int x,y;
  int dist2;
public:
    Point();
    Point(int x1, int y2);
// Здесь полезные функции класса

// Для использования в наших шаблонных функциях, 
// оператор > нужен обязательно
    int operator>(const Point&); 

};

class Rect{
// здесь атрибуты класса
public:
// Здесь функции 

// Оператор > должен быть определен
  int operator>(const Rect&);

};

// Шаблонная функция поиска максимального элемента в массиве

// Здесь два параметра - тип элементов массива и 
// n - их количество
// Функция может искать максимальный элемент если:
// определен оператор присваивания и определен оператор >
// для элемннтов всех классов, которые предполагается обрабатывать этой функцией

template<class T, int n>  T maxT(T a[n]){
        T max;
        max = a[0];
        for(int i=0; i< n; i++){
           max = (a[i] > max)? a[i] : max;
        }
        
        return max;
};

// Оператор > для класса Point
int Point::operator>(const Point& a){
 // Код для сравнения точек
};

// Оператор > для класса Rect
int Point::operator>(const Point& a){
 // Код для сравнения прямоугольников
};

int main(){
  Point a[4]; // массив точек
// Заполнение значений атрибутов точек
 
  Point mx; // точка на максимальном расстоянии от 0

// поиск точки на максимальном расстоянии от 0
// указываем тип элементов массива и его размер
  mx = maxT<Point,4>(a);


// массив из целых чисел
  int z[7]={3,5,1,0,7,100,12};
  int m;
// поиск максимального элемента из массива целых чисел
// указываем тип элементов массиваи его размер
  m = maxT<int,7>(z);
  cout<<m<<endl;

  Rect rz[10];
// Заполнение значений атрибутов примоугольников

  Rect rmax; // самый большой прямоугольник
 
// Поиск самого большого прямоугольника

  rmax = maxT<Rect,10>(rz);
}

REFACTOR Задача 1

Определить массивы для массива пар точек (с расстоянием между ними), для ионов (с массой иона). Написать шаблонные функцию для:
  1. определения минимального элемента
  2. сортировки элементов (можно использовать qsort)
Описать требования к классам элементов массивов для использования этих функций

Классы-шаблоны

Кроме шаблонных функций можно описать и шаблонные классы. Как правило, это классы-контейнеры или классы похлжие на контейнеры.

Рассмотрим шаблонный класс Drob. Он должен работать и с целыми числами (как было реализовано ранее), и с объектами класса Arithmetic. Возможно будут другие классы, например класс для работы с векторной арифметикой, для которых Drob будет полезен.

Вспомним реалтизацию отдельных функций класса Arithmetic.

Интерфейс

#include <iostream>
#include <cstdlib>
#include <string.h>
// Для форматированного вывода
#include <iomanip>

using namespace std;

class Arithmetic{
  unsigned char* digit; // для хранения числа 
  int n;    // размер числа
  
 public:
       Arithmetic(); // конструктор

// инициализирущий конструктор 
// в строке содержатся только цифры.
// Предполагается, что на строку символов до вызова 
// конструктора уже выделено достаточное количество памяти и 
// конец строки уже обозначен '\0'      
       Arithmetic(const char*); 

/*
  Деструктор. 
*/
       ~Arithmetic();

// Копирующий конструктор       
       Arithmetic( const Arithmetic&);
// Инициализирующий конструктор (числом)
       Arithmetic( int);
       
// Оператор копирования 
      
       Arithmetic& operator=(const Arithmetic&);

// Оператор копирования       
       Arithmetic& operator=(int n);

// Операторы сложения
       Arithmetic operator+(const Arithmetic&);
       Arithmetic operator+(int);
// Для отладки. Печатает в обратном порядке
    void print(); 
// Нормальный вывод в поток
       ostream& put(ostream& s);
};

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

#include <string.h>
#include "long.h"

/*
 Конструктор по умолчанию.
 Будем считать, что по-умолчанию, число - 0
*/
Arithmetic::Arithmetic(){
   n = 1;
// Динамически выделяем память под один элемент.   
   digit = new unsigned char;
   digit[0] = 0;
   cout<<"Constructor\n";
};

/*
 Инициализирующий конструктор.
 число задается строкой цифр.
*/
Arithmetic::Arithmetic(const char* s){
    int i;
    int len = strlen(s);
    n = ( len % 2 )? (len >> 1) + 1: len >> 1;

// Выделение памяти под массив    
    digit = new unsigned char[n];
    len--;
    
// Число записывает "задом-наперед" для вычислений
    for(i = 0;i < n;i++){
      digit[i] = s[ len-- ] - '0';
      if( len < 0 )
       break;
      digit[ i ] += ( s[ len-- ] - '0' ) * 10;
   }
};

Arithmetic::Arithmetic(int k){
   int st = 0, check = k;

   while (check > 0){
      st++;
      check /= 100;
   }
   cout<<"k="<<k<<" st="<<st<<endl;
   cout<<"digit:";
   digit = new unsigned char[st];
   digit[0] = 0;
   n = st;
   for(int i = 0; i < n; i++){
      digit[i] = k % 100;
     k /= 100;
//     cout<<"i="<<i<<" k="<<k<<" d= "<<(int)digit[i]<<' ';
   char c;
//   cin>>c;
   }
//   cout<<endl;
};

Arithmetic Arithmetic::operator+(const Arithmetic& a){
    int sumr, pr = 0;
   int zero = 0;
// Будем выделять память на 1 разряд больше чем самый длинный
   int nn = (n>a.n)? n + 1 : a.n + 1;
   unsigned char *tmp = new unsigned char[nn];
   for(int i = 0; i < nn; i++){
      sumr = digit[i] + a.digit[i] + pr;
      pr = sumr / 100;
      tmp[i] = sumr % 100;
// Подсчет количества нулей слева, чтобы не хранить лишние разряды
      if(tmp[i] == 0) 
         zero++;
      else
         zero = 0;
   }   
// Это вернем
   Arithmetic rez;
   rez.n = nn - zero;
   rez.digit = new unsigned char[rez.n];
   memcpy(rez.digit, tmp, sizeof(unsigned char)*(rez.n));
// Удалим tmp!!!
   delete[] tmp;
   return rez;
};

Arithmetic Arithmetic::operator+(int a){
      Arithmetic tmp;
// Получим временный объект из числа и сложим его с нашим
      tmp = (*this) + Arithmetic(a);
      return tmp;      
};

void Arithmetic::print(){
 // Код
};

/*
  Копирующий конструктор. 

*/
Arithmetic::Arithmetic(const Arithmetic& a){
   // Код
};

/*
 Деструктор. Во время работы деструктора будут выполняться 

*/
Arithmetic::~Arithmetic(){
  if(digit)
    delete[] digit;
 
};

/*
 Перегрузка оператора копирования.

Здесь нужно учесть, что у объекта уже выделена память 
под хранение числа. Поэтому ее необходимо освободить 
и выделить новую, соответствующего размера
*/
 Arithmetic& Arithmetic::operator=(const Arithmetic& a){
   if(digit)
      delete[] digit;

   digit = new unsigned char[ a.n ];
   n = a.n;
   memcpy( digit, a.digit, sizeof( unsigned char)*n ); 
   
   return *this;
};

/*
 Присваивание объекта "несоответствующего" типа. 
*/
 Arithmetic& Arithmetic::operator=(int a){
// Код
    return *this;
};

ostream& Arithmetic::put(ostream& s){
    int i;
// Печать "сзади" по две цифры. Если   
   for(i=n-1;i>-1; i--){
// Установка заполняющего символа и ширины поля
     s<<setfill('0')<<setw(2)<<(int)digit[i]<<' ';
   }
// Изменить код так, чтобы первый разряд печатался без незначащего нуля!!
   s<<"  ";
   return s;
};

Описание шаблонного класса Drob

Шаблонный класс становится РЕАЛЬНЫМ классом только тогда, когда объявляется объект этого класса. Поэтому объектный код при компиляции не создается и все шаблонные функции шаблонного класса описываются в заголовочном файле.

Шаблонный класс ВСЕГДА получается из уже отлаженного реального класса.

#include <iostream>
#include <cstdlib>

using namespace std;

// Объявление шаблонного класса Drob
// параметром является класс (T) атрибутов Drob
template <class T> class Drob{
/ тип данных будет известен только после объявления объекта класса 
 T cel, chisl,znam;

public:
  Drob(); // конструктор дроби по-умолчанию

// Инициализирующий конструктор 
// тип данных будет известен только после объявления объекта класса 
  Drob(T,T,T);

  Drob operator+(const Drob&); 
// Другие функции
// .......
  ostream& put(ostream&);

};
// Описание функций шаблонного класса 
// функции шаблонного класса - тоже функции-шаблоны.
template<class T> Drob<T>::Drob(){
  cel = chisl = 0;
  znam = 1; 
};

template<class T> Drob<T>::Drob(T a, T b, T c){
// Для всех классов, с которыми будет работать шаблон должен быть корректно 
// определен оператор копирования
     cel = a;
     chisl = b;
     znam = c;
};

/*
 Это "заглушка" 
 В типе возвращаемого значения и в параметре обязательно указывется 
 для какого класса этот шалон (параметр шаблонного класса)
*/
template<class T> Drob<T> Drob<T>::operator+(const Drob<T>& a){
// Для всех классов T должен быть определен оператор +
     Drob tmp;
     tmp.chisl = chisl + a.chisl;
     tmp.znam = znam;
     return tmp;
};

/*
  Это также "заглушка". 

*/

template<class T> Drob<T>& Drob<T>::operator++(int){
   cel++; 

   return *this;
};  

// Функция вывода в поток.
template <class T> ostream& Drob<T>::put(ostream& s){
// Для всех классов Т должен работать оператор <<
    s<<cel<<'('<<chisl<<'/'<<znam<<')';
   return s;
};

// "Полезная" свободная шаблонная функция переопределнения оператора <<
// Для всех классов Т должна быть оперделна функция ostream& put(ostream&) 
template<class T> ostream& operator<<(ostream& s, T& a){
        return a.put(s);
}

Использование готового шаблона

#include "long.h"
#include "templ_frac.h"

int main(){
//Дроби для int
  Drob<int> c;
  Drob<int> a(0,2,5), b(0,1,5);

  Arithmetic m, m1("12345"), m2(5000);
// Дроби для Arithmetic
// md2 инициализируется числами, так как для Arithmetic
// есть инициализирующий конструктор числами 
  Drob<Arithmetic> md, md1(m,m1,m2), md2(0,1,2) ;

// проверка шаблонной функции 
// переопределения << для  Arithmetic
  cout<<md2<<endl;
// Проверка сложения дробей с числами
  c = a + b;
// Проверка вывода 
  cout<<c<<endl;
// Проверка сложения дробей с Arithmetic
  md = md1 + md2;
  cout<<md<<endl;

  return 0;
}  

REFACTOR Задача 2

Реализовать функции шаблона Drob до полноценной функциональности. Не стоит забывать, что для ВСЕХ классов Т должен быть реализован оператор /. Разбиться на группы по 3 человека и разделить реализацию функций на всех по трудоемкости.

REFACTOR Задача 3

Для задчи "Игровое поле" из раздела "Сложные объекты" реализовать шаблонный класс PoleT для работы с любым классом фишек. Проверить на шашках и крестиках-ноликах.

-- TatyanaOvsyannikova2011 - 11 Nov 2015