Объект уже инициализирован при объявлении?

Я пытаюсь понять что-то на С++. В основном у меня есть это:

class SomeClass {
    public:
        SomeClass();
    private:
        int x;
};

SomeClass::SomeClass(){
    x = 10;
}

int main() {
    SomeClass sc;
    return 0;
}

Я думал, что sc является неинициализированной переменной типа SomeClass, но из различных обучающих программ, которые я нашел, похоже, что это объявление на самом деле является инициализацией, вызывающей конструктор SomeClass(), без необходимости называть "sc = new SomeClass ( );" или что-то в этом роде.

Как я пришел из мира С# (и знаю немного C, но не С++), я пытаюсь понять, когда мне нужны такие вещи, как новые, и когда выпустить такие объекты. Я нашел шаблон под названием RAll, который, как представляется, не имеет отношения.

Как называется этот тип инициализации и как я могу узнать, является ли что-то простым объявлением или полной инициализацией?

Ответ 1

Я думаю, здесь есть несколько вещей:

  • Разница между автоматической переменной и динамически распределенной переменной
  • Срок службы объектов
  • RAII
  • С# parallel

Автоматический vs Динамический

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

int main(int argc, char* argv[])  // 1
{                                 // 2
  SomeClass sc;                   // 3
  sc.foo();                       // 4
  return 0;                       // 5
}                                 // 6

Здесь sc - автоматическая переменная. Он гарантированно будет полностью инициализирован (т.е. Гарантированно будет выполнен конструктор) после успешного выполнения строки (3). Его деструктор будет автоматически вызываться в строке (6).

Мы обычно говорим о сфере действия переменной: от точки объявления до соответствующей закрывающей скобки; и язык гарантирует уничтожение, когда область действия будет удалена, будь то return или исключение.

Конечно, нет гарантии в случае, если вы вызываете страшное "поведение Undefined", которое обычно приводит к сбою.

С другой стороны, С++ также имеет динамические переменные, то есть переменные, которые вы выделяете с помощью new.

int main(int argc, char* argv[])  // 1
{                                 // 2
  SomeClass* sc = 0;              // 3
  sc = new SomeClass();           // 4
  sc->foo();                      // 5
  return 0;                       // 6
}                                 // 7 (!! leak)

Здесь sc по-прежнему является автоматической переменной, однако ее тип отличается: теперь он является указателем на переменную типа SomeClass.

В строке (3) sc присваивается значение нулевого указателя (nullptr в С++ 0x), поскольку оно не указывает на какой-либо экземпляр SomeClass. Обратите внимание, что язык не гарантирует инициализацию самостоятельно, поэтому вам нужно явно назначить что-то иначе, у вас будет значение мусора.

В строке (4) мы создаем динамическую переменную (используя оператор new) и присваиваем свой адрес sc. Обратите внимание, что сама динамическая переменная не называется, система дает нам только указатель (адрес).

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

Время жизни объектов

У Херба Саттера есть очень интересные статьи по этому вопросу. Вот первый.

Как резюме:

  • Объект живет, как только его конструктор заканчивается. Это означает, что если конструктор бросает, объект никогда не жил (считайте это случайностью беременности).
  • Объект мертв, как только его деструктор вызывается, если деструктор выбрасывает (это EVIL), его невозможно повторить, потому что вы не можете вызвать какой-либо метод для мертвого объекта, это поведение undefined.

Если мы вернемся к первому примеру:

int main(int argc, char* argv[])  // 1
{                                 // 2
  SomeClass sc;                   // 3
  sc.foo();                       // 4
  return 0;                       // 5
}                                 // 6

sc жив от строки (4) до строки (5) включительно. В строке (3) она строится (что может закончиться по ряду причин), а в строке (6) она разрушается.

RAII

RAII означает, что "Приобретение ресурсов" является инициализацией. Это идиома для управления ресурсами, и в частности, чтобы убедиться, что ресурсы будут в конечном итоге выпущены после их приобретения.

В С++, поскольку у нас нет коллекции мусора, эта идиома в основном применяется к управлению памятью, но она также полезна для любых других ресурсов: блокировки в многопоточных средах, блокировки файлов, сокеты/соединения в сети и т.д...

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

В своей простейшей форме он связан с одной автоматической переменной:

int main(int argc, char* argv[])
{
  std::unique_ptr<SomeClass> sc = new SomeClass();
  sc->foo();
  return 0;
}

Это очень похоже на первый пример, за исключением того, что я динамически выделяю экземпляр SomeClass. Адрес этого экземпляра затем передается объекту sc типа std::unique_ptr<SomeClass> (это объект С++ 0x, используйте boost::scoped_ptr, если он недоступен). unique_ptr гарантирует, что указанный объект будет уничтожен при уничтожении sc.

В более сложной форме он может быть связан с несколькими автоматическими переменными, использующими (например) std::shared_ptr, который, как следует из названия, позволяет обмениваться объектом и гарантирует, что объект будет уничтожен при уничтожении последнего участника, Помните, что это не эквивалентно использованию сборщика мусора, и могут возникнуть проблемы с циклами ссылок, я не буду углубляться здесь, поэтому просто помните, что std::shared_ptr не является панацеей.

Поскольку очень сложно отлично управлять временем жизни динамической переменной без RAII перед исключениями и многопоточным кодом, рекомендуется следующее:

  • максимально использовать автоматические переменные
  • для динамических переменных, никогда не вызывайте delete самостоятельно и всегда используйте средства RAII

Я лично считаю, что любое появление delete сильно подозрительно, и я всегда прошу его удалить в обзорах кода: это запах кода.

Параметр С#

В С# в основном используются динамические переменные *. Вот почему:

  • Если вы просто объявляете переменную без присвоения, ее значение равно null: по сути, вы только манипулируете указателями, и вы, таким образом, имеете нулевой указатель (инициализация гарантируется, благодаря доброте).
  • Вы используете new для создания значений, это вызывает конструктор вашего объекта и дает вам адрес объекта; обратите внимание, как синтаксис похож на С++ для динамических переменных

Однако, в отличие от С++, С# - сбор мусора, поэтому вам не нужно беспокоиться об управлении памятью.

Сбор мусора также означает, что срок жизни объектов более трудно понять: они создаются, когда вы просите их, но уничтожаетесь при удобстве системы. Это может быть проблемой для реализации RAII, например, если вы действительно хотите быстро освободить блокировку, а язык имеет ряд возможностей, чтобы помочь вам выйти из using ключевого слова + IDisposable интерфейса из памяти.

*: легко проверить, если после объявления переменной его значение равно null, то это будет динамическая переменная. Я считаю, что для int значение будет 0, что указывает на то, что нет, но прошло уже 3 года, так как я играл с С# для проекта курса, поэтому...

Ответ 2

То, что вы делаете в первой строке main(), - это выделить объект SomeClass в стеке. Оператор new вместо этого выделяет объекты в куче, возвращая указатель на экземпляр класса. Это в конечном итоге приводит к двум различным методам доступа через . (с экземпляром) или с помощью -> (с указателем)

Поскольку вы знаете C, вы выполняете распределение стека каждый раз, когда вы говорите, например int i;. С другой стороны, распределение кучи выполняется в C с помощью malloc(). malloc() возвращает указатель на вновь выделенное пространство, которое затем передается в указатель на что-то. Пример:

int *i;
i = (int *)malloc(sizeof(int));
*i=5;

В то время как освобождение выделенного материала в стеке выполняется автоматически, освобождение материала, выделенного в куче, должно выполняться программистом.

Источником вашей путаницы является тот факт, что С# (который я не использую, но я знаю, что он похож на Java) не имеет распределения стека. Что вы делаете, когда говорите SomeClass sc, является объявление ссылки SomeClass, которая в настоящее время неинициализирована, пока вы не скажете new, который является моментом, когда объект возникает. Перед new у вас нет объекта. В С++ это не так. В С++ нет понятия ссылок, которые похожи на С# (или java), хотя у вас есть ссылки на С++ только во время вызовов функций (это на практике парадигма pass-by-reference). По умолчанию С++ проходит по значению, что означает, что вы копировать объекты при вызове функции). Однако это не вся история. Проверьте комментарии для более точных сведений.

Ответ 3

В вашем случае sc выделяется в стеке, используя конструктор по умолчанию для SomeClass. Поскольку он находится в стеке, экземпляр будет разрушен при возврате из функции. (Это было бы более впечатляюще, если вы создали экземпляр SomeClass sc внутри функции, вызванной из main - память, выделенная для sc, была бы нераспределена при возврате к main.)

Вместо ключевого слова new вместо выделения памяти в стеке времени выполнения выделяется память в куче. Поскольку С++ не имеет автоматической сборки мусора, вы (программист) несете ответственность за нераспределение какой-либо памяти, которую вы выделяете в куче (используя ключевое слово delete), чтобы избежать утечек памяти.

Ответ 4

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

Когда объект создается, если у него есть объявленный пользователем конструктор, то для его инициализации используется один из его конструкторов. Similary, если у него есть объявленный пользователем деструктор, это используется, когда объект выходит из области действия для выполнения любых необходимых действий по очистке в тот момент, когда он выходит из области видимости. Это отличается от языков, на которых есть финализаторы, которые могут или не могут выполняться и, конечно, не в детерминированный момент времени. Это больше похоже на using/IDisposable.

A new выражение используется в С++ для динамического создания объекта. Он обычно используется там, где время жизни объекта не может быть привязано к определенному объему. Например, когда он должен продолжать существовать после того, как созданная функция завершится. Он также используется там, где точный тип объекта, который должен быть создан, теперь известен во время компилятора, например. в функции factory. Динамически создавать объекты часто можно избежать во многих случаях, когда они обычно используются в таких языках, как Java и С#.

Когда объект создается с помощью new, он должен в какой-то момент быть уничтожен с помощью выражения delete. Чтобы убедиться, что программисты не забывают это делать, обычно используется какой-то умный объект-указатель, чтобы управлять этим автоматически, например. a shared_ptr из tr1 или boost.

Ответ 5

Некоторые другие ответы в основном говорят вам: "sc выделен в стеке, новый выделяет объект в куче". Я предпочитаю не думать об этом таким образом, поскольку он объединяет детали реализации (стек/кучу) с семантикой кода. Поскольку вы привыкли к тому, как С# делает то, что я думаю, это также создает неоднозначности. Вместо этого способ, которым я предпочитаю думать об этом, - это способ, которым описывает стандарт С++:

sc - переменная типа SomeClass, объявленная в области блока (т.е. фигурные скобки, составляющие основную функцию). Это называется локальной переменной. Поскольку он не объявлен static или extern, это заставляет его иметь автоматическую продолжительность хранения. Это означает, что всякий раз, когда выполняется строка SomeClass sc;, переменная будет инициализирована (путем запуска ее конструктора), и когда переменная выходит из области видимости, выйдя из блока, она будет уничтожена (путем запуска ее деструктора - поскольку у вас его нет, а ваш объект - это простые старые данные, ничего не будет сделано).

Ранее я сказал: "Поскольку он не объявлен static или extern", если вы объявили его как таковой, он будет иметь статическую продолжительность хранения. Он будет инициализирован до запуска программы (технически в области блока он будет инициализирован при первом использовании) и будет уничтожен после завершения программы.

При использовании new для создания объекта вы создаете объект с динамической продолжительностью хранения. Этот объект будет инициализирован при вызове new и будет уничтожен только, если вы наберете delete на нем. Чтобы вызвать delete, вам необходимо сохранить ссылку на него и вызвать удаление, когда вы закончите использовать объект. Хорошо написанный код на С++ обычно не использует этот тип времени хранения очень часто, вместо этого вы обычно ставите объекты значений в контейнеры (например, std::vector), которые управляют временем жизни содержащихся значений. Сама переменная контейнера может находиться в статическом хранилище или в автоматическом хранилище.

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