Variablen in Schleifen deklarieren, gute oder schlechte Praxis?

264

Frage 1: Ist das Deklarieren einer Variablen innerhalb einer Schleife eine gute oder eine schlechte Praxis?

Ich habe in den anderen Threads gelesen, ob es ein Leistungsproblem gibt oder nicht (die meisten sagten nein) und dass Sie Variablen immer so nah wie möglich deklarieren sollten, wo sie verwendet werden sollen. Ich frage mich, ob dies vermieden werden sollte oder ob es tatsächlich bevorzugt wird.

Beispiel:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

Frage 2: Erkennen die meisten Compiler, dass die Variable bereits deklariert wurde, und überspringen diesen Teil einfach, oder erstellt sie tatsächlich jedes Mal einen Platz im Speicher?

JeramyRR
quelle
29
Stellen Sie sie in die Nähe ihrer Verwendung, sofern in der Profilerstellung nichts anderes angegeben ist.
Mooing Duck
3
@drnewman Ich habe diese Threads gelesen, aber sie haben meine Frage nicht beantwortet. Ich verstehe, dass das Deklarieren von Variablen in Schleifen funktioniert. Ich frage mich, ob es eine gute Praxis ist, dies zu tun, oder ob es etwas ist, das vermieden werden sollte.
JeramyRR

Antworten:

348

Dies ist eine ausgezeichnete Praxis.

Durch das Erstellen von Variablen in Schleifen stellen Sie sicher, dass deren Gültigkeitsbereich auf die Schleife beschränkt ist. Es kann nicht außerhalb der Schleife referenziert oder aufgerufen werden.

Diesen Weg:

  • Wenn der Name der Variablen etwas "generisch" ist (wie "i"), besteht kein Risiko, ihn irgendwo später in Ihrem Code mit einer anderen Variablen mit demselben Namen zu mischen (kann auch mithilfe der -WshadowWarnanweisung in GCC verringert werden ).

  • Der Compiler weiß, dass der Variablenbereich auf innerhalb der Schleife beschränkt ist, und gibt daher eine ordnungsgemäße Fehlermeldung aus, wenn auf die Variable versehentlich an anderer Stelle verwiesen wird.

  • Last but not least kann eine bestimmte dedizierte Optimierung vom Compiler effizienter durchgeführt werden (vor allem Registerzuordnung), da er weiß, dass die Variable nicht außerhalb der Schleife verwendet werden kann. Beispielsweise muss das Ergebnis nicht für eine spätere Wiederverwendung gespeichert werden.

Kurz gesagt, Sie haben Recht, es zu tun.

Beachten Sie jedoch, dass die Variable ihren Wert zwischen den einzelnen Schleifen nicht beibehalten soll . In diesem Fall müssen Sie es möglicherweise jedes Mal initialisieren. Sie können auch einen größeren Block erstellen, der die Schleife umfasst, deren einziger Zweck darin besteht, Variablen zu deklarieren, deren Wert von einer Schleife zur anderen beibehalten werden muss. Dies schließt typischerweise den Schleifenzähler selbst ein.

{
    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 */
}

Zu Frage 2: Die Variable wird beim Aufruf der Funktion einmal zugewiesen. Aus Sicht der Zuordnung entspricht dies (fast) der Deklaration der Variablen zu Beginn der Funktion. Der einzige Unterschied ist der Umfang: Die Variable kann nicht außerhalb der Schleife verwendet werden. Es ist sogar möglich, dass die Variable nicht zugewiesen wird, sondern nur ein freier Steckplatz (von einer anderen Variablen, deren Gültigkeitsbereich beendet wurde) erneut verwendet.

Mit dem eingeschränkten und präziseren Umfang kommen genauere Optimierungen. Noch wichtiger ist jedoch, dass Ihr Code sicherer wird und weniger Zustände (dh Variablen) beim Lesen anderer Teile des Codes berücksichtigt werden müssen.

Dies gilt auch außerhalb eines if(){...}Blocks. In der Regel anstelle von:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

es ist sicherer zu schreiben:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

Der Unterschied mag geringfügig erscheinen, insbesondere bei einem so kleinen Beispiel. Bei einer größeren Codebasis hilft dies jedoch: Jetzt besteht kein Risiko mehr, einen resultWert von f1()zu f2()Block zu transportieren. Jedes resultist streng auf seinen eigenen Umfang beschränkt, wodurch seine Rolle genauer wird. Aus Sicht der Rezensenten ist es viel schöner, da er weniger Zustandsvariablen mit großer Reichweite hat, über die er sich Sorgen machen und die er verfolgen muss.

Sogar der Compiler wird besser helfen: vorausgesetzt, dass in Zukunft nach einer fehlerhaften Codeänderung resultnicht richtig mit initialisiert wird f2(). Die zweite Version weigert sich einfach zu arbeiten und gibt beim Kompilieren eine eindeutige Fehlermeldung aus (viel besser als zur Laufzeit). Die erste Version wird nichts erkennen, das Ergebnis von f1()wird einfach ein zweites Mal getestet und für das Ergebnis von verwirrt f2().

Ergänzende Information

Das Open-Source-Tool CppCheck (ein statisches Analysetool für C / C ++ - Code) bietet einige hervorragende Hinweise zum optimalen Umfang von Variablen.

Antwort auf einen Kommentar zur Zuordnung: Die obige Regel gilt für C, gilt jedoch möglicherweise nicht für einige C ++ - Klassen.

Für Standardtypen und -strukturen ist die Größe der Variablen zum Zeitpunkt der Kompilierung bekannt. In C gibt es keine "Konstruktion", daher wird der Platz für die Variable einfach dem Stapel zugewiesen (ohne Initialisierung), wenn die Funktion aufgerufen wird. Aus diesem Grund fallen beim Deklarieren der Variablen innerhalb einer Schleife Kosten von "Null" an.

Für C ++ - Klassen gibt es jedoch diese Konstruktorsache, über die ich viel weniger weiß. Ich denke, die Zuweisung wird wahrscheinlich nicht das Problem sein, da der Compiler klug genug sein wird, um denselben Speicherplatz wiederzuverwenden, aber die Initialisierung wird wahrscheinlich bei jeder Schleifeniteration stattfinden.

Cyan
quelle
4
Tolle Antwort. Genau das habe ich gesucht und mir sogar einen Einblick in etwas gegeben, das ich nicht realisiert habe. Mir war nicht klar, dass der Bereich nur innerhalb der Schleife bleibt. Danke für die Antwort!
JeramyRR
22
"Aber es wird niemals langsamer sein als die Zuweisung zu Beginn der Funktion." Das stimmt nicht immer. Die Variable wird einmal zugewiesen, aber sie wird immer noch so oft wie nötig erstellt und zerstört. Was im Fall des Beispielcodes 11-mal ist. Um Mooings Kommentar zu zitieren: "Stellen Sie sie nahe an ihre Verwendung, sofern die Profilerstellung nichts anderes sagt."
IronMensan
4
@ JeramyRR: Absolut nicht - der Compiler kann nicht wissen, ob das Objekt in seinem Konstruktor oder Destruktor bedeutende Nebenwirkungen hat.
ildjarn
2
@Iron: Wenn Sie andererseits das Element zuerst deklarieren, erhalten Sie nur viele Anrufe an den Zuweisungsoperator. Dies kostet normalerweise ungefähr das gleiche wie das Konstruieren und Zerstören eines Objekts.
Billy ONeal
4
@BillyONeal: Für stringund vectorspeziell kann der Zuweisungsoperator den zugewiesenen Puffer für jede Schleife wiederverwenden, was (abhängig von Ihrer Schleife) eine enorme Zeitersparnis bedeuten kann.
Mooing Duck
22

Im Allgemeinen ist es eine sehr gute Praxis, es sehr nah zu halten.

In einigen Fällen wird eine Überlegung wie die Leistung berücksichtigt, die das Herausziehen der Variablen aus der Schleife rechtfertigt.

In Ihrem Beispiel erstellt und zerstört das Programm die Zeichenfolge jedes Mal. Einige Bibliotheken verwenden eine kleine Zeichenfolgenoptimierung (SSO), sodass die dynamische Zuordnung in einigen Fällen vermieden werden kann.

Angenommen, Sie möchten diese redundanten Kreationen / Zuordnungen vermeiden, dann schreiben Sie sie wie folgt:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

oder Sie können die Konstante herausziehen:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

Erkennen die meisten Compiler, dass die Variable bereits deklariert wurde, und überspringen Sie einfach diesen Teil, oder erstellt sie tatsächlich jedes Mal einen Platz im Speicher?

Es kann den von der Variablen belegten Speicherplatz wiederverwenden und Invarianten aus Ihrer Schleife ziehen. Im Fall des const char-Arrays (oben) könnte dieses Array herausgezogen werden. Der Konstruktor und der Destruktor müssen jedoch bei jeder Iteration im Fall eines Objekts (z. B. std::string) ausgeführt werden. Im Fall von std::stringenthält dieses 'Leerzeichen' einen Zeiger, der die dynamische Zuordnung enthält, die die Zeichen darstellt. Also das:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

würde in jedem Fall redundantes Kopieren und dynamische Zuweisung erfordern und frei, wenn die Variable über dem Schwellenwert für die Anzahl der SSO-Zeichen liegt (und SSO von Ihrer Standardbibliothek implementiert wird).

Dies tun:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

würde immer noch eine physische Kopie der Zeichen bei jeder Iteration erfordern, aber das Formular könnte zu einer dynamischen Zuordnung führen, da Sie die Zeichenfolge zuweisen und die Implementierung erkennen sollte, dass die Größe der Sicherungszuordnung der Zeichenfolge nicht geändert werden muss. Natürlich würden Sie dies in diesem Beispiel nicht tun (da bereits mehrere überlegene Alternativen demonstriert wurden), aber Sie könnten dies in Betracht ziehen, wenn der Inhalt der Zeichenfolge oder des Vektors variiert.

Was machen Sie mit all diesen Optionen (und mehr)? Halten Sie es standardmäßig sehr nah - bis Sie die Kosten gut verstanden haben und wissen, wann Sie abweichen sollten.

Justin
quelle
1
Wird das Deklarieren der Variablen innerhalb der Schleife in Bezug auf grundlegende Datentypen wie float oder int langsamer sein als das Deklarieren dieser Variablen außerhalb der Schleife, da bei jeder Iteration ein Leerzeichen für die Variable zugewiesen werden muss?
Kasparov92
2
@ Kasparov92 Die kurze Antwort lautet "Nein. Ignorieren Sie diese Optimierung und platzieren Sie sie nach Möglichkeit in der Schleife, um die Lesbarkeit / Lokalität zu verbessern. Der Compiler kann diese Mikrooptimierung für Sie durchführen." Im Detail muss der Compiler letztendlich entscheiden, was für die Plattform, die Optimierungsstufen usw. am besten ist. Ein gewöhnliches int / float innerhalb einer Schleife wird normalerweise auf dem Stapel platziert. Ein Compiler kann dies sicherlich außerhalb der Schleife verschieben und den Speicher wiederverwenden, wenn dies optimiert wird. Aus praktischen Gründen wäre dies eine sehr, sehr, sehr kleine Optimierung…
Justin
1
@ Kasparov92… (Forts.), Die Sie nur in Umgebungen / Anwendungen berücksichtigen würden, in denen jeder Zyklus zählt. In diesem Fall möchten Sie möglicherweise nur die Verwendung von Assembly in Betracht ziehen.
Justin
14

Für C ++ hängt es davon ab, was Sie tun. OK, es ist dummer Code, aber stell dir vor

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;

}

Sie werden 55 Sekunden warten, bis Sie die Ausgabe von myFunc erhalten. Nur weil jeder Schleifenkonstruktor und Destruktor zusammen 5 Sekunden benötigt, um fertig zu werden.

Sie benötigen 5 Sekunden, bis Sie die Ausgabe von myOtherFunc erhalten.

Das ist natürlich ein verrücktes Beispiel.

Es zeigt jedoch, dass es zu einem Leistungsproblem werden kann, wenn jede Schleife dieselbe Konstruktion ausführt, wenn der Konstruktor und / oder Destruktor einige Zeit benötigt.

Nobby
quelle
2
Nun, technisch gesehen erhalten Sie in der zweiten Version die Ausgabe in nur 2 Sekunden, da Sie das Objekt noch nicht zerstört haben .....
Chrys
12

Ich habe nicht gepostet, um die Fragen von JeremyRR zu beantworten (da sie bereits beantwortet wurden). Stattdessen habe ich nur gepostet, um einen Vorschlag zu machen.

Für JeremyRR könnten Sie dies tun:

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

Ich weiß nicht, ob Sie erkennen (ich habe es nicht getan, als ich mit dem Programmieren angefangen habe), dass Klammern (solange sie paarweise sind) an einer beliebigen Stelle im Code platziert werden können, nicht nur nach "if", "for", " während "usw.

Mein Code wurde in Microsoft Visual C ++ 2010 Express kompiliert, sodass ich weiß, dass er funktioniert. Außerdem habe ich versucht, die Variable außerhalb der Klammern zu verwenden, in denen sie definiert wurde, und ich habe einen Fehler erhalten, sodass ich weiß, dass die Variable "zerstört" wurde.

Ich weiß nicht, ob es eine schlechte Praxis ist, diese Methode zu verwenden, da viele unbeschriftete Klammern den Code schnell unlesbar machen könnten, aber vielleicht könnten einige Kommentare die Dinge klären.

Fearnbuster
quelle
4
Für mich ist dies eine sehr legitime Antwort, die einen Vorschlag bringt, der direkt mit der Frage zusammenhängt. Du hast meine Stimme!
Alexis Leclerc
0

Es ist eine sehr gute Praxis, da alle obigen Antworten einen sehr guten theoretischen Aspekt der Frage liefern. Lassen Sie mich einen Blick auf Code werfen. Ich habe versucht, DFS über GEEKSFORGEEKS zu lösen. Ich stoße auf das Optimierungsproblem. Wenn Sie es versuchen Wenn Sie den Code lösen, der die Ganzzahl außerhalb der Schleife deklariert, erhalten Sie einen Optimierungsfehler.

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

Fügen Sie nun ganze Zahlen in die Schleife ein, damit Sie die richtige Antwort erhalten ...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

Dies spiegelt vollständig wider, was Sir @justin im 2. Kommentar gesagt hat. Versuchen Sie dies hier https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . Probieren Sie es einfach aus ... Sie werden es bekommen. Hoffen Sie diese Hilfe.

KhanJr
quelle
Ich denke nicht, dass dies auf die Frage zutrifft. In Ihrem obigen Fall ist es natürlich wichtig. Die Frage befasste sich mit dem Fall, dass die Variablendefinition an anderer Stelle definiert werden konnte, ohne das Verhalten des Codes zu ändern.
pcarter
In dem von Ihnen geposteten Code liegt das Problem nicht in der Definition, sondern im Initialisierungsteil. flagsollte bei jeder whileIteration bei 0 neu initialisiert werden. Das ist ein logisches Problem, kein Definitionsproblem.
Martin Véronneau
0

Kapitel 4.8 Blockstruktur in K & R ist die Programmiersprache C 2.Ed. ::

Eine in einem Block deklarierte und initialisierte automatische Variable wird bei jeder Eingabe des Blocks initialisiert.

Möglicherweise habe ich die entsprechende Beschreibung im Buch nicht gesehen:

Eine in einem Block deklarierte und initialisierte automatische Variable wird nur einmal zugewiesen, bevor der Block eingegeben wird.

Ein einfacher Test kann jedoch die Annahme beweisen:

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     
sof
quelle