Recordando un poco, el preincremento
int a = 0;hace a=1 y b=1, mientras que el postincremento
int b = ++a;
int a = 0;hace a=1 y b=0, esto significa que b obtuvo el valor de a anterior al incremento.
int b = a++;
Aquí estamos utilizando el valor de retorno del operador incremento. El código ensamblador es distinto para cada caso. Vamos a echarle una mirada (antes le recomiendo ver este excelente artículo para comprender más sobre el stack y las convenciones de llamadas). En el preincremento el código ensamblador (generado por GCC 3.4.5 para i386) es:
subl $8, %esp // reservamos 8 bytes en el stack (para variables locales)En el postincremento
movl $0, -4(%ebp) // a = 0
leal -4(%ebp), %eax // eax = &a
incl (%eax) // *eax= (*eax) + 1
movl -4(%ebp), %eax // eax = a
movl %eax, -8(%ebp) // b = eax
subl $8, %esp // reservamos 8 bytes en el stackdonde se ve que el registro edx se utiliza para guardar el valor que tenía a antes del incremento para luego asignárselo a b.
movl $0, -4(%ebp) // a = 0
movl -4(%ebp), %edx // edx = a
leal -4(%ebp), %eax // eax = &a
incl (%eax) // *eax= (*eax) + 1
movl %edx, -8(%ebp) // b = edx
Pero la pregunta original es, ¿qué pasa si no usamos el valor de retorno? ¿el código de ++i o i++ es igual? Debería serlo, y de hecho, lo es. Pero como veremos más abajo, esto sólo se cumple si utilizamos tipos de datos built-in (int, double, long, etc.). Miremos el siguiente código:
void func() { }Básico, un for pero con las dos variantes posible de incremento. El código generado para ambos casos (tanto para la función pre como post) es el siguiente:
void pre () { for (int c=0; c<8; ++c) { func(); } }
void post() { for (int c=0; c<8; c++) { func(); } }
int main()
{
pre();
post();
return 0;
}
Realmente es indiferente usar cualquiera de los dos tipos de incremento, salvo, en los tipos definidos por el usuario. C++ ofrece un soporte para tipos de usuario igual a los built-in (bueno, no del todo, pero lo están solucionando). Nos da la posibilidad de sobrecargar los operadores de nuestros propios tipos (clases). Por ejemplo:
pushl %ebp // guardar el viejo puntero a la base del stack
movl %esp, %ebp // establecer la nueva base del stack
subl $4, %esp // guardar 4 bytes en el stack (para variables locales)
movl $0, -4(%ebp) // c = 0
L3:
cmpl $7, -4(%ebp)
jg L2 // si c > 7 entonces ir a L2
call _func // llamar a la función func()
leal -4(%ebp), %eax // eax = &c
incl (%eax) // *eax= (*eax) + 1
jmp L3 // repetir yendo a L3
L2:
leave // restaurar la base del stack (popl %ebp)
ret // retornar al punto de llamada
class tipo {No colocaré el código ensamblador por desbordar belleza, pero el código generado en este caso es distinto: cada incremento llama a la función correspondiente a su operador. Esto es debido a que ambas implementaciones varían considerablemente. Por ejemplo, el postincremento necesita de una instancia extra de tipo para poder devolver el anterior valor de this, en cambio, el preincremento devuelve una referencia al mismo this (sin necesidad de hacer una copia).
int x;
public:
tipo(int y) : x(y) { }
tipo(const tipo& y) : x(y.x) { }
// preincremento
tipo& operator++() {
++x;
return *this;
}
// postincremento
tipo operator++(int) {
tipo tmp(*this);
++x;
return tmp;
}
bool operator<(int y) const { return x < y; }
};
void func() { }
void pre() { for (tipo c=0; c<8; ++c) { func(); } }
void post() { for (tipo c=0; c<8; c++) { func(); } }
int main()
{
post();
return 0;
}
Como dato curioso, algo interesante ocurre al utilizar las optimizaciones del GCC. Si compilamos este último programa con el parámetro -O3, vamos a ver que todas las funciones correspondientes a la clase tipo desaparecen, y todo el código resultantes es inline, dando como resultado un código tan óptimo como si hubiéramos utilizado un int en vez de nuestra clase tipo.
¿Cómo obtengo el código ensamblador desde un archivo C/C++?
Con el compilador gcc, hay que utilizar el parámetro -S:
g++ -S -o archivo.s -c archivo.cppEn archivo.s queda el código ensamblador (sintaxis AT&T).