Опубликован: 10.10.2006 | Доступ: свободный | Студентов: 6088 / 462 | Оценка: 4.26 / 3.88 | Длительность: 31:30:00
Лекция 7:

Перегрузка операций

7.10 Инкремент и декремент

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

void f1(T a)   // традиционное использование
{
  T v[200];
  T* p = &v[0];
  p--;
  *p = a;   // Приехали: 'p' настроен вне массива,
            // и это не обнаружено
  ++p;
  *p = a;   // нормально
}

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

class CheckedPtrToT {
   // ...
};

void f2(T a)  // вариант с контролем
{
  T v[200];
  CheckedPtrToT p(&v[0],v,200);
  p--;
  *p = a;  // динамическая ошибка:
           // `p' вышел за границы массива
  ++p;
  *p = a;  // нормально
}

Инкремент и декремент являются единственными операциями в С++, которые можно использовать как постфиксные и префиксные операции. Следовательно, в определении класса CheckedPtrToT мы должны предусмотреть отдельные функции для префиксных и постфиксных операций инкремента и декремента:

class CheckedPtrToT {
  T* p;
  T* array;
  int size;
public:
       // начальное значение `p'
       // связываем с массивом `a' размера `s'
    CheckedPtrToT(T* p, T* a, int s);
       // начальное значение `p'
       // связываем с одиночным объектом
    CheckedPtrToT(T* p);

    T* operator++();     // префиксная
    T* operator++(int);  // постфиксная

    T* operator--();     // префиксная
    T* operator--(int);  // постфиксная

    T& operator*();      // префиксная
 };

Параметр типа int служит указанием, что функция будет вызываться для постфиксной операции. На самом деле этот параметр является искусственным и никогда не используется, а служит только для различия постфиксной и префиксной операции. Чтобы запомнить, какая версия функции operator++ используется как префиксная операция, достаточно помнить, что префиксной является версия без искусственного параметра, что верно и для всех других унарных арифметических и логических операций. Искусственный параметр используется только для "особых" постфиксных операций ++ и --.

С помощью класса CheckedPtrToT пример можно записать так:

void f3(T a)  // вариант с контролем
{
  T v[200];
  CheckedPtrToT p(&v[0],v,200);
  p.operator--(1);
  p.operator*() = a; // динамическая ошибка:
                     // `p' вышел за границы массива
  p.operator++();
  p.operator*() = a; // нормально
}

В упражнении \S 7.14 [19] предлагается завершить определение класса CheckedPtrToT, а другим упражнением ( \S 9.10[2]) является преобразование его в шаблон типа, в котором для сообщений о динамических ошибках используются особые ситуации. Примеры использования операций ++ и -- для итераций можно найти в \S 8.8.

7.11 Строковый класс

Теперь можно привести более осмысленный вариант класса string. В нем подсчитывается число ссылок на строку, чтобы минимизировать копирование, и используются как константы стандартные строки C++.

#include <iostream.h>
#include <string.h>

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

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

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

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

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

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

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

Конструкторы и деструкторы тривиальны:

string::string()
{
  p = new srep;
  p->s = 0;
}

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

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

string::~string()
{
  if (--p->n == 0) {
     delete[]  p->s;
     delete p;
  }
}

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

string& string::operator=(const char* s)
{
  if (p->n > 1) {  // отсоединяемся от старой строки
      p->n--;
      p = new srep;
  }
  else    // освобождаем строку со старым значением
      delete[] p->s;

  p->s = new char[ strlen(s)+1 ];
  strcpy(p->s, s);
  return *this;
}

string& string::operator=(const string& x)
{
  x.p->n++;  // защита от случая ``st = st''
  if (--p->n == 0) {
     delete[] p->s;
     delete p
  }
  p = x.p;
  return *this;
}

Операция вывода показывает как используется счетчик числа ссылок. Она сопровождает как эхо каждую введенную строку (ввод происходит с помощью операции <<, приведенной ниже):

ostream& operator<<(ostream& s, const string& x)
{
   return s << x.p->s << " [" << x.p->n << "]\n";
}

Операция ввода происходит с помощью стандартной функции ввода символьной строки ( \S 10.3.1):

istream& operator>>(istream& s, string& x)
{
   char buf[256];
   s >> buf;   // ненадежно: возможно переполнение buf
               // правильное решение см. в 10.3.1
   x = buf;
   cout << "echo: " << x << '\n';
   return s;
 }

Операция индексации нужна для доступа к отдельным символам. Индекс контролируется:

void error(const char* p)
  {
    cerr << p << '\n';
    exit(1);
  }

 char& string::operator[](int i)
{
 if (i<0 || strlen(p->s)<i) error("недопустимое значение индекса");
   return p->s[i];
}

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

int main()
 {
   string x[100];
   int n;

   cout << " здесь начало \n";

   for ( n = 0; cin>>x[n]; n++) {
       if (n==100) {
          error("слишком много слов");
          return 99;
       }
       string y;
       cout << (y = x[n]);
       if (y == "done") break;

   }
   cout << "теперь мы идем по словам в обратном порядке \n";
   for (int i=n-1; 0<=i; i--) cout << x[i];
   return 0;
 }
Дарья Федотова
Дарья Федотова
Сергей Березовский
Сергей Березовский

В рамках проф. переподготовки по программе "Программирование"

Есть курсы, которые я уже прошел. Но войдя в курс я вижу, что они не зачтены (Язык Ассемблера и архитектура ЭВМ, Программирование на С++ для профессионалов). Это как?

Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва