Содержание:
Хранение переменных в PHP
Прежде чем перейдем к рассмотрению классов и алгоритма их наследования, мы посмотрим, как PHP хранит переменные в памяти.
Переменные хранятся в таблице переменных (simbol_table), которая представляет из себя ассоциативный массив, ключом элемента массива является имя переменной, а в значении хранится ссылка на контейнер переменной или zval контейнер, в котором хранятся значение переменной, его тип и дополнительная информация о количестве ссылок на переменную. zval контейнер представляет структуру:
typedef struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc; } zval;
zvalue_value хранит само значение переменной и представляет структуру объединение (union):
typedef union _zvalue_value { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } zvalue_value;
В зависимости от типа значения, возможен доступ только к одному полю. Тип переменной хранится в zval.type и может иметь следующие значения:
Поле type | используемое поле для хранения данных |
---|---|
IS_NULL | нет |
IS_BOOL | long lval |
IS_LONG | long lval |
IS_DOUBLE | double dval |
IS_STRING | struct { char *val; int len; } str |
IS_ARRAY | HashTable *ht |
IS_OBJECT | zend_object_value obj |
IS_RESOURCE | long lval |
Теперь мы имеем представление об организации хранения переменных в PHP. Более подробно, особенно про использование refcount__gc и is_ref__gc смотрите в источниках [1,2 и 5]. В дальнейшем я буду ссылаться на zvalконтейнер и вы уже будете знать что это такое и как используется.
Резюме
- Каждая переменная, независимо от типа данных, хранится в едином контейнере, называемый zval.
- Этот контейнер имеет все необходимые параметры для работы с переменными во всех возможных режимах.
Простой класс в PHP
Для начала рассмотрим, как же PHP хранит классы. Пример:
class A { private $count = 1; public function getCount() { return $this->count; } }
Кстати, данный простейший класс из нашего примера в памяти занимает 1160 Байт.
PHP согласно [3] считается транслирующим интерпретатором. Наш скрипт при загрузке в память транслируется в байт-коды и затем полученные байт-коды интерпретируются. Но такие крупные объекты, как классы, в ходе трансляции формируются в определенную структуру zend_class_entry, который хорошо описан в [1] и [3]. А коды методов транслируются в байт коды. Схематично данную структуру можно представить следующим образом:
Если внимательно посмотрите на источники, указанные выше, структура zend_class_entry представляет собой солидный объект, где учтены всевозможные варианты использования классов. Я на рисунке указал только те параметры класса, которые могут быть интересны в рамках настоящей статьи.
Разберем структуру.
name — название класса.
parent — ссылка на класс родителя. В нашем случае NULL, поскольку наш класс ни от кого не наследуется.
function_table — массив методов класса. Каждая ячейка этого массива ссылается на объект метода, который описывается структурой zend_function. Эта структура (в упрощенном виде) имеет поля function_name — имя функции, class — ссылка на объект класса, op_array — массив байт-кодов функции и fn_flags, который имеет биты, определяющие область видимости функции (public, protected или private). Также структура function_table содержит поля, описывающие аргументов, их количество, количество обязательных аргументов, список аргументов, в котором каждый аргумент описывается отдельно и другие параметры, но они в этой статье не указаны, поскольку порядок передачи и использования аргументов в методах здесь не рассматриваются.
properties_info — массив описаний свойств класса. Каждая ячейка этого массива ссылается на описание свойства класса.
Описание свойства класса представлено структурой zend_property_info. Эта структура содержит поля name— имя свойства, class — ссылка на объект класса, offset — индекс в массиве значений по умолчанию и flags — область видимости свойства (public, protected или private).
default_properties_table — массив значений по умолчанию. Каждая ячейка массива ссылается на контейнер zval, который содержит заданное в коде класса значение. В нашем примере это код
private $count = 1;
Параметр offset в структуре properties_info как раз содержит индекс этого массива. Почему так сделано, будет понятно далее, когда мы перейдем на рассмотрение объектов PHP.
Хоть в статье и не используются, но я показал в структуре класса два параметра, это static_members_table — таблица статических свойств и constants_table — таблица констант класса. В нашем примере статические свойства и константы не используются и поэтому их значение NULL.
Примечание. Поля fn_flags и flags кроме модификатора доступа хранят информацию об статических (static) методах или свойствах.
Резюме
- Каждый класс, каждый метод и каждое свойство хранятся в соответствующих структурах и имеют все необходимые параметры для работы с ними.
- Каждая структура, описывающая метод или свойство, имеет обратную ссылку на свой класс, тем самым принадлежность метода или свойтсва четко отслеживается.
Наследование классов
Как правило, классы самодостаточные образования и в рамках возложенных задач обеспечивают выполнение всех необходимых операций. Даже если класс объявлен абстрактным, значит в нем подготовлена основа для своих потомков. И наследуемые классы при этом занимаются только своими задачами, а все общие и сторонние задачи остаются в родительском (абстрактном) классе.
Дополним наш пример:
class A { private $count = 1; public function getCount() { return $this->count; } } class B extends A {}
Пока что наш класс пустой и все поля, ссылающиеся на массив методов и свойств, имеют значение NULL. В памяти теперь содержатся 2 структуры класса и поле parent класса В теперь ссылается на структуру класса А. Добавим в наш класс В свойство и новый метод.
class A { private $count = 1; public function getCount() { return $this->count; } } class B extends A { public $count=5; public function getCountB() { return $this->count; } }
Неожиданно, правда? Такой контрукции на практике, как правило, никто не создает, ибо она не имеет смысла. Но, заданный вопрос по ссылке в начале статьи, в том числе касалось обработки PHP такой конструкции. Ну чтож, попробуем разобраться. Что интересно, PHP ни в ходе трансляции кода, ни в ходе интерпретации при выполнении, никаких ошибок не выдал и это правильно.
Класс В абсолютно ничего не знает о свойстве count класса А, поскольку он приватный, то есть доступен только методам своего класса. Для интереса попробуйте поменять область видимости свойств в классах, вы тут же получите ошибку трансляции, ибо вы в наследуемом классе не можете публичное свойство родителя сделать приватным.
В памяти в это время структура класса В стал похож на структуру класса А из рисунка выше и соответственно поле parent ссылается на класс А.
Резюме
- Каждый класс хранится в структуре zend_class_entry и его свойства и методы в соответствующих структурах.
- При наследовании классов, поле parent наследуемого класса ссылается на структуру класса родителя.
Объекты
В наш код вводим переменные, а именно создадим экземпляр объекта класса В. Код:
$obj = new B(); var_dump($obj);
Вывод:
object(B)#1 (2) { ["count"]=> int(5) ["count":"A":private]=> int(1) }
Как видите, в объекте хранятся свойства обоих классов со своими значениями. Но второе свойство имеет указание, что это свойство класса А.
При создании нашего объекта что происходит:
- В памяти создается структура объекта zend_object.
- В поле ce вносится ссылка на класс В.
- Поле properties содержит массив объявленных свойств во всех классах, начиная с родительского. Каждый элемент массива содержит ссылку в объект zend_property_info классов.
- А поле properties_table является символьной таблицей объекта и содержит ссылки на zval контейнеры. При этом ключами элементов массива являются названия свойств. Обратите внимание, первый элемент обозначен как \0A\0сount (здесь \0 обозначает символ #00, то есть нулевой байт). В этой записи А означает название класса А, а сount соответственно название свойства. Такая запись означает, что count является приватным свойством класса А и доступен только методам данного класса. Второй элемент просто обозначен своим названием, поскольку он публичное свойство и второго такого во всей цепочке не может быть. Что интересно, если свойство count класса В обозначить как protected, то запись в properties_table была бы такой \0 * \0сount, вместо названия класса знак *. Это значит, что свойство доступно всем классам цепочки, но не извне. Такое свойство извне не видно. Кстати, эти же записи показаны в выводе функции
var_dump($obj);
, только в более читабельном виде. - Поле guards используется для защиты от циклических вызовов в методах объекта.
- Созданный объект размещается в хранилище объектов zend_object_store, который обеспечивает хранение объекта в единственном экземпляре. Здесь эту структуру рассматривать не будем, за подробностями обратитесь к источникам.
- Объект привязывается к имени переменной.
Дополним наш код:
echo $obj->getCount(); // выводит 1 echo $obj->getCountB(); // выводит 5
Почему так происходит. При обращении к переменной в методе класса, PHP начинает просмотр массива поля properties_table объекта. Расшифровывает запись \0A\0сount, получает имя класса А и имя переменной count. При случае, если это метод getCount(), то все условия выполняются и значение переменной по ссылке проставляется в выражение. Во второй строке кода запись \0A\0сount не удовлетворяет условиям (метод getCountB() относится к классу В) и PHP в выражении использует значение публичного свойства count, который удовлетворяет всем условиям.
Обратите внимание, на рисунке элементы массива properties_table ссылаются на zval контейнеры значений по умочанию соответствующих классов. Вспомните, раздел настоящей статьи Простой класс в PHP. Для того, чтобы не расходовать лишнюю память, первоначально объект ссылается на значения по умолчанию класса. Но стоит нам изменить какое-либо свойство, в памяти создается новый zval контейнер с новым значением.
А теперь попробуем нашему объекту назначить динамическое свойство. Код:
$obj->newVar = "Это свойство не определено в классах А и В."; var_dump($obj);
Вывод:
object(B)#1 (3) { ["count":protected]=> int(5) ["count":"A":private]=> int(1) ["newVar"]=> string(77) "Это свойство не определено в классах А и В." }
Как видите, наше свойство newVar успешно добавлено в объект и оно определено как публичное свойство (public). Новое свойство добавилось в таблицу свойств properties_table как новый элемент массива со своим контейнером zval.
А как объекты видят статические свойства? Изменим код нашего класса В:
class B extends A { public $count=5; static $staticVar = 7; public function getCountB() { return $this->count; } } $obj = new B(); var_dump($obj);
Вывод:
object(B)#1 (2) { ["count"]=> int(5) ["count":"A":private]=> int(1) }
Как видите, в таблице свойств объекта статического свойства нет. И, соответственно, обращение к этому свойству от объекта приведет к ошибке:
echo $obj->staticVar;
Вывод:
PHP Notice: Undefined property: B::$staticVar in /home/shah/projects/objects/test1.php on line 21
Резюме
- Объекты, в отличие от классов, занимают очень мало места в памяти. Правда, по мере присваивания новых значений свойствам объекта, под них выделяются отдельная память и размер объекта увеличивается.
- Объект хранит ссылки на структуру описание свойства классов, и отдельно, таблицу переменных объекта.
- При обращении к свойствам в методах объекта, элементы массива перебираются последовательно и первое свойство, отвечающее всем условиям (имя переменной, видимость для метода текущего класса) используется в выражении.
- Статические свойства доступны только при обращении как к свойству класса $A::staticVar.