Опубликован: 10.10.2006 | Уровень: специалист | Доступ: свободно
Лекция 5:

Классы

5.2.3 Ссылка на себя

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

class X {
  int m;
public:
  int readm() { return m; }
};

void f(X aa, X bb)
{
  int a = aa.readm();
  int b = bb.readm();
  // ...
}

При первом вызове readm() m обозначает aa.m, а при втором - bb.m.

У функции-члена есть дополнительный скрытый параметр, являющийся указателем на объект, для которого вызывалась функция. Можно явно использовать этот скрытый параметр под именем this. Считается, что в каждой функции-члене класса X указатель this описан неявно как

X *const this;

и инициализируется, чтобы указывать на объект, для которого функция-член вызывалась. Этот указатель нельзя изменять, поскольку он постоянный ( *const ). Явно описать его тоже нельзя, т.к. this - это служебное слово. Можно дать эквивалентное описание класса X:

class X {
   int m;
public:
   int readm() { return this->m; }
};

Для обращения к членам использовать this излишне. В основном this используется в функциях-членах, непосредственно работающих с указателями. Типичный пример - функция, которая вставляет элемент в список с двойной связью:

class dlink {
 dlink* pre;  // указатель на предыдущий элемент
 dlink* suc;  // указатель на следующий элемент
         public:
 void append(dlink*);
 // ...
         };

         void dlink::append(dlink* p)
         {
p->suc = suc;    // т.е. p->suc = this->suc
p->pre = this;   // явное использование "this"
suc->pre = p;    // т.е. this->suc->pre = p
suc = p;         // т.е. this->suc = p
         }

         dlink* list_head;

         void f(dlink* a, dlink* b)
         {
// ...
list_head->append(a);
list_head->append(b);
         }

Списки с такой общей структурой служат фундаментом списочных классов, описываемых в "лекции 8" . Чтобы присоединить звено к списку, нужно изменить объекты, на которые настроены указатели this, pre и suc. Все они имеют тип dlink, поэтому функция-член dlink::append() имеет к ним доступ. Защищаемой единицей в С++ является класс, а не отдельный объект класса.

Можно описать функцию-член таким образом, что объект, для которого она вызывается, будет доступен ей только по чтению. Тот факт, что функция не будет изменять объект, для которого она вызывается (т.е. this *), обозначается служебным словом const в конце списка параметров:

class X {
int m;
 public:
 int readme() const { return m; }
 int writeme(int i) { m = i; }
 };

Функцию-член со спецификацией const можно вызывать для постоянных объектов, а функцию-член без такой спецификации - нельзя:

void f(X& mutable, const X& constant)
 {
   mutable.readme();    // нормально
   mutable.writeme(7);  // нормально
   constant.readme();   // нормально
   constant.writeme(7); // ошибка
 }

В этом примере разумный транслятор смог бы обнаружить, что функция X::writeme() пытается изменить постоянный объект. Однако, это непростая задача для транслятора. Из-за раздельной трансляции он в общем случае не может гарантировать "постоянство" объекта, если нет соответствующего описания со спецификацией const. Например, определения readme() и writeme() могли быть в другом файле:

class X {
int m;
 public:
 int readme() const;
 int writeme(int i);
 };

В таком случае описание readme() со спецификацией const существенно.

Тип указателя this в постоянной функции-члене класса X есть const X *const. Это значит, что без явного приведения с помощью this нельзя изменить значение объекта:

class X {
int m;
 public:
// ...
void implicit_cheat() const { m++; }  // ошибка
void explicit_cheat() const { ((X*)this)->m++; }
     // нормально
};

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

Логического постоянства можно достигнуть приведением, удаляющим спецификацию const:

class calculator1 {
   int cache_val;
   int cache_arg;
   // ...
public:
   int compute(int i) const;
   // ...
};

int calculator1::compute(int i) const
{
  if (i == cache_arg) return cache_val;
  // нелучший способ
  ((calculator1*)this)->cache_arg = i;
  ((calculator1*)this)->cache_val = val;
  return val;
}

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

struct cache {
int val;
int arg;
};

class calculator2 {
cache* p;
// ...
public:
int compute(int i) const;
// ...
};

int calculator2::compute(int i) const
{
  if (i == p->arg) return p->val;
  // нелучший способ
  p->arg = i;
  p->val = val;
  return val;
}

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

Равиль Ярупов
Равиль Ярупов
Привет !
Федор Антонов
Федор Антонов
Оплата и обучение
Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва