Лекция 7:

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

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

Рассмотрим простой строковый класс string:

struct string {
   char* p;
   int size;  // размер вектора, на который указывает p

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

Строка - это структура данных, содержащая указатель на вектор символов и размер этого вектора. Вектор создается конструктором и удаляется деструктором. Но как мы видели в \S 5.5.1 здесь могут возникнуть проблемы:

void f()
{
  string s1(10);
  string s2(20);
  s1 = s2;
}

Здесь будут размещены два символьных вектора, но в результате присваивания s1 = s2 указатель на один из них будет уничтожен, и заменится копией второго. По выходе из f() будет вызван для s1 и s2 деструктор, который дважды удалит один и тот же вектор, результаты чего по всей видимости будут плачевны. Для решения этой проблемы нужно определить соответствующее присваивание объектов типа string:

struct string {
  char* p;
  int size;   // размер вектора, на который указывает p

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

string& string::operator=(const string& a)
{
  if (this !=&a) {   // опасно, когда s=s
     string p;
     p = new char[size=a.size];
     strcpy(p,a.p);
  }
  return *this;
}

При таком определении string предыдущий пример пройдет как задумано. Но после небольшого изменения в f() проблема возникает снова, но в ином обличии:

void f()
{
  string s1(10);
  string s2 = s1;  // инициализация, а не присваивание
}

Теперь только один объект типа string строится конструктором string::string(int), а уничтожаться будет две строки. Дело в том, что пользовательская операция присваивания не применяется к неинициализированному объекту. Достаточно взглянуть на функцию string::operator(), чтобы понять причину этого: указатель p будет тогда иметь неопределенное, по сути случайное значение.

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

struct string {
  char* p;
  int size;   // размер вектора, на который указывает p

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

string::string(const string& a)
{
     p=new char[size=sz];
     strcpy(p,a.p);
}

Инициализация объекта типа X происходит с помощью конструктора X(const X&). Мы не перестаем повторять, что присваивание и инициализация являются разными операциями. Особенно это важно в тех случаях, когда определен деструктор. Если в классе X есть нетривиальный деструктор, например, производящий освобождение объекта в свободной памяти, вероятнее всего, в этом классе потребуется полный набор функций, чтобы избежать копирования объектов по членам:

class X {
  // ...
  X(something);        // конструктор, создающий объект
  X(const X&);         // конструктор копирования
  operator=(const X&); // присваивание:
                       // удаление и копирование
  ~X();                // деструктор, удаляющий объект
                         };

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

string g(string arg)
{
  return arg;
}

main()
{
  string s = "asdf";
  s = g(s);
}

Очевидно, после вызова g() значение s должно быть "asdf". Не трудно записать в параметр s копию значения s, для этого надо вызвать конструктор копирования для string. Для получения еще одной копии значения s по выходе из g() нужен еще один вызов конструктора string(const string&). На этот раз инициализируется временная переменная, которая затем присваивается s. Для оптимизации одну, но не обе, из подобных операций копирования можно убрать. Естественно, временные переменные, используемые для таких целей, уничтожаются надлежащим образом деструктором string::~string() (см. \S R.12.2).

Если в классе X операция присваивания X::operator=(const X&) и конструктор копирования X::X(const X&) явно не заданы программистом, недостающие операции будут созданы транслятором. Эти созданные функции будут копировать по членам для всех членов класса X. Если члены принимают простые значения, как в случае комплексных чисел, это то, что нужно, и созданные функции превратятся в простое и оптимальное поразрядное копирование. Если для самих членов определены пользовательские операции копирования, они и будут вызываться соответствующим образом:

class Record {
  string name, address, profession;
  // ...
};

void f(Record& r1)
{
  Record r2 = r1;
}

Здесь для копирования каждого члена типа string из объекта r1 будет вызываться string::operator=(const string&). В нашем первом и неполноценном варианте строковый класс имеет член-указатель и деструктор. Поэтому стандартное копирование по членам для него почти наверняка неверно. Транслятор может предупреждать о таких ситуациях.

Равиль Ярупов
Равиль Ярупов
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?