มีข้อผิดพลาดมากมายที่ก นักพัฒนา C ++ อาจพบ สิ่งนี้สามารถทำให้การเขียนโปรแกรมคุณภาพทำได้ยากและการบำรุงรักษามีราคาแพงมาก การเรียนรู้ไวยากรณ์ของภาษาและมีทักษะในการเขียนโปรแกรมที่ดีในภาษาที่คล้ายคลึงกันเช่น C # และ Java นั้นไม่เพียงพอที่จะใช้ศักยภาพของ C ++ ได้อย่างเต็มที่ ต้องใช้ประสบการณ์หลายปีและมีระเบียบวินัยที่ดีในการหลีกเลี่ยงข้อผิดพลาดใน C ++ ในบทความนี้เราจะมาดูข้อผิดพลาดทั่วไปที่เกิดขึ้นโดยนักพัฒนาทุกระดับหากพวกเขาไม่ระมัดระวังในการพัฒนา C ++ มากพอ
ไม่ว่าเราจะพยายามแค่ไหนก็ยากมากที่จะปลดปล่อยหน่วยความจำที่จัดสรรแบบไดนามิกทั้งหมดให้เป็นอิสระ แม้ว่าเราจะทำได้ แต่ก็มักไม่ปลอดภัยจากข้อยกเว้น ให้เราดูตัวอย่างง่ายๆ:
void SomeMethod() { ClassA *a = new ClassA; SomeOtherMethod(); // it can throw an exception delete a; }
หากมีข้อยกเว้นเกิดขึ้นวัตถุ“ a” จะไม่ถูกลบ ตัวอย่างต่อไปนี้แสดงวิธีที่ปลอดภัยและสั้นกว่าในการทำเช่นนั้น ใช้ auto_ptr ซึ่งเลิกใช้แล้วใน C ++ 11 แต่มาตรฐานเก่ายังคงใช้กันอย่างแพร่หลาย สามารถแทนที่ด้วย C ++ 11 unique_ptr หรือ scoped_ptr จาก Boost ได้ถ้าเป็นไปได้
void SomeMethod() { std::auto_ptr a(new ClassA); // deprecated, please check the text SomeOtherMethod(); // it can throw an exception }
ไม่ว่าจะเกิดอะไรขึ้นหลังจากสร้างออบเจ็กต์“ a” แล้วจะถูกลบทันทีที่การเรียกใช้โปรแกรมออกจากขอบเขต
อย่างไรก็ตามนี่เป็นเพียงตัวอย่างที่ง่ายที่สุดของปัญหา C ++ นี้ มีตัวอย่างมากมายเมื่อควรทำการลบในที่อื่นบางทีอาจอยู่ในฟังก์ชันภายนอกหรือเธรดอื่น นั่นคือเหตุผลที่ควรหลีกเลี่ยงการใช้ new / delete ในคู่อย่างสมบูรณ์และควรใช้ตัวชี้อัจฉริยะที่เหมาะสมแทน
นี่เป็นหนึ่งในข้อผิดพลาดที่พบบ่อยที่สุดที่นำไปสู่การรั่วไหลของหน่วยความจำภายในคลาสที่ได้รับหากมีการจัดสรรหน่วยความจำแบบไดนามิกภายใน มีบางกรณีที่ไม่พึงปรารถนาตัวทำลายเสมือนเช่นเมื่อคลาสไม่ได้มีไว้สำหรับการสืบทอดขนาดและประสิทธิภาพของมันเป็นสิ่งสำคัญ ตัวทำลายเสมือนหรือฟังก์ชันเสมือนอื่น ๆ จะแนะนำข้อมูลเพิ่มเติมภายในโครงสร้างคลาสเช่นตัวชี้ไปยังตารางเสมือนซึ่งทำให้ขนาดของอินสแตนซ์ใด ๆ ของคลาสใหญ่ขึ้น
อย่างไรก็ตามในกรณีส่วนใหญ่สามารถสืบทอดคลาสได้แม้ว่าจะไม่ได้ตั้งใจไว้ แต่แรกก็ตาม ดังนั้นจึงเป็นแนวทางปฏิบัติที่ดีมากในการเพิ่ม virtual destructor เมื่อมีการประกาศคลาส มิฉะนั้นหากคลาสต้องไม่มีฟังก์ชันเสมือนเนื่องจากเหตุผลด้านประสิทธิภาพการแสดงความคิดเห็นในไฟล์การประกาศคลาสเป็นแนวทางปฏิบัติที่ดีที่ระบุว่าไม่ควรสืบทอดคลาส หนึ่งในตัวเลือกที่ดีที่สุดในการหลีกเลี่ยงปัญหานี้คือการใช้ IDE ที่สนับสนุนการสร้างตัวทำลายเสมือนระหว่างการสร้างคลาส
ประเด็นเพิ่มเติมอีกประการหนึ่งของหัวเรื่องคือคลาส / เทมเพลตจากไลบรารีมาตรฐาน ไม่ได้มีไว้สำหรับการสืบทอดและไม่มีตัวทำลายเสมือน ตัวอย่างเช่นหากเราสร้างคลาสสตริงที่ปรับปรุงใหม่ที่สืบทอดต่อสาธารณะจาก std :: string มีความเป็นไปได้ที่ใครบางคนจะใช้มันอย่างไม่ถูกต้องกับตัวชี้หรือการอ้างอิงถึง std :: string และทำให้หน่วยความจำรั่วไหล
class MyString : public std::string { ~MyString() { // ... } }; int main() { std::string *s = new MyString(); delete s; // May not invoke the destructor defined in MyString }
เพื่อหลีกเลี่ยงปัญหา C ++ ดังกล่าววิธีที่ปลอดภัยกว่าในการนำคลาส / เทมเพลตจากไลบรารีมาตรฐานมาใช้ซ้ำคือการใช้การสืบทอดหรือองค์ประกอบส่วนตัว
ฟังก์ชันคือฟังก์ชันที่เขียนไว้แล้วและจัดให้เป็นส่วนหนึ่งของระบบ
การสร้างอาร์เรย์ชั่วคราวที่มีขนาดไดนามิกมักจำเป็น หลังจากที่ไม่จำเป็นต้องใช้อีกต่อไปสิ่งสำคัญคือต้องเพิ่มหน่วยความจำที่จัดสรรให้ว่าง ปัญหาใหญ่ที่นี่คือ C ++ ต้องการตัวดำเนินการลบพิเศษที่มีเครื่องหมายวงเล็บ [] ซึ่งลืมได้ง่ายมาก ตัวดำเนินการลบ [] จะไม่เพียงแค่ลบหน่วยความจำที่จัดสรรให้กับอาร์เรย์เท่านั้น แต่จะเรียกตัวทำลายของวัตถุทั้งหมดจากอาร์เรย์ก่อน นอกจากนี้ยังไม่ถูกต้องที่จะใช้ตัวดำเนินการลบโดยไม่มีเครื่องหมายวงเล็บ [] สำหรับประเภทดั้งเดิมแม้ว่าจะไม่มีตัวทำลายสำหรับประเภทเหล่านี้ก็ตาม ไม่มีการรับประกันสำหรับคอมไพเลอร์ทุกตัวว่าตัวชี้ไปยังอาร์เรย์จะชี้ไปที่องค์ประกอบแรกของอาร์เรย์ดังนั้นการใช้ลบโดยไม่มีเครื่องหมายวงเล็บ [] อาจทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดได้เช่นกัน
การใช้ตัวชี้อัจฉริยะเช่น auto_ptr, unique_ptr, shared_ptr กับอาร์เรย์ก็ไม่ถูกต้องเช่นกัน เมื่อตัวชี้อัจฉริยะดังกล่าวออกจากขอบเขตจะเรียกตัวดำเนินการลบโดยไม่มีเครื่องหมายวงเล็บ [] ซึ่งส่งผลให้เกิดปัญหาเดียวกันกับที่อธิบายไว้ข้างต้น หากจำเป็นต้องใช้ตัวชี้อัจฉริยะสำหรับอาร์เรย์คุณสามารถใช้ scoped_array หรือ shared_array จาก Boost หรือความเชี่ยวชาญเฉพาะทาง unique_ptr
หากไม่จำเป็นต้องใช้ฟังก์ชันการนับอ้างอิงซึ่งส่วนใหญ่เป็นกรณีของอาร์เรย์วิธีที่ดีที่สุดคือใช้เวกเตอร์ STL แทน พวกเขาไม่เพียงแค่ดูแลการปล่อยหน่วยความจำ แต่ยังมีฟังก์ชันเพิ่มเติมอีกด้วย
ซึ่งส่วนใหญ่เป็นความผิดพลาดของผู้เริ่มต้น แต่ก็ควรกล่าวถึงเนื่องจากมีรหัสเดิมจำนวนมากที่ประสบปัญหานี้ ลองดูโค้ดต่อไปนี้ที่โปรแกรมเมอร์ต้องการเพิ่มประสิทธิภาพบางอย่างโดยหลีกเลี่ยงการคัดลอกโดยไม่จำเป็น:
Complex& SumComplex(const Complex& a, const Complex& b) { Complex result; ….. return result; } Complex& sum = SumComplex(a, b);
ตอนนี้วัตถุ 'ผลรวม' จะชี้ไปที่ 'ผลลัพธ์' ของวัตถุในพื้นที่ แต่วัตถุ 'ผลลัพธ์' อยู่ที่ไหนหลังจากเรียกใช้ฟังก์ชัน SumComplex? ไม่มีที่ไหนเลย มันตั้งอยู่บนสแต็ก แต่หลังจากฟังก์ชั่นส่งคืนสแต็กกลับถูกคลายออกและอ็อบเจ็กต์ในเครื่องทั้งหมดจากฟังก์ชันถูกทำลาย ในที่สุดสิ่งนี้จะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดแม้กระทั่งสำหรับประเภทดั้งเดิม เพื่อหลีกเลี่ยงปัญหาด้านประสิทธิภาพบางครั้งอาจใช้การเพิ่มประสิทธิภาพค่าส่งคืน:
Complex SumComplex(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imaginar + b.imaginar); } Complex sum = SumComplex(a, b);
สำหรับคอมไพเลอร์ส่วนใหญ่ในปัจจุบันหากบรรทัดส่งคืนมีตัวสร้างของอ็อบเจ็กต์โค้ดจะได้รับการปรับให้เหมาะสมเพื่อหลีกเลี่ยงการคัดลอกที่ไม่จำเป็นทั้งหมด - คอนสตรัคเตอร์จะดำเนินการโดยตรงกับอ็อบเจ็กต์ 'sum'
ปัญหา C ++ เหล่านี้เกิดขึ้นบ่อยกว่าที่คุณคิดและมักจะพบในแอปพลิเคชันมัลติเธรด ให้เราพิจารณารหัสต่อไปนี้:
หัวข้อที่ 1:
Connection& connection= connections.GetConnection(connectionId); // ...
หัวข้อที่ 2:
connections.DeleteConnection(connectionId); // …
หัวข้อที่ 1:
connection.send(data);
ในตัวอย่างนี้หากเธรดทั้งสองใช้ ID การเชื่อมต่อเดียวกันสิ่งนี้จะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด ข้อผิดพลาดในการละเมิดการเข้าถึงมักจะพบได้ยากมาก
s corp หรือ c corp ความแตกต่าง
ในกรณีเหล่านี้เมื่อมีเธรดมากกว่าหนึ่งเธรดเข้าถึงทรัพยากรเดียวกันจะมีความเสี่ยงมากที่จะเก็บพอยน์เตอร์หรือการอ้างอิงไปยังรีซอร์สเนื่องจากเธรดอื่นบางเธรดสามารถลบได้ การใช้ตัวชี้อัจฉริยะกับการนับอ้างอิงจะปลอดภัยกว่ามากเช่น shared_ptr จาก Boost ใช้การดำเนินการของอะตอมเพื่อเพิ่ม / ลดตัวนับอ้างอิงดังนั้นจึงปลอดภัยต่อเธรด
ไม่จำเป็นบ่อยครั้งที่จะต้องทิ้งข้อยกเว้นจากตัวทำลาย ถึงอย่างนั้นมีวิธีที่ดีกว่าในการทำเช่นนั้น อย่างไรก็ตามข้อยกเว้นส่วนใหญ่ไม่ได้ถูกโยนทิ้งจากผู้ทำลายอย่างชัดเจน อาจเกิดขึ้นได้ที่คำสั่งง่ายๆในการบันทึกการทำลายวัตถุทำให้เกิดข้อยกเว้นในการขว้างปา ลองพิจารณารหัสต่อไปนี้:
class A { public: A(){} ~A() { writeToLog(); // could cause an exception to be thrown } }; // … try { A a1; A a2; } catch (std::exception& e) { std::cout << 'exception caught'; }
ในโค้ดด้านบนหากมีข้อยกเว้นเกิดขึ้นสองครั้งเช่นระหว่างการทำลายวัตถุทั้งสองคำสั่ง catch จะไม่ถูกดำเนินการ เนื่องจากมีข้อยกเว้นสองข้อควบคู่กันไม่ว่าจะเป็นประเภทเดียวกันหรือคนละประเภทก็ตามสภาพแวดล้อมรันไทม์ C ++ ไม่ทราบวิธีจัดการและเรียกฟังก์ชันยุติซึ่งส่งผลให้การดำเนินการของโปรแกรมสิ้นสุดลง
ดังนั้นกฎทั่วไปคือ: ไม่อนุญาตให้มีข้อยกเว้นออกจากผู้ทำลาย แม้ว่ามันจะน่าเกลียด แต่ก็ต้องมีการป้องกันข้อยกเว้นเช่นนี้:
try { writeToLog(); // could cause an exception to be thrown } catch (...) {}
เทมเพลต auto_ptr เลิกใช้งานจาก C ++ 11 เนื่องจากสาเหตุหลายประการ ยังคงใช้กันอย่างแพร่หลายเนื่องจากโครงการส่วนใหญ่ยังคงได้รับการพัฒนาใน C ++ 98 มีลักษณะเฉพาะบางอย่างที่ผู้พัฒนา C ++ อาจไม่คุ้นเคยและอาจทำให้เกิดปัญหาร้ายแรงสำหรับผู้ที่ไม่ระมัดระวัง การคัดลอกออบเจ็กต์ auto_ptr จะโอนความเป็นเจ้าของจากวัตถุหนึ่งไปยังอีกวัตถุหนึ่ง ตัวอย่างเช่นรหัสต่อไปนี้:
auto_ptr a(new ClassA); // deprecated, please check the text auto_ptr b = a; a->SomeMethod(); // will result in access violation error
…จะส่งผลให้เกิดข้อผิดพลาดในการละเมิดการเข้าถึง เฉพาะออบเจ็กต์“ b” เท่านั้นที่จะมีตัวชี้ไปยังวัตถุของคลาส A ในขณะที่“ a” จะว่างเปล่า การพยายามเข้าถึงสมาชิกคลาสของออบเจ็กต์“ a” จะทำให้เกิดข้อผิดพลาดในการละเมิดการเข้าถึง มีหลายวิธีในการใช้ auto_ptr อย่างไม่ถูกต้อง สี่สิ่งที่สำคัญมากที่ต้องจำเกี่ยวกับพวกเขาคือ:
ห้ามใช้ auto_ptr ภายในคอนเทนเนอร์ STL การคัดลอกคอนเทนเนอร์จะทำให้คอนเทนเนอร์ต้นทางมีข้อมูลที่ไม่ถูกต้อง อัลกอริทึม STL บางอย่างอาจทำให้ 'auto_ptr' ใช้ไม่ได้
อย่าใช้ auto_ptr เป็นอาร์กิวเมนต์ของฟังก์ชันเนื่องจากจะนำไปสู่การคัดลอกและปล่อยให้ค่าที่ส่งผ่านไปยังอาร์กิวเมนต์ไม่ถูกต้องหลังจากเรียกฟังก์ชัน
หากใช้ auto_ptr สำหรับสมาชิกข้อมูลของคลาสต้องแน่ใจว่าได้ทำสำเนาที่ถูกต้องภายในตัวสร้างสำเนาและตัวดำเนินการกำหนดหรือไม่อนุญาตการดำเนินการเหล่านี้โดยกำหนดให้เป็นแบบส่วนตัว
มูลค่าของตัวเลือกการใส่
เมื่อใดก็ตามที่เป็นไปได้ให้ใช้ตัวชี้อัจฉริยะที่ทันสมัยอื่น ๆ แทน auto_ptr
เป็นไปได้ที่จะเขียนหนังสือทั้งเล่มในเรื่องนี้ คอนเทนเนอร์ STL ทุกตัวมีเงื่อนไขเฉพาะบางอย่างที่ทำให้ตัววนซ้ำและการอ้างอิงไม่ถูกต้อง สิ่งสำคัญคือต้องทราบรายละเอียดเหล่านี้ขณะใช้การดำเนินการใด ๆ เช่นเดียวกับปัญหา C ++ ก่อนหน้าปัญหานี้อาจเกิดขึ้นได้บ่อยมากในสภาพแวดล้อมแบบมัลติเธรดดังนั้นจึงจำเป็นต้องใช้กลไกการซิงโครไนซ์เพื่อหลีกเลี่ยงปัญหานี้ ให้ดูโค้ดลำดับต่อไปนี้เป็นตัวอย่าง:
vector v; v.push_back(“string1”); string& s1 = v[0]; // assign a reference to the 1st element vector::iterator iter = v.begin(); // assign an iterator to the 1st element v.push_back(“string2”); cout << s1; // access to a reference of the 1st element cout << *iter; // access to an iterator of the 1st element
จากมุมมองเชิงตรรกะดูเหมือนว่าโค้ดจะใช้ได้ดี อย่างไรก็ตามการเพิ่มองค์ประกอบที่สองลงในเวกเตอร์อาจส่งผลให้เกิดการจัดสรรหน่วยความจำของเวกเตอร์ใหม่ซึ่งจะทำให้ทั้งตัววนซ้ำและข้อมูลอ้างอิงไม่ถูกต้องและส่งผลให้เกิดข้อผิดพลาดในการละเมิดการเข้าถึงเมื่อพยายามเข้าถึงใน 2 บรรทัดสุดท้าย
คุณคงทราบดีว่าการส่งผ่านวัตถุตามมูลค่าเป็นความคิดที่ดีเนื่องจากผลกระทบด้านประสิทธิภาพ หลายคนปล่อยไว้แบบนั้นเพื่อหลีกเลี่ยงการพิมพ์อักขระพิเศษหรืออาจคิดว่าจะกลับมาเพิ่มประสิทธิภาพในภายหลัง โดยปกติจะไม่เคยทำสำเร็จและด้วยเหตุนี้จึงนำไปสู่โค้ดและโค้ดที่มีประสิทธิภาพน้อยกว่าซึ่งมีแนวโน้มที่จะเกิดพฤติกรรมที่ไม่คาดคิด:
class A { public: virtual std::string GetName() const {return 'A';} … }; class B: public A { public: virtual std::string GetName() const {return 'B';} ... }; void func1(A a) { std::string name = a.GetName(); ... } B b; func1(b);
รหัสนี้จะรวบรวม การเรียกใช้ฟังก์ชัน“ func1” จะสร้างสำเนาบางส่วนของอ็อบเจ็กต์“ b” กล่าวคือจะคัดลอกเฉพาะคลาส“ A” ของอ็อบเจ็กต์“ b” ไปยังอ็อบเจ็กต์“ a” (“ ปัญหาการแบ่งส่วน”) ดังนั้นภายในฟังก์ชันจะเรียกเมธอดจากคลาส“ A” แทนเมธอดจากคลาส“ B” ซึ่งส่วนใหญ่จะไม่ใช่สิ่งที่ใครบางคนเรียกใช้ฟังก์ชันนี้
ปัญหาที่คล้ายกันเกิดขึ้นเมื่อพยายามจับข้อยกเว้น ตัวอย่างเช่น:
class ExceptionA: public std::exception; class ExceptionB: public ExceptionA; try { func2(); // can throw an ExceptionB exception } catch (ExceptionA ex) { writeToLog(ex.GetDescription()); throw; }
เมื่อข้อยกเว้นของประเภท ExceptionB ถูกโยนออกจากฟังก์ชัน“ func2” มันจะถูกจับโดยบล็อก catch แต่เนื่องจากปัญหาการแบ่งส่วนเฉพาะบางส่วนจากคลาส ExceptionA เท่านั้นที่จะถูกคัดลอกวิธีการที่ไม่ถูกต้องจะถูกเรียกและโยนซ้ำด้วย จะโยนข้อยกเว้นที่ไม่ถูกต้องไปยังบล็อกทดลองจับภายนอก
ในการสรุปให้ส่งผ่านวัตถุโดยการอ้างอิงเสมอไม่ใช่ตามค่า
แม้แต่ Conversion ที่ผู้ใช้กำหนดเองก็มีประโยชน์มากในบางครั้ง แต่อาจนำไปสู่ Conversion ที่ไม่ได้คาดการณ์ซึ่งหาได้ยากมาก สมมติว่ามีคนสร้างไลบรารีที่มีคลาสสตริง:
class String { public: String(int n); String(const char *s); …. }
วิธีแรกมีไว้เพื่อสร้างสตริงที่มีความยาว n และวิธีที่สองมีไว้เพื่อสร้างสตริงที่มีอักขระที่กำหนด แต่ปัญหาจะเริ่มทันทีที่คุณมีสิ่งนี้:
String s1 = 123; String s2 = ‘abc’;
ในตัวอย่างด้านบน s1 จะกลายเป็นสตริงขนาด 123 ไม่ใช่สตริงที่มีอักขระ“ 123” ตัวอย่างที่สองประกอบด้วยเครื่องหมายคำพูดเดี่ยวแทนอัญประกาศคู่ (ซึ่งอาจเกิดขึ้นโดยไม่ได้ตั้งใจ) ซึ่งจะส่งผลให้มีการเรียกตัวสร้างตัวแรกและสร้างสตริงที่มีขนาดใหญ่มาก นี่เป็นตัวอย่างง่ายๆจริงๆและยังมีกรณีที่ซับซ้อนอีกมากมายที่นำไปสู่ความสับสนและการแปลงที่คาดเดาไม่ได้ซึ่งหาได้ยากมาก มีกฎทั่วไป 2 ข้อในการหลีกเลี่ยงปัญหาดังกล่าว:
กำหนดตัวสร้างด้วยคำหลักที่ชัดเจนเพื่อไม่อนุญาตให้มีการแปลงโดยนัย
ฉันจะทำอย่างไรกับโหนด js
แทนที่จะใช้ตัวดำเนินการแปลงให้ใช้วิธีการสนทนาที่ชัดเจน ต้องพิมพ์มากกว่านี้เล็กน้อย แต่อ่านได้สะอาดกว่ามากและช่วยหลีกเลี่ยงผลลัพธ์ที่คาดเดาไม่ได้
C ++ เป็นภาษาที่มีประสิทธิภาพ ในความเป็นจริงแอปพลิเคชันจำนวนมากที่คุณใช้ทุกวันบนคอมพิวเตอร์และชื่นชอบอาจสร้างขึ้นโดยใช้ C ++ ในฐานะภาษา C ++ ให้ไฟล์ ความยืดหยุ่นจำนวนมาก สำหรับนักพัฒนาผ่านคุณสมบัติที่ซับซ้อนที่สุดที่เห็นในภาษาโปรแกรมเชิงวัตถุ อย่างไรก็ตามคุณสมบัติที่ซับซ้อนหรือความยืดหยุ่นเหล่านี้มักจะกลายเป็นสาเหตุของความสับสนและความยุ่งยากสำหรับนักพัฒนาหลายคนหากไม่ได้ใช้อย่างมีความรับผิดชอบ หวังว่ารายการนี้จะช่วยให้คุณเข้าใจว่าข้อผิดพลาดทั่วไปเหล่านี้มีผลต่อสิ่งที่คุณสามารถบรรลุได้อย่างไรด้วย C ++
ที่เกี่ยวข้อง: วิธีการเรียนรู้ภาษา C และ C ++: รายการที่ดีที่สุด