กฎของสามคืออะไร


2147
  • อะไรคัดลอกวัตถุหมายถึง?
  • ตัวสร้างสำเนาและตัวดำเนินการกำหนดค่าการคัดลอกคืออะไร
  • ฉันต้องประกาศตัวเองเมื่อใด
  • ฉันจะป้องกันไม่ให้คัดลอกวัตถุของฉันได้อย่างไร

52
โปรดอ่านนี้ด้ายทั้งหมดและวิกิพีเดียแท็กc++-faqก่อนที่คุณจะลงคะแนนใกล้
sbi

13
@Binary: อย่างน้อยใช้เวลาในการอ่านการอภิปรายแสดงความคิดเห็นก่อนที่คุณจะลงคะแนน ข้อความที่เคยเรียบง่ายกว่านี้มาก แต่ Fred ถูกขอให้ขยายออก นอกจากนี้ในขณะที่คำถามสี่ข้อทางไวยากรณ์มันเป็นเพียงคำถามเดียวที่มีหลายด้าน (ถ้าคุณไม่เห็นด้วยกับที่แล้วพิสูจน์ POV ของคุณโดยการตอบแต่ละคำถามผู้ที่อยู่ในตัวของมันเองและแจ้งให้เราออกเสียงลงคะแนนในผล.)
เอสบีไอ

1
เฟร็ดที่นี่นอกจากนี้ที่น่าสนใจที่จะเป็นคำตอบของคุณเกี่ยวกับ C ++ 1x: stackoverflow.com/questions/4782757/... เราจะจัดการกับสิ่งนี้ได้อย่างไร
sbi

6
ที่เกี่ยวข้อง: กฎของ The Big Two
Nemanja Trifunovic

4
โปรดจำไว้ว่าตั้งแต่ C ++ 11 ฉันคิดว่าสิ่งนี้ได้รับการอัปเกรดเป็นกฎห้าหรืออะไรทำนองนั้น
paxdiablo

คำตอบ:


1794

บทนำ

C ++ ตัวแปรถือว่าผู้ใช้กำหนดประเภทที่มีความหมายค่า ซึ่งหมายความว่าวัตถุจะถูกคัดลอกโดยปริยายในบริบทต่าง ๆ และเราควรเข้าใจว่า "การคัดลอกวัตถุ" หมายถึงอะไรจริง ๆ

ขอให้เราพิจารณาตัวอย่างง่ายๆ

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(หากคุณสับสนโดยname(name), age(age)ส่วนนี้เรียกว่ารายการสมาชิกเริ่มต้น )

ฟังก์ชั่นสมาชิกพิเศษ

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

เนื่องจากเราไม่ได้ประกาศตัวสร้างสำเนาหรือตัวดำเนินการกำหนด (หรือตัวทำลาย) เองเราจึงกำหนดสิ่งเหล่านี้โดยปริยาย อ้างอิงจากมาตรฐาน:

ตัวสร้างการคัดลอก [... ] และตัวดำเนินการกำหนดค่าการคัดลอก [... ] และ destructor เป็นฟังก์ชันสมาชิกพิเศษ [ หมายเหตุ : การใช้งานจะประกาศฟังก์ชั่นสมาชิกเหล่านี้โดยนัยสำหรับคลาสบางประเภทเมื่อโปรแกรมไม่ได้ประกาศอย่างชัดเจน การใช้งานจะกำหนดโดยนัยหากมีการใช้งาน [... ] end note ] [n3126.pdf ตอนที่ 12 §1]

โดยค่าเริ่มต้นการคัดลอกวัตถุหมายถึงการคัดลอกสมาชิก:

ตัวสร้างสำเนาที่กำหนดโดยนัยสำหรับ non-union class X ทำการคัดลอกรหัสสมาชิกของ subobjects [n3126.pdf ส่วนที่ 12.8 §16]

ตัวดำเนินการกำหนดค่าการคัดลอกที่กำหนดโดยนัยสำหรับ non-union class X ดำเนินการกำหนดสำเนาของสมาชิกย่อยของ subobjects [n3126.pdf ส่วนที่ 12.8 §30]

คำจำกัดความโดยนัย

ฟังก์ชั่นสมาชิกพิเศษที่กำหนดโดยนัยสำหรับpersonลักษณะดังนี้:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

การคัดลอกแบบสมาชิกเป็นสิ่งที่เราต้องการในกรณีนี้ nameและageคัดลอกดังนั้นเราจึงได้personวัตถุที่มีอยู่ในตัวและเป็นอิสระ destructor ที่กำหนดโดยปริยายนั้นว่างเปล่าเสมอ ในกรณีนี้ก็ดีเช่นกันเนื่องจากเราไม่ได้รับทรัพยากรใด ๆ ในตัวสร้าง destructors ของสมาชิกจะถูกเรียกโดยปริยายหลังจากตัวpersonทำลายเสร็จสิ้น:

หลังจากดำเนินการร่างของ destructor และทำลายวัตถุอัตโนมัติใด ๆ ที่จัดสรรภายในร่างกาย destructor สำหรับคลาส X เรียก destructors สำหรับสมาชิก X โดยตรง [... ] [n3126.pdf 12.4 §6]

การจัดการทรัพยากร

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

ให้เราย้อนเวลากลับไปสู่มาตรฐาน C ++ ล่วงหน้า ไม่มีสิ่งเช่นstd::stringนั้นและโปรแกรมเมอร์ก็หลงรักพอยน์เตอร์ personชั้นอาจจะมีการมองเช่นนี้

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

แม้กระทั่งทุกวันนี้ผู้คนยังคงเขียนคลาสในลักษณะนี้และมีปัญหา: " ฉันผลักคนไปสู่เวกเตอร์และตอนนี้ฉันได้รับข้อผิดพลาดที่หน่วยความจำบ้า! " โปรดจำไว้ว่าโดยค่าเริ่มต้นการคัดลอกวัตถุหมายถึงการคัดลอกnameสมาชิก คัดลอกตัวชี้ไม่ใช่อาร์เรย์อักขระที่ชี้ไป! สิ่งนี้มีผลกระทบที่ไม่พึงประสงค์หลายประการ:

  1. เปลี่ยนผ่านสามารถสังเกตได้ผ่านทางab
  2. เมื่อbถูกทำลายa.nameจะเป็นตัวชี้ห้อย
  3. หากaถูกทำลายลบผลผลิตตัวชี้ห้อยไม่ได้กำหนดพฤติกรรม
  4. เนื่องจากการบ้านไม่ได้คำนึงถึงสิ่งที่nameชี้ไปก่อนการมอบหมายไม่ช้าก็เร็วคุณจะได้รับการรั่วไหลของหน่วยความจำทั่วทุกสถานที่

คำจำกัดความที่ชัดเจน

เนื่องจากการคัดลอกแบบสมาชิกแบบไม่มีผลตามที่ต้องการเราต้องกำหนดตัวสร้างการคัดลอกและตัวดำเนินการกำหนดค่าการคัดลอกอย่างชัดเจนเพื่อทำสำเนาลึกของอาร์เรย์อักขระ:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

จดบันทึกความแตกต่างระหว่างการเริ่มต้นและการกำหนด: เราต้องทำลายสถานะเก่าก่อนกำหนดnameเพื่อป้องกันการรั่วไหลของหน่วยความจำ นอกจากนี้เรายังต้องป้องกันการมอบหมายรูปแบบx = xตนเอง หากไม่มีการตรวจสอบนั้นdelete[] nameจะลบอาร์เรย์ที่มีสตริงต้นฉบับเพราะเมื่อคุณเขียนx = xทั้งสองthis->nameและthat.nameมีตัวชี้เดียวกัน

ข้อยกเว้นด้านความปลอดภัย

น่าเสียดายที่โซลูชันนี้จะล้มเหลวหากnew char[...]ส่งข้อยกเว้นเนื่องจากหน่วยความจำหมด ทางออกหนึ่งที่เป็นไปได้คือการแนะนำตัวแปรท้องถิ่นและจัดลำดับคำสั่งใหม่:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

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

ทรัพยากรที่ไม่สามารถคัดลอกได้

ทรัพยากรบางอย่างไม่สามารถหรือไม่ควรคัดลอกเช่นตัวจัดการไฟล์หรือ mutex ในกรณีนี้ให้ประกาศตัวสร้างการคัดลอกและตัวดำเนินการกำหนดค่าการคัดลอกprivateโดยไม่ต้องให้คำจำกัดความ:

private:

    person(const person& that);
    person& operator=(const person& that);

หรือคุณสามารถรับช่วงต่อจากboost::noncopyableหรือประกาศเป็นลบ (ใน C ++ 11 ขึ้นไป):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

กฎข้อที่สาม

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

หากคุณต้องการประกาศตัว Destructor ตัวคัดลอก Constructor หรือตัวดำเนินการกำหนดค่าด้วยตนเองอย่างชัดเจนคุณอาจต้องประกาศทั้งสามอย่างชัดเจน

(น่าเสียดายที่ "กฎ" นี้ไม่ได้บังคับใช้โดยมาตรฐาน C ++ หรือคอมไพเลอร์ใด ๆ ที่ฉันรู้จัก)

กฎห้าข้อ

ตั้งแต่ C ++ 11 เป็นต้นไปวัตถุจะมีฟังก์ชันสมาชิกพิเศษเพิ่มอีก 2 ฟังก์ชันคือตัวสร้างการย้ายและการกำหนดย้าย กฎของห้ารัฐเพื่อใช้ฟังก์ชันเหล่านี้เช่นกัน

ตัวอย่างที่มีลายเซ็นต์:

class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};

กฎของศูนย์

กฎ 3/5 ยังถูกอ้างถึงเป็นกฎของ 3/3/5 ส่วนที่เป็นศูนย์ของกฎระบุว่าคุณได้รับอนุญาตให้ไม่เขียนฟังก์ชันสมาชิกพิเศษใด ๆ เมื่อสร้างคลาสของคุณ

คำแนะนำ

ส่วนใหญ่คุณไม่จำเป็นต้องจัดการทรัพยากรด้วยตัวเองเพราะคลาสที่มีอยู่แล้วเช่นนั้นstd::stringมีไว้สำหรับคุณแล้ว เพียงเปรียบเทียบรหัสอย่างง่ายโดยใช้std::stringสมาชิกกับทางเลือกที่ซับซ้อนและผิดพลาดโดยใช้ a char*และคุณควรมั่นใจ ตราบใดที่คุณอยู่ห่างจากสมาชิกตัวชี้แบบดิบกฎสามข้อนั้นไม่น่าจะเกี่ยวกับรหัสของคุณเอง


4
เฟร็ดฉันจะรู้สึกดีขึ้นเกี่ยวกับการลงคะแนนของฉันถ้า (A) คุณจะไม่สะกดคำที่ได้รับมอบหมายในการคัดลอกโค้ดที่ไม่ดีและเพิ่มข้อความที่บอกว่ามันผิดและดูที่อื่นในสิ่งพิมพ์ อาจใช้ c & s ในรหัสหรือเพียงข้ามการใช้งานสมาชิกเหล่านี้ (B) คุณจะสั้นลงครึ่งแรกซึ่งมีน้อยที่จะทำกับ RoT; (C) คุณจะหารือเกี่ยวกับการแนะนำความหมายของการย้ายและสิ่งที่มีความหมายสำหรับ RoT
sbi

7
แต่แล้วโพสต์ควรจะทำ C / W ฉันคิดว่า ฉันชอบที่คุณรักษาข้อกำหนดส่วนใหญ่ไว้อย่างถูกต้อง (เช่นคุณพูดว่า " ผู้ดำเนินการกำหนดสิทธิ์การคัดลอก " และคุณไม่แตะลงในกับดักทั่วไปที่การมอบหมายไม่สามารถบอกเป็นนัยได้)
Johannes Schaub - litb

4
@rasoon: ฉันไม่คิดว่าการตัดคำตอบออกครึ่งหนึ่งจะถูกมองว่าเป็น "การแก้ไขที่เป็นธรรม" ของคำตอบที่ไม่ใช่แบบ CW
sbi

69
มันจะดีถ้าคุณอัปเดตโพสต์ของคุณสำหรับ C ++ 11 (เช่นย้ายคอนสตรัคเตอร์ / การมอบหมาย)
Alexander Malakhov

5
@solalito ทุกสิ่งที่คุณต้องปลดปล่อยหลังการใช้งาน: ล็อคการทำงานพร้อมกัน, จัดการไฟล์, การเชื่อมต่อฐานข้อมูล, ซ็อกเก็ตเครือข่าย, หน่วยความจำฮีป ...
fredoverflow

509

กฎสามเป็นกฎของหัวแม่มือสำหรับ C ++ โดยทั่วไปกล่าวว่า

หากชั้นเรียนของคุณต้องการ

  • นวกรรมิกสำเนา ,
  • ผู้ประกอบการที่ได้รับมอบหมาย ,
  • หรือdestructor ,

กำหนด explictly แล้วมันเป็นแนวโน้มที่จะต้องทั้งสามของพวกเขา

เหตุผลนี้เป็นเพราะทั้งสามคนมักจะใช้ในการจัดการทรัพยากรและถ้าคลาสของคุณจัดการทรัพยากรก็มักจะต้องจัดการการคัดลอกเช่นเดียวกับการปลดปล่อย

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

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


3
อีกวิธีในการป้องกันการคัดลอกคือการสืบทอด (แบบส่วนตัว) จากคลาสที่ไม่สามารถคัดลอกได้ (เช่นboost::noncopyable) นอกจากนี้ยังสามารถชัดเจนมาก ฉันคิดว่า C ++ 0x และความเป็นไปได้ในการ "ลบ" ฟังก์ชั่นสามารถช่วยได้ที่นี่ แต่ลืมไวยากรณ์: /
Matthieu M.

2
@ Matthieu: ใช่นั่นก็ใช้ได้เหมือนกัน แต่ถ้าnoncopyableเป็นส่วนหนึ่งของ std lib ฉันไม่คิดว่ามันจะเป็นการปรับปรุงมากนัก (Oh, และหากคุณลืมไวยากรณ์การลบให้คุณลืมอีธานหมอที่ผมเคยรู้. :))
เอสบีไอ

3
@Daan: ดูคำตอบนี้ แต่ผมอยากแนะนำให้ไปติดมาติน 's กฎของศูนย์ สำหรับฉันนี่เป็นกฎสำคัญข้อหนึ่งที่สำคัญที่สุดสำหรับ C ++ ที่ประกาศใช้ในทศวรรษที่ผ่านมา
sbi

3
Rule of Zero ของ Martinho ตอนนี้ดีกว่า (โดยไม่มีการครอบครองแอดแวร์ที่ชัดเจน) ตั้งอยู่ที่archive.org
Nathan Kidd

161

กฎของทั้งสามนั้นเป็นไปตามที่ระบุไว้ข้างต้น

ตัวอย่างง่ายๆในภาษาอังกฤษธรรมดา ๆ ของปัญหาที่แก้ได้:

ตัวทำลายที่ไม่ใช่ค่าเริ่มต้น

คุณจัดสรรหน่วยความจำในตัวสร้างของคุณและคุณต้องเขียน destructor เพื่อลบ มิฉะนั้นคุณจะทำให้หน่วยความจำรั่ว

คุณอาจคิดว่านี่เป็นงานที่ทำเสร็จแล้ว

ปัญหาจะเกิดขึ้นถ้าสำเนาทำจากวัตถุของคุณสำเนาก็จะชี้ไปยังหน่วยความจำเดียวกันกับวัตถุต้นฉบับ

เมื่อหนึ่งในสิ่งเหล่านี้ลบหน่วยความจำใน destructor ของอื่น ๆ จะมีตัวชี้ไปยังหน่วยความจำที่ไม่ถูกต้อง (นี้เรียกว่าตัวชี้ห้อย) เมื่อพยายามที่จะใช้มันสิ่งที่จะได้รับขน

ดังนั้นคุณเขียนตัวสร้างการคัดลอกเพื่อที่จะจัดสรรวัตถุใหม่ชิ้นส่วนหน่วยความจำของตัวเองที่จะทำลาย

ผู้ประกอบการกำหนดและคัดลอกคอนสตรัค

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

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

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


4
ดังนั้นถ้าเราใช้ตัวสร้างการคัดลอกแล้วการคัดลอกจะทำ แต่ในตำแหน่งหน่วยความจำที่แตกต่างกันโดยสิ้นเชิงและถ้าเราไม่ใช้ตัวสร้างการคัดลอกแล้วคัดลอกจะทำ แต่มันชี้ไปที่ตำแหน่งหน่วยความจำเดียวกัน นั่นคือสิ่งที่คุณพยายามจะพูด? ดังนั้นการคัดลอกโดยไม่มีตัวสร้างการคัดลอกหมายความว่าตัวชี้ใหม่จะอยู่ที่นั่น แต่ชี้ไปยังตำแหน่งหน่วยความจำเดียวกัน แต่ถ้าเรามีตัวสร้างสำเนาที่กำหนดโดยผู้ใช้อย่างชัดเจนแล้วเราจะมีตัวชี้แยกต่างหากชี้ไปยังตำแหน่งหน่วยความจำอื่น
Unbreakable

4
ขออภัยฉันตอบไปทุกเพศทุกวัยที่ผ่านมา แต่การตอบกลับของฉันดูเหมือนจะยังคงอยู่ที่นี่ :-( โดยทั่วไปใช่ - คุณได้รับ :-)
สเตฟาน

1
หลักการทำงานกับผู้ดำเนินการกำหนดค่าคัดลอกอย่างไร คำตอบนี้จะมีประโยชน์มากกว่าหากกล่าวถึงข้อ 3 ในกฎข้อสาม
DBedrenko

1
@ DBedrenko "คุณเขียนตัวสร้างการคัดลอกเพื่อที่จะจัดสรรวัตถุใหม่ชิ้นส่วนของตัวเองหน่วยความจำ ... " นี่เป็นหลักการเดียวกันที่ขยายไปถึงผู้ประกอบการมอบหมายการคัดลอก คุณไม่คิดว่าฉันทำแบบนั้นชัดเจนหรือไม่?
สเตฟาน

2
@DBedrenko ฉันได้เพิ่มข้อมูลเพิ่มเติม นั่นทำให้ชัดเจนขึ้นหรือไม่?
Stefan

44

โดยทั่วไปหากคุณมี destructor (ไม่ใช่ destructor เริ่มต้น) หมายความว่าคลาสที่คุณกำหนดมีการจัดสรรหน่วยความจำบางส่วน สมมติว่ามีการใช้คลาสภายนอกโดยรหัสลูกค้าหรือคุณ

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

ถ้า MyClass มีสมาชิกที่พิมพ์แบบดั้งเดิมเพียงตัวเดียวตัวดำเนินการกำหนดค่าเริ่มต้นจะทำงาน แต่ถ้ามีสมาชิกตัวชี้และวัตถุที่ไม่มีตัวดำเนินการกำหนดค่าผลลัพธ์จะไม่สามารถคาดเดาได้ ดังนั้นเราจึงสามารถพูดได้ว่าหากมีสิ่งที่จะลบใน destructor ของชั้นเรียนเราอาจต้องการตัวดำเนินการคัดลอกลึกซึ่งหมายความว่าเราควรให้ตัวสร้างสำเนาและตัวดำเนินการกำหนด


36

การคัดลอกวัตถุหมายถึงอะไร มีหลายวิธีที่คุณสามารถคัดลอกวัตถุได้ - ลองพูดถึง 2 ชนิดที่คุณน่าจะหมายถึง - สำเนาลึกและสำเนาตื้น ๆ

เนื่องจากเราใช้ภาษาเชิงวัตถุ (หรืออย่างน้อยก็สมมติ) ดังนั้นสมมุติว่าคุณมีหน่วยความจำที่จัดสรรไว้ เนื่องจากเป็นภาษา OO เราสามารถอ้างถึงกลุ่มของหน่วยความจำที่เราจัดสรรได้อย่างง่ายดายเพราะโดยปกติแล้วจะเป็นตัวแปรดั้งเดิม (ints, chars, bytes) หรือคลาสที่เรากำหนดไว้ซึ่งทำจากประเภทและดั้งเดิมของเราเอง สมมุติว่าเรามีคลาสของ Car ดังนี้:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

สำเนาลึกคือถ้าเราประกาศวัตถุแล้วสร้างสำเนาของวัตถุที่แยกจากกันอย่างสมบูรณ์ ... เราจบด้วย 2 วัตถุใน 2 ชุดหน่วยความจำอย่างสมบูรณ์

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

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

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

ดังนั้นไม่ว่าคุณจะเขียนด้วยภาษาใดให้ระวังเกี่ยวกับสิ่งที่คุณหมายถึงเมื่อมันมาถึงการคัดลอกวัตถุเพราะส่วนใหญ่เวลาที่คุณต้องการสำเนาลึก

ตัวสร้างสำเนาและตัวดำเนินการกำหนดค่าการคัดลอกคืออะไร ฉันใช้ไปแล้วข้างต้น ตัวสร้างการคัดลอกจะถูกเรียกใช้เมื่อคุณพิมพ์รหัสเช่นที่Car car2 = car1; สำคัญถ้าคุณประกาศตัวแปรและกำหนดในบรรทัดเดียวนั่นคือเมื่อตัวสร้างการคัดลอกถูกเรียก ผู้ดำเนินการที่ได้รับมอบหมายคือสิ่งที่เกิดขึ้นเมื่อคุณใช้เครื่องหมายเท่ากับcar2 = car1;- ประกาศcar2ไม่ได้ประกาศในคำสั่งเดียวกัน โค้ดสองชิ้นที่คุณเขียนสำหรับการดำเนินการเหล่านี้มีแนวโน้มที่คล้ายกันมาก ในความเป็นจริงรูปแบบการออกแบบโดยทั่วไปมีฟังก์ชั่นอื่นที่คุณเรียกใช้เพื่อตั้งค่าทุกอย่างเมื่อคุณพอใจกับการคัดลอก / การมอบหมายเริ่มต้นที่ถูกต้อง - ถ้าคุณดูรหัสยาว ๆ ที่ฉันเขียน

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

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


5
คำถามถูกติดแท็ก C ++ การแจกแจงรหัสหลอกนี้จะช่วยชี้แจงอะไรเกี่ยวกับ "Rule Of Three" ที่กำหนดไว้อย่างดีที่สุดและเพียงแค่กระจายความสับสนที่เลวร้ายที่สุด
sehe

26

ฉันต้องประกาศตัวเองเมื่อใด

กฎของสามระบุว่าถ้าคุณประกาศใด ๆ

  1. ตัวสร้างสำเนา
  2. ผู้ประกอบการที่ได้รับมอบหมายคัดลอก
  3. destructor

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

  • การจัดการทรัพยากรใด ๆ ที่กำลังทำอยู่ในการคัดลอกหนึ่งครั้งอาจจำเป็นต้องทำในการคัดลอกอื่นและ

  • ตัวทำลายคลาสจะเข้าร่วมในการจัดการทรัพยากร (โดยปกติจะปล่อยออกมา) ทรัพยากรแบบคลาสสิกที่จะจัดการคือหน่วยความจำและนี่คือสาเหตุที่คลาสไลบรารีมาตรฐานทั้งหมดที่จัดการหน่วยความจำ (เช่นคอนเทนเนอร์ STL ที่ดำเนินการจัดการหน่วยความจำแบบไดนามิก) ประกาศทั้งหมดว่า "สามตัวใหญ่": ทั้งการคัดลอกและ destructor

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

ฉันจะป้องกันไม่ให้คัดลอกวัตถุของฉันได้อย่างไร

ประกาศตัวคัดลอกคอนสตรัค & ตัวดำเนินการกำหนดค่าให้เป็นตัวระบุการเข้าถึงส่วนตัว

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

ใน C ++ 11 เป็นต้นไปคุณสามารถประกาศตัวสร้างสำเนาและตัวดำเนินการกำหนดค่าที่ถูกลบ

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

16

คำตอบที่มีอยู่จำนวนมากสัมผัสตัวสร้างสำเนาตัวดำเนินการกำหนดและ destructor แล้ว อย่างไรก็ตามในการโพสต์ C ++ 11 การแนะนำของ semantic ย้ายอาจขยายเกิน 3

เมื่อเร็ว ๆ นี้ Michael Claisse ได้พูดคุยที่แตะหัวข้อนี้: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


10

กฎสามข้อใน C ++ เป็นหลักการพื้นฐานของการออกแบบและการพัฒนาข้อกำหนดสามข้อที่หากมีคำจำกัดความที่ชัดเจนในหนึ่งในฟังก์ชั่นสมาชิกต่อไปนี้โปรแกรมเมอร์ควรกำหนดฟังก์ชั่นสมาชิกอีกสองคนด้วยกัน ฟังก์ชันสมาชิกสามรายการต่อไปนี้จำเป็นอย่างยิ่ง: destructor, ตัวสร้างสำเนา, ตัวดำเนินการกำหนดค่าการคัดลอก

ตัวสร้างการคัดลอกใน C ++ เป็นตัวสร้างพิเศษ มันถูกใช้เพื่อสร้างวัตถุใหม่ซึ่งเป็นวัตถุใหม่ที่เทียบเท่ากับสำเนาของวัตถุที่มีอยู่

ตัวดำเนินการกำหนดค่าการคัดลอกเป็นตัวดำเนินการกำหนดพิเศษที่มักใช้เพื่อระบุวัตถุที่มีอยู่ให้ผู้อื่นของวัตถุชนิดเดียวกัน

มีตัวอย่างรวดเร็ว:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

7
สวัสดีคำตอบของคุณไม่ได้เพิ่มอะไรใหม่ คนอื่น ๆ ครอบคลุมหัวข้อในเชิงลึกมากขึ้นและแม่นยำยิ่งขึ้น - คำตอบของคุณนั้นประมาณและจริง ๆ แล้วผิดในบางสถานที่ (กล่าวคือไม่มี "ต้อง" ที่นี่; มันเป็น "น่าจะ" มันจะไม่คุ้มค่ากับคุณในขณะที่โพสต์คำตอบประเภทนี้กับคำถามที่ได้รับการตอบอย่างถี่ถ้วนแล้ว ถ้าคุณไม่มีสิ่งใหม่ที่จะเพิ่ม
Mat

1
นอกจากนี้ยังมีสี่ตัวอย่างรวดเร็วซึ่งได้รับการอย่างใดที่เกี่ยวข้องกับสองของสามว่ากฎข้อที่สามมีการพูดคุยเกี่ยวกับ ความสับสนมากเกินไป
Anatolyg
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.