c++ - programación - ¿Declarar variables dentro de bucles, buenas prácticas o malas prácticas?
cuáles son las buenas prácticas de programación (4)
En general, es una muy buena práctica mantenerlo muy cerca.
En algunos casos, habrá una consideración como el rendimiento que justifica sacar la variable del ciclo.
En su ejemplo, el programa crea y destruye la cadena cada vez. Algunas bibliotecas utilizan una pequeña optimización de cadenas (SSO), por lo que la asignación dinámica podría evitarse en algunos casos.
Supongamos que quieres evitar esas creaciones / asignaciones redundantes, lo escribirías como:
for (int counter = 0; counter <= 10; counter++) {
// compiler can pull this out
const char testing[] = "testing";
cout << testing;
}
o puedes sacar la constante:
const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
cout << testing;
}
¿La mayoría de los compiladores se dan cuenta de que la variable ya ha sido declarada y se saltean esa parte, o realmente crean un lugar para ella en la memoria cada vez?
Puede reutilizar el espacio que consume la variable , y puede extraer invariantes de su ciclo. En el caso de la matriz const char (arriba), esa matriz podría ser extraída. Sin embargo, el constructor y el destructor se deben ejecutar en cada iteración en el caso de un objeto (como std::string
). En el caso de std::string
, ese ''espacio'' incluye un puntero que contiene la asignación dinámica que representa los caracteres. Así que esto:
for (int counter = 0; counter <= 10; counter++) {
string testing = "testing";
cout << testing;
}
requeriría una copia redundante en cada caso, y una asignación dinámica y libre si la variable se encuentra por encima del umbral para el recuento de caracteres SSO (y SSO es implementado por su biblioteca std).
Haciendo esto:
string testing;
for (int counter = 0; counter <= 10; counter++) {
testing = "testing";
cout << testing;
}
aún requeriría una copia física de los caracteres en cada iteración, pero el formulario podría dar como resultado una asignación dinámica porque usted asigna la cadena y la implementación debería ver que no hay necesidad de cambiar el tamaño de la asignación de respaldo de la cadena. Por supuesto, no harías eso en este ejemplo (porque ya se han demostrado múltiples alternativas superiores), pero podrías considerarlo cuando varía el contenido de la cadena o el vector.
Entonces, ¿qué haces con todas esas opciones (y más)? Manténgalo cerca de manera predeterminada, hasta que comprenda bien los costos y sepa cuándo debe desviarse.
Pregunta # 1: ¿Declarar una variable dentro de un ciclo una buena práctica o una mala práctica?
He leído los otros hilos sobre si hay o no un problema de rendimiento (la mayoría dijo que no), y que siempre debe declarar las variables lo más cerca posible de dónde se van a usar. Lo que me pregunto es si esto se debe evitar o si realmente se prefiere.
Ejemplo:
for(int counter = 0; counter <= 10; counter++)
{
string someString = "testing";
cout << someString;
}
Pregunta n. ° 2: ¿La mayoría de los compiladores se dan cuenta de que la variable ya ha sido declarada y se saltean esa parte, o realmente crean un lugar para ella en la memoria cada vez?
Esta es una excelente práctica.
Al crear variables dentro de los bucles, se asegura de que su alcance esté restringido dentro del bucle. No se puede hacer referencia ni llamar fuera del bucle.
De esta manera:
Si el nombre de la variable es un poco "genérico" (como "i"), no hay riesgo de mezclarlo con otra variable del mismo nombre en algún lugar posterior de tu código (también se puede mitigar usando la instrucción de advertencia de
-Wshadow
en GCC )El compilador sabe que el ámbito de la variable está limitado al interior del ciclo y, por lo tanto, emitirá un mensaje de error adecuado si la variable se llama por error en otro lugar.
Por último, el compilador puede realizar algunas optimizaciones específicas de forma más eficiente (lo que es más importante, registrar la asignación), ya que sabe que la variable no se puede usar fuera del ciclo. Por ejemplo, no es necesario almacenar el resultado para su posterior reutilización.
En resumen, tienes razón para hacerlo.
Sin embargo, tenga en cuenta que no se supone que la variable retenga su valor entre cada ciclo. En tal caso, es posible que deba inicializarlo cada vez. También puede crear un bloque más grande, que abarque el bucle, cuyo único propósito es declarar variables que deben retener su valor de un bucle a otro. Esto generalmente incluye el contador de bucle en sí.
{
int i, retainValue;
for (i=0; i<N; i++)
{
int tmpValue;
/* tmpValue is uninitialized */
/* retainValue still has its previous value from previous loop */
/* Do some stuff here */
}
/* Here, retainValue is still valid; tmpValue no longer */
}
Para la pregunta n. ° 2: la variable se asigna una vez, cuando se llama a la función. De hecho, desde una perspectiva de asignación, es (casi) lo mismo que declarar la variable al comienzo de la función. La única diferencia es el alcance: la variable no se puede usar fuera del ciclo. Incluso es posible que la variable no esté asignada, simplemente reutilizando un espacio libre (de otra variable cuyo alcance ha finalizado).
Con un alcance restringido y más preciso, se obtienen optimizaciones más precisas. Pero lo más importante es que hace que su código sea más seguro, con menos estados (es decir, variables) de los que preocuparse cuando lee otras partes del código.
Esto es cierto incluso fuera de un ciclo if(){...}
. Por lo general, en lugar de:
int result;
(...)
result = f1();
if (result) then { (...) }
(...)
result = f2();
if (result) then { (...) }
es más seguro escribir:
(...)
{
int const result = f1();
if (result) then { (...) }
}
(...)
{
int const result = f2();
if (result) then { (...) }
}
La diferencia puede parecer menor, especialmente en un ejemplo tan pequeño. Pero en una base de código más grande, ayudará: ahora no hay riesgo de transportar algún valor de result
desde el bloque f1()
a f2()
. Cada result
está estrictamente limitado a su propio alcance, por lo que su función es más precisa. Desde la perspectiva del crítico, es mucho más agradable, ya que tiene menos variables de estado de largo alcance de las que preocuparse y rastrear.
Incluso el compilador ayudará mejor: suponiendo que, en el futuro, después de un cambio erróneo de código, el result
no se inicialice correctamente con f2()
. La segunda versión simplemente se negará a funcionar, indicando un mensaje de error claro en tiempo de compilación (mucho mejor que el tiempo de ejecución). La primera versión no detectará nada, el resultado de f1()
simplemente se probará una segunda vez, confundiéndose por el resultado de f2()
.
Información complementaria
La herramienta de código abierto CppCheck (una herramienta de análisis estático para el código C / C ++) proporciona algunos consejos excelentes sobre el alcance óptimo de las variables.
En respuesta a un comentario sobre la asignación: la regla anterior es verdadera en C, pero puede que no sea para algunas clases de C ++.
Para tipos y estructuras estándar, el tamaño de la variable se conoce en el momento de la compilación. No existe tal cosa como "construcción" en C, por lo que el espacio para la variable simplemente se asignará a la pila (sin ninguna inicialización), cuando se llame a la función. Es por eso que hay un costo "cero" al declarar la variable dentro de un bucle.
Sin embargo, para las clases de C ++, existe esta cosa del constructor de la cual sé mucho menos. Supongo que la asignación probablemente no sea el problema, ya que el compilador será lo suficientemente astuto como para reutilizar el mismo espacio, pero es probable que la inicialización tenga lugar en cada iteración de bucle.
No publiqué para responder las preguntas de JeremyRR (ya que ya fueron respondidas); en su lugar, publiqué simplemente para dar una sugerencia.
Para JeremyRR, podrías hacer esto:
{
string someString = "testing";
for(int counter = 0; counter <= 10; counter++)
{
cout << someString;
}
// The variable is in scope.
}
// The variable is no longer in scope.
No sé si te das cuenta (no lo hice cuando comencé a programar), que los corchetes (siempre que estén en pares) pueden colocarse en cualquier lugar dentro del código, no solo después de "si", "para", " while ", etc.
Mi código compilado en Microsoft Visual C ++ 2010 Express, así que sé que funciona; Además, he tratado de usar la variable fuera de los corchetes en los que estaba definido y recibí un error, así que sé que la variable fue "destruida".
No sé si es una mala práctica utilizar este método, ya que una gran cantidad de corchetes no etiquetados podrían hacer que el código no se pueda leer rápidamente, pero tal vez algunos comentarios podrían aclarar las cosas.
Para C ++ depende de lo que estás haciendo. OK, es un código estúpido, pero imagina
class myTimeEatingClass { public: //constructor myTimeEatingClass() { sleep(2000); ms_usedTime+=2; } ~myTimeEatingClass() { sleep(3000); ms_usedTime+=3; } const unsigned int getTime() const { return ms_usedTime; } static unsigned int ms_usedTime; };
myTimeEatingClass::ms_CreationTime=0;
myFunc()
{
for (int counter = 0; counter <= 10; counter++) {
myTimeEatingClass timeEater();
//do something
}
cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;
}
myOtherFunc()
{
myTimeEatingClass timeEater();
for (int counter = 0; counter <= 10; counter++) {
//do something
}
cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;
}
Esperará 55 segundos hasta que obtenga el resultado de myFunc. Solo porque cada contructor de bucle y destructor juntos necesitan 5 segundos para terminar.
Necesitarás 5 segundos hasta que obtengas la salida de myOtherFunc.
Por supuesto, este es un ejemplo loco.
Pero ilustra que podría convertirse en un problema de rendimiento cuando cada ciclo se realiza la misma construcción cuando el constructor y / o destructor necesita algo de tiempo.