เหตุใดคลาสพื้นฐานจำเป็นต้องมี destructor เสมือนจริงที่นี่ถ้าคลาสที่ได้รับจัดสรรหน่วยความจำแบบไดนามิกดิบไม่ได้?


12

รหัสต่อไปนี้ทำให้หน่วยความจำรั่ว:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

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


4
คุณกำลังสมมติว่า destructor มีอยู่ถ้าเขียนด้วยตนเองเท่านั้น ข้อสันนิษฐานนี้เป็นความผิดพลาด: ภาษาให้~derived()ผู้ได้รับมอบหมายนั้นเป็นผู้ทำลายล้างของ vec หรือคุณกำลังสมมติว่าunique_ptr<base> ptจะรู้ว่าตัวทำลายที่ได้รับมา หากไม่มีวิธีเสมือนสิ่งนี้จะไม่เป็นเช่นนั้น ในขณะที่ unique_ptr อาจได้รับฟังก์ชั่นการลบที่เป็นพารามิเตอร์แม่แบบโดยไม่ต้องมีตัวแทน runtime และคุณลักษณะนั้นไม่มีประโยชน์สำหรับรหัสนี้
amon

เราสามารถใส่เครื่องมือจัดฟันในบรรทัดเดียวกันเพื่อทำให้รหัสสั้นลงได้หรือไม่? ตอนนี้ฉันต้องเลื่อน
laike9m

คำตอบ:


14

เมื่อคอมไพเลอร์ไปดำเนินการโดยปริยายdelete _ptr;ภายในของunique_ptrdestructor (ซึ่ง_ptrเป็นตัวชี้เก็บไว้ในunique_ptr) มันรู้สองสิ่งอย่างแม่นยำ:

  1. ที่อยู่ของวัตถุที่จะลบ
  2. ชนิดของตัวชี้ซึ่ง_ptrก็คือ ตั้งแต่ตัวชี้อยู่ในunique_ptr<base>นั่นหมายความว่าเป็นประเภท_ptrbase*

นี่คือคอมไพเลอร์ทั้งหมดที่รู้ ดังนั้นให้ว่ามันลบวัตถุของการพิมพ์ก็จะเรียกbase~base()

ดังนั้น ... ส่วนไหนที่ทำลายderviedวัตถุที่ชี้ไปจริง ๆ เพราะถ้าคอมไพเลอร์ไม่ทราบว่ามันกำลังทำลายกderivedแล้วมันก็ไม่รู้ว่าderived::vec มีอยู่จริงให้นับประสาว่ามันควรจะถูกทำลาย ดังนั้นคุณได้ทำลายวัตถุโดยปล่อยครึ่งหนึ่งของมันไม่ถูกทำลาย

คอมไพเลอร์ไม่สามารถสันนิษฐานได้ว่ามีbase*การถูกทำลายเป็นจริงderived*; baseหลังจากทั้งหมดอาจจะมีจำนวนของชั้นเรียนที่ได้มาจากส่วนใด มันจะรู้ได้อย่างไรว่าสิ่งนี้ประเภทใดbase*ชี้ไปที่จริง?

สิ่งที่คอมไพเลอร์ต้องทำคือหาตัวทำลายที่ถูกต้องเพื่อเรียก (ใช่derivedมีตัวทำลาย) ถ้าคุณ= deleteเป็นตัวทำลายทุกชั้นมีตัวทำลายไม่ว่าคุณจะเขียนหนึ่งหรือไม่ก็ตาม เมื่อต้องการทำเช่นนี้มันจะต้องใช้ข้อมูลบางอย่างที่เก็บไว้ในbaseการรับที่อยู่ที่ถูกต้องของรหัส destructor เพื่อเรียกข้อมูลที่กำหนดโดยตัวสร้างของคลาสที่แท้จริง จากนั้นก็จะต้องใช้ข้อมูลนี้เพื่อแปลงbase*ตัวชี้ไปเป็นที่อยู่ของderivedคลาสที่เกี่ยวข้อง(ซึ่งอาจจะหรืออาจจะไม่อยู่ที่อื่นก็ได้ใช่จริงๆ) จากนั้นมันก็สามารถเรียกใช้ตัวทำลายนั้นได้

กลไกที่ฉันเพิ่งอธิบาย โดยทั่วไปเรียกว่า "การกระจายเสมือนจริง": aka สิ่งที่เกิดขึ้นทุกครั้งที่คุณเรียกใช้ฟังก์ชันที่มีเครื่องหมายvirtualเมื่อคุณมีตัวชี้ / การอ้างอิงถึงคลาสพื้นฐาน

หากคุณต้องการที่จะเรียกฟังก์ชั่นชั้นมาเมื่อสิ่งที่คุณต้องเป็นตัวชี้ชั้นฐาน / virtualอ้างอิงฟังก์ชั่นที่จะต้องประกาศ Destructors นั้นไม่มีความแตกต่างในเรื่องนี้


0

มรดก

จุดรวมของการสืบทอดคือการใช้อินเทอร์เฟซทั่วไปและโปรโตคอลร่วมกันระหว่างการนำไปใช้งานที่แตกต่างกันหลายอย่างเช่นอินสแตนซ์ของคลาสที่ได้รับสามารถรักษาเหมือนกันกับอินสแตนซ์อื่นจากประเภทอื่น ๆ

ในการสืบทอด C ++ ยังนำมาซึ่งรายละเอียดการใช้งานการทำเครื่องหมาย (หรือไม่ทำเครื่องหมาย) ตัวทำลายที่เสมือนเป็นหนึ่งในรายละเอียดการใช้งานดังกล่าว

ฟังก์ชั่นเข้าเล่ม

ตอนนี้เมื่อฟังก์ชั่นหรือกรณีพิเศษใด ๆ เช่นคอนสตรัคเตอร์หรือ destructor เรียกว่าคอมไพเลอร์จะต้องเลือกการใช้งานฟังก์ชั่นที่มีความหมาย จากนั้นจะต้องสร้างรหัสเครื่องที่ตามความตั้งใจนี้

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

ถ้าเรามีคลาสพื้นฐานที่มีฟังก์ชั่น (อาจเป็นฟังก์ชั่นใด ๆ รวมถึงตัวสร้างหรือ destructor) และรหัสของคุณเรียกใช้ฟังก์ชั่นในสิ่งนี้หมายความว่าอย่างไร

การจากตัวอย่างของคุณถ้าคุณเรียกว่าinitialize_vector()คอมไพเลอร์ที่มีการตัดสินใจว่าคุณหมายจริงๆจะเรียกการดำเนินงานที่พบในหรือการดำเนินการที่พบในBase Derivedมีสองวิธีในการตัดสินใจ:

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

คอมไพเลอร์ ณ จุดนี้สับสนทั้งสองตัวเลือกนั้นใช้ได้อย่างเท่าเทียมกัน นี่คือเมื่อvirtualเข้ามาในการผสม เมื่อคำหลักนี้นำเสนอคอมไพเลอร์เลือกตัวเลือก 2 ชะลอการตัดสินใจระหว่างการใช้งานที่เป็นไปได้ทั้งหมดจนกระทั่งรหัสกำลังทำงานด้วยค่าจริง เมื่อคำหลักนี้ขาดคอมไพเลอร์เลือกตัวเลือก 1 เพราะนั่นเป็นพฤติกรรมปกติ

คอมไพเลอร์อาจยังคงเลือกตัวเลือก 1 ในกรณีของการเรียกฟังก์ชันเสมือน แต่ถ้ามันสามารถพิสูจน์ได้ว่าเป็นเช่นนี้เสมอ

ตัวสร้างและ Destructors

เหตุใดเราไม่ระบุตัวสร้างเสมือน

คอมไพเลอร์จะเลือกระหว่างการใช้งานที่เหมือนกันของ Constructor สำหรับDerivedและDerived2? มันค่อนข้างเรียบง่าย แต่ทำไม่ได้ ไม่มีค่าที่มีอยู่ล่วงหน้าซึ่งคอมไพเลอร์สามารถเรียนรู้สิ่งที่ตั้งใจจริง ไม่มีค่าที่มีอยู่ล่วงหน้าเพราะนั่นคืองานของตัวสร้าง

เหตุใดเราจึงต้องระบุตัวทำลายเสมือน

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

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

วิธีที่ถูกต้องในการประกาศพื้นฐานหรือประเภทอินเตอร์เฟสใด ๆ ใน C ++ คือการประกาศ destructor เสมือนจริงเพื่อให้ destructor ที่ถูกต้องนั้นถูกเรียกสำหรับอินสแตนซ์ที่กำหนดของลำดับชั้นชนิดนั้น ๆ สิ่งนี้ทำให้ฟังก์ชั่นที่มีความรู้มากที่สุดของอินสแตนซ์สามารถทำความสะอาดอินสแตนซ์นั้นได้อย่างถูกต้อง

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