Подтвердить что ты не робот

New int [размер] vs std::vector

Чтобы выделить динамическую память, я все время использовал векторы в С++. Но в последнее время, читая некоторый исходный код, я нашел использование "new int [size]" и в некоторых исследованиях обнаружил, что он также выделяет динамическую память.

Может ли кто-нибудь дать мне совет относительно того, что лучше? Я смотрю с алгоритмической точки зрения и ICPC?

4b9b3361

Ответ 1

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

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

Руководство:

int *i = 0, *y = 0;
try {
    i = new int [64];
    y = new int [64];
} catch (...) {
    delete [] y;
    delete [] i;
}

Если мы хотим, чтобы наши переменные имели только ту область, в которой они действительно нужны, она становится вонючей:

int *i = 0, *y = 0;
try {
    i = new int [64];
    y = new int [64];
    // code that uses i and y
    int *p;
    try { 
        p = new int [64];
        // code that uses p, i, y
    } catch(...) {}
    delete [] p;
} catch (...) {}
delete [] y;
delete [] i;

Или просто:

std::vector<int> i(64), y(64);
{
    std::vector<int> p(64);
}

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


Хорошо здесь.

Класс для копирования - ручное управление ресурсами и контейнеры

У нас есть этот невинно выглядящий класс. Как оказалось, это довольно зло. Мне напомнили об американской Макги Алисе:

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
    Bar  *b_;
    Frob *f_;
};

Утечки. Большинство начинающих программистов на C++ распознают, что там отсутствует удаление. Добавьте их:

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete f_; delete b_; }
private:
    Bar  *b_;
    Frob *f_;
};

Undefined. Промежуточные программисты на C++ признают, что используется неправильный оператор удаления. Исправьте это:

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete [] f_; delete [] b_; }
private:
    Bar  *b_;
    Frob *f_;
};

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

Немножко более опытные программисты на C++ признают, что правило 3 не соблюдалось, в котором говорится, что если вы явно написали какой-либо деструктор, назначение копии или конструктор копирования, вам, вероятно, также придется выписывать остальные или делать их частными без реализации:

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    ~Foo() { delete [] f_; delete [] b_; }

    Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64])
    {
        *this = f;
    }
    Foo& operator= (Foo const& rhs) {
        std::copy (rhs.b_, rhs.b_+64, b_);
        std::copy (rhs.f_, rhs.f_+64, f_);
        return *this;
    }
private:
    Bar  *b_;
    Frob *f_;
};

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

Страна чудес написания кода исключения исключений.

Строительство

Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
  • В: Что произойдет, если не удалось выполнить инициализацию f_?
  • A: Все Frobs, которые были построены, уничтожены. Представьте себе, что 20 Frob были построены, а 21-й провалится. Затем, в порядке LIFO, первые 20 Frob будут правильно уничтожены.

Что это. Значит: теперь у вас 64 зомби Bars. Сам объект Foos никогда не оживает, поэтому его деструктор не будет называться.

Как сделать это исключение безопасным?

Конструктор должен всегда полностью или полностью завершаться. Он не должен быть полуживым или полумертвым. Решение:

Foo() : b_(0), f_(0)
{
    try {    
        b_ = new Bar[64];
        f_ = new Foo[64];
    } catch (std::exception &e) {
        delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
        delete [] b_;
        throw; // don't forget to abort this object, do not let it come to life
    }
}

Копирование

Помните наши определения для копирования:

Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64]) {
    *this = f;
}
Foo& operator= (Foo const& rhs) {
    std::copy (rhs.b_, rhs.b_+64, b_);
    std::copy (rhs.f_, rhs.f_+64, f_);
    return *this;
}
  • В: Что произойдет, если какая-либо копия не удалась? Возможно, Bar придется копировать тяжелые ресурсы под капот. Он может потерпеть неудачу, он будет.
  • A: В момент исключения все объекты, скопированные до сих пор, останутся такими.

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

Решение состоит в использовании икону копирования и свопинга (http://gotw.ca/gotw/059.htm).

Сначала мы уточним наш конструктор копирования:

Foo (Foo const &f) : f_(0), b_(0) { 
    try {    
        b_ = new Bar[64];
        f_ = new Foo[64];

        std::copy (rhs.b_, rhs.b_+64, b_); // if this throws, all commited copies will be thrown away
        std::copy (rhs.f_, rhs.f_+64, f_);
    } catch (std::exception &e) {
        delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
        delete [] b_;
        throw; // don't forget to abort this object, do not let it come to life
    }
}

Затем мы определяем неперебрасываемую функцию свопинга

class Foo {
public:
    friend void swap (Foo &, Foo &);
};

void swap (Foo &lhs, Foo &rhs) {
    std::swap (lhs.f_, rhs.f_);
    std::swap (lhs.b_, rhs.b_);
}

Теперь мы можем использовать наш новый безопасный экземпляр-конструктор исключаемых исключений и безопасную функцию swap для записи безопасного для исключения экземпляра оператора-копии:

Foo& operator= (Foo const &rhs) {
    Foo tmp (rhs);     // if this throws, everything is released and exception is propagated
    swap (tmp, *this); // cannot throw
    return *this;      // cannot throw
} // Foo::~Foo() is executed

Что случилось? Сначала мы создаем новое хранилище и копируем его в него. Это может бросить, но если это произойдет, наше состояние не будет изменено, и объект останется в силе.

Затем мы обмениваем наши кишки с временными кишками. Временное получает то, что больше не нужно, и выпускает этот материал в конце области действия. Мы эффективно использовали tmp в качестве мусорной корзины и правильно выбираем RAII в качестве службы сбора мусора.

Вы можете посмотреть http://gotw.ca/gotw/059.htm или прочитать Exceptional C++ для получения дополнительной информации об этом методе и написании кода исключения исключений.

Объединение вместе

Резюме того, что нельзя бросить или не разрешено бросать:

  • копировать примитивные типы никогда не бросает
  • деструкторам не разрешено бросать (поскольку в противном случае исключающий код безопасности вообще невозможен)
  • swap функции не должны бросать ** (а программисты на С++, а также вся стандартная библиотека ожидают, что они не будут бросать)

И вот, наконец, наша тщательно разработанная, безопасная, исправленная версия Foo:

class Foo {
public:
    Foo() : b_(0), f_(0)
    {
        try {    
            b_ = new Bar[64];
            f_ = new Foo[64];
        } catch (std::exception &e) {
            delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
            delete [] b_;
            throw; // don't forget to abort this object, do not let it come to life
        }
    }

    Foo (Foo const &f) : f_(0), b_(0)
    { 
        try {    
            b_ = new Bar[64];
            f_ = new Foo[64];

            std::copy (rhs.b_, rhs.b_+64, b_);
            std::copy (rhs.f_, rhs.f_+64, f_);
        } catch (std::exception &e) {
            delete [] f_;
            delete [] b_;
            throw;
        }
    }

    ~Foo()
    {
        delete [] f_;
        delete [] b_;
    }

    Foo& operator= (Foo const &rhs)
    {
        Foo tmp (rhs);     // if this throws, everything is released and exception is propagated
        swap (tmp, *this); // cannot throw
        return *this;      // cannot throw
    }                      // Foo::~Foo() is executed

    friend void swap (Foo &, Foo &);

private:
    Bar  *b_;
    Frob *f_;
};

void swap (Foo &lhs, Foo &rhs) {
    std::swap (lhs.f_, rhs.f_);
    std::swap (lhs.b_, rhs.b_);
}

Сравните это с нашим первоначальным, невиновным кодом, который является злым для костей:

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
    Bar  *b_;
    Frob *f_;
};

Лучше не добавлять к нему больше переменных. Рано или поздно вы забудете добавить правильный код в каком-либо месте, и весь ваш класс заболеет.

Или сделать его не скопированным.

class Foo {
public:
    Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
    Foo (Foo const &) = delete;
    Foo& operator= (Foo const &) = delete;
private:
    Bar  *b_;
    Frob *f_;
};

Для некоторых классов это имеет смысл (потоки, для экземпляра, для обмена потоками, явное с std:: shared_ptr), но для многих это не так.

Реальное решение.

class Foo {
public:
    Foo() : b_(64), f_(64) {}
private:
    std::vector<Bar>  b_;
    std::vector<Frob> f_;
};

Этот класс имеет чистую семантику копирования, безопасен для исключения (помните: безопасное исключение не означает, что он не бросает, а скорее не течет и, возможно, имеет семантику транзакции) и не течет.

Ответ 2

В любой ситуации предпочтительнее std::vector. У этого есть деструктор, чтобы освободить память, тогда как управляемая вручную память должна быть явно удалена, как только вы закончите с ней. Очень легко вводить утечки памяти, например, если что-то генерирует исключение, прежде чем оно будет удалено. Например:

void leaky() {
    int * stuff = new int[10000000];
    do_something_with(stuff);
    delete [] stuff; // ONLY happens if the function returns
}

void noleak() {
    std::vector<int> stuff(10000000);
    do_something_with(stuff);
} // Destructor called whether the function returns or throws

Это также более удобно, если вам нужно изменить размер или скопировать массив.

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

В редких случаях, когда эти проблемы могут быть важны, вы должны рассмотреть std::unique_ptr<int[]>; он имеет деструктор, который предотвратит утечку памяти и не будет превышать затраты времени выполнения по сравнению с необработанным массивом.

Ответ 3

Я не думаю, что когда-либо был случай, когда new int[size] предпочтительнее. Иногда вы увидите это в стандартном коде, но даже тогда я не думаю, что это было когда-нибудь хорошее решение; в предстандартные дни, если у вас не было эквивалента std::vector в вашем наборе инструментов, вы написали один. Единственные причины, по которым вы можете использовать new int[size], - это реализация стандартного векторного класса. (Мое собственное выделенное выделение и инициализация, как и контейнеры в стандартной библиотеке, но это может быть излишним для очень простого векторного класса.)

Ответ 4

Несмотря на то, что оба метода выделяют динамическую память, один объект предназначен для обработки данных произвольной длины (std::vector<T>), а другой - только указатель на последовательную линию слотов памяти размером N (int в этом случае).


Среди других различий

  • A std::vector<T> автоматически изменит размер выделенной памяти для ваших данных, если вы попытаетесь добавить новое значение и на нем закончится свободное пространство. A int * не будет.

  • A std::vector<T> освободит выделенную память, когда вектор выходит из области видимости, a int * не будет.

  • A int * будет иметь незначительные накладные расходы (по сравнению с вектором), хотя std::vector<T> не совсем новая вещь и, как правило, очень оптимизирована. Ваша шея бутылки, вероятно, будет в другом месте.

    Но a std::vector<int> всегда будет потреблять больше памяти, чем int *, а некоторые операции всегда будут занимать немного больше времени.

    Итак, если есть ограничения памяти/ЦП, и вы хотите сбрить каждый отдельный цикл, который вы, возможно, можете.. используйте int *.


Бывают ситуации, когда один из них предпочтительнее другого!

  • Если вам нужна "сырая" / "настоящая" память и полный контроль, operator new - ваш лучший выбор.

    Например, если вы используете placement new.

    по raw/real memory Я имею в виду то, что не управляется через контейнер-контейнер, например std::vector<T>.

  • Когда вы ищете контейнер для обработки произвольных и не хотите изобретать колесо в отношении управления памятью; std::vector<T> (или любой другой соответствующий контейнер STL)

Ответ 5

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

Ответ 6

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

Ответ 7

Может ли кто-нибудь дать мне совет относительно того, что лучше?

Вектор лучше, если

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

...

  1. Довольно много.

Ручное управление памятью может легко и часто приводить к утечке памяти, а еще хуже - к undefined.