Передмова
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
це по суті скорочення, щоб перевірити, чи test
NULL чи недійсний покажчик, що означає, що передача не вдалася.
Переваги автоматичних змінних
Побачивши всі чудові речі, які може зробити динамічний розподіл, ви, напевно, задаєтесь питанням, чому б ніхто не використовував динамічний розподіл весь час? Я вже сказав вам одну причину, купа йде повільно. І якщо вам не потрібна вся ця пам’ять, ви не повинні її зловживати. Ось ось деякі недоліки в конкретному порядку:
Він схильний до помилок. Ручне розподілення пам'яті небезпечно, і ви схильні до протікання. Якщо ви не досвідчені у використанні налагоджувача або valgrind
(інструменту протікання пам’яті), ви можете витягнути волосся з голови. На щастя ідіоми RAII та розумні покажчики це трохи полегшують, але ви повинні бути знайомі з такими практиками, як Правило трьох та Правило п'яти. Багато інформації потрібно взяти, і в цю пастку потрапляють початківці, які або не знають, або байдуже.
Це не обов'язково. На відміну від Java та C #, де ідіоматично використовувати new
ключове слово скрізь, у C ++ ви повинні використовувати його лише у разі потреби. Загальна фраза іде, все виглядає як цвях, якщо у вас є молоток. Тоді як початківці, які починають із C ++, лякаються покажчиків і вчаться користуватися змінними стека за звичкою, програмісти Java та C # починають використовувати вказівники, не розуміючи цього! Це буквально відступає від неправильної ноги. Ви повинні відмовитися від усього, що знаєте, бо синтаксис - це одне, вивчення мови - інше.
1. (N) RVO - Aka, (названа) Оптимізація зворотного значення
Однією з оптимізацій, яку роблять багато компіляторів, є речі, які називаються елісією та оптимізацією зворотного значення . Ці речі можуть позбавити зайвих копій, що корисно для дуже великих об'єктів, таких як вектор, що містить багато елементів. Зазвичай загальною практикою є використання покажчиків для передачі права власності, а не копіювання великих об'єктів для переміщення їх. Це призвело до появи семантики руху та розумних покажчиків .
Якщо ви використовуєте покажчики, (N) RVO НЕ виникає. Вигідніше і менше схильних до помилок скористатися (N) RVO, а не повертати або пропускати покажчики, якщо ви переживаєте за оптимізацію. Витік помилок може статися, якщо абонент функції відповідає за delete
використання динамічно виділеного об'єкта тощо. Відстежити право власності на об’єкт може бути важко, якщо вказівники передаються навколо, як гаряча картопля. Просто використовуйте змінні стека, тому що це простіше і краще.