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