การประกาศตัวแปรภายในลูปการฝึกฝนที่ดีหรือการฝึกฝนที่ไม่ดี?


265

คำถาม # 1: การประกาศตัวแปรภายในลูปเป็นแนวปฏิบัติที่ดีหรือไม่ดีหรือไม่?

ฉันได้อ่านเธรดอื่น ๆ เกี่ยวกับว่ามีปัญหาเรื่องประสิทธิภาพหรือไม่ (ส่วนใหญ่บอกว่าไม่มี) และคุณควรประกาศตัวแปรใกล้เคียงกับที่จะใช้งาน สิ่งที่ฉันสงสัยคือควรหลีกเลี่ยงสิ่งนี้หรือไม่หรือเป็นที่ต้องการจริง

ตัวอย่าง:

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

   cout << someString;
}

คำถาม # 2:คอมไพเลอร์ส่วนใหญ่รู้หรือไม่ว่ามีการประกาศตัวแปรแล้วและข้ามส่วนนั้นหรือไม่หรือมันสร้างจุดในหน่วยความจำทุกครั้งหรือไม่


29
วางไว้ใกล้กับการใช้งานของพวกเขาเว้นแต่การทำโปรไฟล์พูดอย่างอื่น
Mooing Duck

1
นี่คือบางส่วนคำถามที่คล้ายกันคือ: stackoverflow.com/questions/982963/... stackoverflow.com/questions/407255/...
drnewman

3
@drnewman ฉันอ่านกระทู้เหล่านั้นแล้ว แต่พวกเขาไม่ได้ตอบคำถามของฉัน ฉันเข้าใจว่าการประกาศตัวแปรภายในลูปทำงานได้ ฉันสงสัยว่ามันเป็นวิธีปฏิบัติที่ดีในการทำเช่นนั้นหรือถ้าเป็นสิ่งที่ต้องหลีกเลี่ยง
JeramyRR

คำตอบ:


348

นี่คือการปฏิบัติที่ยอดเยี่ยม

ด้วยการสร้างตัวแปรภายในลูปคุณมั่นใจได้ว่าขอบเขตของพวกมันถูก จำกัด ไว้ในลูป ไม่สามารถอ้างอิงหรือเรียกใช้ภายนอกลูป

ทางนี้:

  • หากชื่อของตัวแปรเป็นบิต "ทั่วไป" (เช่น "ฉัน") มีความเสี่ยงที่จะผสมกับตัวแปรที่มีชื่อเดียวกันอื่นที่ไหนสักแห่งในภายหลังในรหัสของคุณ (นอกจากนี้ยังสามารถลดลงได้โดยใช้-Wshadowคำแนะนำคำเตือนบน GCC)

  • คอมไพเลอร์รู้ว่าขอบเขตตัวแปรนั้น จำกัด อยู่ภายในลูปดังนั้นจึงจะออกข้อความแสดงข้อผิดพลาดที่เหมาะสมหากตัวแปรนั้นถูกอ้างถึงโดยไม่ได้ตั้งใจที่อื่น

  • สุดท้าย แต่ไม่ท้ายสุดการเพิ่มประสิทธิภาพเฉพาะบางอย่างสามารถทำได้อย่างมีประสิทธิภาพมากขึ้นโดยคอมไพเลอร์ (ที่สำคัญที่สุดคือการจัดสรรการลงทะเบียน) เนื่องจากรู้ว่าตัวแปรไม่สามารถใช้นอกลูปได้ ตัวอย่างเช่นไม่จำเป็นต้องเก็บผลลัพธ์ไว้เพื่อนำกลับมาใช้ใหม่ในภายหลัง

ในระยะสั้นคุณมีสิทธิ์ที่จะทำ

อย่างไรก็ตามโปรดทราบว่าตัวแปรไม่ควรเก็บค่าไว้ระหว่างแต่ละลูป ในกรณีเช่นนี้คุณอาจต้องเริ่มต้นใหม่ทุกครั้ง นอกจากนี้คุณยังสามารถสร้างบล็อกที่มีขนาดใหญ่กว่าโดยครอบคลุมลูปซึ่งมีวัตถุประสงค์เพียงอย่างเดียวคือการประกาศตัวแปรที่ต้องเก็บค่าของลูปจากที่หนึ่งไปยังอีกที่หนึ่ง โดยทั่วไปจะมีตัวนับลูป

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

สำหรับคำถาม # 2: ตัวแปรถูกจัดสรรหนึ่งครั้งเมื่อเรียกใช้ฟังก์ชัน ในความเป็นจริงจากมุมมองการจัดสรรมันเป็น (เกือบ) เหมือนกับการประกาศตัวแปรที่จุดเริ่มต้นของฟังก์ชัน ความแตกต่างเพียงอย่างเดียวคือขอบเขต: ตัวแปรไม่สามารถใช้ภายนอกลูป อาจเป็นไปได้ว่าตัวแปรไม่ได้ถูกจัดสรรเพียงแค่ใช้สล็อตฟรีบางตัวอีกครั้ง (จากตัวแปรอื่นที่สิ้นสุดขอบเขต)

ด้วยขอบเขตที่ จำกัด และแม่นยำยิ่งขึ้น แต่ที่สำคัญกว่านั้นคือทำให้รหัสของคุณปลอดภัยยิ่งขึ้นโดยมีสถานะน้อยลง (เช่นตัวแปร) ที่ต้องกังวลเมื่ออ่านส่วนอื่น ๆ ของรหัส

สิ่งนี้เป็นจริงแม้จะอยู่นอกif(){...}บล็อก โดยทั่วไปแทนที่จะเป็น:

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

มันปลอดภัยกว่าที่จะเขียน:

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

ความแตกต่างอาจดูเล็กน้อยโดยเฉพาะอย่างยิ่งในตัวอย่างเล็ก ๆ แต่ในฐานรหัสขนาดใหญ่ก็จะช่วยให้: ตอนนี้มีความเสี่ยงในการขนส่งบางไม่มีresultค่าจากf1()การf2()บล็อก แต่ละresultข้อ จำกัด ขอบเขตของตัวเองอย่างเคร่งครัดทำให้บทบาทของมันแม่นยำยิ่งขึ้น จากมุมมองของผู้ตรวจสอบมันยอดเยี่ยมกว่าเนื่องจากเขามีตัวแปรสถานะระยะยาวน้อยกว่าที่ต้องกังวลและติดตาม

แม้คอมไพเลอร์จะช่วยให้ดีขึ้น: สมมติว่าในอนาคตหลังจากที่มีการเปลี่ยนแปลงที่ผิดพลาดของรหัสไม่ได้เริ่มต้นอย่างถูกต้องกับresult f2()รุ่นที่สองก็จะปฏิเสธที่จะทำงานโดยระบุข้อผิดพลาดที่ชัดเจนในเวลารวบรวม (ทางดีกว่าเวลาทำงาน) รุ่นแรกจะไม่เห็นอะไรเลยผลลัพธ์ของf1()การทดสอบเพียงครั้งที่สองจะสับสนกับผลลัพธ์ของf2()ก็จะได้รับการทดสอบเป็นครั้งที่สองถูกสับสนสำหรับผลมาจากการ

ข้อมูลเสริม

CppCheckเครื่องมือโอเพนซอร์ส (เครื่องมือวิเคราะห์แบบคงที่สำหรับรหัส C / C ++) ให้คำแนะนำที่ดีเกี่ยวกับขอบเขตที่เหมาะสมของตัวแปร

เพื่อตอบสนองต่อความคิดเห็นเกี่ยวกับการจัดสรร: กฎข้างต้นเป็นจริงใน C แต่อาจไม่เหมาะกับคลาส C ++ บางคลาส

สำหรับชนิดและโครงสร้างมาตรฐานขนาดของตัวแปรจะทราบได้ในเวลารวบรวม ไม่มีสิ่งที่เรียกว่า "การก่อสร้าง" ใน C ดังนั้นพื้นที่สำหรับตัวแปรจะถูกจัดสรรลงในสแต็ก (โดยไม่มีการกำหนดค่าเริ่มต้น) เมื่อเรียกใช้ฟังก์ชัน นั่นเป็นเหตุผลว่าทำไมต้นทุน "ศูนย์" เมื่อประกาศตัวแปรภายในลูป

อย่างไรก็ตามสำหรับคลาส C ++ มีคอนสตรัคเตอร์ตัวนี้ที่ฉันรู้น้อยมาก ฉันเดาว่าการจัดสรรอาจจะไม่เป็นปัญหาเนื่องจากคอมไพเลอร์จะฉลาดพอที่จะนำพื้นที่เดิมมาใช้ใหม่ได้ แต่การกำหนดค่าเริ่มต้นน่าจะเกิดขึ้นในการวนซ้ำแต่ละครั้ง


4
คำตอบที่ยอดเยี่ยม นี่คือสิ่งที่ฉันกำลังมองหาและแม้แต่ทำให้ฉันมีความเข้าใจในสิ่งที่ฉันไม่ได้ตระหนักถึง ฉันไม่ได้ตระหนักว่าขอบเขตยังคงอยู่ในวงเท่านั้น ขอบคุณสำหรับคำตอบ!
JeramyRR

22
"แต่มันจะไม่ช้ากว่าการจัดสรรที่จุดเริ่มต้นของฟังก์ชั่น" สิ่งนี้ไม่เป็นความจริงเสมอไป ตัวแปรจะได้รับการจัดสรรหนึ่งครั้ง แต่จะยังคงถูกสร้างขึ้นและถูกทำลายหลายครั้งตามที่ต้องการ ซึ่งในกรณีของรหัสตัวอย่างคือ 11 ครั้ง เพื่ออ้างอิงความคิดเห็นของ Mooing "วางไว้ใกล้กับการใช้งาน
IronMensan

4
@ JeramyRR: ไม่อย่างแน่นอน - ผู้เรียบเรียงไม่มีทางรู้ว่าวัตถุนั้นมีผลข้างเคียงที่มีความหมายในตัวสร้างหรือผู้ทำลาย
ildjarn

2
@Iron: ในทางกลับกันเมื่อคุณประกาศรายการแรกคุณจะได้รับสายจำนวนมากไปยังผู้ดำเนินการที่ได้รับมอบหมาย ซึ่งโดยทั่วไปแล้วจะมีค่าใช้จ่ายประมาณเดียวกันกับการสร้างและทำลายวัตถุ
Billy ONeal

4
@BillyONeal: สำหรับstringและvectorโดยเฉพาะผู้ประกอบการที่ได้รับมอบหมายสามารถนำบัฟเฟอร์ที่จัดสรรกลับมาใช้ใหม่แต่ละวงซึ่ง (ขึ้นอยู่กับวงของคุณ) อาจเป็นการประหยัดเวลาได้มาก
Mooing Duck

22

โดยทั่วไปแล้วเป็นวิธีปฏิบัติที่ดีมากเพื่อให้ใกล้เคียงที่สุด

ในบางกรณีจะมีการพิจารณาเช่นประสิทธิภาพซึ่งปรับการดึงตัวแปรออกจากลูป

ในตัวอย่างของคุณโปรแกรมสร้างและทำลายสตริงในแต่ละครั้ง บางไลบรารีใช้การเพิ่มประสิทธิภาพสตริงขนาดเล็ก (SSO) ดังนั้นการจัดสรรแบบไดนามิกสามารถหลีกเลี่ยงได้ในบางกรณี

สมมติว่าคุณต้องการหลีกเลี่ยงการสร้าง / จัดสรรซ้ำคุณจะเขียนเป็น:

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

หรือคุณสามารถดึงค่าคงที่ออกมา:

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

คอมไพเลอร์ส่วนใหญ่ทราบหรือไม่ว่ามีการประกาศตัวแปรแล้วและข้ามส่วนนั้นไปหรือไม่หรือสร้างจุดในหน่วยความจำทุกครั้งหรือไม่

มันสามารถใช้พื้นที่ที่ตัวแปรใช้ซ้ำและสามารถดึงค่าคงที่ออกจากลูปของคุณ ในกรณีของอาร์เรย์ const char (ด้านบน) - อาร์เรย์นั้นสามารถดึงออกมาได้ อย่างไรก็ตามตัวสร้างและ destructor ต้องถูกดำเนินการในแต่ละการวนซ้ำในกรณีของวัตถุ (เช่นstd::string) ในกรณีของstd::string'ช่องว่าง' นั้นจะมีตัวชี้ซึ่งมีการจัดสรรแบบไดนามิกที่แสดงถึงตัวอักษร ดังนั้นนี่:

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

จะต้องมีการคัดลอกซ้ำซ้อนในแต่ละกรณีและการจัดสรรแบบไดนามิกและฟรีหากตัวแปรตั้งอยู่เหนือขีด จำกัด สำหรับจำนวนอักขระ SSO (และ SSO ถูกนำไปใช้โดยไลบรารี std ของคุณ)

ทำสิ่งนี้:

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

จะยังคงต้องการสำเนาแบบฟิสิคัลของอักขระในแต่ละการวนซ้ำ แต่แบบฟอร์มอาจส่งผลให้เกิดการจัดสรรแบบไดนามิกเดียวเนื่องจากคุณกำหนดสตริงและการนำไปใช้ควรเห็นว่าไม่จำเป็นต้องปรับขนาดการจัดสรรการสำรองข้อมูลของสตริง แน่นอนคุณจะไม่ทำอย่างนั้นในตัวอย่างนี้ (เพราะมีทางเลือกที่เหนือกว่าหลายตัวได้แสดงให้เห็นแล้ว) แต่คุณอาจพิจารณาเมื่อสตริงหรือเวกเตอร์มีเนื้อหาที่แตกต่างกันไป

ดังนั้นคุณจะทำอย่างไรกับตัวเลือกเหล่านี้ (และอื่น ๆ )? เก็บไว้ใกล้มากเป็นค่าเริ่มต้น - จนกว่าคุณจะเข้าใจค่าใช้จ่ายได้ดีและรู้ว่าเมื่อไหร่ควรเบี่ยงเบน


1
เกี่ยวกับประเภทข้อมูลพื้นฐานเช่น float หรือ int จะประกาศตัวแปรภายในลูปจะช้ากว่าการประกาศตัวแปรที่อยู่นอกลูปเพราะมันจะต้องจัดสรรพื้นที่สำหรับตัวแปรแต่ละรอบซ้ำหรือไม่
Kasparov92

2
@ Kasparov92 คำตอบสั้น ๆ คือ"ไม่สนใจการเพิ่มประสิทธิภาพนั้นและวางไว้ในลูปเมื่อเป็นไปได้เพื่อปรับปรุงความสามารถในการอ่าน / ตำแหน่งท้องถิ่นคอมไพเลอร์สามารถดำเนินการเพิ่มประสิทธิภาพแบบไมโครสำหรับคุณได้" ในรายละเอียดเพิ่มเติมท้ายที่สุดแล้วสำหรับคอมไพเลอร์ในการตัดสินใจตามสิ่งที่ดีที่สุดสำหรับแพลตฟอร์มระดับการปรับให้เหมาะสม ฯลฯ int / float ปกติภายในลูปจะถูกวางลงบนสแต็ก คอมไพเลอร์สามารถย้ายที่นอกลูปและนำที่เก็บมาใช้ซ้ำได้หากมีการปรับให้เหมาะสม สำหรับการใช้งานจริงนี่จะเป็นการเพิ่มประสิทธิภาพที่เล็กมาก ๆ …
justin

1
@ Kasparov92 ... (ต่อ) ซึ่งคุณจะต้องพิจารณาในสภาพแวดล้อม / แอพพลิเคชั่นที่มีการนับทุกรอบ ในกรณีนี้คุณอาจต้องการลองใช้แอสเซมบลี
justin

14

สำหรับ C ++ ขึ้นอยู่กับสิ่งที่คุณกำลังทำ ตกลงมันเป็นรหัสโง่ แต่คิด

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;

}

คุณจะรอ 55 วินาทีจนกว่าคุณจะได้ผลลัพธ์ของ myFunc เพียงเพราะแต่ละตัวสร้างลูปและ destructor ร่วมกันต้องใช้เวลา 5 วินาที

คุณจะต้อง 5 วินาทีจนกว่าคุณจะได้รับผลลัพธ์ของ myOtherFunc

แน่นอนว่านี่เป็นตัวอย่างที่บ้าคลั่ง

แต่มันแสดงให้เห็นว่ามันอาจกลายเป็นปัญหาด้านประสิทธิภาพเมื่อแต่ละลูปมีการก่อสร้างเดียวกันเมื่อตัวสร้างและ / หรือ destructor ต้องการเวลา


2
ดีในทางเทคนิคในรุ่นที่สองคุณจะได้รับผลในเวลาเพียง 2 วินาทีเพราะคุณยังไม่ได้ destructed วัตถุเลย .....
Chrys

12

ฉันไม่ได้โพสต์เพื่อตอบคำถามของ JeremyRR (เพราะได้รับคำตอบแล้ว); ฉันโพสต์เพียงเพื่อให้คำแนะนำแทน

สำหรับ JeremyRR คุณสามารถทำได้:

{
  string someString = "testing";   

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

  // The variable is in scope.
}

// The variable is no longer in scope.

ฉันไม่รู้ว่าคุณรู้หรือไม่ (ฉันไม่ได้เริ่มเขียนโปรแกรมครั้งแรก) วงเล็บนั้น (ตราบเท่าที่พวกเขาอยู่เป็นคู่) สามารถวางที่ใดก็ได้ภายในรหัสไม่ใช่แค่หลังจาก "ถ้า", "สำหรับ", " ในขณะที่ "ฯลฯ

รหัสของฉันรวบรวมใน Microsoft Visual C ++ 2010 Express ดังนั้นฉันรู้ว่ามันใช้งานได้ นอกจากนี้ฉันได้ลองใช้ตัวแปรนอกวงเล็บที่กำหนดไว้และฉันได้รับข้อผิดพลาดดังนั้นฉันจึงรู้ว่าตัวแปรนั้น "ทำลาย"

ฉันไม่ทราบว่าเป็นวิธีปฏิบัติที่ไม่ถูกต้องหรือไม่ที่จะใช้วิธีนี้เนื่องจากวงเล็บที่ไม่มีป้ายกำกับจำนวนมากสามารถทำให้โค้ดอ่านไม่ได้อย่างรวดเร็ว


4
สำหรับฉันนี่คือคำตอบที่ถูกต้องตามกฎหมายซึ่งนำข้อเสนอแนะที่เชื่อมโยงโดยตรงกับคำถาม คุณมีคะแนนของฉัน!
Alexis Leclerc

0

มันเป็นวิธีปฏิบัติที่ดีมากเนื่องจากคำตอบข้างต้นให้แง่มุมทางทฤษฎีที่ดีมากของคำถามให้ฉันมองเหลือบของรหัสฉันพยายามที่จะแก้ปัญหา DFS ผ่าน GEEKSFORGEEKS ฉันพบปัญหาการเพิ่มประสิทธิภาพ ...... หากคุณพยายามที่จะ แก้รหัสประกาศจำนวนเต็มนอกวงจะให้ข้อผิดพลาดการเพิ่มประสิทธิภาพ ..

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();
    }
}

ทีนี้ก็ใส่จำนวนเต็มเข้าไปในลูปนี้จะให้คำตอบที่ถูกต้อง ...

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();
    }
}

นี้อย่างสมบูรณ์สะท้อนให้เห็นถึงสิ่งที่คุณชาย @justin บอกว่าในความคิดเห็นที่ 2 .... ลองนี้ที่นี่ https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 เพียงแค่ให้มันยิง .... คุณจะได้รับมันหวังว่าจะช่วย


ฉันไม่คิดว่าสิ่งนี้ใช้ได้กับคำถาม เห็นได้ชัดว่าในกรณีของคุณข้างต้นมันเป็นเรื่องสำคัญ คำถามคือการจัดการกับกรณีเมื่อนิยามตัวแปรที่สามารถกำหนดที่อื่นโดยไม่เปลี่ยนพฤติกรรมของรหัส
pcarter

ในรหัสที่คุณโพสต์ปัญหาไม่ใช่คำจำกัดความ แต่เป็นส่วนเริ่มต้น flagควรเริ่มต้นใหม่ที่ 0 whileซ้ำแต่ละครั้ง นั่นเป็นปัญหาตรรกะไม่ใช่ปัญหานิยาม
Martin Véronneau

0

บทที่ 4.8 โครงสร้างบล็อกในภาษาโปรแกรม Cของ K & R :

ตัวแปรอัตโนมัติที่ประกาศและกำหนดค่าเริ่มต้นในบล็อกนั้นจะเริ่มต้นทุกครั้งที่มีการป้อนบล็อก

ฉันอาจไม่ได้เห็นคำอธิบายที่เกี่ยวข้องในหนังสือเช่น:

ตัวแปรอัตโนมัติที่ประกาศและเริ่มต้นในบล็อกนั้นได้รับการจัดสรรเพียงครั้งเดียวก่อนที่บล็อกจะถูกป้อน

แต่การทดสอบอย่างง่าย ๆ สามารถพิสูจน์สมมติฐานที่จัดขึ้น:

 #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;                                                                                                         
 }                                                                                                                     
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.