raw, slab_ptr, unique_ptr, shared_ptr і т. д. ... Як їх правильно вибрати?


33

У C ++ є багато покажчиків, але якщо чесно, через 5 років або близько того в програмуванні на C ++ (конкретно з Qt Framework), я використовую лише старий необроблений покажчик:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

Я знаю, що існує багато інших "розумних" покажчиків:

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

Але я не маю ні найменшого уявлення про те, що з ними робити і що вони можуть запропонувати мені порівняно із сирими вказівниками.

Наприклад, у мене є такий заголовок класу:

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

Це, очевидно, не вичерпно, але для кожного з цих 3 покажчиків це нормально, щоб залишити їх "сирими" чи я повинен використовувати щось більш відповідне?

А у другий раз, якщо роботодавець прочитає код, чи буде він суворий щодо того, якими вказівниками я користуюся чи ні?


Ця тема видається настільки підходящою для ТА. це було у 2008 році . І ось Який тип покажчика я використовую, коли? . Я впевнений, що ви можете знайти ще кращі матчі. Це були тільки перші я побачив
sehe

imo це є кордоном, оскільки йдеться стільки про концептуальне значення / наміри цих класів, скільки про технічні деталі їх поведінки та реалізації. Оскільки прийнята відповідь схиляється до колишньої, я задоволений тим, що це буде "версія PSE" цього питання "ТАК".
Ixrec

Відповіді:


70

"Сирий" вказівник не управляється. Тобто наступний рядок:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

... буде протікати пам'ять, якщо супровід deleteне буде виконаний у відповідний час.

auto_ptr

З метою мінімізації цих випадків std::auto_ptr<>було введено. Зважаючи на обмеження рівня C ++ до стандарту 2011 року, проте все ще дуже легко auto_ptrпросочити пам'ять. Однак достатньо для обмежених випадків, таких як цей:

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

Один з його найслабших випадків використання - у контейнерах. Це тому, що якщо auto_ptr<>зроблена копія і стара копія не ретельно скидається, контейнер може видалити покажчик і втратити дані.

unique_ptr

В якості заміни C ++ 11 представив std::unique_ptr<>:

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

Таке unique_ptr<>заповіт буде правильно очищено, навіть якщо воно передане між функціями. Це робиться шляхом семантичного представлення "власності" на вказівник - "власник" очищає його. Це робить його ідеальним для використання в контейнерах:

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

На відміну від цього auto_ptr<>, unique_ptr<>тут добре поводиться, і коли vectorрозміри не змінюються, жоден із об’єктів не буде випадково видалений під час vectorкопіювання його зберігання.

shared_ptr і weak_ptr

unique_ptr<>Це корисно, напевно, але є випадки, коли ви хочете, щоб дві частини вашої кодової бази могли посилатися на один і той же об'єкт і копіювати вказівник навколо, при цьому все ще гарантується належне очищення. Наприклад, дерево може виглядати приблизно так, коли використовується std::shared_ptr<>:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

У цьому випадку ми можемо навіть утримувати декілька копій кореневого вузла, і дерево буде належним чином очищено, коли всі копії кореневого вузла будуть знищені.

Це працює, тому що кожен shared_ptr<>тримає на себе не лише вказівник на об’єкт, але і кількість посилань усіх shared_ptr<>об'єктів, які посилаються на один і той же покажчик. Коли буде створено нове, кількість рахунків збільшується. Коли хтось знищений, кількість знижується. Коли кількість досягне нуля, вказівник deleted.

Отже, це вводить проблему: подвійно пов'язані структури закінчуються циркулярними посиланнями. Скажімо, ми хочемо додати parentпокажчик до нашого дерева Node:

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Тепер, якщо ми видалимо а Node, на нього є циклічне посилання. Це ніколи не буде deleted, тому що його посилання ніколи не буде дорівнює нулю.

Щоб вирішити цю проблему, ви використовуєте std::weak_ptr<>:

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

Тепер все буде працювати правильно, і видалення вузла не залишить застряглих посилань на батьківський вузол. Однак це робить ходьбу по дереву дещо складнішою:

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

Таким чином, ви можете заблокувати посилання на вузол, і ви маєте обґрунтовану гарантію, що він не зникне під час роботи над ним, оскільки ви тримаєтесь за shared_ptr<>нього.

make_shared і make_unique

Зараз є деякі незначні проблеми, shared_ptr<>і їх unique_ptr<>слід вирішити. Наступні два рядки мають проблему:

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

Якщо thrower()викидає виняток, обидва рядки просочать пам'ять. Більше того, shared_ptr<>відлік відліку є далеко від об'єкта, на який він вказує, і це може означати повторне виділення). Це зазвичай не бажано.

C ++ 11 забезпечує, std::make_shared<>()а C ++ 14 забезпечує std::make_unique<>()вирішення цієї проблеми:

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

Зараз в обох випадках, навіть якщо thrower()викине виняток, не буде витоку пам'яті. Як бонус, make_shared<>()має можливість створити свою кількість відліку в тому ж просторі пам'яті, що і керований об’єкт, який може бути швидшим і може заощадити кілька байт пам'яті, надаючи при цьому гарантію безпеки винятку!

Примітки про Qt

Слід зазначити, однак, що Qt, який повинен підтримувати компілятори pre-C ++ 11, має власну модель збору сміття: у багатьох QObjectє механізм, де вони будуть належним чином знищені, не потребуючи в deleteних користувача.

Я не знаю, як QObjectбуде поводитися s, коли керуються C ++ 11 керованими покажчиками, тому не можу сказати, що shared_ptr<QDialog>це гарна ідея. У мене недостатньо досвіду роботи з Qt, щоб сказати напевно, але я вважаю, що Qt5 було скориговано для цього випадку використання.


1
@ Zilators: Будь ласка, зверніть увагу на мій доданий коментар щодо Qt Відповідь на ваше запитання про те, чи слід керувати всіма трьома вказівниками, залежить від того, чи добре поводяться об'єкти Qt.
greyfade

2
"обидва роблять окреме виділення для утримання покажчика"? Ні, unique_ptr ніколи не виділяє нічого зайвого, лише shared_ptr повинен виділяти опорний підрахунок + алокатор-об'єкт. "обидва рядки просочать пам'ять"? ні, тільки може, навіть не гарантія поганої поведінки.
Дедуплікатор

1
@Deduplicator: Моє формулювання повинно бути незрозумілим: shared_ptrце окремий об'єкт - окремий розподіл - від newоб'єкта ed. Вони існують у різних місцях. make_sharedмає можливість розміщувати їх у одному місці, що покращує кеш-пам'ять, серед іншого.
greyfade

2
@greyfade: Nononono. shared_ptrє об’єктом. А для управління об'єктом він повинен виділити (облік посилань (слабкий + сильний) + руйнівник) -об'єкт. make_sharedдозволяє виділити це та керований об'єкт як один елемент. unique_ptrне використовує ці, тому немає відповідної переваги, окрім того, щоб переконатися, що об'єктом завжди належить смарт-покажчик. В бік може бути те, shared_ptrщо є власником основного об'єкта і являє собою nullptr, або яке не є власником і являє собою ненульовий вказівник.
Дедуплікатор

1
Я подивився на це, і, здається, виникає загальна плутанина щодо того, що shared_ptrробить: 1. Він поділяє право власності на якийсь об'єкт (представлений внутрішнім динамічно виділеним об'єктом, що має слабкий і сильний відліку, а також делетером) . 2. Він містить вказівник. Ці дві частини незалежні. make_uniqueі make_sharedобидва переконайтеся, що виділений об’єкт надійно розміщений у смарт-покажчику. Крім того, make_sharedдозволяє розподілити об'єкт власності та керований покажчик разом.
Дедуплікатор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.