Przyrost w C ++ - Kiedy używać x ++ lub ++ x?

93

Obecnie uczę się C ++, a o inkrementacji dowiedziałem się jakiś czas temu. Wiem, że możesz użyć „++ x”, aby dokonać inkrementacji przed i „x ++”, aby zrobić to po.

Mimo to naprawdę nie wiem, kiedy użyć któregokolwiek z tych dwóch… Tak naprawdę nigdy nie użyłem „++ x” i jak dotąd wszystko działało dobrze - więc kiedy powinienem go użyć?

Przykład: Kiedy w pętli for najlepiej jest użyć „++ x”?

Czy ktoś mógłby dokładnie wyjaśnić, jak działają różne przyrosty (lub dekrementacje)? Byłbym naprawdę wdzięczny.

Jesse Emond
źródło

Odpowiedzi:

118

To nie jest kwestia preferencji, ale logiki.

x++zwiększa wartość zmiennej x po przetworzeniu bieżącej instrukcji.

++xzwiększa wartość zmiennej x przed przetworzeniem bieżącej instrukcji.

Więc po prostu zdecyduj się na logikę, którą napiszesz.

x += ++izwiększy i i doda i + 1 do x. x += i++doda i do x, a następnie zwiększy i.

Oliver Friedrich
źródło
27
i zwróć uwagę, że w pętli for, na elementach pierwotnych, nie ma absolutnie żadnej różnicy. Wiele stylów kodowania zaleca, aby nigdy nie używać operatora inkrementacji, jeśli mógłby zostać źle zrozumiany; tzn. x ++ lub ++ x powinny istnieć tylko w osobnej linii, nigdy jako y = x ++. Osobiście mi się to nie podoba, ale jest to rzadkie
Mikeage
2
A jeśli zostanie użyty w osobnym wierszu, wygenerowany kod prawie na pewno będzie taki sam.
Nosredna
14
Może się to wydawać pedanterią (głównie dlatego, że tak jest :)), ale w C ++ x++jest to wartość r o wartości xprzed inkrementacją, x++to lwartość o wartości xpo inkrementacji. Żadne z wyrażeń nie gwarantuje, że rzeczywista zwiększona wartość zostanie zapisana z powrotem do x, gwarantuje jedynie, że nastąpi to przed następnym punktem sekwencji. „po przetworzeniu bieżącej instrukcji” nie jest ściśle dokładne, ponieważ niektóre wyrażenia mają punkty sekwencji, a niektóre instrukcje są instrukcjami złożonymi.
CB Bailey
10
Właściwie odpowiedź jest myląca. Moment, w którym zmienna x jest modyfikowana, prawdopodobnie nie różni się w praktyce. Różnica polega na tym, że x ++ jest zdefiniowane tak, aby zwracać wartość r poprzedniej wartości x, podczas gdy ++ x nadal odnosi się do zmiennej x.
sellibitze
5
@BeowulfOF: Odpowiedź sugeruje porządek, który nie istnieje. W normie nie ma nic do powiedzenia, kiedy nastąpi wzrost. Kompilator jest uprawniony do implementacji "x + = i ++" jako: int j = i; i = i + 1; x + = j; ”(tj.„ i ”zwiększane przed„ przetwarzaniem bieżącej instrukcji ”). Dlatego„ i = i ++ ”ma niezdefiniowane zachowanie i dlatego uważam, że odpowiedź wymaga„ poprawienia ”. Opis„ x + = ++ i "jest poprawne, ponieważ nie ma sugestii kolejności:" zwiększy i i doda i + 1 do x ".
Richard Corden
53

Scott Meyers mówi, żebyś wolał przedrostek, z wyjątkiem tych przypadków, w których logika dyktuje, że ten przyrostek jest odpowiedni.

„Bardziej efektywny C ++” punkt 6 - to dla mnie wystarczający autorytet.

Dla tych, którzy nie są właścicielami książki, oto stosowne cytaty. Od strony 32:

Z czasów, gdy byłeś programistą języka C, możesz sobie przypomnieć, że postać przedrostkowa operatora inkrementacji jest czasami nazywana „inkrementuj i pobieraj”, podczas gdy forma postfiksowa jest często nazywana „pobierz i zwiększ”. Te dwa wyrażenia są ważne do zapamiętania, ponieważ wszystkie działają tylko jako formalne specyfikacje ...

A na stronie 34:

Jeśli jesteś osobą, która martwi się o wydajność, prawdopodobnie spociłeś się, gdy po raz pierwszy zobaczyłeś funkcję przyrostu przyrostka. Ta funkcja musi utworzyć obiekt tymczasowy dla swojej wartości zwracanej, a powyższa implementacja tworzy również jawny obiekt tymczasowy, który należy skonstruować i zniszczyć. Funkcja przyrostu prefiksu nie ma takich tymczasowych ...

duffymo
źródło
4
Jeśli kompilator nie zdaje sobie sprawy, że wartość przed przyrostem jest nierównomierna, może zaimplementować przyrost przyrostka w kilku instrukcjach - skopiuj starą wartość, a następnie zwiększ. Przyrost prefiksu powinien być zawsze jedną instrukcją.
gnud
8
Zdarzyło mi się przetestować to wczoraj z gcc: w pętli for, w której wartość jest wyrzucana po wykonaniu i++lub ++iwygenerowany kod jest taki sam.
Giorgio
Wypróbuj poza pętlą for. Zachowanie w zadaniu musi być inne.
duffymo
Wyraźnie nie zgadzam się ze Scottem Meyersem w jego drugiej kwestii - zwykle jest to nieistotne, ponieważ 90% lub więcej przypadków „x ++” lub „++ x” jest zwykle izolowanych od dowolnego przypisania, a optymalizatorzy są na tyle sprytni, aby rozpoznać, że żadne tymczasowe zmienne nie wymagają być tworzone w takich przypadkach. W takim przypadku te dwie formy są całkowicie zamienne. Konsekwencją tego jest to, że stare bazy kodu pełne "x ++" powinny zostać pozostawione w spokoju - bardziej prawdopodobne jest, że wprowadzisz subtelne błędy zmieniając je na "++ x", niż gdziekolwiek poprawić wydajność. Prawdopodobnie lepiej jest użyć "x ++" i skłonić ludzi do myślenia.
omatai
2
Możesz ufać Scottowi Meyersowi, ile chcesz, ale jeśli Twój kod jest tak zależny od wydajności, że każda różnica w wydajności między ++xi x++faktycznie ma znaczenie, o wiele ważniejsze jest, aby faktycznie użyć kompilatora, który może całkowicie i prawidłowo zoptymalizować każdą wersję, bez względu na to, kontekst. „Ponieważ używam tego gównianego starego młotka, mogę wbijać gwoździe tylko pod kątem 43,7 stopnia” to kiepski argument za budowaniem domu przez wbijanie gwoździ pod kątem zaledwie 43,7 stopnia. Użyj lepszego narzędzia.
Andrew Henle
28

Z cppreference podczas zwiększania iteratorów:

Jeśli nie zamierzasz używać starej wartości, powinieneś preferować operator preinkrementacji (++ iter) zamiast operatora post-inkrementacji (iter ++). Post-inkrementacja jest generalnie implementowana w następujący sposób:

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

Oczywiście jest mniej wydajna niż preinkrementacja.

Preinkrementacja nie generuje tymczasowego obiektu. Może to mieć istotne znaczenie, jeśli tworzenie obiektu jest kosztowne.

Phillip Ngan
źródło
8

Chcę tylko zauważyć, że kod genowany jest często taki sam, jeśli używasz inkrementacji pre / post, gdzie semantyczny (pre / post) nie ma znaczenia.

przykład:

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"
kleń
źródło
5
W przypadku typu pierwotnego, takiego jak liczba całkowita, tak. Czy sprawdziłeś, jaka okazała się różnica w przypadku czegoś takiego jak a std::map::iterator? Oczywiście te dwa operatory są różne, ale jestem ciekawy, czy kompilator zoptymalizuje przedrostek do przedrostka, jeśli wynik nie zostanie użyty. Nie sądzę, żeby było to dozwolone - biorąc pod uwagę, że wersja postfix może zawierać efekty uboczne.
seh
Ponadto, `` kompilator prawdopodobnie zda sobie sprawę, że nie potrzebujesz efektu ubocznego i zoptymalizuj go '' nie powinno być wymówką do pisania niechlujnego kodu, który używa bardziej złożonych operatorów postfiksowych bez żadnego powodu, poza prawdopodobnie faktem, że tak wiele rzekome materiały dydaktyczne używają postfiksu bez wyraźnego powodu i są kopiowane w sprzedaży hurtowej.
underscore_d
6

Najważniejszą rzeczą, o której należy pamiętać, imo, jest to, że x ++ musi zwrócić wartość, zanim faktycznie nastąpił przyrost - dlatego musi wykonać tymczasową kopię obiektu (przed inkrementacją). Jest to mniej wydajne niż ++ x, które jest zwiększane w miejscu i zwracane.

Warto jednak wspomnieć, że większość kompilatorów będzie w stanie zoptymalizować takie niepotrzebne rzeczy, gdy tylko będzie to możliwe, na przykład obie opcje będą prowadzić do tego samego kodu tutaj:

for (int i(0);i<10;++i)
for (int i(0);i<10;i++)
rmn
źródło
5

Zgadzam się z @BeowulfOF, choć dla jasności zawsze opowiadałbym się za podzieleniem wypowiedzi tak, aby logika była absolutnie jasna, tj .:

i++;
x += i;

lub

x += i;
i++;

Więc moja odpowiedź brzmi: jeśli piszesz czysty kod, to rzadko powinno to mieć znaczenie (a jeśli ma to znaczenie, Twój kod prawdopodobnie nie jest wystarczająco jasny).

Oferty
źródło
2

Chciałem tylko ponownie podkreślić, że oczekuje się, że ++ x będzie szybsze niż x ++ (zwłaszcza jeśli x jest obiektem dowolnego typu), więc jeśli nie jest to wymagane ze względów logicznych, powinno być używane ++ x.

Shailesh Kumar
źródło
2
Chcę tylko podkreślić, że ten nacisk jest bardziej niż prawdopodobnie mylący. Jeśli patrzysz na jakąś pętlę, która kończy się izolowanym „x ++” i myślisz „Aha! - to jest powód, dla którego działa to tak wolno!” i zmieniasz to na „++ x”, a potem nie oczekuj dokładnie żadnej różnicy. Optymalizatory są na tyle sprytne, aby rozpoznać, że nie trzeba tworzyć zmiennych tymczasowych, gdy nikt nie będzie wykorzystywał ich wyników. Wynika z tego, że stare bazy kodu pełne "x ++" powinny zostać pozostawione w spokoju - bardziej prawdopodobne jest, że wprowadzisz błędy, zmieniając je, niż gdziekolwiek polepszasz wydajność.
omatai
1

Poprawnie wyjaśniłeś różnicę. Zależy to tylko od tego, czy chcesz, aby x zwiększał się przed każdym przebiegiem pętli, czy po tym. To zależy od logiki programu, co jest właściwe.

Istotną różnicą w pracy z Iteratorami STL (które również implementują te operatory) jest to, że ++ tworzy kopię obiektu, na który wskazuje iterator, następnie zwiększa, a następnie zwraca kopię. ++ Z drugiej strony najpierw dokonuje inkrementacji, a następnie zwraca referencję do obiektu, na który teraz wskazuje iterator. Jest to głównie istotne tylko wtedy, gdy liczy się każda wydajność lub gdy wdrażasz własny iterator STL.

Edycja: naprawiono pomieszanie notacji przedrostków i przyrostków

Björn Pollex
źródło
Mówienie o iteracji pętli „przed / po” ma znaczenie tylko wtedy, gdy w warunku występuje przyrost / dekrementacja przed / po. Częściej będzie w klauzuli kontynuacji, gdzie nie może zmienić żadnej logiki, chociaż może być wolniejsze dla typów klas, aby używać postfiksa i ludzie nie powinni używać tego bez powodu.
underscore_d
1

Postfiksowa postać ++, - operator podąża za regułą użyj-potem-zmień ,

Forma prefiksu (++ x, - x) jest zgodna z regułą zmień i użyj .

Przykład 1:

Gdy wiele wartości są kaskadowo z użyciem << cout następnie obliczeń (jeśli występują) odbywają się od prawej do lewej, ale drukowanie odbywa się od lewej do prawej, np, (jeśli val jeśli początkowo 10)

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

spowoduje

12    10    10 

Przykład 2:

W Turbo C ++, jeśli w wyrażeniu znajduje się wiele wystąpień ++ lub (w dowolnej formie), to najpierw obliczane są wszystkie formy prefiksów, następnie wartościowane jest wyrażenie i na końcu obliczane są formularze przyrostków, np.

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

To będzie wyjście w Turbo C ++

48 13

Podczas gdy w dzisiejszym kompilatorze będzie to wynik (ponieważ ściśle przestrzegają zasad)

45 13
  • Uwaga: Nie zaleca się wielokrotnego stosowania operatorów zwiększania / zmniejszania tej samej zmiennej w jednym wyrażeniu. Obsługa / wyniki takich
    wyrażeń różnią się w zależności od kompilatora.
Sunil Dhillon
źródło
Nie chodzi o to, że wyrażenia zawierające wiele operacji zwiększania / zmniejszania „różnią się od kompilatora do kompilatora”, ale raczej gorzej: takie wielokrotne modyfikacje między punktami sekwencji mają niezdefiniowane zachowanie i zatruwają program.
underscore_d
0

Zrozumienie składni języka jest ważne przy rozważaniu przejrzystości kodu. Rozważ skopiowanie ciągu znaków, na przykład z post-inkrementacją:

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

Chcemy, aby pętla była wykonywana poprzez napotkanie znaku zerowego (który testuje fałsz) na końcu łańcucha. Wymaga to przetestowania wartości przed inkrementacją, a także zwiększenia indeksu. Ale niekoniecznie w tej kolejności - sposobem na zakodowanie tego z preinkrementacją byłoby:

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

Jest to kwestia gustu, który jest wyraźniejszy i jeśli maszyna ma garść rejestrów, oba powinny mieć identyczny czas wykonania, nawet jeśli [i] jest funkcją, która jest kosztowna lub ma skutki uboczne. Istotną różnicą może być wartość wyjściowa indeksu.

shkeyser
źródło