В C ++, где в памяти помещаются функции класса?

17

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

Правильно ли это?

Итак, если я создаю 100 объектов на C ++, я могу оценить , что мне понадобится пространство для всех переменных-членов, которым принадлежит объект, умноженное на 100 (возможные проблемы выравнивания здесь), а затем мне нужно пространство в сегменте кода для одной копии кода для каждой функции-члена для этого типа объекта (не 100 копий кода).

Влияют ли виртуальные функции, полиморфизм, фактор наследования?

Как насчет объектов из динамически связанных библиотек? Я полагаю, что dlls получают свои собственные сегменты стека, кучи, кода и данных.

Простой пример (не может быть синтаксически правильным):

// parent class
class Bar
{
public:
    Bar()  {};
    ~Bar() {};

    // pure virtual function
    virtual void doSomething() = 0;

protected:
    // a protected variable
    int mProtectedVar;
}

// our object class that we'll create multiple instances of
class Foo : public Bar
{
public:
    Foo()  {};
    ~Foo() {};

    // implement pure virtual function
    void doSomething()          { mPrivate = 0; }

    // a couple public functions
    int getPrivateVar()         { return mPrivate; }
    void setPrivateVar(int v)   { mPrivate = v; }

    // a couple public variables
    int mPublicVar;
    char mPublicVar2;

private:
    // a couple private variables
    int mPrivate;
    char mPrivateVar2;        
}

О том, сколько памяти должно содержать 100 динамически выделенных объектов типа Foo, включая комнату для кода и всех переменных?

    
задан petrocket 15.03.2009 в 23:10
источник

9 ответов

27

Не обязательно верно, что «каждому объекту - при создании - будет предоставлено пространство в HEAP для переменных-членов». Каждый объект, который вы создаете, займет некоторое ненулевое пространство где-то для его переменных-членов, но где зависит от того, как вы сами выделяете объект. Если объект имеет автоматическое (стековое) распределение, то также будут его элементы данных. Если объект выделен в свободном хранилище (куче), то тоже будут его членами данных. В конце концов, каково распределение объекта, отличного от объекта его данных?

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

Для объектов с виртуальными функциями каждый будет иметь указатель vtable, выделенный, как если бы он был явно объявленным элементом данных внутри класса.

Что касается функций-членов, код для них, скорее всего, не отличается от кода свободной функции в терминах того, куда он идет в исполняемом изображении. В конце концов, функция-член в основном является свободной функцией с неявным «этим» указателем в качестве первого аргумента.

Наследование ничего не меняет.

Я не уверен, что вы имеете в виду, когда DLL получает свой собственный стек. DLL не является программой и не нуждается в стеке (или куче), поскольку объекты, которые он выделяет, всегда выделяются в контексте программы, которая имеет свой собственный стек и кучу. То, что в DLL есть код (текст) и сегменты данных, имеет смысл, хотя я не разбираюсь в реализации таких вещей в Windows (которые, я полагаю, вы используете, учитывая вашу терминологию).

    
ответ дан John Zwinck 15.03.2009 в 23:17
  • Ваше предположение «Если у объекта есть автоматическое (стековое) распределение, то и его члены данных« неверны ». Возьмите std :: vector, объявленный как переменная стека. Если вы вставляете новые значения в вектор, они будут выделять их в кучу - не в стеке –  newgre 16.03.2009 в 00:02
  • Вот почему я также сказал в своем ответе «Если объект, выделенный стекю, содержит указатель или другой тип, который затем используется для выделения в куче, это распределение будет происходить в куче независимо от того, где был создан сам объект «. std :: vector обычно реализуется как содержащий три указателя. –  John Zwinck 16.03.2009 в 04:30
  • вы правы, я неправильно понял ваше сообщение –  newgre 16.03.2009 в 22:11
  • СПАСИБО за отличный ответ. Ваше понимание очень полезно. Этот сайт лучший! Единственное, что я хотел бы добавить к вашему ответу, это фактические размеры (математика), которые включают в себя некоторые другие ответы. –  petrocket 18.03.2009 в 19:55
6

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

Тогда он становится несколько сложным. Некоторые из проблем ...

  • Компилятор может, если он хочет или инструктирован, встроенный код. Таким образом, хотя это может быть простая функция, если она используется во многих местах и ​​выбрана для встраивания, может быть сгенерировано много кода (распространяется по всему программному коду).
  • Виртуальное наследование увеличивает размер полиморфизма каждого члена. VTABLE (виртуальная таблица) скрывается вместе с каждым экземпляром класса с использованием виртуального метода, содержащего информацию для диспетчеризации времени выполнения. Эта таблица может расти довольно большой, если у вас много виртуальных функций или множественное (виртуальное) наследование. Уточнение: VTABLE относится к классу, но указатели на VTABLE сохраняются в каждом экземпляре (в зависимости от структуры типа предка объекта).
  • Шаблоны могут вызвать раздувание кода. Каждое использование шаблонного класса с новым набором параметров шаблона может генерировать совершенно новый код для каждого участника. Современные компиляторы стараются и сворачивают это как можно больше, но это сложно.
  • Выравнивание / добавление структуры может привести к тому, что простые экземпляры классов будут больше, чем вы ожидаете, поскольку компилятор создает структуру целевой архитектуры.

При программировании используйте оператор sizeof для определения размера объекта - никогда не жесткого кода. Используйте приблизительную метрику «Сумма размера переменной члена + какая-то VTABLE (если она существует)» при оценке того, насколько дороги будут большие группы экземпляров, и не беспокойтесь о размере кода. Оптимизируйте позже, и если какой-либо из неочевидных проблем вернется к чему-то, я буду очень удивлен.

    
ответ дан Adam Wright 15.03.2009 в 23:21
  • Адам: Вы должны упомянуть, что накладные расходы по размеру VTable берутся по принципу каждого типа, а не по каждому объекту, хотя класс должен указывать на него (да, да, поэтому ему нужно сохранить указатель для каждый класс, но это он). –  Arafangion 15.03.2009 в 23:36
  • Верно, я уточню это. Но множественное наследование и виртуальное наследование могут содержать более одного указателя VTABLE для каждого экземпляра. –  Adam Wright 15.03.2009 в 23:41
  • Спасибо за добавление деталей к ответам на этот вопрос! Очень полезный ответ и красиво организованный. –  petrocket 18.03.2009 в 19:52
2

Хотя некоторые аспекты этого являются зависимыми от поставщика компилятора. Весь скомпилированный код переходит в раздел памяти в большинстве систем под названием «текст». это отдельно от секций кучи и стека (четвертый раздел «данные» содержит большинство констант). Создание нескольких экземпляров класса приводит к пробелу во время выполнения только для его переменных экземпляра, а не для любых его функций. Если вы используете виртуальные методы, вы получите дополнительный, но небольшой бит памяти, зарезервированный для виртуальной таблицы поиска (или эквивалентной для компиляторов, которые используют какую-то другую концепцию), но ее размер определяется количеством виртуальные методы умножают количество виртуальных классов и не зависят от количества экземпляров во время выполнения

Это относится к статически и динамически связанному коду. Фактический код все живет в «текстовом» регионе. Большинство операционных систем на самом деле могут совместно использовать DLL-код для нескольких приложений, поэтому, если несколько приложений используют одну и ту же DLL, только одна копия находится в памяти, и оба приложения могут ее использовать. Очевидно, что нет дополнительной экономии от общей памяти, если только одно приложение использует связанный код.

    
ответ дан SingleNegationElimination 15.03.2009 в 23:20
  • Обратите внимание, что «виртуальное наследование» имеет конкретное значение, которое отличается от того, что вы, вероятно, имели в виду («виртуальные методы»). –  John Zwinck 16.03.2009 в 13:33
  • Хм ... ты прав ... отредактирован! –  SingleNegationElimination 16.03.2009 в 14:13
1

Вы не можете полностью точно сказать, сколько памяти будет занимать класс или объекты X в ОЗУ.

Однако, чтобы ответить на ваши вопросы, вы правы, что код существует только в одном месте, он никогда не «распределяется». Таким образом, код является per-class и существует независимо от того, создаете ли вы объекты или нет. Размер кода определяется вашим компилятором, и даже тогда компиляторам часто может потребоваться оптимизировать размер кода, что приводит к различным результатам.

Виртуальные функции ничем не отличаются, сохраняя (небольшие) дополнительные накладные расходы таблицы виртуальных методов, которая обычно находится в классе.

Что касается библиотек DLL и других библиотек ... правила не различаются в зависимости от того, откуда пришел код, поэтому это не является фактором использования памяти.

    
ответ дан MattJ 15.03.2009 в 23:21
1

, если скомпилировано как 32 бит. то sizeof (Bar) должен давать 4. Foo должен добавить 10 байт (2 ints + 2 символа).

Так как Foo наследуется от Bar. Это не менее 4 + 10 байт = 14 байт.

GCC имеет атрибуты для упаковки структур, поэтому нет отступов. В этом случае 100 записей занимают 1400 байт + небольшую накладную плату для выравнивания распределения + некоторые накладные расходы для управления памятью.

Если атрибут упакованного не указан, он зависит от выравнивания компиляторов.

Но это не учитывает, сколько памяти vtable занимает и размер скомпилированного кода.

    
ответ дан neoneye 15.03.2009 в 23:23
  • Спасибо за выполнение математики! Очень полезная перспектива gcc. –  petrocket 18.03.2009 в 19:52
1

Информация, приведенная выше, очень помогает и дает мне некоторое представление о структуре памяти C ++. Но я хотел бы добавить, что независимо от количества виртуальных функций в классе всегда будет только 1 VPTR и 1 VTABLE для каждого класса. В конце концов VPTR указывает на VTABLE, поэтому нет необходимости в более чем одном VPTR в случае нескольких виртуальных функций.

    
ответ дан Ayush 03.11.2009 в 08:53
0

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

Элементы-члены (и виртуальные функции) из любых базовых классов также являются частью класса, поэтому включите их.

Как и в c, вы можете использовать оператор sizeof (classname / datatype) для получения размера в байтах класса.

    
ответ дан Dan O 15.03.2009 в 23:22
0

Да, это правильно, код не дублируется при создании экземпляра объекта. Что касается виртуальных функций, то правильный вызов функции определяется с помощью vtable , но это не влияет на создание объекта за се.

DLL (общие / динамические библиотеки в целом) отображаются в пространство памяти процесса. Каждая модификация выполняется как Copy-On-Write (COW): одна DLL загружается только один раз в память, и для каждой записи в изменяемое пространство создается копия этого пространства (обычно размер страницы).

    
ответ дан Eduard - Gabriel Munteanu 15.03.2009 в 23:23
0

Очень сложно дать точный ответ на ваш вопрос, поскольку это зависит от реализации, но приблизительные значения для 32-разрядной реализации могут быть:

int Bar::mProtectedVar;    // 4 bytes
int Foo::mPublicVar;        // 4 bytes
char Foo::mPublicVar2;     // 1 byte

Здесь есть все проблемы, и итоговая сумма может составлять 12 байт. У вас также будет vptr - сказать anoter 4 байта. Таким образом, общий размер данных составляет около 16 байт на экземпляр. Невозможно сказать, сколько места займет код, но вы правы, думая, что есть только одна копия кода, разделяемого между всеми экземплярами.

Когда вы спрашиваете

  

Я предполагаю, что dlls получают свой собственный стек,   кучи, кода и данных.

В ответ на то, что между данными в DLL и данными в приложении действительно нет большой разницы - в основном они разделяют все между ними. Это должно быть так, когда вы думаете об этом - если у них разные стеки ( например), как могут работать вызовы функций?

    
ответ дан anon 15.03.2009 в 23:28
  • Благодарим вас за то, что вы указали объемы памяти (на обычном ПК) и разъяснили проблему, связанную с DLL. –  petrocket 18.03.2009 в 19:54