Материалы книги получены с 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*. В любом случае все завершается
косвенным получением адреса некоторой части объекта так, словно вы получили явный адрес переменной класса. Указатели на члены классов также могут применяться для косвенных ссылок на функции, а не на переменные класса, но это не относится к нашей теме - управление памятью. К тому же я не хочу взваливать на себя лишнюю головную боль.
Последствия
Все сказанное обладает фундаментальными последствиями для управления памятью. Чтобы переместить объект в памяти, вам придется проследить за тем, чтобы перемещался вмещающий объект верхнего уровня, а не некоторый вложенный объект, адрес которого у вас имеется. Более того, при перемещении объекта придется обновлять все указатели - не только на сам объект, но и на все вложенные объекты и базовые классы. Если вы хотите узнать, существуют ли ссылки на некоторый объект, придется искать указатели не только на начало объекта, но и на все его переменные и базовые классы.
Назад Содержание Далее
|