#c #memory
#c #память
Вопрос:
Предположим, у нас есть класс:
class Foo
{
private:
int a;
public:
void func()
{
a = 0;
printf("In Funcn");
}
}
int main()
{
Foo *foo = new Foo();
foo->func();
return 0;
}
Когда объект класса Foo создается и инициализируется, я понимаю, что целое число a займет 4 байта памяти. Как хранится функция? Что происходит в памяти / стеке / регистрах / со счетчиком программ при вызове foo->func()?
Ответ №1:
Краткий ответ: Он будет сохранен в разделе текста или кода двоичного файла только один раз, независимо от количества экземпляров созданного класса.
Функции нигде не хранятся отдельно для каждого экземпляра класса. Они обрабатываются так же, как и любая другая функция, не являющаяся членом. Единственное отличие заключается в том, что компилятор фактически добавляет дополнительный параметр к функции, который является указателем типа класса. Например, компилятор сгенерирует прототип функции следующим образом:
void func(Foo* this);
(Обратите внимание, что это может быть не окончательная подпись. Окончательная подпись может быть гораздо более загадочной в зависимости от различных факторов, включая компилятор)
Любая ссылка на переменную-член будет заменена на
this-><member> //for your ex. a=0 translates to this->a = 0;
Итак, строка foo->func(); примерно переводится как:
- Поместите значение Foo* в стек. #Зависит от компилятора
- вызовите функцию, которая заставит указатель инструкции перейти к смещению функции в исполняемом файле #architecture dependent Прочитайте это и это
- Функция извлекает значение из стека. Любой дальнейшей ссылке на переменную-член будет предшествовать разыменование этого значения
Комментарии:
1. 1; что касается причины, по которой вы упомянули о термине подпись , иногда используется альтернативный прототип , хотя это использование здесь можно оспорить.
2. Поскольку я также упомянул возвращаемый тип, prototype должен быть словом записи. Отредактировал ответ, чтобы отразить то же самое.
3. такой отличный ответ
Ответ №2:
Ваша функция не виртуальная, она вызывается статически : компилятор вставляет переход к сегменту кода, соответствующему вашей функции. Дополнительная память для каждого экземпляра не используется.
Если бы ваша функция была виртуальной, ваш экземпляр нес бы vpointer , который был бы разыменован, чтобы найти vtable своего класса, который затем был бы проиндексирован, чтобы найти указатель на вызываемую функцию, и, наконец, перейти туда. Таким образом, дополнительная стоимость составляет одну виртуальную таблицу на класс (вероятно, размер одного указателя функции, умноженный на количество виртуальных функций вашего класса) и один указатель на экземпляр.
Обратите внимание, что это обычная реализация виртуальных вызовов, но стандарт никоим образом не применяется, поэтому на самом деле она вообще не может быть реализована подобным образом, но ваши шансы довольно высоки. Компилятор также часто может вообще обойти систему виртуальных вызовов, если ему известен статический тип вашего экземпляра во время компиляции.
Комментарии:
1. «компилятор вставляет переход к сегменту кода». Где хранится эта информация о переходе?
2. @shaveenk непосредственно на вызывающем сайте, то есть в другой части сегмента кода. Для статического вызова он жестко кодируется, поэтому вы просто получаете простой goto 0xSomewhere; в сборке (после некоторой настройки параметров)
Ответ №3:
Функции-члены точно так же, как обычные функции, они хранятся в разделе «код» или «текст». Есть одна особенность в (нестатических) функциях-членах, и это «скрытый» this
аргумент, который передается вместе с функцией. Итак, в вашем случае адрес в foo
будет передан func
.
Как именно передается этот аргумент и что происходит с регистрами и стеком, определяется ABI (двоичный интерфейс приложения) и варьируется от процессора к процессору. Для этого нет строгого определения, если только вы не скажете нам, какая комбинация компилятора, ОС и процессора используется (и если предположить, что информация тогда общедоступна — не все производители компиляторов / ОС скажут это очень четко). В качестве примера, x86-64 будет использовать RCX
for this
в Windows и RDI
в Linux, а команда вызова автоматически отправит адрес возврата в стек. На процессоре ARM [в Linux, но я думаю, что то же самое относится и к Windows, я просто никогда не смотрел на это], R0 используется для this
указателя, а инструкция BX используется для вызова, который как часть себя сохраняется lr
вместе с pc
инструкцией для возврата. lr
затем должен быть сохранен [вероятно, в стеке] в func
, поскольку он вызывает printf
.