Incrementando em C ++ - Quando usar x ++ ou ++ x?


91

Atualmente estou aprendendo C ++ e aprendi sobre incrementação há um tempo. Eu sei que você pode usar "++ x" para fazer a incrementação antes e "x ++" para fazer depois.

Ainda assim, eu realmente não sei quando usar nenhum dos dois ... Eu nunca usei "++ x" e as coisas sempre funcionaram bem até agora - então, quando devo usá-lo?

Exemplo: Em um loop for, quando é preferível usar "++ x"?

Além disso, alguém poderia explicar exatamente como funcionam as diferentes incrementações (ou decrementações)? Eu realmente apreciaria isto.

Respostas:


112

Não é uma questão de preferência, mas de lógica.

x++incrementa o valor da variável x após processar a instrução atual.

++xincrementa o valor da variável x antes de processar a instrução atual.

Portanto, basta decidir sobre a lógica que você escreve.

x += ++iirá incrementar i e adicionar i + 1 a x. x += i++irá adicionar i a x e, em seguida, incrementar i.


27
e observe que em um loop for, em primativos, não há absolutamente nenhuma diferença. Muitos estilos de codificação recomendam nunca usar um operador de incremento onde possa ser mal interpretado; ou seja, x ++ ou ++ x deve existir apenas em sua própria linha, nunca como y = x ++. Pessoalmente, não gosto disso, mas é incomum
Mikeage

2
E se usado em sua própria linha, o código gerado é quase o mesmo.
Nosredna

14
Isso pode parecer pedantismo (principalmente porque é :)), mas em C ++, x++é um rvalue com o valor de xantes do incremento, x++é um lvalue com o valor de xdepois de um incremento. Nenhuma das expressões garante quando o valor real incrementado é armazenado de volta para x, só é garantido que isso aconteça antes do próximo ponto de sequência. 'após o processamento da instrução atual' não é estritamente preciso, pois algumas expressões têm pontos de sequência e algumas instruções são instruções compostas.
CB Bailey

10
Na verdade, a resposta é enganosa. O momento em que a variável x é modificada provavelmente não difere na prática. A diferença é que x ++ é definido para retornar um rvalue do valor anterior de x enquanto ++ x ainda se refere à variável x.
sellibitze

5
@BeowulfOF: A resposta implica uma ordem que não existe. Não há nada no padrão a dizer quando os incrementos ocorrem. O compilador pode implementar "x + = i ++" como: int j = i; i = i + 1; x + = j; "(ou seja, 'i' incrementado antes de" processar a instrução atual "). É por isso que" i = i ++ "tem comportamento indefinido e é por isso que acho que a resposta precisa de" ajustes ". A descrição de" x + = ++ i "está correto, pois não há sugestão de ordem:" aumentará i e adicionará i + 1 a x ".
Richard Corden

53

Scott Meyers diz para você preferir prefixo, exceto nas ocasiões em que a lógica ditaria que o pós-fixo é apropriado.

Item 6 do "C ++ Mais Eficaz" - é autoridade suficiente para mim.

Para quem não possui o livro, aqui estão as citações pertinentes. Da página 32:

De seus dias como um programador C, você deve se lembrar que a forma de prefixo do operador de incremento é algumas vezes chamada de "incremento e busca", enquanto a forma de pós-fixada costuma ser conhecida como "busca e incremento". As duas frases são importantes para lembrar, porque todas elas atuam como especificações formais ...

E na página 34:

Se você é do tipo que se preocupa com a eficiência, provavelmente começou a suar quando viu a função de incremento postfix. Essa função deve criar um objeto temporário para seu valor de retorno e a implementação acima também cria um objeto temporário explícito que deve ser construído e destruído. A função de incremento de prefixo não tem tais temporários ...


4
Se o compilador não perceber que o valor antes do incremento não é natural, ele pode implementar o incremento pós-fixado em várias instruções - copie o valor antigo e depois incremente. O incremento do prefixo deve ser sempre apenas uma instrução.
gnud

8
Eu testei isso ontem com gcc: em um loop for no qual o valor é jogado fora após a execução i++ou ++i, o código gerado é o mesmo.
Giorgio de

Experimente fora do loop for. O comportamento em uma atribuição deve ser diferente.
duffymo

Eu discordo explicitamente de Scott Meyers em seu segundo ponto - geralmente é irrelevante, pois 90% ou mais casos de "x ++" ou "++ x" são normalmente isolados de qualquer atribuição e os otimizadores são inteligentes o suficiente para reconhecer que nenhuma variável temporária precisa ser criado em tais casos. Nesse caso, as duas formas são completamente intercambiáveis. A implicação disso é que as bases de código antigas crivadas de "x ++" devem ser deixadas em paz - é mais provável que você introduza erros sutis alterando-as para "++ x" do que para melhorar o desempenho em qualquer lugar. Provavelmente é melhor usar "x ++" e fazer as pessoas pensarem.
omatai

2
Você pode confiar em Scott Meyers o quanto quiser, mas se seu código é tão dependente do desempenho que qualquer diferença de desempenho entre ++xe x++realmente importa, é muito mais importante que você realmente use um compilador que possa otimizar completa e adequadamente qualquer versão, não importa qual contexto. "Já que estou usando este martelo velho de baixa qualidade, só posso cravar os pregos em um ângulo de 43,7 graus" é um argumento fraco para construir uma casa enfiando os pregos a apenas 43,7 graus. Use uma ferramenta melhor.
Andrew Henle

28

De cppreference ao incrementar iteradores:

Você deve preferir o operador pré-incremento (++ iter) ao operador pós-incremento (iter ++) se não for usar o valor antigo. O pós-incremento é geralmente implementado da seguinte forma:

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

Obviamente, é menos eficiente do que o pré-incremento.

O pré-incremento não gera o objeto temporário. Isso pode fazer uma diferença significativa se o seu objeto for caro para criar.


8

Eu só quero notar que o código gerado é frequentemente o mesmo se você usar pré / pós-incremento onde a semântica (de pré / pós) não importa.

exemplo:

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
Para um tipo primitivo como um inteiro, sim. Você verificou qual é a diferença para algo como um std::map::iterator? Claro que os dois operadores são diferentes, mas estou curioso para saber se o compilador otimizará postfix para prefix se o resultado não for usado. Não acho que seja permitido - visto que a versão pós-fixada pode conter efeitos colaterais.
setembro

Além disso, ' o compilador provavelmente vai perceber que você não precisa do efeito colateral e otimizá-lo ' não deve ser uma desculpa para escrever um código desleixado que usa os operadores postfix mais complexos sem qualquer razão, além do presumível fato de que tantos supostos materiais de ensino usam postfix sem motivo aparente e são copiados no atacado.
sublinhado_d

6

O mais importante a se ter em mente, imo, é que x ++ precisa retornar o valor antes que o incremento realmente ocorra - portanto, ele deve fazer uma cópia temporária do objeto (pré-incremento). Isso é menos eficiente do que ++ x, que é incrementado no local e retornado.

Outra coisa que vale a pena mencionar, entretanto, é que a maioria dos compiladores serão capazes de otimizar essas coisas desnecessárias quando possível, por exemplo, ambas as opções levarão ao mesmo código aqui:

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

4

Eu concordo com @BeowulfOF, embora, para maior clareza, eu sempre defendesse a divisão das declarações para que a lógica fosse absolutamente clara, ou seja:

i++;
x += i;

ou

x += i;
i++;

Portanto, minha resposta é: se você escrever um código claro, isso raramente importará (e se for importante, seu código provavelmente não é claro o suficiente).


2

Só queria enfatizar novamente que se espera que ++ x seja mais rápido que x ++, (especialmente se x for um objeto de algum tipo arbitrário), portanto, a menos que seja necessário por razões lógicas, ++ x deve ser usado.


2
Só quero enfatizar que essa ênfase é mais do que provavelmente enganosa. Se você está olhando para algum loop que termina com um "x ++" isolado e pensando "Aha! - é por isso que está executando tão lento!" e você muda para "++ x", então não espera exatamente nenhuma diferença. Os otimizadores são inteligentes o suficiente para reconhecer que nenhuma variável temporária precisa ser criada quando ninguém usará seus resultados. A implicação é que as bases de código antigas crivadas de "x ++" devem ser deixadas em paz - é mais provável que você introduza erros alterando-as do que para melhorar o desempenho em qualquer lugar.
omatai

1

Você explicou a diferença corretamente. Depende apenas se você deseja que x seja incrementado antes de cada execução em um loop, ou depois disso. Depende da lógica do seu programa, o que é apropriado.

Uma diferença importante ao lidar com Iteradores STL (que também implementam esses operadores) é que ++ cria uma cópia do objeto para o qual o iterador aponta, a seguir incrementa e retorna a cópia. ++ ele, por outro lado, faz o incremento primeiro e depois retorna uma referência ao objeto para o qual o iterador agora aponta. Isso é principalmente relevante apenas quando cada bit de desempenho conta ou quando você implementa seu próprio iterador de STL.

Editar: corrigiu a confusão de notação de prefixo e sufixo


A conversa de "antes / depois" da iteração de um loop só tem sentido se o aumento / diminuição pré / pós ocorrer na condição. Mais frequentemente, estará na cláusula de continuação, onde não pode mudar nenhuma lógica, embora possa ser mais lento para os tipos de classe usarem postfix e as pessoas não devam usar isso sem motivo.
sublinhado_d

1

Forma pós-fixada de ++, operador - segue a regra use-então-mude ,

A forma do prefixo (++ x, - x) segue a regra mude-depois-use .

Exemplo 1:

Quando vários valores são colocados em cascata com << usando cout , os cálculos (se houver) ocorrem da direita para a esquerda, mas a impressão ocorre da esquerda para a direita, por exemplo, (se val se inicialmente 10)

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

resultará em

12    10    10 

Exemplo 2:

No Turbo C ++, se múltiplas ocorrências de ++ ou (em qualquer forma) são encontradas em uma expressão, então primeiro todas as formas de prefixo são calculadas, então a expressão é avaliada e finalmente as formas pós-fixadas são calculadas, por exemplo,

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

Sua saída em Turbo C ++ será

48 13

Considerando que sua saída no compilador moderno será (porque eles seguem as regras estritamente)

45 13
  • Nota: O uso múltiplo de operadores de incremento / decremento na mesma variável em uma expressão não é recomendado. O tratamento / resultados de tais
    expressões variam de compilador para compilador.

Não é que as expressões contendo múltiplas operações de aumento / diminuição "variam de compilador para compilador", mas pior: tais modificações múltiplas entre pontos de sequência têm comportamento indefinido e envenenam o programa.
sublinhado_d

0

Compreender a sintaxe da linguagem é importante ao considerar a clareza do código. Considere copiar uma sequência de caracteres, por exemplo, com pós-incremento:

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

Queremos que o loop seja executado encontrando o caractere zero (que é falso) no final da string. Isso requer o teste do pré-incremento do valor e também o incremento do índice. Mas não necessariamente nessa ordem - uma maneira de codificar isso com o pré-incremento seria:

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

É uma questão de gosto o que fica mais claro e se a máquina tem um punhado de registradores, ambos devem ter tempos de execução idênticos, mesmo que a [i] seja uma função cara ou tenha efeitos colaterais. Uma diferença significativa pode ser o valor de saída do índice.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.