Опубликован: 25.11.2008 | Уровень: специалист | Доступ: свободно | ВУЗ: Нижегородский государственный университет им. Н.И.Лобачевского
Лекция 15:

Классы. Создание новых типов данных

< Лекция 14 || Лекция 15: 12345 || Лекция 16 >
Аннотация: Материалы данной лекции посвящены созданию новых типов данных. Приводятся примеры программного кода с использованием новых типов данных

Одно из важнейших достижений языка C++ – возможность объявления нового типа данных и описания тех операций, которые компилятор должен научиться делать с новыми данными. Именно таким образом состав входного языка пополнился классами данных String (строки), Set (множества), Complex (комплексные переменные) и др.

14.1. Школьные дроби на базе структур

Мы продемонстрируем технику создания таких данных на примере дробно-рациональных чисел, представленных в виде пары целых чисел – числителя ( num ) и знаменателя ( denum ). Этот пример подробно исследован в книгах В. Лаптева (С++. Экспресс-курс. СПб.: БХВ-Петербург, 2004. – 512 с), У. Торпа и У. Форда ("Структуры данных в С++",...).

В качестве первого инструмента воспользуемся механизмом структур:

struct Rational {unsigned num,denum;};

Теперь имя структуры Rational может использоваться для объявления переменных или массивов нового типа:

Rational x,y,z[20];
  x.num=1;  x.denum=3;

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

unsigned gcd(unsigned x,unsigned y)
{//Поиск наибольшего общего делителя
  if(y==0) return x;
  return gcd(y,x%y); 
}
void reduce(Rational &c)
{//Сокращение дроби
  unsigned t=((c.num>c.denum)?gcd(c.num,c.denum):gcd(c.denum,c.num));
  c.num /= t;  c.denum /= t;
}

Теперь начинается самое интересное – надо научить компилятор выполнять простейшие операции над дробями. По существу, мы должны переопределить некоторые действия, которые, будучи записаны в естественном для человека виде, должны правильно интерпретироваться и компилятором. В терминологии C++ такое "волшебство" называется перегрузкой операций. Сначала мы перегрузим операцию +=, которая знакома нам по школе:

\frac{a}{b}+\frac{c}{d}=\frac{ad+cb}{bd}
Rational& operator+=(Rational &a, const Rational &b)
{ a.num = a.num*b.denum + b.num*a.denum;
  a.denum *= b.denum;
  reduce(a); return a;
}

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

Rational operator+(Rational &a,const Rational &b)
{ Rational t=a; t += b; reduce(t); return t; }

Немного сложнее выглядит переопределение операций потокового ввода/вывода. Во-первых, среди аргументов функции мы должны предусмотреть ссылку на входной ( istream & ) или выходной ( ostream & ) поток. Во-вторых, мы должны организовать ввод или вывод по указанной ссылке и возвратить ее в качестве значения функции. Поэтому тип возвращаемого значения тоже должен быть ссылкой на входной или выходной поток:

istream& operator>>(istream &t, Rational &a)
{ char s;
  t >> a.num;  t>>s;  t>> a.denum;
  reduce(a); return t;
}

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

ostream& operator<<(ostream &t, const Rational &a)
{ t << a.num << '/'<<a.denum;
  return t;
}

На базе построенных функций уже можно организовать простейшую программу:

void main()
{ Rational A,B,C;
  A.num=1; A.denum=2;
  B.num=1; B.denum=3;
  C=A+B;
  cout << C << endl;
  getch();
}
//===Результат работы ===
5/6

Операцию " = " мы не перегружали, хотя и использовали в программе. Но дело в том, что структуры одинакового типа можно присваивать. Точно также, не перегружая операцию индексирования (" [] ") можно работать с элементами массивов:

void main()
{ Rational d[5],c;
  int i;
  cout<<"Enter 5 rational number:"
  cout<<" num / denum <Enter>"<<endl;
  for(i=0; i<5;i++) cin>>d[i];
  for(i=0; i<5;i++) cout<<d[i]<<' ';
  c=d[0]+d[1];
  c += d[2]; c += d[3]; c+=d[4];
  cout<<endl<<c<<endl;
  getch();
}
//=== Результат работы ===
Enter 5 rational number: num / denum <Enter>
1/1
2/2
3/3
4/4
5/5
1/1 1/1 1/1 1/1 1/1
5/1

К уже переопределенным операциям можно добавить еще несколько операций умножения, когда оба сомножителя дробно-рациональные или один из них целочисленный:

Rational operator*(const Rational &a, const Rational &b)
{ Rational c;
  c.num=a.num*b.num;
  c.denum=a.denum*b.denum;
  reduce(c);  return c;
}
Rational operator*(const Rational &a, const unsigned &b)
{ Rational c;
  c.num=a.num*b;
  c.denum=a.denum;
  reduce(c);  return c;
}
Rational operator*(const unsigned &a, const Rational &b)
{ Rational c;
  c.num=a*b.num;
  c.denum=b.denum;
  reduce(c);  return c;
}

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

Rational operator+=(Rational &a, const unsigned &b)
{ a.num=a.num+b*a.denum;
  reduce(a);  return a;
}

С операциями x++ и ++x дело обстоит не так просто. Дело в том, что обозначения этих операций одинаковы ( operator++ ), но выполняются они по-разному. В первом случае сначала используется старое значение переменной x, а уже потом ее значение увеличивается на 1. А во втором случае сначала увеличивается x, а уже потом используется новое значение переменной. Игра строится на том, что если формула заканчивается знаком + или , то компилятор добавляет несуществующее слагаемое, равное 0. Поэтому мы в одной операции ( ++x ) оставим один аргумент, а во второй ( x++ ) добавим неиспользуемый аргумент типа int. Хотя он не влияет на результат операции, но по его фиктивному присутствию компилятор правильно сориентируется между похожими функциями:

Rational operator++(Rational &a)
{//Переопределение операции ++a 
  a += 1;  return a; 
}
Rational operator++(Rational &a, int)
{//Переопределение операции a++ 
  Rational t=a;  a += 1;  return t;
}

Оглядываясь на все наши перегруженные функции, невольно задаешься вопросом, а стоило ли городить весь этот огород. Не проще ли было непосредственно в программе расписывать операции над числителями и знаменателями. Если все это делается только один раз с целью демонстрации новых технологий, то, наверное, затея выеденного яйца не стоила. Но теперь мы спрячем все наши описания и новые функции в файл, который назовем rational.h. После этого программа, использующая наши навороты, выглядит совсем неплохо:

#include <stdio.h>
#include <iostream.h>
#include <conio.h>
#include "rational.h"
void main()
{ Rational A,B,C;
  A.num=1; A.denum=2;
  B.num=1; B.denum=3;
  C=A+B;
  cout << C << endl;
  getch();
}

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

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

Надо отметить, что объявление нового типа данных с использованием структур и внешних функций не приветствуется современными технологиями. В нашем варианте функции не защищены от внешнего вмешательства, они не объединены в некую целостную структуру, в которой не все предназначено для внешнего использования. Т.е. то, что демонстрирует разбираемый пример – это только объяснение некоторых идей, получивших в C++ существенно более серьезное развитие.

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

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

//Конструктор по умолчанию
  Rational() { num=0; denum=1; }
//Конструктор инициализации
  Rational(unsigned n) { num=n; denum=1; }
//Еще один конструктор инициализации
  Rational(unsigned n,unsigned d) {if(d!=0){num=n; denum=d;}}
//Конструктор копирования
  Rational(const Rational &r) { num=r.num; denum=r.denum; }

В языке C++ более распространен другой способ объявления inline -конструкторов, использующий так называемые списки инициализации:

Rational():num(0),denum(1){}
  Rational(unsigned n):num(n),denum(1){}
  Rational(unsigned n,unsigned d):num(n),denum((d!=0)?d:1){}
  Rational(const Rational &r):num(r.num),denum(r.denum){}

Включение таких конструкторов в файл rational.h сделает процедуру объявления и инициализации новых данных более цивилизованной:

Rational A(1,2),B(1,3),C;

Вы, наверное, помните, что компилятор языка C берет на себя преобразования типов аргумента при обращении к математическим функциям. По прототипам, как правило, их аргументы имеют тип double, но мы можем обращаться к ним и с данными типа float, и с целочисленными аргументами. Необходимое преобразование аргумента компилятор выполняет сам. Аналогичные преобразования следовало бы предусмотреть и в нашем пакете обработки дробно-рациональных данных. Если бы в нашем распоряжении оказались средства по прямому и обратному преобразованию данных типов Rational и unsigned, то не пришлось бы писать по три варианта операций умножения ( Rational* Rational, Rational*unsigned, unsigned* Rational ).

Роль преобразования данных могут выполнять конструкторы и специальные функции. Например, для преобразования типа unsigned->Rational можно было бы написать следующий конструктор:

Rational(unsigned n=0,unsigned d=1)
{ if(d!=0){num=n; denum=d; }}

Наличие в конструкторе параметров по умолчанию позволяет теперь объявлять переменные типа Rational следующим образом:

Rational x(5,1);    //по-старому
  Rational x(5);      //по-новому, с преобразованием unsigned->Rational
  Rational x=5;       //по-новому

Для обратного преобразования Rational->unsigned необходимо написать специальную функцию, которую надо объявить внутри структуры:

operator unsigned(){return num/denum;}

После этого целочисленным данным типа unsigned можно присваивать значения дробно-рациональных данных.

< Лекция 14 || Лекция 15: 12345 || Лекция 16 >
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?

 

 

 

Даниил Варов
Даниил Варов
Австралия, Комбоддж