Чому я повинен використовувати вказівник, а не сам об’єкт?


1602

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

Object *myObject = new Object;

а не:

Object myObject;

Або замість використання функції, скажімо testFunc()так:

myObject.testFunc();

ми повинні написати:

myObject->testFunc();

Але я не можу зрозуміти, чому ми повинні робити це так. Я би припустив, що це стосується ефективності та швидкості, оскільки ми отримуємо прямий доступ до адреси пам'яті. Чи правий я?


405
Кудо вам, щоб поставити під сумнів цю практику, а не просто дотримуватися її. Більшу частину часу покажчики надмірно використовуються.
Лучіан Григоре

120
Якщо ви не бачите причин використовувати покажчики, не робіть цього. Віддавайте перевагу об’єктам. Віддавайте перевагу об'єктам перед unique_ptr перед shared_ptr перед необробленими покажчиками.
Стефан

113
зауважте: у Java все (крім основних типів) - покажчик. тож скоріше слід запитати навпаки: навіщо мені потрібні прості предмети?
Каролі Горват

119
Зауважте, що в Java покажчики приховані синтаксисом. У C ++ різниця між вказівником і не вказівником робиться явною в коді. Java всюди використовує покажчики.
Даніель Мартін

216
Закрити як занадто широкий ? Серйозно? Зверніть увагу, що цей спосіб програмування Java ++ є дуже поширеним явищем і є однією з найважливіших проблем спільноти C ++ . До нього слід ставитися серйозно.
Manu343726

Відповіді:


1573

Дуже прикро, що динамічний розподіл ви бачите так часто. Це просто показує, скільки є поганих програмістів на C ++.

У певному сенсі у вас є два питання, зв'язані в одне. Перший - коли нам слід використовувати динамічне розподілення (використання new)? Друге - коли ми повинні використовувати вказівники?

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

Динамічне розподіл

У своєму запитанні ви продемонстрували два способи створення об’єкта. Основна відмінність - тривалість зберігання об’єкта. Виконуючи Object myObject;всередині блоку, об'єкт створюється з автоматичною тривалістю зберігання, а це означає, що він автоматично буде знищений, коли він вийде за межі області. Коли ви це робите new Object(), об’єкт має динамічну тривалість зберігання, а це означає, що він залишається живим, поки ви явно deleteце не зробите . Ви повинні використовувати динамічну тривалість зберігання лише тоді, коли вам це потрібно. Тобто завжди слід віддавати перевагу створенню об'єктів із автоматичною тривалістю зберігання, коли можна .

Основні дві ситуації, в яких вам може знадобитися динамічний розподіл:

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

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

Покажчики

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

  1. Вам потрібна довідкова семантика . Іноді ви хочете передати об’єкт за допомогою вказівника (незалежно від того, яким чином він був призначений), оскільки ви хочете, щоб функція, якій ви передаєте його, отримала доступ до цього конкретного об'єкта (а не його копії). Однак у більшості ситуацій слід віддати перевагу типам посилань перед покажчиками, оскільки саме для цього вони призначені. Зверніть увагу, що це не обов'язково стосується продовження терміну експлуатації об'єкта за межами поточного обсягу, як у ситуації 1 вище. Як і раніше, якщо ви добре з передачею копії об'єкта, вам не потрібна довідкова семантика.

  2. Вам потрібен поліморфізм . Функції можна викликати лише поліморфно (тобто відповідно до динамічного типу об'єкта) через покажчик або посилання на об'єкт. Якщо така поведінка вам потрібна, тоді вам потрібно використовувати вказівники або посилання. Знову слід віддати перевагу посиланням.

  3. Ви хочете уявити, що об'єкт необов'язковий , дозволяючи nullptrпередавати a, коли об'єкт опускається. Якщо це аргумент, вам слід віддати перевагу аргументам за замовчуванням або перевантаженням функції. В іншому випадку вам слід використовувати тип, який інкапсулює таку поведінку, як, наприклад, std::optional(введено в C ++ 17 - з більш ранніми стандартами C ++, використання boost::optional).

  4. Ви хочете роз'єднати одиниці компіляції, щоб покращити час компіляції . Корисна властивість вказівника полягає в тому, що вам потрібно лише попереднє оголошення вказаного типу (щоб фактично використовувати об'єкт, вам знадобиться визначення). Це дозволяє роз’єднати частини вашого процесу компіляції, що може значно покращити час компіляції. Дивіться ідіому Пімпла .

  5. Потрібно взаємодіяти з бібліотекою С або бібліотекою у стилі С. На цьому етапі ви змушені використовувати необроблені покажчики. Найкраще, що ви можете зробити - переконайтесь, що ви оставите лише сирі вказівники вільними в останній можливий момент. Ви можете отримати необроблений покажчик із розумного вказівника, наприклад, використовуючи його getфункцію-член. Якщо бібліотека виконує певне виділення для вас, яке очікує, що ви будете розміщуватись через ручку, ви можете часто загортати ручку в інтелектуальний покажчик зі спеціальним делетором, який буде належним чином розміщувати об'єкт.


83
"Вам потрібен об'єкт, щоб пережити поточний діапазон." - Додаткова примітка з цього приводу: є випадки, коли здається, що вам потрібен об'єкт, щоб пережити поточний обсяг, але ви насправді цього не робите. Якщо розмістити об’єкт всередині вектора, наприклад, об'єкт буде скопійовано (або переміщено) у вектор, а оригінальний об’єкт безпечно знищити, коли його область закінчиться.

25
Пам'ятайте, що s / copy / move / у багатьох місцях зараз. Повернення предмета однозначно не означає руху. Також слід зазначити, що доступ до об'єкта через вказівник є ортогональним щодо того, як він був створений.
Щеня

15
Я пропускаю чітке посилання на RAII на цю відповідь. C ++ - це все (майже все) про управління ресурсами, а RAII - це спосіб зробити це на C ++ (І головна проблема, яку генерують сировинні покажчики: Breaking RAII)
Manu343726

11
Розумні покажчики існували до C ++ 11, наприклад, boost :: shared_ptr та boost :: scoped_ptr. Інші проекти мають власний еквівалент. Ви не можете отримати семантику переміщення, а призначення std :: auto_ptr є помилковим, тому C ++ 11 покращує речі, але поради все-таки хороші. (І сумний чіплятися, це не досить , щоб мати доступ до в C ++ 11 компілятора, це необхідно , щоб всі компілятори ви могли б , можливо , хочете , щоб ваш код для роботи з підтримкою C ++ 11. Так, Oracle Solaris студія, я дивлячись на тебе.)
armb

7
@ MDMoore313 Можна написатиObject myObject(param1, etc...)
user000001

173

Існує багато випадків використання покажчиків.

Поліморфна поведінка . Для поліморфних типів вказівники (або посилання) використовуються для уникнення нарізки:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Довідкова семантика та уникнення копіювання . Для неполіморфних типів вказівник (або посилання) уникатиме копіювання потенційно дорогого об’єкта

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Зауважте, що C ++ 11 має семантику переміщення, що дозволяє уникнути багатьох копій дорогих об'єктів у аргументі функції та як повернені значення. Але використання вказівника, безумовно, уникне їх і дозволить декілька покажчиків на один і той же об'єкт (тоді як об'єкт можна перемістити лише один раз).

Придбання ресурсів . Створення покажчика на ресурс за допомогою newоператора є антидіаграмою в сучасному C ++. Використовуйте спеціальний клас ресурсів (один із контейнерів Standard) або смарт-покажчик ( std::unique_ptr<>або std::shared_ptr<>). Поміркуйте:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Сирий вказівник повинен використовуватися лише як "вид" і жодним чином не брати участь у власності, будь то шляхом прямого створення або неявно через повернені значення. Дивіться також це запитання і відповіді у поширених запитаннях C ++ .

Більш чіткий контроль життєвого часу Кожен раз, коли копіюється спільний покажчик (наприклад, як аргумент функції), ресурс, на який він вказує, зберігається в живих. Регулярні об'єкти (не створені newні вами, ні всередині класу ресурсів) знищуються, коли виходять із сфери застосування.


17
"Створення вказівника на ресурс за допомогою нового оператора є анти-шаблоном". Я думаю, ви навіть могли б покращити те, що наявність сировинного вказівника щось є антидіаграмою . Не тільки створення, але й передача необроблених покажчиків як аргументів або зворотних значень, що передбачають передачу права власності на IMHO, застаріло з моменту unique_ptr/ переміщення семантики
dyp

1
@dyp tnx, оновлено та посилається на C ++ FAQ Q&A на цю тему.
TemplateRex

4
Використання розумних покажчиків скрізь є анти-закономірністю. Існує кілька спеціальних випадків, коли це застосовно, але більшість випадків однакова причина, яка стверджує динамічне розподіл (довільний термін експлуатації), суперечить і будь-якому із звичайних розумних покажчиків.
Джеймс Канзе

2
@JamesKanze Я не мав на увазі того, що розумні покажчики повинні використовуватися скрізь, лише для власності, а також, що сировинні покажчики повинні використовуватися не для власності, а лише для переглядів.
TemplateRex

2
@TemplateRex Це здається трохи нерозумним, враховуючи, що hun(b)також потрібні знання підпису, якщо ви не добре, якщо не знаєте, що ви вказали неправильний тип до компіляції. Хоча питання посилання зазвичай не потрапляє під час компіляції та потребує більше зусиль для налагодження, якщо ви перевіряєте підпис, щоб переконатися, що аргументи є правильними, ви також зможете побачити, чи є якийсь із аргументів посиланням тому довідковий біт стає чимось непроблемним (особливо при використанні IDE або текстових редакторів, які показують підпис вибраних функцій). Також const&.
JAB

130

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

Розберемо ситуацію, порівнюючи дві мови:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

Найближчий еквівалент цьому:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Давайте подивимось альтернативний спосіб C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

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

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Однак, використання покажчиків НЕ обов'язково є ні правильним, ні неправильним способом поводження з речами; однак інші відповіді висвітлювали це задовільно. Загальна думка, однак, полягає в тому, що в C ++ ви маєте набагато більший контроль над життям об'єктів та над тим, де вони будуть жити.

Прийміть домашню точку - Object * object = new Object()конструкція насправді є найбільш близькою до типової семантики Java (або C # для цього питання).


7
Object2 is now "dead": Я думаю, ти маєш на увазі myObject1чи точніше the object pointed to by myObject1.
Клімент

2
Справді! Перефразувавши трохи.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();дуже поганий код. Другий новий або другий конструктор Object може кинути, і тепер object1 просочився. Якщо ви використовуєте сировину news, вам слід як newможна швидше загортати об'єкти в обгортки RAII.
PSkocik

8
Дійсно, це було б, якби це була програма, і навколо неї нічого більше не відбувалося. На щастя, це лише фрагмент пояснення, який показує, як поводиться вказівник на C ++ - і одне з небагатьох місць, де об’єкт RAII не може бути замінений необробленим вказівником, вивчає та дізнається про сирі вказівники ...
Герасим, R

80

Ще одним вагомим приводом для використання покажчиків буде подання попередніх оголошень . У досить великому проекті вони дійсно можуть пришвидшити час збирання.


7
це дійсно доповнює сукупність корисної інформації, тому радий, що ви зробили це відповіддю!
TemplateRex

3
std :: shared_ptr <T> також працює з попередніми заявами T. (std :: unique_ptr <T> doesn )
berkus

13
@berkus: std::unique_ptr<T>працює з поданням декларацій T. Вам просто потрібно переконатися, що коли std::unique_ptr<T>виклик деструктора Tє повноцінним типом. Зазвичай це означає ваш клас, який містить std::unique_ptr<T>заяву про свій деструктор у файлі заголовка та реалізує його у файлі cpp (навіть якщо реалізація порожня).
Девід Стоун

Чи виправлять це модулі?
Тревор Хікі

@TrevorHickey Старий коментар Я знаю, але відповісти на нього все одно. Модулі не знімуть залежність, але повинні зробити в тому числі залежність дуже дешевою, майже безкоштовною з точки зору вартості продуктивності. Крім того, якщо загального прискорення роботи модулів буде достатньо для того, щоб отримати час компіляції у прийнятному діапазоні, це вже не проблема.
Айдіакапі

79

Передмова

Java - це не що інше, як C ++, що суперечить ажіотажу. Машина hype-машини хотіла б, щоб ви повірили, що оскільки у Java є синтаксис на C ++, то мови схожі. Ніщо не може бути далі від істини. Ця дезінформація є частиною причини, по якій Java-програмісти переходять на C ++ та використовують синтаксис, схожий на Яву, не розуміючи наслідків свого коду.

Вперед ми йдемо

Але я не можу зрозуміти, чому ми повинні робити це так. Я би припустив, що це стосується ефективності та швидкості, оскільки ми отримуємо прямий доступ до адреси пам'яті. Чи правий я?

Навпаки, насправді. Купа набагато повільніше, ніж стек, тому що стек дуже простий у порівнянні з групою. Автоматичні змінні зберігання (ака-змінні стека) викликають деструктори, коли вони виходять із сфери застосування. Наприклад:

{
    std::string s;
}
// s is destroyed here

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

{
    std::string* s = new std::string;
}
delete s; // destructor called

Це не має нічого спільного з newсинтаксисом, який є поширеним у C # та Java. Їх використовують для зовсім інших цілей.

Переваги динамічного розподілу

1. Вам не потрібно знати заздалегідь розмір масиву

Однією з перших проблем, з якою стикаються багато програмістів на C ++, є те, що коли вони приймають довільний ввід від користувачів, ви можете виділити лише фіксований розмір для змінної стека. Ви також не можете змінити розмір масивів. Наприклад:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Звичайно, якщо ви скористалися std::stringнатомість, std::stringвнутрішній розмір себе змінює, щоб не виникало проблем. Але по суті рішення цієї проблеми - динамічне розподіл. Ви можете виділити динамічну пам'ять залежно від входу користувача, наприклад:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Бічна примітка : однією помилкою, яку роблять багато початківців, є використання масивів змінної довжини. Це розширення GNU, а також одне в Clang, оскільки вони відображають багато розширень GCC. Тож int arr[n]не слід покладатися на наступне .

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

2. Масиви не є покажчиками

Як це користь, яку ви запитуєте? Відповідь стане зрозумілою, як тільки ви зрозумієте плутанину / міф за масивами та покажчиками. Загальноприйнято вважати, що вони однакові, але вони не є. Цей міф походить від того, що покажчики можуть бути підписані так само, як масиви, і через те, що масиви розпадаються на покажчики на верхньому рівні в оголошенні функції. Однак, як тільки масив відхиляється до вказівника, вказівник втрачає свою sizeofінформацію. Так sizeof(pointer)вийде розмір вказівника в байтах, який зазвичай становить 8 байт у 64-бітній системі.

Ви не можете призначити масиви, лише ініціалізуйте їх. Наприклад:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

З іншого боку, ви можете робити все, що завгодно, за допомогою покажчиків. На жаль, оскільки відмінність між вказівниками та масивами розмахується рукою в Java та C #, початківці не розуміють різниці.

3. Поліморфізм

У Java та C # є засоби, які дозволяють вам ставитись до об'єктів як до іншого, наприклад, використовуючи asключове слово. Отже, якщо хтось хотів розглянути Entityоб'єкт як Playerоб'єкт, можна зробити це. Player player = Entity as Player;Це дуже корисно, якщо ви маєте намір викликати функції на однорідному контейнері, який повинен застосовуватися лише до конкретного типу. Функціональність може бути досягнута аналогічно нижче:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Так що скажіть, якби тільки трикутники мали функцію Rotate, це була б помилка компілятора, якби ви спробували викликати її на всіх об'єктах класу. Використовуючи dynamic_cast, ви можете імітувати asключове слово. Щоб було зрозуміло, якщо випуск не вдається, він повертає недійсний покажчик. Таким чином, !testце по суті скорочення, щоб перевірити, чи testNULL чи недійсний покажчик, що означає, що передача не вдалася.

Переваги автоматичних змінних

Побачивши всі чудові речі, які може зробити динамічний розподіл, ви, напевно, задаєтесь питанням, чому б ніхто не використовував динамічний розподіл весь час? Я вже сказав вам одну причину, купа йде повільно. І якщо вам не потрібна вся ця пам’ять, ви не повинні її зловживати. Ось ось деякі недоліки в конкретному порядку:

  • Він схильний до помилок. Ручне розподілення пам'яті небезпечно, і ви схильні до протікання. Якщо ви не досвідчені у використанні налагоджувача або valgrind(інструменту протікання пам’яті), ви можете витягнути волосся з голови. На щастя ідіоми RAII та розумні покажчики це трохи полегшують, але ви повинні бути знайомі з такими практиками, як Правило трьох та Правило п'яти. Багато інформації потрібно взяти, і в цю пастку потрапляють початківці, які або не знають, або байдуже.

  • Це не обов'язково. На відміну від Java та C #, де ідіоматично використовувати newключове слово скрізь, у C ++ ви повинні використовувати його лише у разі потреби. Загальна фраза іде, все виглядає як цвях, якщо у вас є молоток. Тоді як початківці, які починають із C ++, лякаються покажчиків і вчаться користуватися змінними стека за звичкою, програмісти Java та C # починають використовувати вказівники, не розуміючи цього! Це буквально відступає від неправильної ноги. Ви повинні відмовитися від усього, що знаєте, бо синтаксис - це одне, вивчення мови - інше.

1. (N) RVO - Aka, (названа) Оптимізація зворотного значення

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

Якщо ви використовуєте покажчики, (N) RVO НЕ виникає. Вигідніше і менше схильних до помилок скористатися (N) RVO, а не повертати або пропускати покажчики, якщо ви переживаєте за оптимізацію. Витік помилок може статися, якщо абонент функції відповідає за deleteвикористання динамічно виділеного об'єкта тощо. Відстежити право власності на об’єкт може бути важко, якщо вказівники передаються навколо, як гаряча картопля. Просто використовуйте змінні стека, тому що це простіше і краще.


"Отже! Тест по суті є скороченням для перевірки того, чи є тест NULL чи недійсний покажчик, що означає, що передача не вдалася." Я думаю, що це речення потрібно переписати для ясності.
berkus

4
"Автоматизована машина Java хотіла б, щоб ви повірили" - можливо, в 1997 році, але це зараз анахронічно, більше немає мотивації порівнювати Java з C ++ у 2014 році.
Метт R

15
Старе питання, але в сегменті коду { std::string* s = new std::string; } delete s; // destructor called.... напевно це deleteне буде працювати, оскільки компілятор більше не знатиме, що sє?
badger5000

2
Я НЕ даю -1, але я не згоден із вступними словами, як написано. По-перше, я не погоджуюся, що існує якийсь "галас" - можливо, це було навколо Y2K, але тепер обидві мови добре розуміються. По-друге, я б заперечував, що вони досить схожі - C ++ є дитиною C, який одружений з Simula, Java додає Virtual Machine, Garbage Collector і HEAVILY скорочує функції, а C # упорядковує та вводить у Java відсутні функції. Так, це робить шаблони та дійсне використання ГОЛОВНО різними, але вигідно розуміти загальну інфраструктуру / дизайн, щоб можна було побачити відмінності.
Герасимос Р

1
@James Matta: Ви, звичайно, правильні, що пам'ять - це пам'ять, і вони обидва виділяються з однієї фізичної пам'яті, але слід врахувати одне, що дуже часто отримувати кращі характеристики продуктивності роботи з виділеними стеками об'єктами, тому що стек - або, принаймні, його найвищі рівні - мають дуже високий шанс бути "гарячим" в кеші, коли функції входять і виходять, тоді як купа не має такої переваги, тому якщо ви вказуєте на вказівник у купі, ви можете отримати кілька пропусків кешу, які ви, ймовірно , не були б на стеці. Але вся ця "випадковість" зазвичай сприяє стеку.
Герасимос R

23

C ++ дає три способи передачі об'єкта: за вказівником, посиланням та значенням. Java обмежує вас останнім (єдиний виняток - примітивні типи, такі як int, boolean тощо). Якщо ви хочете використовувати C ++ не просто як дивну іграшку, то вам краще ознайомитись із різницею цих трьох способів.

Java робить вигляд, що немає такої проблеми, як "хто і коли повинен це знищити?". Відповідь: Колекціонер сміття, великий і жахливий. Тим не менш, він не може забезпечити 100% захист від витоку пам'яті (так, Java може просочити пам'ять ). Насправді GC дає помилкове відчуття безпеки. Чим більший ваш позашляховик, тим довше ваш шлях до евакуатора.

C ++ залишає вас віч-на-віч із керуванням життєвим циклом об'єкта. Що ж, з цим вирішуються засоби ( сім'я розумних вказівників , QObject у Qt тощо), але жоден з них не може використовуватися таким чином, як «GC» та «забути», як GC: завжди слід пам’ятати про обробку пам’яті. Якщо ви не тільки дбаєте про знищення об'єкта, ви також повинні уникати знищення одного і того ж об'єкта не один раз.

Ще не злякалися? Гаразд: циклічні посилання - обробляйте їх самі, людино. І пам’ятайте: вбивайте кожен предмет точно один раз, ми на C ++ не любимо тих, хто возиться з трупами, залишає мертвих у спокої.

Отже, повернемось до вашого питання.

Коли ви передаєте об'єкт навколо за значенням, а не за вказівником чи посиланням, ви копіюєте об'єкт (весь об'єкт, будь то пара байтів чи величезний дамп бази даних - ви досить розумні, щоб дбати про те, щоб уникнути останнього, чи не так " t you?) щоразу, коли ви робите '='. І для доступу до членів об’єкта ви використовуєте '.' (крапка).

Передаючи об'єкт за вказівником, ви копіюєте лише кілька байтів (4 в 32-бітових системах, 8 на 64-бітних), а саме - адресу цього об’єкта. І щоб показати це всім, ви використовуєте цей химерний оператор '->', коли ви отримуєте доступ до членів. Або ви можете використовувати комбінацію '*' і '.'.

Використовуючи посилання, ви отримуєте вказівник, який видає себе за значення. Це вказівник, але ви отримуєте доступ до членів через "."

І, щоб ще раз задуматися: коли ви оголошуєте кілька змінних, розділених комами, тоді (дивіться на руки):

  • Тип надається всім
  • Модифікатор значення / покажчика / посилання індивідуальний

Приклад:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrзастаріло, не використовуйте його.
Ніл

2
Досить впевнений, що ви не можете мати посилання в якості члена, не надавши конструктору список ініціалізації, що включає посилання змінної. (Посилання має бути негайно
ініційоване

20

У C ++ об'єкти, виділені на стеці (використовуючи Object object;оператор у блоці), будуть жити лише в межах, про які вони оголошені. Коли блок коду завершує виконання, оголошений об'єкт знищується. Якщо ви виділяєте пам'ять на купу, використовуючи Object* obj = new Object(), вони продовжують жити в купі, поки ви не зателефонуєте delete obj.

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


6
Object objне завжди є в стеці - наприклад, глобальні або членські змінні.
десять четвертих

2
@LightnessRacesinOrbit Я згадував лише про об'єкти, виділені в блоці, а не про глобальні та членські змінні. Справа в тому, що це було не ясно, тепер її виправили - додали "у межах блоку" у відповідь. Сподіваюсь, що зараз його неправдиві відомості :)
Karthik Kalyanasundaram

20

Але я не можу зрозуміти, чому ми повинні використовувати це так?

Я порівняю, як це працює всередині функції функції, якщо ви використовуєте:

Object myObject;

Всередині функції ваша система myObjectбуде знищена, як тільки ця функція повернеться. Тож це корисно, якщо вам не потрібен об’єкт поза вашою функцією. Цей об'єкт буде розміщений у поточному стеці потоку.

Якщо ви пишете всередині функції функції:

 Object *myObject = new Object;

тоді екземпляр класу Object, на який вказує, myObjectне буде знищений, коли функція закінчиться, і розподіл буде в купі.

Тепер, якщо ви програміст Java, то другий приклад ближче до того, як працює розподіл об’єктів під Java. Цей рядок: Object *myObject = new Object;еквівалентно java : Object myObject = new Object();. Різниця полягає в тому, що під Java myObject збирається сміття, тоді як під c ++ він не звільниться, потрібно десь явно зателефонувати `delete myObject; ' інакше ви введете витоки пам'яті.

Оскільки c ++ 11, ви можете використовувати безпечні способи динамічного розподілу:, new Objectзберігаючи значення у shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Також об'єкти дуже часто зберігаються в контейнерах, як-от map-s або vector-s, вони автоматично керують життям ваших об’єктів.


1
then myObject will not get destroyed once function endsЦе абсолютно буде.
Гонки легкості на Орбіті

6
У випадку вказівника myObjectвсе одно буде знищено, як і будь-яка інша локальна змінна. Різниця полягає в тому, що його значення - вказівник на об'єкт, а не на сам об'єкт, і руйнування німого вказівника не впливає на його покажчик. Так об’єкт переживе вказане руйнування.
cHao

Виправлено, що локальні змінні (що включає вказівник) звісно будуть звільнені - вони знаходяться в стеці.
marcinj

13

Технічно це питання розподілу пам'яті, однак тут є ще два практичних аспекту. Це пов'язане з двома речами: 1) Сфера застосування, коли ви визначаєте об'єкт без вказівника, ви більше не зможете отримати доступ до нього після блоку коду, в якому визначено, тоді як якщо ви визначите покажчик з "новим", то ви Ви можете отримати доступ до нього з будь-якого місця, де у вас є вказівник на цю пам’ять, поки ви не зателефонуєте на "видалити" за тим самим вказівником. 2) Якщо ви хочете передати аргументи функції, ви хочете передати вказівник або посилання для підвищення ефективності. Коли ви передаєте Object, об'єкт копіюється, якщо це об'єкт, який використовує багато пам'яті, це може зайняти процесор (наприклад, ви копіюєте вектор, повний даних). Коли ви передаєте вказівник, все, що ви передаєте, - це один int (залежно від реалізації, але більшість з них - один int).

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


6

Ну головне питання: Чому я повинен використовувати вказівник, а не сам об’єкт? І моя відповідь, ви (майже) ніколи не повинні використовувати вказівник замість об’єкта, оскільки C ++ має посилання , це безпечніше, ніж покажчики та гарантує таку ж ефективність, як покажчики.

Ще одна річ, яку ви згадали у своєму запитанні:

Object *myObject = new Object;

Як це працює? Він створює вказівник Objectтипу, виділяє пам'ять під один об'єкт і викликає конструктор за замовчуванням, добре звучить, правда? Але насправді це не так добре, якщо ви динамічно розподіляли пам'ять (використано ключове слово new), ви також повинні звільнити пам'ять вручну, це означає, що у коді ви повинні мати:

delete myObject;

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


А зараз деякий вступ закінчений і поверніться до питання.

Ви можете використовувати вказівники замість об'єктів для кращої продуктивності під час передачі даних між функціями.

Погляньте, у вас є std::string(він також є об'єктом), і він містить дуже багато даних, наприклад, великий XML, тепер вам потрібно його розібрати, але для цього у вас є функція, void foo(...)яку можна оголосити різними способами:

  1. void foo(std::string xml); У цьому випадку ви скопіюєте всі дані зі змінної у стек функцій, це займе певний час, тому ваша продуктивність буде низькою.
  2. void foo(std::string* xml); У цьому випадку ви передасте вказівник на об’єкт із такою ж швидкістю, як і прохідна size_tзмінна, проте ця декларація має схильність до помилок, оскільки ви можете передавати NULLпокажчик або недійсний покажчик. Покажчики зазвичай використовуються, Cоскільки в ньому немає посилань.
  3. void foo(std::string& xml); Тут ви передаєте посилання, в основному це те саме, що передає вказівник, але компілятор виконує деякі речі, і ви не можете передати недійсну посилання (насправді можна створити ситуацію з недійсним посиланням, але це обмацує компілятор).
  4. void foo(const std::string* xml); Тут те саме, що і друге, просто значення вказівника неможливо змінити.
  5. void foo(const std::string& xml); Тут те саме, що і третє, але значення об'єкта неможливо змінити.

Більше того, я хочу зазначити, ви можете використовувати ці 5 способів передачі даних незалежно від обраного способу розподілу (з newабо звичайного ).


Ще одна річ, яку слід зазначити, коли ви створюєте об'єкт регулярно , ви виділяєте пам'ять у стеці, але, створюючи її, newви виділяєте купу. Набагато швидше виділити стек, але це невеликий розмір для дійсно великих масивів даних, тому якщо вам потрібен великий об’єкт, ви повинні використовувати купу, тому що ви можете отримати переповнення стека, але зазвичай це питання вирішується за допомогою контейнерів STL і пам’ятайте std::stringтеж є контейнером, деякі хлопці забули це :)


5

Скажімо, що у вас є class Aте, що class Bви хочете, щоб викликати якусь функцію class Bзовні, class Aви просто отримаєте вказівник на цей клас, і ви зможете робити все, що завгодно, і це також змінить контекст class Bу вашомуclass A

Але будьте обережні з динамічним об'єктом


5

Є багато переваг використання покажчиків для об'єкта -

  1. Ефективність (як ви вже вказали). Передача об'єктів функцій означає створення нових копій об'єкта.
  2. Робота з об’єктами з сторонніх бібліотек. Якщо ваш об'єкт належить до стороннього коду, а автори мають намір використовувати їх об'єкти лише за допомогою покажчиків (без конструкторів копій тощо), єдиним способом обходу цього об'єкта є використання покажчиків. Перехід за значенням може спричинити проблеми. (Проблеми з глибокою копією / дрібною копією).
  3. якщо об'єкт володіє ресурсом, і ви хочете, щоб право власності не було збережено з іншими об'єктами.

3

Про це йшлося досить довго, але на Java все є вказівником. Це не робить різниці між розподілом стека та купи (усі об'єкти розподіляються на купі), тому ви не розумієте, що використовуєте покажчики. У програмі C ++ ви можете змішати ці дві, залежно від потреб у пам'яті. Продуктивність та використання пам’яті є більш детермінованими в C ++ (duh).


3
Object *myObject = new Object;

Це створить посилання на Об'єкт (на купі), який потрібно явно видалити, щоб уникнути витоку пам'яті .

Object myObject;

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


1

Вказівник безпосередньо посилається на місце пам'яті об'єкта. У Java нічого подібного немає. У Java є посилання, які посилаються на розташування об'єкта через хеш-таблиці. З цими посиланнями ви не можете нічого подібного до арифметики вказівника.

Щоб відповісти на ваше запитання, це лише ваші вподобання. Я вважаю за краще використовувати синтаксис, схожий на Java.


Хеш-столи? Можливо, у деяких JVM, але на це не розраховуйте.
Зан Лінкс

Що з JVM, що постачається з Java? Звичайно, ви можете реалізувати будь-що, ви можете думати, як JVM, який використовує вказівники безпосередньо, або метод, який займається математикою вказівника. Це як сказати: "люди не вмирають від застуди" і отримують відповідь "Можливо, більшість людей не робить, але на це не розраховує!" Ха-ха.
RioRicoRick

2
@RioRicoRick HotSpot реалізує посилання на Java як вказівники, див. Docs.oracle.com/javase/7/docs/technotes/guides/vm/… Наскільки я бачу, JRockit робить те ж саме. Вони обидва підтримують стиснення OOP, але ніколи не використовують хеш-таблиці. Наслідки виконання, ймовірно, будуть катастрофічними. Крім того, "це просто ваше вподобання", мабуть, випливає, що вони є просто різними синтаксисами для еквівалентної поведінки, що, звичайно, вони не є.
Макс Барраклу


0

За допомогою покажчиків ,

  • може безпосередньо говорити з пам'яттю.

  • може запобігти багато витоків пам'яті програми, маніпулюючи покажчиками.


4
" в C ++, використовуючи покажчики, ви можете створити спеціальний сміттєзбірник для власної програми ", що звучить як жахлива ідея.
Quant

0

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


0

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


0

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

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Тому в цьому випадку ви не можете оголосити bObj як прямий об'єкт, у вас повинен бути покажчик.


-5

"Необхідність - це винахідництво". Найбільш важливою відмінністю, яку я хотів би зазначити, є результат власного досвіду кодування. Іноді потрібно передати об’єкти функціям. У такому випадку, якщо ваш об’єкт дуже великого класу, передаючи його як об’єкт, він скопіює його стан (який ви не хочете .. 4-байтний розмір (якщо 32-бітний). Інші причини вже згадуються вище ...


14
вам слід віддати перевагу переходу посилань
bolov

2
Я рекомендую переходити через постійні посилання, як для змінної std::string test;, void func(const std::string &) {}але у випадку, якщо функція не повинна змінити вхід, в цьому випадку я рекомендую використовувати вказівники (щоб кожен, хто читає код, помітив &, і зрозумів, що функція може змінити його вхід)
Top- Майстер

-7

Вже є багато відмінних відповідей, але дозвольте навести один приклад:

У мене простий клас предметів:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Я роблю вектор, щоб вмістити їх.

std::vector<Item> inventory;

Я створюю один мільйон предметів предмета і відштовхую їх назад на вектор. Я сортую вектор за назвою, а потім виконую простий ітеративний двійковий пошук певного імені елемента. Я тестую програму, і на її виконання потрібно 8 хвилин. Потім я змінюю свій інвентарний вектор так:

std::vector<Item *> inventory;

... і створюйте свої об'єкти мільйонних елементів за допомогою нових. Тільки зміни, які я вношу до свого коду, полягають у використанні покажчиків на «Елементи», виключаючи цикл, який я додаю для очищення пам’яті наприкінці. Ця програма працює за 40 секунд або краще, ніж 10-кратне збільшення швидкості. EDIT: Код розміщено на веб-сайті http://pastebin.com/DK24SPeW. Оптимізація компілятора показує лише 3,4-кратне збільшення на машині, на яку я щойно тестував, що все ще є значним.


2
Ви порівнюєте покажчики тоді чи все ще порівнюєте фактичні об’єкти? Я дуже сумніваюся, що інший рівень непрямості може покращити продуктивність. Введіть код! Ви правильно прибираєте після цього?
stefan

1
@stefan Я порівнюю дані (конкретно, поле імен) об'єктів як для сортування, так і для пошуку. Я прибираю належним чином, як я вже згадував у пості. прискорення, ймовірно, пов'язане з двома факторами: 1) std :: вектор push_back () копіює об'єкти, тому версії вказівника потрібно копіювати лише один покажчик на об'єкт. Це має багаторазовий вплив на продуктивність, оскільки не тільки копіюється менше даних, але і розподільник пам'яті векторного класу стає меншим.
Даррен

2
Ось код, що показує практично ніяку різницю для вашого прикладу: сортування. Код вказівника на 6% швидше, ніж код не вказівника для сортування, але в цілому він на 10% повільніше, ніж код не вказівника. ideone.com/G0c7zw
stefan

3
Ключові слова: push_back. Звичайно, це копії. Ви повинні були бути emplaceна місці, створюючи свої об'єкти (якщо вам не потрібно їх кешування в іншому місці).
підкреслюй_d

1
Вектори покажчиків майже завжди помиляються. Не рекомендуйте їх, не пояснюючи детально, застереження та плюси та мінуси. Ви, здається, знайшли одного профі, який є лише наслідком погано зашифрованого зустрічного прикладу, і його неправильно представили
Гонки легкості на орбіті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.