Глава 6 Перегрузка Операций

List Banner Exchange

Здесь водятся Драконы!
- старинная карта

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

6.1 Введение

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

  class complex { 
    double re, im;
  public:
    complex(double r,  double i) { re=r;  im=i;}
    friend complex operator+(complex, complex);
    friend complex operator*(complex, complex);
  };
определяет простую реализацию понятия комплексного числа, в которой число представляется парой чисел с плавающей точкой двойной точности, работа с которыми осуществляется посредством операций + и * (и только). Программист задает смысл операций + и * с
  void f() {
      complex a  = complex(1,  3.1);
      complex b = complex(1.2,  2);
      complex c = b;
      a = b+c;
      b = b+c*a;
      c = a*b+complex(1,2);
  }
Выполняются обычные правила приоритетов, поэтому второй оператор означает b=b+(c*a), а не b=(b+c)*a.

6.2 Функции Операции

Можно описывать функции, определяющие значения следующих операций:

  + - * / % ^ & | ~ !
  = < > += -= *= /= %= ^= &=
  |= << >> >>= <<= == != <= >= &&
  || ++ -- [] () new delete
Последние четыре - это индексирование (#6.7), вызов функции (#6.8), выделение свободной памяти и освобождение свободной памяти (#3.2.6). Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нель

Имя функции операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator<<. Функция операция описывается и может вызываться так же, как любая другая функция. Использование операции - это лишь с

  void f(complex a, complex b) {
      complex c =  a  +  b;  //  сокращенная  запись  complex  d  =
      operator+(a,b); // явный вызов
  }
При наличии предыдущего описания complex оба инициализатора являются синонимами.

6.2.1 Бинарные и Унарные Операции

Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb),

  class X {
  // друзья
      friend X operator-(X);   // унарный минус
      friend X operator-(X,X);  //  бинарный минус 
      friend X operator-();  // ошибка:  нет операндов
      friend X operator-(X,X,X);  // ошибка: тернарная

  // члены (с неявным первым параметром: this)

      X* operator&();// унарное & (взятие адреса) 
      X operator&(X);// бинарное & (операция И)
      X operator&(X,X);  // ошибка: тернарное

  };
Когда операции ++ и -- перегружены, префиксное использование и постфиксное различить невозможно.

6.2.2 Предопределенный Смысл Операций

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

Значения некоторых встроенных операций определены как равносильные определенным комбинациям других операций над теми же аргументами. Например, если a является int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения для определ

По историческому совпадению операции = и & имеют определенный смысл для объектов классов. Никакого элегантного способа "не определить" эти две операции не существует. Их можно, однако, сделать недееспособными для класса X. Можно, напр

* В некоторых системах компоновщик настолько "умен", что ругается, даже если неопределена неиспользуемая функция. В таких системах этим методом воспользоваться нельзя. (прим автора)

6.2.3 Операции и Определяемые Пользователем Типы

Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (функциям, которые переопределяют операции new и delete, это делать необязательно). Это правило гарантирует, что пользователь не может измен

Функция операция, первым параметром которой предполагается основной встроенный тип, не может быть функцией членом. Рассмотрим, например, сложение комплексной переменной aa с целым 2: aa+2, при подходящим образом описанной функции члене, может быть про

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

6.3 Определяемое Пользователем Преобразование Типа

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

  class complex { double re, im;
  public:
      complex(double r, double i) { re=r; im=i; }

      friend complex operator+(complex, complex);
      friend complex operator+(complex, double);
      friend complex operator+(double, complex);

      friend complex operator-(complex, complex);
      friend complex operator-(complex, double);
      friend complex operator-(double, complex);
      complex operator-() // унарный -

      friend complex operator*(complex, complex);
      friend complex operator*(complex, double);
      friend complex operator*(double, complex);

      // ...
  };
Теперь, имея описание complex, мы можем написать:
  void f() {
    complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5);
    a = -b-c;
    b = c*2.0*c;
    c = (d+e)*a;
  }

Но писать функцию для каждого сочетания complex и double, как это делалось выше для operator+(), невыносимо нудно. Кроме того, близкие к реальности средства комплексной арифметики должны предоставлять по меньшей мере дюжину таких функций. Посмотрите,

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

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

  class complex {
      // ...
      complex(double r) { re=r; im=0; }
  };
Конструктор, требующий только один параметр, необязательно вызывать явно:
  complex z1 = complex(23); complex z2 = 23;
И z1, и z2 будут инициализированы вызовом complex(23).

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

  class complex { double re, im;
  public:
      complex(double r, double i = 0) { re=r; im=i; }

      friend complex operator+(complex,  complex);
      friend complex operator*(complex, complex);
  };
и действия, в которые будут входить переменные complex и целые константы, стали бы допустимы. Целая константа будет интерпретироваться как complex с нулевой мнимой частью. Например, a=b*2 означает:
  a=operator*( b, complex( double(2), double(0) ) )
Определенное пользователем преобразование типа применяется неявно только тогда, когда оно является единственным.

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

6.3.2 Операции Преобразования

Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут оказаться нежелательными:

[1] Не может быть неявного преобразования из определенного пользователем типа в основной тип (поскольку основные типы не являются классами)

[2] Невозможно задать преобразование из нового типа в старый, не изменяя описание старого

[3] Невозможно иметь конструктор с одним параметром, не имея при этом преобразования.

Последнее не является серьезной проблемой, а с первыми двумя можно справиться, определив для исходного типа операцию преобразования. Функция член X::operator T(), где T - имя типа, определяет преобразование из X в T. Например, можно определить т

  class tiny  { 
    char  v;
    int assign(int i) { return v = (i&~63) ?
      (error("ошибка диапазона"),0) : i; }
  public:
    tiny(int i) { assign(i);  }
    tiny(tiny& i) { v =  t.v;  }
    int operator=(tiny& i) {return v = t.v; }
    int operator=(int i) {return assign(i); }
    operator int() { return v; }
  }
Диапазон значения проверяется всегда, когда tiny инициализируется int, и всегда, когда ему присваивается int. Одно tiny может присваиваться другому без проверки диапазона. Чтобы разрешить выполнять над переменными tiny обычные целые операции,
  void main() {
      tiny c1 = 2;
      tiny c2 = 62;
      tiny c3 = c2 - c1; // c3 = 60
      tiny c4 = c3; // нет проверки диапазона (необязательна)
      int i = c1 + c2; // i = 64
      c1 = c2 + 2 * c1; // ошибка диапазона: c1 = 0 (а не 66) 
      c2 = c1 -i; // ошибка диапазона: c2 = 0
      c3 = c2; // нет проверки диапазона (необязательна)
  }
Тип вектор из tiny может оказаться более полезным, поскольку он экономит пространство. Чтобы сделать этот тип более удобным в обращении, можно использовать операцию индексирования.

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

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

Типы istream и ostream опираются на функцию преобразования, чтобы сделать возможными такие операторы, как

  while (cin>>x) cout<<x;
Действие ввода cin>>x выше возвращает istream&. Это значение неявно преобразуется к значению, которое указывает состояние cin, а уже это значение может проверяться оператором while (см. #8.4.2). Однако определять преобразование из оного типа в друг

6.3.3 Неоднозначности

Присваивание объекту (или инициализация объекта) класса X является допустимым, если или присваиваемое значение является X, или существует единственное преобразование присваиваемого значения в тип X.

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

  class x { /* ...  */ x(int);  x(char*);  };
  class y { /* ...  */ y(int); };
  class z { /* ... */ z(x); };

  overload f; x f(x); y f(y);

  z g(z);

  f(1); //  недопустимо:  неоднозначность f(x(1)) или f(y(1))
  f(x(1));
  f(y(1));
  g("asdf"); // недопустимо: g(z(x("asdf"))) не пробуется g(z("asdf"));
Определяемые пользователем преобразования рассматриваются только в том случае, если без них вызов разрешить нельзя. Например:
  class x { /* ... */ x(int); }
  overload h(double), h(x); h(1);
Вызов мог бы быть проинтерпретирован или как h(double(1)), или как h(x(1)), и был бы недопустим по правилу единственности. Но первая интерпретация использует только стандартное преобразование и она будет выбрана по правилам, приведенным в #4.6.7.

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

Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Например, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность

6.4 Константы

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

6.5 Большие Объекты

При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр передается копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сож

  class matrix { double m[4][4];
  public:
    matrix();
    friend matrix operator+(matrix&,  matrix&);
    friend matrix operator*(matrix&, matrix&);
  };
Ссылки позволяют использовать выражения, содержащие обычные арифметические операции над большими объектами, без ненужного копирования. Указатели применять нельзя, потому что невозможно для применения к указателю смысл операции переопределить невозможно.
  matrix operator+(matrix&, matrix&); {
   matrix sum; for (int i=0; i<4; i++)
     for (int  j=0;  j<4;  j++) 
       sum.m[i][j]  = arg1.m[i][j] + arg2.m[i][j];
   return sum;
  }
Эта operator+() обращается к операндам + через ссылки, но возвращает значение объекта. Возврат ссылки может оказаться более эффективным:
  class matrix {
     // ...
     friend matrix& operator+(matrix&,  matrix&);
     friend matrix& operator*(matrix&, matrix&);
  };
Это является допустимым, но приводит к сложности с выделением памяти. Поскольку ссылка на результат будет передаваться из функции как ссылка на возвращаемое значение, оно не может быть автоматической переменной. Поскольку часто операция используется

6.6 Присваивание и Инициализация

Рассмотрим очень простой класс строк string:

  struct string { 
     char* p;
     int size; // размер вектора, на который указывает p
      string(int sz) { p = new char[size=sz]; }
      ~string() { delete p; }
  };
Строка - это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:
  void f() {
      string s1(10); string s2(20); s1 = s2;
  }
будет размещать два вектора символов, а присваивание s1=s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последс
  struct string {
   char* p;
   int size; // размер вектора, на который указывает p

   string(int sz) { p = new char[size=sz]; }
   ~string() { delete p; }
   void operator=(string&)
  };

  void string::operator=(string& a) {
   if (this == &a) return;  // остерегаться s=s;
   delete p; p=new
   char[size=a.size];
   strcpy(p,a.p);
  }
Это определение string гарантирует,и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приведет к появлению той же проблемы в новом облике:
  void f() {
      string s1(10); s2 = s1;
  }
Теперь создается только одна строка, а уничтожается две. К неинициализированному объекту определяемая пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было бы неразумно так делать: указа
  struct string {
    char* p;
    int size; // размер вектора, на который указывает p

    string(int sz) { p = new char[size=sz]; }
    ~string() { delete p; }
    void operator=(string&); string(string&);
  };

  void string::string(string& a) {
      p=new char[size=a.size]; strcpy(p,a.p);
  }
Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&). Нельзя не подчеркнуть еще раз, что присваивание и инициализация - разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор X(X&), выпо
  class X {
      // ...
      X(something); //конструктор: создает объект
      X(&X); //конструктор:  копирует  в  инициализации 
      operator=(X&);//присваивание: чистит и копирует
      ~X(); // деструктор: чистит
  };
Есть еще два случая, когда объект копируется: как параметр функции и как возвращаемое значение. Когда передается параметр, инициализируется неинициализированная до этого переменная - формальный параметр. Семантика идентична семантике инициализации. Т
  string g(string arg) {
      return arg;
  }

  main() {
      string s = "asdf"; s = g(s);
  }
Ясно, что после вызова g() значение s обязано быть "asdf". Копирование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&); на это

6.7 Индексирование

Чтобы задать смысл индексов для объектов класса, используется функция operator[]. Второй параметр (индекс) функции operator[] может быть любого типа. Это позволяет определять ассоциативные массивы и т.п. В качестве примера давайте перепишем пример и
  struct pair { char* name; int val;
  };
  class assoc { pair* vec; int max; int free;
  public:
    assoc(int);
    int& operator[](char*);
    void print_all();
  };
В assoc хранится вектор пар pair длины max. Индекс первого неиспользованного элемента вектора находится в free. Конструктор выглядит так:
  assoc::assoc(int s) {
    max = (s<16) ? s : 16;
    free = 0;
    vec = new pair[max];
  }
При реализации применяется все тот же простой и неэффективный метод поиска, что использовался в #2.3.10. Однако при переполнении assoc увеличивается:
  #include <string.h>

  int assoc::operator[](char* p)
  /*
      работа с множеством пар "pair":
      поиск p,  возврат ссылки на целую часть 
      его "pair" делает новую "pair", 
      если p не встречалось
  */
  { register pair* pp;

   for (pp=&vec[free-1]; vec<=pp; pp--)
     if (strcmp(p,pp->name)==0) return pp->val;

   if (free==max) { // переполнение:  вектор увеличивается
      pair* nvec = new pair[max*2];
    for  ( int i=0;  i<max;  i++)
        nvec[i] = vec[i];
    delete vec; vec = nvec; max = 2*max;
  }

      pp = &vec[free++];
      pp->name = new  char[strlen(p)+1];
      strcpy(pp->name,p);  pp->val = 0;  // начальное  значение:  0
      return pp->val;
  }
Поскольку представление assoc скрыто, нам нужен способ его печати. В следующем разделе будет показано, как определить подходящий итератор, а здесь мы используем простую функцию печати:
  vouid assoc::print_all() {
   for (int i = 0;  i<free;  i++)
    cout << vec[i].name << ": "<< vec[i].val<<"\n";
  }
Мы можем, наконец, написать простую главную программу:
  main() // считает вхождения каждого слова во вводе
 {
   const MAX  =  256;  //  больше  самого  большого  слова
   char buf[MAX];
   assoc vec(512);
   while (cin>>buf) vec[buf]++;
   vec.print_all();
  }

6.8 Вызов Функции

Вызов функции, то есть запись выражение(список_выражений), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие операции. Список параметров функции operator() вычисляется и проверяется в соответс

Для типа ассоциативного массива assoc мы не определили итератор. Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к данным, которые

  class assoc { friend class assoc_iterator;
      pair* vec; int max; int free;
  public:
      assoc(int); int& operator[](char*);
  };
Итератор определяется как
  class assoc_iterator{
   assoc* cs;  // текущий массив
   assoc int  i; // текущий индекс
  public:
   assoc_iterator(assoc& s){cs = &s; i = 0;}
   pair* operator()()
     { return (ifree)? &cs->vec[i++] : 0; }
  };
Надо инициализировать assoc_iterator для массива assoc, после чего он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать операцией (). По достижении конца массива он возвращает 0:
  main() // считает вхождения каждого слова во вводе
  {
   const MAX  =  256;  //  больше  самого  большого  слова
   char  buf[MAX];
   assoc vec(512);
   while  (cin>>buf)  vec[buf]++;
   assoc_iterator next(vec);
   pair* p;
   while (p=next()) cout << p->name << ": " << p->val << "\n";
  }
Итераторный тип вроде этого имеет преимущество перед набором функций, которые выполняют ту же работу: у него есть собственные закрытые данные для хранения хода итерации. К тому же обычно существенно, чтобы одновременно могли работать много ит

Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).

6.9 Класс String

Вот довольно реалистичный пример класса строк string. В нем производится учет ссылок на строку с целью минимизировать копирование и в качестве констант применяются стандартные символьные строки С++.

  #include <stream.h>
  #include <string.h>

  class string { struct srep {
    char* s; // указатель на данные
    int n; // счетчик ссылок
  };
  srep *p;

  public:
    string(char *);  // string x = "abc" 
    string();  // string x;
    string(string &); //string x = string  ...
    string& operator=(char *);
    string& operator=(string  &);  ~string();
    char& operator[](int i);

    friend ostream& operator<<(ostream&,string&);
    friend istream& operator>>(istream&, string&);

    friend int operator==(string& x,char* s)
       {return strcmp(x.p->s, s) == 0;}

    friend int operator==(string& x,string& y)
       {return strcmp(x.p->s, y.p->s) == 0; }

    friend int operator!=(string& x, char* s)
       {return strcmp(x.p->s, s) != 0; }

    friend int operator!=(string& x, string& y)
       {return strcmp(x.p->s, y.p->s) != 0; }

  };
Конструкторы и деструкторы просты (как обычно):
  string::string() {
   p = new srep; p->s = 0; p->n = 1;
  }

  string::string(char* s) {
   p = new srep;  p->s = new char[ strlen(s)+1  ];
   strcpy(p->s,s); p->n = 1;
  }

  string::string(string& x) {
   x.p->n++; p = x.p;
  }

  string::~string() {
   if (--p->n == 0) { delete p->s; delete p;}
  }
Как обычно, операции присваивания очень похожи на конструкторы. Они должны обрабатывать очистку своего первого (левого) операнда:
 string& string::operator=(char* s)
 {
  if (p->n > 1) { // разъединить себя
    p->n--; p = new srep;
    }
  else if (p->n == 1) delete p->s;
  p->s = new char[ strlen(s)+1 ];
  strcpy(p->s,  s);  p->n = 1;
  return *this;
 }
Благоразумно обеспечить, чтобы присваивание объекта самому себе работало правильно:
  string& string::operator=(string& x)
  {
   x.p->n++; if (--p->n == 0) 
    {
      delete p->s; delete p;
    }
   p = x.p; return *this;
  }
Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью операции <<, которая определяется позднее):
  ostream& operator<<(ostream& s, string& x)
  { 
   return s << x.p->s << " [" << x.p->n << "]\n";
  }
Операция ввода использует стандартную функцию ввода символьной строки (#8.4.1).
  istream& operator>>(istream& s, string& x)
  {
   char buf[256];
   s >> buf;
   x = buf;
   cout << "echo: " << x << "\n"; return s;
  }
Для доступа к отдельным символам предоставлена операция индексирования. Осуществляется проверка индекса:
  void error(char* p) {
      cerr << p << "\n"; exit(1);
  }

  char& string::operator[](int i) {
    if (i<0  ||  strlen(p->s)s[i];
  }
Головная программа просто немного опробует действия над строками. Она читает слова со ввода в строки, а потом эти строки печатает. Она продолжает это делать до тех пор, пока не распознает строку done, которая завершает сохранение слов в строках
  main() {
  string x[100]; int n;

  cout << "отсюда начнем\n";
  for (n = 0; cin>>x[n]; n++)
  {
   string y;
   if (n==100) error("слишком много строк");
   cout << (y = x[n]);
   if (y=="done") break;
  }
  cout << "отсюда мы пройдем обратно\n";
  for (int i=n-1; 0<=i; i--) cout << x[i];
  }

6.10 Друзья и Члены

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

  class X {
   // ...
   X(int);
   int m();
   friend int f(X&);
  };
Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызыватьс
  void g() {
   1.m(); // ошибка
   f(1); // f(x(1));
  }
Поэтому операция, изменяющая состояние объекта, должна быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++, *= и т.д.), наиболее естественно определяются как члены

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

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

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

6.11 Предостережение

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

Изложенный аппарат должен уберечь программиста/читателя от худших крайностей применения перегрузки, потому что программист предохранен от изменения значения операций для основных типов данных вроде int, а также потому, что синтаксис выражений и прио

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

6.12 Упражнения

  1. (*2) Определите итератор для класса string. Определите операцию конкатенации + и операцию "добавить в конец" +=. Какие еще операции над string вы хотели бы иметь возможность осуществлять?
  2. (*1.5) Задайте с помощью перегрузки () операцию выделения подстроки для класса строк.
  3. (*3) Постройте класс string так, чтобы операция выделения подстроки могла использоваться в левой части присваивания. Напишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут б
  4. (*2) Постройте класс string так, чтобы для присваивания, передачи параметров и т.п. он имел семантику по значению, то есть, когда копируется строковое представление, а не просто управляющая структура данных класса sring.
  5. (*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходимо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не будет изменена. Не пытайтесь од
  6. (*4) Разработайте класс string с семантикой по значению, копированием с задержкой и операцией подстроки, которая может стоять в левой части.
  7. (*2) Какие преобразования используются в каждом выражении следующей программы:
      struct X { int i; X(int); operator+(int); };
      struct Y { int i; Y(X); operator+(X); operator int(); };
      X operator* (X,Y); int f(X);
      X x = 1; Y y = x; int i = 2;
      main() {
       i + 10;  y + 10;  y + 10 * y;
       x + y + i; x * x + i; f(7);
       f(y); y + y; 106 + y;
      }
    
    Определите X и Y так, чтобы они оба были целыми типами. Измените программу так, чтобы она работала и печатала значения всех допустимых выражений.
  8. (*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().
  9. (*1) Определите класс RINT, который ведет себя в точности как int за исключением того, что единственные возможные операции - это + (унарный и бинарный), - (унарный и бинарный), *, /, %. Подсказка: не определяйте INT::operator int().
  10. (*3) Определите класс LINT, ведущий себя как RINT, за исключением того, что имеет точность не менее 64 бит.
  11. (*4) Определите класс, который реализует арифметику с произвольной точностью. Подсказка: вам надо управлять памятью аналогично тому, как это делалось для класса string.
  12. (*2) Напишите программу, доведенную до нечитаемого состояния с помощью макросов и перегрузки операций. Вот идея: определите для INT + так, чтобы он означал -, и наоборот, а потом с помощью макроопределения определите int как INT. Переопределение ча
  13. (*3) Поменяйтесь со своим другом программами, которые у вас получились в предыдущем упражнении. Не запуская ее попытайтесь понять, что делает программа вашего друга. После выполнения этого упражнения вы будете знать, чего следует избегать.
  14. (*2) Перепишите примеры с comlpex (#6.3.1), tiny (#6.3.2) и string (#6.9) не используя friend функций. Используйте только функции члены. Протестируйте каждую из новых версий. Сравните их с версиями, в которых используются функции друзья. Еще раз
  15. (*2) Определите тип vec4 как вектор их четырех float. Определите operator[] для vec4. Определите операции +, -, *, /, =, +=, -=, *=, /= для сочетаний векторов и чисел с плавающей точкой.
  16. (*3) Определите класс mat4 как вектор из четырех vec4. Определите для mat4 operator[], возвращающий vec4. Определите для этого типа обычные операции над матрицами. Определите функцию, выполняющие для mat4 исключение Гаусса.
  17. (*2) Определите класс vector, аналогичный vec4, но с длиной, которая задается как параметр конструктора vector::vector(int).
  18. (*3) Определите класс matrix, аналогичный mat4, но с размерностью, задаваемой параметрами конструктора matrix::matrix(int,int).

<--НАЗАД        ДАЛЕЕ-->

Содержание Глава 1 Глава 2 Глава 3 Глава 4 Глава 5 Глава 6 Глава 7 Глава 8

Сделай САМ 

 

Copyright © by Alex Skums




TopList
Сайт создан в системе uCoz