Збільшення в C ++ - Коли використовувати x ++ або ++ x?


91

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

Тим не менше, я справді не знаю, коли використовувати будь-який з цих двох ... Я ніколи не використовував "++ x", і досі речі завжди працювали нормально - так, коли мені це використовувати?

Приклад: Коли у циклі for, коли переважно використовувати "++ x"?

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

Відповіді:


114

Це питання не переваги, а логіки.

x++збільшує значення змінної x після обробки поточного оператора.

++xзбільшує значення змінної x перед обробкою поточного оператора.

Тож просто визначтесь із логікою, яку ви пишете.

x += ++iзбільшить i і додасть i + 1 до x. x += i++додасть i до x, а потім збільшить i.


27
і зауважте, що у циклі for для примативів немає абсолютно ніякої різниці. Багато стилів кодування рекомендують ніколи не використовувати оператор збільшення, де це може бути неправильно зрозуміле; тобто x ++ або ++ x повинні існувати лише у власному рядку, ніколи як y = x ++. Особисто мені це не подобається, але це рідко
Mikeage

2
І якщо його використовувати у своєму рядку, згенерований код майже напевно буде однаковим.
Носредна

14
Це може здатися педантичністю (головним чином тому, що це :)), але в C ++ x++є значенням r зі значенням xдо приросту, x++є значенням зі значенням xпісля збільшення. Жоден вираз не гарантує, коли фактичне збільшене значення зберігається назад до x, гарантується лише, що це відбувається до наступної точки послідовності. "після обробки поточного висловлювання" не є суворо точним, оскільки деякі вирази мають точки послідовності, а деякі висловлювання - це складені висловлювання.
CB Bailey

10
Власне, відповідь оманлива. Момент часу, коли змінна x модифікується, ймовірно, не відрізняється на практиці. Різниця полягає в тому, що x ++ визначено для повернення значення попереднього значення x, тоді як ++ x все ще посилається на змінну x.
sellibitze

5
@BeowulfOF: Відповідь передбачає порядок, якого не існує. У стандарті немає нічого сказати, коли відбуваються збільшення. Компілятор має право реалізувати "x + = i ++" як: int j = i; i = i + 1; x + = j; "(тобто." i "збільшено перед" обробкою поточного висловлювання "). Ось чому" i = i ++ "має невизначену поведінку, і тому, на мою думку, відповідь потребує" налаштування ". Опис" x + = ++ i "є правильним, оскільки немає пропозицій щодо замовлення:" буде збільшувати i і додавати i + 1 до x ".
Річард Корден

53

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

"Ефективніший C ++", пункт 6 - це достатньо повноважень для мене.

Для тих, хто не є власником книги, ось відповідні цитати. Зі сторінки 32:

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

І на сторінці 34:

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


4
Якщо компілятор не усвідомлює, що значення перед приростом є непростим, він може реалізувати приріст постфіксу в декількох інструкціях - скопіюйте старе значення, а потім зростіть. Приріст префіксу завжди повинен бути лише однією інструкцією.
gnud

8
Мені довелося тестувати це вчора за допомогою gcc: у циклі for, в якому значення викидається після виконання, i++або ++iзгенерований код однаковий.
Джорджо

Спробуйте за межами циклу for. Поведінка в дорученні повинна бути іншою.
duffymo

Я явно не погоджуюся з Скоттом Мейєрсом щодо його другого пункту - це, як правило, не має значення, оскільки 90% або більше випадків "x ++" або "++ x" зазвичай ізольовані від будь-якого призначення, а оптимізатори досить розумні, щоб визнати, що тимчасові змінні не потребують бути створеними в таких випадках. У цьому випадку ці дві форми повністю взаємозамінні. Наслідком цього є те, що старі бази коду, заповнені "x ++", слід залишити в спокої - ви, швидше за все, призводите до незначних помилок, змінюючи їх на "++ x", ніж для підвищення продуктивності де завгодно. Можливо, краще використовувати "x ++" і змусити людей замислитися.
omatai

2
Ви можете довіряти Скотту Мейерсу все, що завгодно, але якщо ваш код настільки залежить від продуктивності, що будь-яка різниця в продуктивності між ++xі x++насправді має значення, набагато важливіше, щоб ви насправді використовували компілятор, який може повністю та належним чином оптимізувати будь-яку версію, незалежно від контекст. "Оскільки я використовую цей похмурий старий молоток, я можу забивати цвяхи лише під кутом 43,7 градуса" - поганий аргумент для будівництва будинку, забиваючи цвяхи лише на 43,7 градуса. Використовуйте кращий інструмент.
Ендрю Хенле,

28

З cppreference при збільшенні ітераторів:

Вам слід віддати перевагу оператору попереднього збільшення (iter ++) перед оператором після збільшення (iter ++), якщо ви не збираєтесь використовувати старе значення. Постінкремент, як правило, реалізується наступним чином:

   Iter operator++(int)   {
     Iter tmp(*this); // store the old value in a temporary object
     ++*this;         // call pre-increment
     return tmp;      // return the old value   }

Очевидно, що це менш ефективно, ніж попереднє збільшення.

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


8

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

приклад:

pre.cpp:

#include <iostream>

int main()
{
  int i = 13;
  i++;
  for (; i < 42; i++)
    {
      std::cout << i << std::endl;
    }
}

post.cpp:

#include <iostream>

int main()
{

  int i = 13;
  ++i;
  for (; i < 42; ++i)
    {
      std::cout << i << std::endl;
    }
}

_

$> g++ -S pre.cpp
$> g++ -S post.cpp
$> diff pre.s post.s   
1c1
<   .file   "pre.cpp"
---
>   .file   "post.cpp"

5
Для примітивного типу, такого як ціле число, так. Ви перевіряли, яка різниця виявляється для чогось на зразок std::map::iterator? Звичайно, два оператори різні, але мені цікаво, чи оптимізує компілятор постфікс до префіксу, якщо результат не використовується. Я не думаю, що це дозволено - враховуючи, що версія postfix може містити побічні ефекти.
seh

Крім того, " компілятор, мабуть, зрозуміє, що вам не потрібен побічний ефект і оптимізує його ", не повинен бути приводом для написання недбалого коду, який використовує більш складні оператори постфіксу без будь-яких причин, окрім, мабуть, того факту, що так багато передбачувані навчальні матеріали використовують постфікс без видимих ​​причин і копіюють їх оптом.
underscore_d

6

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

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

for (int i(0);i<10;++i)
for (int i(0);i<10;i++)

5

Я погоджуюсь з @BeowulfOF, хоча для ясності я б завжди виступав за розбиття тверджень так, щоб логіка була абсолютно зрозумілою, тобто:

i++;
x += i;

або

x += i;
i++;

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


2

Просто хотів ще раз наголосити, що ++ x, як очікується, буде швидшим за x ++ (особливо якщо x є об'єктом якогось довільного типу), тому, якщо це не потрібно з логічних причин, слід використовувати ++ x.


2
Я просто хочу підкреслити, що цей наголос більш ніж імовірно вводить в оману. Якщо ви дивитесь на якийсь цикл, який закінчується ізольованим "x ++", і думаєте: "Ага! - це причина, що це працює так повільно!" і ви зміните його на "++ x", тоді точно не очікуйте різниці. Оптимізатори досить розумні, щоб зрозуміти, що тимчасові змінні не потрібно створювати, коли ніхто не збирається використовувати їх результати. Наслідком цього є те, що старі бази коду, заповнені "x ++", слід залишити в спокої - ви, швидше за все, призводите до помилок, змінюючи їх, ніж для підвищення продуктивності в будь-якому місці.
omatai

1

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

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

Редагувати: виправлено змішування позначень префіксів та суфіксів


Розмова про "до / після" ітерації циклу є значущою лише в тому випадку, якщо попереднє / після включення / зменшення відбувається в умові. Частіше це буде в реченні продовження, де воно не може змінити жодної логіки, хоча для типів класів може бути повільніше використовувати постфікс, і люди не повинні використовувати це без причини.
underscore_d

1

Форма постфіксу ++, - оператор дотримується правила use-then-change ,

Префіксна форма (++ x, - x) відповідає правилу зміна, а потім використання .

Приклад 1:

Коли кілька значень каскадуються за допомогою << cout, тоді обчислення (якщо такі є) проводяться справа наліво, але друк відбувається зліва направо, наприклад, (якщо val, якщо спочатку 10)

 cout<< ++val<<" "<< val++<<" "<< val;

призведе до

12    10    10 

Приклад 2:

У Turbo C ++, якщо у виразі знайдено кілька випадків ++ або (в будь-якій формі), спочатку обчислюються всі форми префіксів, потім обчислюється вираз і, нарешті, обчислюються форми постфіксу, наприклад,

int a=10,b;
b=a++ + ++a + ++a + a;
cout<<b<<a<<endl;

Це буде виведено в Turbo C ++

48 13

Тоді як це буде виведено в сучасному компіляторі (оскільки вони суворо дотримуються правил)

45 13
  • Примітка: Не рекомендується багаторазове використання операторів збільшення / зменшення однієї і тієї ж змінної в одному виразі. Обробка / результати таких
    виразів різняться залежно від компілятора.

Справа не в тому, що вирази, що містять кілька операцій inc / decrement "варіюються від компілятора до компілятора", але швидше гірше: такі множинні модифікації між точками послідовності мають невизначену поведінку і отруюють програму.
underscore_d

0

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

char a[256] = "Hello world!";
char b[256];
int i = 0;
do {
  b[i] = a[i];
} while (a[i++]);

Ми хочемо, щоб цикл виконувався через зустріч з нульовим символом (який перевіряє хибність) у кінці рядка. Для цього потрібно перевірити попереднє збільшення значення, а також збільшити індекс. Але не обов'язково в такому порядку - спосіб кодування цього з попереднім приростом буде:

int i = -1;
do {
  ++i;
  b[i] = a[i];
} while (a[i]);

Це питання смаку, що зрозуміліше, і якщо у машини є декілька регістрів, обидва повинні мати однаковий час виконання, навіть якщо [i] - це функція, яка є дорогою або має побічні ефекти. Значною різницею може бути вихідне значення індексу.

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