C ++ Огляд об'єкта


113

Я програміст на С, який намагається зрозуміти C ++. Багато навчальних посібників демонструють ідентифікацію об'єкта за допомогою фрагмента, наприклад:

Dog* sparky = new Dog();

що означає, що згодом ви зробите:

delete sparky;

що має сенс. Тепер, у випадку, коли динамічне розподілення пам’яті непотрібне, чи є причина використовувати вищезазначене замість

Dog sparky;

і нехай викличе деструктор, як тільки іскри вийде за межі?

Дякую!

Відповіді:


166

Навпаки, завжди слід віддавати перевагу розподілу стеків, якщо ви, як правило, ніколи не повинні мати нові / видаляти у своєму коді користувача.

Як ви кажете, коли змінна оголошена в стеку, її деструктор автоматично викликається, коли вона виходить за рамки, що є вашим головним інструментом для відстеження терміну служби ресурсу та уникнення витоків.

Тож загалом, кожного разу, коли потрібно виділяти ресурс, будь то пам'ять (виклик нового), ручки файлів, сокети або щось інше, загортайте його в клас, де конструктор отримує ресурс, а деструктор випускає його. Тоді ви можете створити об’єкт цього типу на стеці, і ви гарантуєтеся, що ваш ресурс буде звільнений, коли він вийде за межі області. Таким чином, вам не доведеться скрізь відслідковувати ваші нові / видаляти пари, щоб уникнути витоку пам'яті.

Найпоширеніша назва цієї ідіоми - RAII

Також розгляньте класи класів розумних покажчиків, які використовуються для обгортання отриманих покажчиків на рідкісні випадки, коли вам потрібно виділити щось нове поза виділеним об’єктом RAII. Ви натомість передаєте вказівник розумному вказівнику, який потім відстежує його термін служби, наприклад, шляхом підрахунку посилань, і викликає деструктора, коли остання посилання виходить за межі області. Стандартна бібліотека має std::unique_ptrпросте управління, засноване на масштабах, і std::shared_ptrщо робить підрахунок посилань для реалізації спільної власності.

Багато навчальних посібників демонструють інстанціювання об'єкта за допомогою фрагмента, наприклад ...

Отже, ви виявили, що більшість навчальних посібників смоктають. ;) Більшість навчальних посібників навчають вас манірних практик C ++, включаючи виклик нових / видалення для створення змінних, коли це не потрібно, та важко відстежуючи термін експлуатації ваших виділень.


Сирі покажчики корисні, коли ви хочете, щоб семантика, що нагадує auto_ptr (передача права власності), але зберігала операцію свопу, що не кидає, і не хочете накладних підрахунків посилань. Корпус краю може, але корисний.
Грег Роджерс

2
Це правильна відповідь, але причина, коли я ніколи не впав би у звичку створювати об’єкти на стеці, полягає в тому, що ніколи не буває абсолютно очевидним, наскільки великим буде цей об’єкт. Ви просто запитуєте виняток переповнення стека.
dviljoen

3
Грег: Безумовно. Але, як ви кажете, крайній випадок. Загалом, вказівників найкраще уникати. Але вони в мові чомусь, не заперечуючи цього. :) dviljoen: Якщо об'єкт великий, ви загортаєте його в об'єкт RAII, який можна виділити на стек, і містить вказівник на виділені вами дані.
джельф

3
@dviljoen: Ні, я ні. Компілятори C ++ не надмірно роздувають об'єкти. Найгірше, що ви побачите, як правило, воно округляється до найближчого кратного чотирьох байтів. Зазвичай клас, що містить вказівник, займе стільки ж місця, скільки й сам вказівник, тому нічого не коштує при використанні стеку.
джельф

20

Хоча наявність у стеку речей може бути перевагою з точки зору розподілу та автоматичного звільнення, це має деякі недоліки.

  1. Можливо, ви не хочете виділяти величезні об'єкти на стеку.

  2. Динамічна відправка! Розглянемо цей код:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Це надрукує "B". Тепер давайте подивимося, що відбувається під час використання Stack:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Це надрукує "A", що може бути не інтуїтивно зрозумілим для тих, хто знайомий з Java або іншими об'єктно-орієнтованими мовами. Причина полягає в тому, що ви більше не маєте вказівника на екземпляр B. Натомість створюється екземпляр Bі копіюється в aзмінну типу A.

Деякі речі можуть трапитися ненавмисно, особливо коли ви не знайомі зі C ++. У C у вас є вказівники, і все. Ви знаєте, як їх використовувати, і вони ВЖЕ завжди те ж саме. У C ++ це не так. Тільки уявіть , що відбувається, коли ви використовуєте в цьому прикладі в якості аргументу для методу - все стає більш складним , і це робить величезну різницю , якщо aмає тип Aабо A*навіть A&(виклик за посиланням). Можливо багато комбінацій, і всі вони поводяться по-різному.


2
-1: люди, не здатні зрозуміти ціннісну семантику, - простий факт, що поліморфізм потребує посилання / покажчика (щоб уникнути нарізання об'єктів), не становить жодної проблеми з мовою. Сила c ++ не повинна вважатися недоліком лише тому, що деякі люди не можуть вивчити її основні правила.
підкреслюй_

Дякую за голову вгору У мене була аналогічна проблема з методом взяття об'єкта замість вказівника чи посилання, і який це був безлад. Об'єкт має вказівники всередині, і вони занадто рано були видалені через таке подолання.
BoBoDev

@underscore_d Я згоден; останній абзац цієї відповіді слід вилучити. Не сприймайте те, що я відредагував цю відповідь, означає, що я згоден з нею; Я просто не хотів, щоб помилки в ньому були прочитані.
TamaMcGlinn

@TamaMcGlinn погодився. Дякуємо за гарне редагування. Я прибрав думку думки.
UniversE

13

Ну, причина використання вказівника була б точно такою ж, як причина використання покажчиків на C, виділених malloc: якщо ви хочете, щоб ваш об'єкт жив довше, ніж ваша змінна!

Навіть дуже рекомендується НЕ використовувати нового оператора, якщо ви зможете цього уникнути. Особливо, якщо ви використовуєте винятки. Взагалі набагато безпечніше дозволити компілятору звільнити ваші об’єкти.


13

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

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);

Як варіант: недійсний FeedTheDog (собака та голодний собака); Собака собака; FeedTheDog (собака);
Скотт Ленгем

1
@ScottLangham правда, але це було не те, що я намагався зробити. Іноді ви викликаєте функцію, яка містить, наприклад, необов'язковий вказівник NULL, або, можливо, це вже існуючий API, який неможливо змінити.
Марк Викуп

7

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

(1). Вам не потрібно турбуватися про розмотування стека у випадку винятків

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


1
Слід враховувати розмір об'єкта. Стек обмежений за розміром, тому слід купувати дуже великі об'єкти.
Адам

1
@Adam Я думаю, що Naveen все ще вірно тут. Якщо ви можете покласти великий стек на стек, то зробіть це, тому що це краще. Є багато причин, які ви, мабуть, не можете. Але я думаю, що він правильний.
Маккей

5

Єдине, чому я хвилююся, - це те, що Собака зараз виділяється на стек, а не на купу. Отже, якщо собака має мегабайти в розмірі, у вас можуть виникнути проблеми,

Якщо вам потрібно пройти новий / видалити маршрут, будьте обережні до винятків. І через це вам слід використовувати auto_ptr або один із типів інтелектуального вказівника для підвищення рівня життя об'єкта.


1

Немає підстав для нового (на купі), коли ви можете виділити його на стек (якщо з якоїсь причини у вас є невеликий стек і ви хочете використовувати купу.

Ви можете розглянути можливість використання shared_ptr (або одного з його варіантів) зі стандартної бібліотеки, якщо ви хочете виділити їх на купі. Це допоможе зробити видалення для вас, коли всі посилання на shared_ptr вже не існують.


Є багато причин, наприклад, я бачу відповідь.
dangerousdave

Я думаю, ви можете хотіти, щоб об'єкт пережив сферу функції, але ОП, здається, не підказує, що саме вони намагаються зробити.
Скотт Ленгем

0

Є додаткова причина, яку ніхто більше не згадував, чому ви можете вирішити створити свій об’єкт динамічно. Динамічні об’єкти на основі купи дозволяють використовувати поліморфізм .


2
Ви також можете працювати з об'єктами на основі стека поліморфно, я не бачу різниці між виділеними об'єктами стека / і купою в цьому плані. Наприклад: недійсний MyFunc () {Собака собака; Корм (собака); } недійсний корм (тварини та тварини) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (їжа); } // У цьому прикладі GetFavouriteFood може бути віртуальною функцією, яку кожна тварина перекриває шляхом власної реалізації. Це буде працювати поліморфно, без участі купи.
Скотт Ленгем

-1: поліморфізм вимагає лише посилання або вказівника; це абсолютно агностично щодо тривалості зберігання базового екземпляра.
підкреслюй_

@underscore_d "Використання універсального базового класу означає вартість: Об'єкти повинні бути розподілені купою, щоб бути поліморфними;" - bjarne stroustrup stroustrup.com/bs_faq2.html#object
dangerousdave

ЛОЛ, мені байдуже, чи сказав це сам Струструп, але це неймовірно бентежить цитата для нього, бо він помиляється. Самостійно це не важко перевірити, ви знаєте: інстанціювати деякі DerivedA, DerivedB та DerivedC на стеку; інстанціюйте також виділений стеком покажчик на Base і підтвердьте, що ви можете розмістити його за допомогою A / B / C і використовувати їх поліморфно. Застосовуйте критичну думку та стандартне посилання на претензії, навіть якщо вони є оригінальним автором мови. Тут: stackoverflow.com/q/5894775/2757035
підкреслити

Якщо говорити так, у мене є об'єкт, що містить 2 окремих сім'ї з майже 1000 поліморфних об'єктів, з автоматичною тривалістю зберігання. Я інстанціюю цей об'єкт на стеку, і, посилаючись на цих членів за допомогою посилань чи покажчиків, він набуває повних можливостей поліморфізму над ними. Ніщо в моїй програмі не динамічно розподіляється / купується (окрім ресурсів, що належать класам, виділеним стеком), і це не змінює можливості мого об'єкта на йоту.
підкреслюйте_24

-4

У мене була така ж проблема у Visual Studio. Ви повинні використовувати:

yourClass-> classMethod ();

а не:

yourClass.classMethod ();


3
Це не дає відповіді на запитання. Ви скоріше зауважите, що для доступу до об'єктів, виділених у купі (через вказівник на об'єкт), та об'єкту, виділеному у стеці, потрібно використовувати різний синтаксис.
Олексій Іванов
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.