С++ - язык, который изучается постепенно.ГЛАВА 15. Мама, откуда берутся указатели?
                   Справочники Всё для создания сайта

Ссылки


Home
Бизнес
Справочники
Советы







Материалы книги получены с http://www.itlibitum.ru/

Мама, откуда берутся указатели?

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

Адреса переменных класса

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

class Foo {

private:

int x;

String y;

public:

int& X() { return x; } // Ссылка на x

String* Name() { return &y; } // Адрес y

};

Каждый экземпляр Foo выглядит примерно так, как показано на представленной ниже диаграмме

(вообще говоря, все зависит от компилятора, но в большинстве компиляторов дело обстоит именно

так):

Как правило, несколько первых байт занимает указатель на v-таблицу для класса данного объекта. За ним следуют переменные класса в порядке их объявления. Если вы получаете адрес переменной класса в виде ссылки или указателя, возникает указатель на середину объекта.

Адреса базовых классов

Наследование также может вызвать массу положительных эмоций.

class A {...}; // Один базовый класс

class B {...}; // Другой базовый класс

class C : public A, public B {...}; // Множественное наследование

При одиночном наследовании преобразование от derived* к base* (где base - базовый, а derived - производный класс) адрес остается прежним, даже если компилятор полагает, что тип изменился.

При множественном наследовании дело обстоит несколько сложнее.

C* c = new C;

A* a = c; // Преобразование от производного к первому базовому классу

B* b = c; // Преобразование от производного ко второму базовому классу

cout << c << endl;

cout << a << endl;

cout << b << endl;

Вроде бы все просто, но в действительности компилятор проделывает довольно-таки хитрый фокус. При преобразовании C* к A* указатель остается прежним. Однако при преобразовании C* к B* компилятор действительно изменяет адрес. Это связано с тем, как объект хранится в памяти (структура объектов зависит от компилятора, но сказанное относится ко всем компиляторам, с которыми я работал).

Компилятор строит объект в порядке появления базовых классов, за которыми следует производный

класс. Когда компилятор преобразует C* к A*, он словно набрасывает черное покрывало на

составляющие B и C и убеждает клиентский код, что тот имеет дело с самым настоящим A.

Размещение v-таблицы в начале объекта приводит к тому, что принадлежащие C реализации

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

На самом деле v-таблиц две. Одна находится в начале объекта и содержит все виртуальные функции, первоначально объявленные в A или C, а другая - в начале компонента B и содержит виртуальные функции, объявленные в B. Это означает, что преобразование типа от производного к базовому классу в С++ может при некоторых обстоятельствах породить указатель на середину объекта (по аналогии с указателями на переменные класса, о которых говорилось выше). Кроме того, в С++ открывается возможность дурацких фокусов:

C* anotherC = C*(void*(B*(c)));

anotherC->MemberOfC();

Видите, в чем проблема? Преобразование B*(c) смещает указатель. Затем он преобразуется к типу void*. Далее следует обратное преобразование к C* - и наша программа будет уверена, что C начинается с неверного адреса. Без преобразования к void* все работает, поскольку компилятор может определить смещение B* в C*. В сущности, преобразование от base* к derived* (где base - базовый, а derived - производный класс) выполняется каждый раз, когда клиент вызывает виртуальную функцию B, переопределенную в C. Но когда происходит преобразование от void* к C*, компилятор лишь наивно полагает, что программист действует сознательно.

Запомните: каждый программист на С++ за свою карьеру проводит как минимум одну бессонную ночь, пытаясь понять, почему его объект бредит. Потом приходит какой-нибудь гуру, с ходу ставит диагноз «синдром класс-void-класс» - притом так, чтобы слышали окружающие - и разражается злорадным смехом. Впрочем, я отклонился от темы.

Виртуальные базовые классы

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

class Base {...};

class A : virtual public Base {...};

class B : virtual public Base {...};

class Foo : public A, public B {...};

Тьфу. Компилятору так стыдно, что Base приходится реализовывать как виртуальный базовый класс, что он прячет его как можно дальше, под Foo. A и B содержат указатели на экземпляр Baseда, все верно, указатели, то есть непосредственные адреса в памяти. Вы не имеете доступа к этим указателям и, следовательно, не сможете обновить их при перемещении объекта в памяти.

Указатель на переменную класса

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

class Foo {

private:

int x;

public:

static int& Foo::*X() { return &Foo::x; }

};

Foo f = new Foo; // Создать экземпляр

int& Foo::*pm = Foo::X(); // Вычислить смещение int

int& i = f->*pm; // Применить смещение к экземпляру

Функция X() возвращает не ссылку на int, а смещение некоторого int в экземплярах класса Foo

Функция Foo::X() объявлена статической, поскольку относится не к конкретному экземпляру, а к классу в целом. Команда return &Foo::x; определяет смещение конкретной переменной, x. В строке int& Foo::*pm = Foo::X(); объявляется переменная pm, которая содержит смещение переменной int класса Foo. Она инициализируется смещением, полученным от Foo::X(). Наконец, в строке int& i = f->*pm; смещение применяется к конкретному экземпляру для вычисления адреса конкретного int. Обратите внимание: значение pm само по себе бесполезно до тех пор, пока вы не примение его к объекту.

Все эти int& с таким же успехом можно заменить на int*. В любом случае все завершается

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

Последствия

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


Назад    Содержание    Далее    

Home  Создание сайтов  Учебник по записи CD  Справочник Web дизайнера Самоучитель IE PHP и MySQL Компьютерные сети С++ E-mail me

Copyright 2007. Климов Александр. All Right Reserved.
Hosted by uCoz