C++. Бархатный путь. Часть 2 - Указатель this

ОГЛАВЛЕНИЕ

Указатель this

Продолжаем определение класса ComplexType. Теперь объявим и определим функцию-член PrintVal, которая будет выводить значение чисел-объектов.

Прототип функции разместим в классе:

void PrintVal();

При определении функции используется квалифицированное имя:

void ComplexType::PrintVal()
{
cout << "(" << real << ", " << imag << "i)" << endl;
cout << (int)CTcharVal << ", " << x << "…" << endl;
}

Значения данных-членов объекта выводятся при выполнении выражения вызова функции PrintVal:

CDw1.PrintVal();

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

Среди операторов функции-члена PrintVal() нет ни одного оператора, который позволял бы определить, какому объекту принадлежат данные-члены. И, тем не менее, вызов этой функции для каждого из определённых и различным образом проинициализированных объектов, в том числе и для безымянного объекта, который создаётся в результате непосредственного вызова конструктора:

ComplexType(0.0,0.0, 1).PrintVal(); ,

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

pCD->PrintVal();

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

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

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

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

На первом этапе каждая нестатическая функция-член преобразуется в функцию с уникальным именем и дополнительным параметром - константным указателем на объект класса. Затем преобразуются обращения к нестатическим данным-членам в операторах функции-члена. Они переопределяются с учётом нового параметра. В C++ при подобном преобразовании для обозначения дополнительного параметра-указателя (константного указателя) и постфиксного выражения с операциями обращения для обращения к нестатическим данным-членам используется одно и то же имя this. Вот как могла бы выглядеть функция-член PrintVal после её переопределения:

void ComplexType::ComplexType_PrintVal(ComplexType const *this)
{
cout << "(" << this->real << "," << this->imag << "i)" << endl;
cout << int(this->CTcharVal) << "," << x << "…" << endl;
}

На втором этапе преобразуются вызовы функций-членов. К списку значений параметров выражения вызова добавляется выражение, значением которого является адрес данного объекта. Это вполне корректное преобразование. Дело в том, что нестатические функции-члены всегда вызываются для конкретного объекта. И потому не составляет особого труда определить адрес объекта. Например, вызов функции-члена PrintVal() для объекта CDw1, который имеет вид

CDw1.PrintVal();

после преобразования принимает вид:

ComplexType_PrintVal(&CDw1);

А вызов функции-члена безымянного объекта, адресуемого указателем pCD

pCD->PrintVal();

преобразуется к виду

ComplexType_PrintVal(&(*pCD));

что эквивалентно следующему оператору:

ComplexType_PrintVal(pCD);

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

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

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

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

void ComplexType::PrintVal()
{
cout << "(" << this->real << "," << this->imag << "i)" << endl;
cout << int(this->CTcharVal) << "," << x << "…" << endl;
}

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

В ряде случаев при написании программы оправдано явное использование указателя this. При этом выражение

this

представляет адрес объекта, а выражение

*this

представляет сам объект:

this->ВЫРАЖЕНИЕ
(*this).ВЫРАЖЕНИЕ

(здесь нетерминальный символ ВЫРАЖЕНИЕ обозначает член класса). Эти выражения обеспечивают доступ к членам уникального объекта, представленного указателем this с целью изменения значения данного, входящего в этот объект или вызова функции-члена.

Следует помнить о том, что this указатель является константным указателем. Это означает, что непосредственное изменение его значение (перенастройка указателя, например, this++) недопустимо. Указатель this с самого начала настраивается на определённый объект.

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

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

Описатель ::=
Описатель (СписокОбъявленийПараметров) [СписокCVОписателей]
::= *****
CVОписатель ::= const
::= *****

Так что небольшая модификация функции-члена PrintVal, связанная с добавлением cvОписателя const:

void PrintVal() const;

в прототипе и

void ComplexType::PrintVal() const
{
:::::
}

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

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

this->CTcharVal = 125;
real = imag*25;
imag++;

cvОписатель const в заголовке функции не допускает непосредственной модификации значений принадлежащих объекту данных.

Заметим также, что this указатель включается также в виде дополнительного параметра в список параметров конструктора. И в этом нет ничего удивительного, поскольку его значением является всего лишь область памяти, занимаемая объектом.