ฉันรู้ว่าคอมไพเลอร์ C ++ สร้างตัวสร้างการคัดลอกสำหรับคลาส ในกรณีใดที่เราต้องเขียนตัวสร้างสำเนาที่ผู้ใช้กำหนดเอง คุณสามารถยกตัวอย่างได้หรือไม่?
ฉันรู้ว่าคอมไพเลอร์ C ++ สร้างตัวสร้างการคัดลอกสำหรับคลาส ในกรณีใดที่เราต้องเขียนตัวสร้างสำเนาที่ผู้ใช้กำหนดเอง คุณสามารถยกตัวอย่างได้หรือไม่?
คำตอบ:
ตัวสร้างการคัดลอกที่สร้างโดยคอมไพลเลอร์ทำการคัดลอกสมาชิกที่ชาญฉลาด บางครั้งนั่นก็ไม่เพียงพอ ตัวอย่างเช่น:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
ในกรณีนี้การคัดลอกสมาชิกอย่างชาญฉลาดของstored
สมาชิกจะไม่ทำสำเนาบัฟเฟอร์ (เฉพาะตัวชี้เท่านั้นที่จะถูกคัดลอก) ดังนั้นสำเนาแรกที่ถูกทำลายการแชร์บัฟเฟอร์จะเรียกได้delete[]
สำเร็จและตัวที่สองจะทำงานในลักษณะที่ไม่ได้กำหนด คุณต้องมีตัวสร้างการคัดลอกการคัดลอกแบบลึก (และตัวดำเนินการกำหนดด้วย)
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
และฉันเชื่อว่ามันควรจะเป็นdelete [] stored;
std::string
แต่คุณควรจะชี้ทางออกที่ดีคือการใช้งาน แนวคิดทั่วไปคือเฉพาะคลาสยูทิลิตี้ที่จัดการทรัพยากรเท่านั้นที่ต้องใช้งาน Big Three มากเกินไปและคลาสอื่น ๆ ทั้งหมดควรใช้คลาสยูทิลิตี้เหล่านั้นโดยไม่จำเป็นต้องกำหนด Big Three ใด ๆ
ฉันรู้สึกแย่ที่กฎของการRule of Five
ไม่ถูกอ้างถึง
กฎนี้ง่ายมาก:
กฎข้อที่ห้า :
เมื่อใดก็ตามที่คุณเขียน Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor หรือ Move Assignment Operator คุณอาจต้องเขียนอีกสี่ตัว
แต่มีแนวทางทั่วไปที่คุณควรปฏิบัติตามซึ่งเกิดจากความจำเป็นในการเขียนโค้ดที่มีข้อยกเว้น:
ทรัพยากรแต่ละรายการควรได้รับการจัดการโดยวัตถุเฉพาะ
ที่นี่@sharptooth
's ยังคงเป็นรหัส (ส่วนใหญ่) ที่ดี แต่ถ้าเขาจะเพิ่มแอตทริบิวต์ที่สองให้กับชั้นเรียนของเขามันจะไม่เป็น พิจารณาคลาสต่อไปนี้:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
จะเกิดอะไรขึ้นถ้าnew Bar
พ่น? คุณจะลบวัตถุที่ชี้ไปได้mFoo
อย่างไร? มีวิธีแก้ปัญหา (ระดับฟังก์ชั่นลอง / จับ ... ) พวกเขาไม่ได้ปรับขนาด
วิธีที่เหมาะสมในการจัดการกับสถานการณ์คือใช้คลาสที่เหมาะสมแทนตัวชี้ดิบ
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
ด้วยการใช้งานตัวสร้างเดียวกัน (หรือใช้จริงmake_unique
) ตอนนี้ฉันมีข้อยกเว้นด้านความปลอดภัยฟรี !!! มันไม่น่าตื่นเต้น? และเหนือสิ่งอื่นใดฉันไม่ต้องกังวลเกี่ยวกับผู้ทำลายที่เหมาะสมอีกต่อไป! ฉันจำเป็นต้องเขียนของตัวเองCopy Constructor
และAssignment Operator
แม้ว่าunique_ptr
ไม่ได้กำหนดการดำเนินการเหล่านี้ ... แต่มันไม่สำคัญที่นี่;)
ดังนั้นsharptooth
ชั้นเรียนจึงกลับมาอีกครั้ง:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
ฉันไม่รู้เกี่ยวกับคุณ แต่ฉันคิดว่าของฉันง่ายกว่า;)
ฉันจำได้จากการฝึกฝนของฉันและนึกถึงกรณีต่อไปนี้เมื่อต้องจัดการกับการประกาศ / กำหนดตัวสร้างสำเนาอย่างชัดเจน ฉันได้แบ่งกรณีออกเป็นสองประเภท
ฉันวางไว้ในส่วนนี้ในกรณีที่จำเป็นต้องประกาศ / กำหนดตัวสร้างการคัดลอกเพื่อการทำงานที่ถูกต้องของโปรแกรมโดยใช้ประเภทนั้น
หลังจากอ่านหัวข้อนี้แล้วคุณจะได้เรียนรู้เกี่ยวกับข้อผิดพลาดหลายประการในการอนุญาตให้คอมไพเลอร์สร้างตัวสร้างการคัดลอกด้วยตัวมันเอง ดังนั้นตามที่seandระบุไว้ในคำตอบของเขาจึงปลอดภัยเสมอที่จะปิดความสามารถในการคัดลอกสำหรับคลาสใหม่และจงใจเปิดใช้งานภายหลังเมื่อจำเป็นจริงๆ
ประกาศตัวสร้างการคัดลอกส่วนตัวและอย่าจัดเตรียมการนำไปใช้งาน (เพื่อให้บิวด์ล้มเหลวในขั้นตอนการเชื่อมโยงแม้ว่าอ็อบเจ็กต์ประเภทนั้นจะถูกคัดลอกในขอบเขตของคลาสหรือโดยเพื่อนก็ตาม)
ประกาศตัวสร้างการคัดลอกด้วย=delete
ตอนท้าย
นี่เป็นกรณีที่เข้าใจได้ดีที่สุดและเป็นกรณีเดียวที่กล่าวถึงในคำตอบอื่น ๆ shaprtoothได้ครอบคลุมมันสวยดี ฉันต้องการเพิ่มทรัพยากรการคัดลอกอย่างลึกซึ้งซึ่งควรเป็นของวัตถุโดยเฉพาะสามารถใช้กับทรัพยากรประเภทใดก็ได้ซึ่งหน่วยความจำที่จัดสรรแบบไดนามิกเป็นเพียงประเภทเดียว หากจำเป็นอาจต้องใช้การคัดลอกวัตถุอย่างละเอียด
พิจารณาคลาสที่อ็อบเจ็กต์ทั้งหมดไม่ว่าจะถูกสร้างขึ้นด้วยวิธีใด - ต้องลงทะเบียนอย่างใด ตัวอย่างบางส่วน:
ตัวอย่างที่ง่ายที่สุด: การรักษาจำนวนรวมของวัตถุที่มีอยู่ในปัจจุบัน การลงทะเบียนวัตถุเป็นเพียงการเพิ่มตัวนับแบบคงที่
ตัวอย่างที่ซับซ้อนมากขึ้นคือการมีการลงทะเบียนแบบซิงเกิลตันซึ่งการอ้างอิงถึงอ็อบเจ็กต์ที่มีอยู่ทั้งหมดของประเภทนั้นจะถูกเก็บไว้ (เพื่อให้สามารถส่งการแจ้งเตือนไปยังอ็อบเจ็กต์ทั้งหมดได้)
ตัวชี้สมาร์ทที่นับการอ้างอิงถือได้ว่าเป็นกรณีพิเศษในหมวดหมู่นี้: ตัวชี้ใหม่ "ลงทะเบียน" กับทรัพยากรที่ใช้ร่วมกันแทนที่จะเป็นทะเบียนส่วนกลาง
การดำเนินการลงทะเบียนด้วยตนเองดังกล่าวจะต้องดำเนินการโดยตัวสร้างประเภทใด ๆ และตัวสร้างการคัดลอกก็ไม่มีข้อยกเว้น
วัตถุบางอย่างอาจมีโครงสร้างภายในที่ไม่สำคัญโดยมีการอ้างอิงไขว้โดยตรงระหว่างออบเจ็กต์ย่อยที่แตกต่างกัน (อันที่จริงการอ้างอิงโยงภายในเพียงอันเดียวก็เพียงพอที่จะเรียกใช้กรณีนี้) ตัวสร้างสำเนาที่คอมไพเลอร์จัดเตรียมไว้จะทำลายการเชื่อมโยงภายในอ็อบเจ็กต์ภายในโดยแปลงเป็นการเชื่อมโยงระหว่างอ็อบเจ็กต์
ตัวอย่าง:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
อาจมีคลาสที่อ็อบเจ็กต์สามารถคัดลอกได้อย่างปลอดภัยในบางสถานะ (เช่น default-built-state) และไม่ปลอดภัยที่จะคัดลอกเป็นอย่างอื่น หากเราต้องการอนุญาตให้คัดลอกอ็อบเจ็กต์ที่ปลอดภัยต่อการคัดลอกดังนั้นหากตั้งโปรแกรมป้องกันเราจำเป็นต้องตรวจสอบรันไทม์ในตัวสร้างสำเนาที่ผู้ใช้กำหนดเอง
บางครั้งคลาสที่ควรจะคัดลอกได้จะรวมอ็อบเจ็กต์ย่อยที่คัดลอกไม่ได้ โดยปกติแล้วสิ่งนี้จะเกิดขึ้นกับวัตถุที่มีสถานะไม่สามารถสังเกตได้ (กรณีนี้จะกล่าวถึงโดยละเอียดในส่วน "การเพิ่มประสิทธิภาพ" ด้านล่าง) คอมไพเลอร์ช่วยในการรับรู้กรณีนั้นเท่านั้น
คลาสที่ควรสามารถคัดลอกได้อาจรวมอ็อบเจ็กต์ย่อยของชนิดที่สามารถคัดลอกได้ ประเภทที่สามารถคัดลอกได้ไม่ได้จัดเตรียมตัวสร้างการคัดลอกในความหมายที่เข้มงวด แต่มีตัวสร้างอื่นที่อนุญาตให้สร้างสำเนาแนวคิดของวัตถุ เหตุผลในการสร้างประเภทกึ่งสำเนาได้คือเมื่อไม่มีข้อตกลงทั้งหมดเกี่ยวกับความหมายของการคัดลอกประเภท
ตัวอย่างเช่นการทบทวนกรณีการลงทะเบียนด้วยตนเองของออบเจ็กต์เราสามารถโต้แย้งได้ว่าอาจมีสถานการณ์ที่ต้องลงทะเบียนอ็อบเจ็กต์กับ global object manager ก็ต่อเมื่อเป็นอ็อบเจ็กต์เดี่ยวที่สมบูรณ์เท่านั้น หากเป็นอ็อบเจ็กต์ย่อยของอ็อบเจ็กต์อื่นความรับผิดชอบในการจัดการกับอ็อบเจ็กต์ที่มีอยู่
หรือต้องรองรับการคัดลอกทั้งแบบตื้นและแบบลึก (ไม่มีการรองรับการคัดลอกแบบตื้นและแบบลึก)
จากนั้นการตัดสินใจขั้นสุดท้ายจะถูกปล่อยให้กับผู้ใช้ประเภทนั้น - เมื่อคัดลอกวัตถุพวกเขาจะต้องระบุวิธีการคัดลอกที่ตั้งใจไว้อย่างชัดเจน (ผ่านอาร์กิวเมนต์เพิ่มเติม)
ในกรณีของวิธีการเขียนโปรแกรมแบบไม่ป้องกันอาจเป็นไปได้ว่ามีทั้งตัวสร้างสำเนาปกติและตัวสร้างกึ่งสำเนา สิ่งนี้สามารถเป็นธรรมได้เมื่อในกรณีส่วนใหญ่ควรใช้วิธีการคัดลอกเดียวในขณะที่ควรใช้วิธีการคัดลอกทางเลือกในสถานการณ์ที่หายาก แต่เข้าใจดี จากนั้นคอมไพเลอร์จะไม่บ่นว่าไม่สามารถกำหนดตัวสร้างการคัดลอกโดยปริยายได้ ผู้ใช้จะต้องรับผิดชอบ แต่เพียงผู้เดียวในการจดจำและตรวจสอบว่าควรคัดลอกออบเจ็กต์ย่อยของประเภทนั้นผ่านตัวสร้างกึ่งคัดลอกหรือไม่
ในบางกรณีส่วนย่อยของสถานะที่สังเกตได้ของวัตถุอาจเป็น (หรือถือเป็น) ส่วนที่แยกออกจากกันไม่ได้ของตัวตนของวัตถุและไม่ควรถ่ายโอนไปยังวัตถุอื่น ๆ (แม้ว่าสิ่งนี้จะขัดแย้งกันอยู่บ้าง)
ตัวอย่าง:
UID ของออบเจ็กต์ (แต่อันนี้เป็นของกรณี "การลงทะเบียนด้วยตนเอง" จากด้านบนด้วยเนื่องจากต้องได้รับรหัสจากการลงทะเบียนด้วยตนเอง)
ประวัติของอ็อบเจ็กต์ (เช่น Undo / Redo stack) ในกรณีที่อ็อบเจ็กต์ใหม่ต้องไม่สืบทอดประวัติของอ็อบเจ็กต์ต้นทาง แต่ให้เริ่มต้นด้วยไอเท็มประวัติเดียว " คัดลอกเมื่อ <TIME> จาก <OTHER_OBJECT_ID> "
ในกรณีเช่นนี้ตัวสร้างการคัดลอกจะต้องข้ามการคัดลอกวัตถุย่อยที่เกี่ยวข้อง
ลายเซ็นของตัวสร้างสำเนาที่คอมไพเลอร์จัดเตรียมไว้ขึ้นอยู่กับตัวสร้างการคัดลอกที่พร้อมใช้งานสำหรับอ็อบเจ็กต์ย่อย หากออบเจ็กต์ย่อยอย่างน้อยหนึ่งชิ้นไม่มีตัวสร้างสำเนาจริง (การใช้อ็อบเจ็กต์ต้นทางโดยการอ้างอิงแบบคงที่) แต่มีตัวสร้างการคัดลอกที่กลายพันธุ์แทน(การใช้อ็อบเจ็กต์ต้นทางโดยการอ้างอิงแบบไม่คงที่) คอมไพเลอร์จะไม่มีทางเลือก แต่เพื่อประกาศโดยปริยายแล้วกำหนดตัวสร้างสำเนาที่กลายพันธุ์
ตอนนี้จะเกิดอะไรขึ้นถ้าตัวสร้างการคัดลอก "การกลายพันธุ์" ของประเภทของวัตถุย่อยไม่ได้กลายพันธุ์ออบเจ็กต์ต้นทาง (และเขียนโดยโปรแกรมเมอร์ที่ไม่รู้เกี่ยวกับconst
คีย์เวิร์ด)? หากเราไม่สามารถแก้ไขรหัสนั้นได้โดยการเพิ่มสิ่งที่ขาดหายไปconst
ตัวเลือกอื่นคือการประกาศตัวสร้างสำเนาที่ผู้ใช้กำหนดเองด้วยลายเซ็นที่ถูกต้องและกระทำบาปในการเปลี่ยนเป็นconst_cast
ไฟล์.
คอนเทนเนอร์ COW ที่ให้การอ้างอิงโดยตรงไปยังข้อมูลภายในต้องคัดลอกในระดับลึกในขณะก่อสร้างมิฉะนั้นอาจทำงานเป็นหมายเลขอ้างอิงการนับอ้างอิง
แม้ว่า COW จะเป็นเทคนิคการเพิ่มประสิทธิภาพ แต่ตรรกะในตัวสร้างการคัดลอกนี้มีความสำคัญอย่างยิ่งสำหรับการนำไปใช้งานที่ถูกต้อง นั่นคือเหตุผลที่ฉันวางกรณีนี้ไว้ที่นี่แทนที่จะอยู่ในส่วน "การเพิ่มประสิทธิภาพ" ซึ่งเราจะดำเนินการต่อไป
ในกรณีต่อไปนี้คุณอาจต้องการ / จำเป็นต้องกำหนดตัวสร้างสำเนาของคุณเองโดยไม่คำนึงถึงการเพิ่มประสิทธิภาพ:
พิจารณาคอนเทนเนอร์ที่รองรับการดำเนินการลบองค์ประกอบ แต่สามารถทำได้โดยทำเครื่องหมายองค์ประกอบที่ลบแล้วว่าลบแล้วและรีไซเคิลสล็อตในภายหลัง เมื่อมีการสร้างสำเนาของคอนเทนเนอร์ดังกล่าวอาจเป็นเรื่องที่สมเหตุสมผลที่จะกระชับข้อมูลที่ยังมีชีวิตอยู่แทนที่จะเก็บสล็อต "ที่ถูกลบ" ตามที่เป็น
วัตถุอาจมีข้อมูลที่ไม่ได้เป็นส่วนหนึ่งของสถานะที่สังเกตได้ โดยปกติแล้วข้อมูลนี้จะถูกเก็บไว้ในแคช / บันทึกข้อมูลตลอดอายุการใช้งานของวัตถุเพื่อเร่งความเร็วในการดำเนินการสืบค้นที่ช้าบางอย่างที่ดำเนินการโดยวัตถุ สามารถข้ามการคัดลอกข้อมูลนั้นได้อย่างปลอดภัยเนื่องจากจะมีการคำนวณใหม่เมื่อ (และถ้า!) มีการดำเนินการที่เกี่ยวข้อง การคัดลอกข้อมูลนี้อาจไม่เป็นธรรมเนื่องจากอาจถูกยกเลิกได้อย่างรวดเร็วหากสถานะที่สังเกตได้ของวัตถุ (ซึ่งได้รับข้อมูลแคชมา) ถูกแก้ไขโดยการดำเนินการกลายพันธุ์ (และถ้าเราจะไม่แก้ไขวัตถุทำไมเราถึงสร้าง deep ก็อปปี้?)
การเพิ่มประสิทธิภาพนี้จะเป็นธรรมเฉพาะในกรณีที่ข้อมูลเสริมมีขนาดใหญ่เมื่อเทียบกับข้อมูลที่แสดงสถานะที่สังเกตได้
C ++ explicit
ช่วยให้การคัดลอกนัยปิดการใช้งานด้วยการประกาศตัวสร้างสำเนา จากนั้นอ็อบเจ็กต์ของคลาสนั้นจะไม่สามารถส่งผ่านไปยังฟังก์ชันและ / หรือส่งคืนจากฟังก์ชันตามค่าได้ เคล็ดลับนี้สามารถใช้สำหรับประเภทที่ดูเหมือนจะมีน้ำหนักเบา แต่มีราคาแพงมากในการคัดลอก (แม้ว่าการทำสำเนาเสมือนอาจเป็นทางเลือกที่ดีกว่า)
ใน C ++ 03 การประกาศตัวสร้างสำเนาจำเป็นต้องกำหนดด้วยเช่นกัน (แน่นอนถ้าคุณตั้งใจจะใช้) ดังนั้นการใช้ตัวสร้างสำเนาดังกล่าวเป็นเพียงความกังวลที่กำลังพูดถึงนั่นหมายความว่าคุณต้องเขียนโค้ดเดียวกับที่คอมไพเลอร์จะสร้างให้คุณโดยอัตโนมัติ
C ++ 11 และมาตรฐานที่ใหม่กว่าอนุญาตให้ประกาศฟังก์ชันสมาชิกพิเศษ (ตัวสร้างค่าเริ่มต้นและตัวสร้างการคัดลอกตัวดำเนินการกำหนดสำเนาและตัวทำลาย) พร้อมกับการร้องขออย่างชัดเจนเพื่อใช้การใช้งานเริ่มต้น (เพียงแค่จบการประกาศด้วย
=default
)
คำตอบนี้สามารถปรับปรุงได้ดังนี้:
- เพิ่มโค้ดตัวอย่างเพิ่มเติม
- ยกตัวอย่างกรณี "Objects with internal cross-references"
- เพิ่มลิงค์
หากคุณมีคลาสที่จัดสรรเนื้อหาแบบไดนามิก ตัวอย่างเช่นคุณจัดเก็บชื่อหนังสือเป็นตัวอักษร * และตั้งชื่อเรื่องใหม่สำเนาจะไม่ทำงาน
คุณจะต้องเขียนตัวสร้างสำเนาที่ไม่แล้วtitle = new char[length+1]
strcpy(title, titleIn)
ตัวสร้างสำเนาจะทำสำเนา "ตื้น"
Copy Constructor ถูกเรียกเมื่ออ็อบเจ็กต์ถูกส่งโดยค่าส่งคืนด้วยค่าหรือคัดลอกอย่างชัดเจน หากไม่มีตัวสร้างการคัดลอก c ++ จะสร้างตัวสร้างสำเนาเริ่มต้นซึ่งจะสร้างสำเนาแบบตื้น หากวัตถุไม่มีตัวชี้ไปยังหน่วยความจำที่จัดสรรแบบไดนามิกสำเนาตื้นจะทำ
มักเป็นความคิดที่ดีที่จะปิดการใช้งาน copy ctor และ operator = เว้นแต่ว่าคลาสจะต้องการโดยเฉพาะ สิ่งนี้อาจป้องกันความไร้ประสิทธิภาพเช่นการส่ง arg by value เมื่อมีการอ้างอิง นอกจากนี้วิธีการสร้างคอมไพลเลอร์อาจไม่ถูกต้อง
ลองพิจารณาข้อมูลโค้ดด้านล่าง:
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
ให้เอาต์พุตขยะเนื่องจากมีตัวสร้างการคัดลอกที่กำหนดโดยผู้ใช้ที่สร้างขึ้นโดยไม่มีโค้ดที่เขียนขึ้นเพื่อคัดลอกข้อมูลอย่างชัดเจน ดังนั้นคอมไพเลอร์ไม่สร้างเหมือนกัน
คิดเพียงว่าจะแบ่งปันความรู้นี้กับทุกคนแม้ว่าพวกคุณส่วนใหญ่จะรู้อยู่แล้วก็ตาม
ไชโย ... โครตมีความสุข !!!