новый int [размер] vs std :: vector - PullRequest
13 голосов
/ 16 марта 2012

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

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

Ответы [ 7 ]

32 голосов
/ 16 марта 2012

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

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

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

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_;
};

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

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

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

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

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_;
};

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

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

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

Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
  • Q: Что произойдет, если инициализация 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_);
}

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

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

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

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

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

Собираем все вместе

Краткое описание того, что нельзя бросить или не разрешено бросать:

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

И вот, наконец, наша тщательно разработанная, безопасная, исправленная версия 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_;
};

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

14 голосов
/ 16 марта 2012

В любой ситуации предпочтительным является 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 голосов
/ 16 марта 2012

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


Среди других отличий

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

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

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

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

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


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

  • Когда вам нужна «сырая» / «реальная» память и полный контроль над ней , operator new - ваш лучший выбор.

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

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

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

3 голосов
/ 16 марта 2012

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

1 голос
/ 09 января 2017

Кто-нибудь может дать мне совет, какой из них лучше?

Вектор лучше, когда

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

...

  1. Почти всегда.

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

1 голос
/ 16 марта 2012

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

1 голос
/ 16 марта 2012

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...