การแบ่งวัตถุคืออะไร


คำตอบ:


609

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

ตัวอย่างเช่น,

class A {
   int foo;
};

class B : public A {
   int bar;
};

ดังนั้นเป้าหมายของการพิมพ์ที่Bมีสองสมาชิกข้อมูลและfoobar

ถ้าคุณจะเขียนสิ่งนี้:

B b;

A a = b;

จากนั้นข้อมูลในการbเกี่ยวกับสมาชิกจะหายไปในbara


66
มีข้อมูลมาก แต่ดูstackoverflow.com/questions/274626#274636สำหรับตัวอย่างของวิธีการแบ่งส่วนที่เกิดขึ้นในระหว่างการเรียกใช้เมธอด
แบลร์คอนราด

55
น่าสนใจ ฉันได้เขียนโปรแกรมใน C ++ เป็นเวลา 15 ปีและปัญหานี้ไม่เคยเกิดขึ้นกับฉันเพราะฉันมักจะผ่านวัตถุโดยการอ้างอิงเป็นเรื่องของประสิทธิภาพและสไตล์ส่วนตัว ไปเพื่อแสดงว่านิสัยที่ดีสามารถช่วยคุณได้อย่างไร
Karl Bielefeldt

10
@Felix ขอบคุณ แต่ผมไม่คิดว่าหล่อกลับมา (ตั้งแต่ไม่ได้เป็นตัวชี้ทางคณิตศาสตร์) จะทำงานA a = b; aอยู่ในขณะนี้วัตถุชนิดซึ่งมีสำเนาของA B::fooมันคงเป็นความผิดพลาดที่จะโยนมันกลับไปตอนนี้ฉันคิดว่า

37
นี่ไม่ใช่ "การแบ่ง" หรืออย่างน้อยก็เป็นตัวแปรที่ไม่เป็นอันตราย B b1; B b2; A& b2_ref = b2; b2 = b1ปัญหาที่แท้จริงเกิดขึ้นถ้าคุณทำ คุณอาจคิดว่าคุณได้คัดลอกb1ไปb2แล้ว แต่คุณยังไม่ได้! คุณได้คัดลอกส่วนหนึ่งของb1ไปยังb2(ส่วนหนึ่งของb1ที่BสืบทอดมาA) และออกจากส่วนอื่น ๆ ของb2ไม่เปลี่ยนแปลง b2ตอนนี้ก็เป็นสิ่งมีชีวิตที่ Frankensteinian ประกอบด้วยกี่บิตของตามด้วยชิ้นบางb1 b2ฮึ Downvoting เพราะฉันคิดว่าคำตอบนั้นเป็นขีปนาวุธมาก
fgp

24
@fgp ความคิดเห็นของคุณควรอ่านB b1; B b2; A& b2_ref = b2; b2_ref = b1" ปัญหาจริงเกิดขึ้นถ้าคุณ " ... ได้รับมาจากคลาสที่มีตัวดำเนินการที่ไม่ใช่เสมือน คือAตั้งใจแม้สำหรับมา? มันไม่มีฟังก์ชั่นเสมือนจริง หากคุณได้รับจากประเภทคุณต้องจัดการกับความจริงที่ว่าฟังก์ชั่นสมาชิกสามารถเรียกได้!
curiousguy

509

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

ในสถานการณ์เช่นนี้, C ++ ช่วยให้คุณผ่านตัวอย่างของBการ Aดำเนินการกำหนด 's (และยัง constructor สำเนา) สิ่งนี้ได้ผลเพราะอินสแตนซ์ของBสามารถถูกแปลงเป็น a const A&ซึ่งเป็นสิ่งที่ผู้ประกอบการที่ได้รับมอบหมายและตัวสร้างสำเนาคาดว่าข้อโต้แย้งของพวกเขาจะเป็น

กรณีที่อ่อนโยน

B b;
A a = b;

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

คดีทุจริต

B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!

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

เกิดอะไรขึ้น? ดี, C ++ virtualโดยค่าเริ่มต้นไม่ผู้ประกอบการที่ได้รับมอบหมายไม่ได้รักษาเป็น ดังนั้นสายa_ref = b1จะเรียกผู้ประกอบการที่ได้รับมอบหมายของไม่ว่าA Bเพราะนี่คือการทำงานที่ไม่เสมือนประกาศ (อย่างเป็นทางการ: คงที่ ) ประเภท (ซึ่งเป็นA&) กำหนดซึ่งฟังก์ชั่นที่เรียกว่าเมื่อเทียบกับที่เกิดขึ้นจริง (อย่างเป็นทางการ: แบบไดนามิก ) ประเภท (ซึ่งจะมีBตั้งแต่a_refการอ้างอิงตัวอย่างของB) . ตอนนี้Aผู้ดำเนินการที่ได้รับมอบหมายอย่างชัดเจนรู้เพียงเกี่ยวกับสมาชิกที่ประกาศในAดังนั้นมันจะคัดลอกเฉพาะผู้ที่ออกจากสมาชิกเพิ่มในBไม่เปลี่ยนแปลง

วิธีแก้ปัญหา

การกำหนดให้บางส่วนของวัตถุโดยปกติแล้วจะมีเหตุผลเล็กน้อย แต่ C ++ โชคไม่ดีที่ไม่สามารถห้ามสิ่งนี้ได้ อย่างไรก็ตามคุณสามารถม้วนของคุณเอง ขั้นตอนแรกคือการทำให้ผู้ประกอบการที่ได้รับมอบหมายเสมือน สิ่งนี้จะรับประกันได้ว่ามันเป็นผู้ดำเนินการที่ได้รับมอบหมายประเภทจริงเสมอซึ่งเรียกว่าไม่ใช่ประเภทที่ประกาศ ขั้นตอนที่สองคือการใช้dynamic_castเพื่อตรวจสอบว่าวัตถุที่กำหนดมีชนิดที่เข้ากันได้ ขั้นตอนที่สามคือการทำการกำหนดที่เกิดขึ้นจริงใน (ล็อก) สมาชิกassign()ตั้งแต่B's assign()อาจจะต้องการใช้A' s assign()เพื่อคัดลอกAของสมาชิก

class A {
public:
  virtual A& operator= (const A& a) {
    assign(a);
    return *this;
  }

protected:
  void assign(const A& a) {
    // copy members of A from a to this
  }
};

class B : public A {
public:
  virtual B& operator= (const A& a) {
    if (const B* b = dynamic_cast<const B*>(&a))
      assign(*b);
    else
      throw bad_assignment();
    return *this;
  }

protected:
  void assign(const B& b) {
    A::assign(b); // Let A's assign() copy members of A from b to this
    // copy members of B from b to this
  }
};

โปรดทราบว่าเพื่อความสะดวกบริสุทธิ์B's operator=covariantly แทนที่ชนิดกลับเพราะมันรู้Bว่ามันจะกลับมาตัวอย่างของ


11
IMHO ปัญหาคือว่ามีสองประเภทที่แตกต่างกันของการทดแทนที่อาจจะบอกเป็นนัยโดยการสืบทอด: derivedค่าใด ๆ ที่อาจจะได้รับรหัสที่คาดหวังbaseค่าหรือการอ้างอิงใด ๆ ที่ได้มาอาจจะใช้เป็นฐานอ้างอิง ฉันอยากจะเห็นภาษาที่มีระบบการพิมพ์ที่เน้นแนวคิดทั้งสองแยกจากกัน มีหลายกรณีที่การอ้างอิงที่ได้รับมาควรถูกทดแทนสำหรับการอ้างอิงพื้นฐาน แต่อินสแตนซ์ที่ได้มานั้นไม่ควรทดแทนได้สำหรับการอ้างอิงพื้นฐาน ยังมีอีกหลายกรณีที่อินสแตนซ์ควรแปลงได้ แต่การอ้างอิงไม่ควรแทนที่
supercat

16
ฉันไม่เข้าใจว่ามีอะไรเลวร้ายในคดี "ทรยศ" ของคุณ คุณระบุว่าคุณต้องการ: 1) รับการอ้างอิงไปยังวัตถุของคลาส A และ 2) โยนวัตถุ b1 ไปยังคลาส A และคัดลอกเนื้อหาไปยังการอ้างอิงของคลาส A สิ่งที่ผิดจริง ๆ นี่คือตรรกะที่เหมาะสมที่อยู่เบื้องหลัง รหัสที่ได้รับ พูดอีกอย่างคือคุณเอากรอบรูปเล็ก ๆ (A) วางไว้เหนือรูปภาพที่ใหญ่กว่า (B) และคุณวาดผ่านกรอบนั้นบ่นในภายหลังว่าภาพใหญ่ของคุณตอนนี้ดูน่าเกลียด :) แต่ถ้าเราพิจารณาพื้นที่กรอบนั้น มันดูค่อนข้างดีเหมือนที่จิตรกรต้องการใช่ไหม :)
Mladen B.

12
ปัญหาคือที่แตกต่างกันที่ C ++ โดยค่าเริ่มต้นถือว่าเป็นประเภทที่แข็งแกร่งมากของsubstitutability - มันต้องมีการดำเนินงานระดับฐานที่จะทำงานได้อย่างถูกต้องในกรณีคลาสย่อย และแม้กระทั่งสำหรับการดำเนินการที่คอมไพเลอร์สร้างขึ้นโดยอัตโนมัติเช่นการมอบหมาย ดังนั้นจึงไม่เพียงพอที่จะไม่ทำให้การทำงานของคุณวุ่นวายในเรื่องนี้คุณต้องปิดการใช้งานที่ผิดที่สร้างโดยคอมไพเลอร์อย่างชัดเจน หรือแน่นอนอยู่ห่างจากมรดกสาธารณะซึ่งมักจะเป็นคำแนะนำที่ดีต่อไป ;-)
fgp

14
อีกวิธีการทั่วไปคือเพียงแค่ปิดการใช้งานตัวดำเนินการคัดลอกและกำหนด สำหรับคลาสที่อยู่ในลำดับชั้นการสืบทอดมักจะไม่มีเหตุผลที่จะใช้ค่าแทนการอ้างอิงหรือตัวชี้
Siyuan Ren

13
อะไรนะ ฉันไม่มีความคิดที่ผู้ประกอบการสามารถทำเครื่องหมายเสมือนได้
paulm

153

หากคุณมีคลาสฐานAและคลาสที่ได้รับBจากนั้นคุณสามารถทำดังต่อไปนี้

void wantAnA(A myA)
{
   // work with myA
}

B derived;
// work with the object "derived"
wantAnA(derived);

ตอนนี้วิธีการที่ต้องการสำเนาของwantAnA derivedอย่างไรก็ตามderivedไม่สามารถคัดลอกวัตถุได้อย่างสมบูรณ์เนื่องจากคลาสBสามารถประดิษฐ์ตัวแปรสมาชิกเพิ่มเติมซึ่งไม่ได้อยู่ในคลาสพื้นฐานAได้

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

  • มันอาจจะไม่สมบูรณ์
  • มันจะทำงานเหมือนA-object (พฤติกรรมพิเศษทั้งหมดของคลาสBจะสูญหายไป)

40
C ++ ไม่ใช่ Java! หากwantAnA(ตามชื่อของมันหมายถึง!) ต้องการAนั้นก็คือสิ่งที่ได้รับ และเป็นตัวอย่างของจะเอ่อทำตัวเหมือนA Aเป็นเรื่องที่น่าแปลกใจขนาดไหน?
fgp

82
@fgp: มันน่าแปลกใจเพราะคุณไม่ผ่าน Aไปยังฟังก์ชั่น
Black

10
@fgp: พฤติกรรมคล้ายกัน อย่างไรก็ตามสำหรับโปรแกรมเมอร์ C ++ โดยเฉลี่ยอาจมีความชัดเจนน้อยกว่า เท่าที่ฉันเข้าใจคำถามไม่มีใคร "บ่น" เป็นเพียงเกี่ยวกับวิธีคอมไพเลอร์จัดการสถานการณ์ อิมโฮมันจะดีกว่าที่จะหลีกเลี่ยงการแบ่งเลยโดยการอ้างอิง (const)
ดำ

8
@ThomasW ไม่ฉันจะไม่ทิ้งมรดก แต่ใช้การอ้างอิง หากลายเซ็นของ wantAnA จะเป็นโมฆะ wantAnA (const A & myA) แสดงว่าไม่มีการแบ่งส่วน แทนการอ้างอิงแบบอ่านอย่างเดียวกับวัตถุของผู้โทรจะถูกส่งผ่าน
ดำ

14
ปัญหาที่เกิดขึ้นส่วนใหญ่จะเป็นในการหล่ออัตโนมัติที่จะดำเนินการคอมไพเลอร์จากการพิมพ์derived Aการคัดเลือกโดยปริยายนั้นเป็นสาเหตุของพฤติกรรมที่ไม่คาดคิดใน C ++ เสมอเนื่องจากมักจะยากที่จะเข้าใจจากการดูรหัสในเครื่องที่นักแสดงเกิดขึ้น
pqnet

41

นี่คือคำตอบที่ดีทั้งหมด ฉันแค่ต้องการเพิ่มตัวอย่างการดำเนินการเมื่อผ่านวัตถุโดยค่า vs โดยอ้างอิง:

#include <iostream>

using namespace std;

// Base class
class A {
public:
    A() {}
    A(const A& a) {
        cout << "'A' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am an 'A'" << endl; }
};

// Derived class
class B: public A {
public:
    B():A() {}
    B(const B& a):A(a) {
        cout << "'B' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am a 'B'" << endl; }
};

void g(const A & a) {
    a.run();
}

void h(const A a) {
    a.run();
}

int main() {
    cout << "Call by reference" << endl;
    g(B());
    cout << endl << "Call by copy" << endl;
    h(B());
}

ผลลัพธ์คือ:

Call by reference
I am a 'B'

Call by copy
'A' copy constructor
I am an 'A'

สวัสดี. คำตอบที่ดี แต่ฉันมีหนึ่งคำถาม ถ้าฉันทำสิ่งนี้ ** dev d; base * b = & d; ** การแบ่งส่วนเกิดขึ้นหรือไม่
เอเดรียน

@ เอเดรียถ้าคุณแนะนำฟังก์ชั่นสมาชิกใหม่หรือตัวแปรสมาชิกในชั้นเรียนที่ได้รับแล้วเหล่านั้นจะไม่สามารถเข้าถึงได้จากตัวชี้ระดับฐานโดยตรง อย่างไรก็ตามคุณยังสามารถเข้าถึงได้จากภายในฟังก์ชันเสมือนคลาสพื้นฐานที่โอเวอร์โหลด ดูสิ่งนี้: godbolt.org/z/LABx33
Vishal Sharma

30

การจับคู่ที่สามใน google สำหรับ "การแบ่ง C ++" ให้บทความวิกิพีเดียนี้แก่ฉันhttp://en.wikipedia.org/wiki/Object_slicingและสิ่งนี้ (ได้รับความร้อน แต่โพสต์สองสามข้อแรกระบุปัญหา): http://bytes.com/ ฟอรั่ม / thread163565.html

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

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


29

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

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


3
โปรดอธิบายว่าหน่วยความจำล่มสามารถเกิดขึ้นได้อย่างไร
foraidt

4
ฉันลืมว่าสำเนา ctor จะรีเซ็ต vptr ผิดพลาดของฉัน แต่คุณยังสามารถได้รับความเสียหายหาก A มีตัวชี้และ B ตั้งค่าให้ชี้ไปที่ส่วนของ B ที่ถูกตัดออก
Walter Bright

18
ปัญหานี้ไม่ได้ จำกัด อยู่แค่ในการแบ่งเท่านั้น คลาสใด ๆ ที่มีพอยน์เตอร์จะมีพฤติกรรมที่น่าสงสัยกับตัวดำเนินการกำหนดค่าเริ่มต้นและตัวสร้างการคัดลอก
Weeble

2
@Weeble - ซึ่งเป็นเหตุผลที่คุณแทนที่ destructor ที่เป็นค่าเริ่มต้นตัวดำเนินการกำหนดค่าและตัวสร้างสำเนาในกรณีเหล่านี้
Bjarke Freund-Hansen

7
@Weeble: สิ่งที่ทำให้การแบ่งวัตถุเลวร้ายยิ่งกว่าการแก้ไขตัวชี้ทั่วไปคือเพื่อให้แน่ใจว่าคุณได้ป้องกันการแบ่งส่วนเกิดขึ้นคลาสพื้นฐานจะต้องให้คอนสตรัคเตอร์การแปลงสำหรับคลาสที่ได้รับมาทั้งหมด (เพราะเหตุใดคลาสที่ได้รับใด ๆ ที่ไม่ได้รับจะอ่อนไหวต่อการถูกคัดลอกโดย ctor สำเนาของคลาสฐานเนื่องจากDerivedถูกแปลงเป็นปริยายBase)
j_random_hacker

10

ใน C ++ วัตถุคลาสที่ได้รับมาสามารถกำหนดให้กับวัตถุคลาสพื้นฐาน แต่ไม่สามารถใช้วิธีอื่นได้

class Base { int x, y; };

class Derived : public Base { int z, w; };

int main() 
{
    Derived d;
    Base b = d; // Object Slicing,  z and w of d are sliced off
}

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


8

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

คำตอบสั้น ๆ คือให้คุณทำการแบ่งวัตถุด้วยการกำหนดวัตถุที่ได้รับมาให้กับวัตถุฐานโดยค่าเช่นวัตถุที่เหลืออยู่เป็นเพียงส่วนหนึ่งของวัตถุที่ได้รับ เพื่อรักษาความหมายของค่าการแบ่งเป็นพฤติกรรมที่สมเหตุสมผลและมีการใช้งานที่ค่อนข้างหายากซึ่งไม่มีอยู่ในภาษาอื่น ๆ ส่วนใหญ่ บางคนคิดว่ามันเป็นคุณสมบัติของ C ++ ในขณะที่หลายคนคิดว่ามันเป็นหนึ่งใน quirks / misfeatures ของ C ++


5
" 'ปกติ' พฤติกรรมของวัตถุ " ที่ไม่ "พฤติกรรมของวัตถุปกติ" ว่าอ้างอิงความหมาย และเกี่ยวข้องในทางไม่กับ Cstructเข้ากันได้หรือความรู้สึกอื่น ๆ ที่ไม่ใช่นักบวช OOP สุ่มบอกคุณ
curiousguy

4
@curtguy อาเมนพี่ชาย เป็นเรื่องน่าเศร้าที่เห็นว่า C ++ ถูกทุบจากการไม่ได้เป็น Java เมื่อความหมายของค่าเป็นหนึ่งในสิ่งที่ทำให้ C ++ มีประสิทธิภาพมากขึ้นอย่างไม่น่าเชื่อ
fgp

นี่ไม่ใช่คุณสมบัติไม่ใช่การเล่นโวหาร / ผิดพลาด มันเป็นพฤติกรรมปกติในการคัดลอกสแต็กนับตั้งแต่เรียกฟังก์ชั่นที่มี arg หรือ (เดียวกัน) การจัดสรรตัวแปรสแต็คของประเภทBaseจะต้องใช้sizeof(Base)ไบต์ในหน่วยความจำตรงกับการจัดตำแหน่งที่เป็นไปได้บางทีนั่นอาจเป็นเหตุผลว่า ) จะไม่คัดลอกสมาชิกของคลาสที่ได้มา แต่ออฟเซ็ตของพวกเขามีขนาดภายนอก เพื่อหลีกเลี่ยง "การสูญเสียข้อมูล" ชี้การใช้งานเพียงแค่เหมือนคนอื่นเนื่องจากหน่วยความจำตัวชี้ได้รับการแก้ไขในสถานที่และขนาดในขณะที่สแต็คเป็นอย่างมาก volitile
Croll

แน่นอนความผิดพลาดของ C ++ การกำหนดวัตถุที่ได้รับให้กับวัตถุฐานควรถูกแบนในขณะที่ผูกวัตถุที่ได้รับมากับการอ้างอิงหรือตัวชี้ของระดับฐานควรจะตกลง
John Z. Li

7

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

ยังคิดว่าบางคนควรพูดถึงสิ่งที่คุณควรทำเพื่อหลีกเลี่ยงการแบ่ง ... รับสำเนาของมาตรฐานการเข้ารหัส C ++, แนวทางกฎ 101 ข้อและแนวทางปฏิบัติที่ดีที่สุด การจัดการกับการแบ่งเป็น # 54

มันแนะนำรูปแบบที่ค่อนข้างซับซ้อนในการจัดการกับปัญหาอย่างสมบูรณ์: มีตัวสร้างสำเนาที่ได้รับการป้องกัน DoClone เสมือนที่ได้รับการป้องกันที่บริสุทธิ์และ Clone สาธารณะที่มีการยืนยันซึ่งจะบอกคุณว่าคลาสที่ได้รับ (เพิ่มเติม) ล้มเหลว (วิธีการโคลนทำการคัดลอกวัตถุ polymorphic ที่เหมาะสม)

นอกจากนี้คุณยังสามารถทำเครื่องหมายตัวสร้างการคัดลอกบนฐานที่ชัดเจนซึ่งอนุญาตให้มีการแบ่งอย่างชัดเจนหากต้องการ


3
" คุณสามารถทำเครื่องหมายตัวสร้างสำเนาบนฐานชัดเจน " ซึ่งไม่ได้ช่วยเลย
curiousguy

6

1. คำจำกัดความของปัญหาช่องว่าง

ถ้า D เป็นคลาสที่ได้รับของคลาสฐาน B จากนั้นคุณสามารถกำหนดวัตถุประเภทที่ได้รับมาให้กับตัวแปร (หรือพารามิเตอร์) ของประเภทฐาน

ตัวอย่าง

class Pet
{
 public:
    string name;
};
class Dog : public Pet
{
public:
    string breed;
};

int main()
{   
    Dog dog;
    Pet pet;

    dog.name = "Tommy";
    dog.breed = "Kangal Dog";
    pet = dog;
    cout << pet.breed; //ERROR

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

2. วิธีแก้ไขปัญหาการเสียดสี

ในการเอาชนะปัญหาเราใช้พอยน์เตอร์เป็นตัวแปรแบบไดนามิก

ตัวอย่าง

Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 

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


7
ฉันเข้าใจส่วน "การหั่น" แต่ฉันไม่เข้าใจ "ปัญหา" มันเป็นปัญหาที่รัฐบางวิธีdogที่ไม่ได้เป็นส่วนหนึ่งของชั้นPet(คนbreedข้อมูลสมาชิก) ไม่ได้คัดลอกในตัวแปรpet? รหัสนี้มีความสนใจเฉพาะPetสมาชิกข้อมูลเท่านั้น การแบ่งเป็นปัญหาแน่นอนถ้ามันไม่พึงประสงค์ แต่ฉันไม่เห็นที่นี่
curiousguy

4
" ((Dog *)ptrP)" ผมขอแนะนำให้ใช้static_cast<Dog*>(ptrP)
curiousguy

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

24
-1 สิ่งนี้ไม่สามารถอธิบายปัญหาที่แท้จริงได้อย่างสมบูรณ์ C ++ มีค่าซีแมนทิกส์ไม่ใช่ซีแมนทิกส์อ้างอิงเช่น Java ดังนั้นทั้งหมดนี้คาดหวังไว้ทั้งหมด และ "แก้ไข" เป็นตัวอย่างของรหัส C ++ ที่น่ากลัวอย่างแท้จริง "การแก้ไข" ปัญหาที่ไม่มีอยู่เช่นการแบ่งประเภทนี้โดยหันไปใช้การจัดสรรแบบไดนามิกเป็นสูตรสำหรับโค้ดบั๊กกี้หน่วยความจำรั่วและประสิทธิภาพที่น่ากลัว โปรดทราบว่ามีหลายกรณีที่การแบ่งส่วนไม่ดี แต่คำตอบนี้ไม่สามารถชี้ให้เห็นได้ คำแนะนำ: เริ่มต้นปัญหาถ้าคุณกำหนดผ่านการอ้างอิง
fgp

คุณเข้าใจหรือไม่ว่าการพยายามเข้าถึงสมาชิกประเภทที่ไม่ได้กำหนดไว้ ( Dog::breed) ไม่เป็นข้อผิดพลาดที่เกี่ยวข้องกับ SLICING
Croll

4

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

หากฉันส่งวัตถุ subclass เป็นพารามิเตอร์ไปยังวิธีการซึ่งใช้พารามิเตอร์ประเภท superclass ฉันควรทราบอย่างแน่นอนและรู้ว่าภายในวิธีที่เรียกใช้จะทำงานกับวัตถุ superclass (aka baseclass) เท่านั้น

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


3
แต่จำไว้มิโนคว่าคุณไม่ได้อ้างอิงวัตถุนั้น คุณกำลังส่งสำเนาใหม่ของวัตถุนั้น แต่ใช้คลาสฐานเพื่อคัดลอกในกระบวนการ
Arafangion

ป้องกันการคัดลอก / การมอบหมายในคลาสฐานและปัญหานี้ได้รับการแก้ไข
Dude

1
คุณถูก. แนวปฏิบัติที่ดีคือการใช้คลาสฐานนามธรรมหรือเพื่อ จำกัด การเข้าถึงเพื่อคัดลอก / การมอบหมาย อย่างไรก็ตามมันไม่ง่ายเลยที่จะมองเห็นเมื่ออยู่ที่นั่นและลืมดูแลได้ง่าย การเรียกวิธีการเสมือนด้วยการแบ่งส่วน * สิ่งนี้สามารถทำให้เกิดสิ่งลึกลับเกิดขึ้นได้หากคุณไม่อยู่ที่นี่โดยไม่ละเมิดการเข้าถึง
Dude

1
ฉันจำได้จากหลักสูตรการเขียนโปรแกรม C ++ ของฉันในมหาวิทยาลัยว่ามีแนวปฏิบัติที่ดีที่สุดสำหรับทุกชั้นเรียนที่เราสร้างขึ้นเราจำเป็นต้องเขียนตัวสร้างเริ่มต้นคัดลอกตัวสร้างและตัวดำเนินการกำหนดรวมทั้งตัวทำลายระบบ วิธีนี้ทำให้คุณมั่นใจได้ว่าการสร้างสำเนาและสิ่งที่คล้ายกันเกิดขึ้นตามที่คุณต้องการในขณะที่เขียนชั้นเรียน ... แทนที่จะแสดงพฤติกรรมแปลก ๆ ในภายหลัง
Minok

3

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

สถานการณ์เลวร้ายที่อาจส่งผลให้เกิดความเสียหายของหน่วยความจำคือ:

  • Class ให้การมอบหมาย (โดยไม่ตั้งใจ, อาจสร้างคอมไพเลอร์) ในคลาสพื้นฐานแบบ polymorphic
  • ไคลเอ็นต์คัดลอกและแบ่งอินสแตนซ์ของคลาสที่ได้รับ
  • ไคลเอนต์เรียกใช้ฟังก์ชันสมาชิกเสมือนที่เข้าถึงสถานะที่ถูกแบ่งออก

3

การแบ่งส่วนข้อมูลหมายถึงข้อมูลที่ถูกเพิ่มโดยคลาสย่อยถูกทิ้งเมื่อวัตถุของคลาสย่อยถูกส่งหรือส่งคืนโดยค่าหรือจากฟังก์ชันที่ต้องการวัตถุคลาสพื้นฐาน

คำอธิบาย: พิจารณาการประกาศคลาสต่อไปนี้:

           class baseclass
          {
                 ...
                 baseclass & operator =(const baseclass&);
                 baseclass(const baseclass&);
          }
          void function( )
          {
                baseclass obj1=m;
                obj1=m;
          }

ในฐานะที่เป็นฟังก์ชั่นการคัดลอก baseclass ไม่ทราบอะไรเกี่ยวกับการได้รับเพียงส่วนฐานของการได้รับการคัดลอก โดยทั่วไปเรียกว่าการแบ่งส่วน


1
class A 
{ 
    int x; 
};  

class B 
{ 
    B( ) : x(1), c('a') { } 
    int x; 
    char c; 
};  

int main( ) 
{ 
    A a; 
    B b; 
    a = b;     // b.c == 'a' is "sliced" off
    return 0; 
}

4
คุณจะให้รายละเอียดเพิ่มเติมไหม? คำตอบของคุณแตกต่างจากคำตอบที่โพสต์แล้ว?
Alexis Pigeon

2
ฉันเดาว่าคำอธิบายเพิ่มเติมจะไม่เลว
Looper

-1

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

class Base { 
int x;
 };

class Derived : public Base { 
 int z; 
 };

 int main() 
{
Derived d;
Base b = d; // Object Slicing,  z of d is sliced off
}

-1

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

นี่คือตัวอย่าง:

#include<bits/stdc++.h>
using namespace std;
class Base
{
    public:
        int a;
        int b;
        int c;
        Base()
        {
            a=10;
            b=20;
            c=30;
        }
};
class Derived : public Base
{
    public:
        int d;
        int e;
        Derived()
        {
            d=40;
            e=50;
        }
};
int main()
{
    Derived d;
    cout<<d.a<<"\n";
    cout<<d.b<<"\n";
    cout<<d.c<<"\n";
    cout<<d.d<<"\n";
    cout<<d.e<<"\n";


    Base b = d;
    cout<<b.a<<"\n";
    cout<<b.b<<"\n";
    cout<<b.c<<"\n";
    cout<<b.d<<"\n";
    cout<<b.e<<"\n";
    return 0;
}

มันจะสร้าง:

[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'

ลงเนื่องจากไม่ใช่ตัวอย่างที่ดี มันจะไม่ทำงานหากแทนที่การคัดลอก d ไปยัง b คุณจะต้องใช้ตัวชี้ในกรณีที่ d และ e ยังคงอยู่ แต่ Base ไม่มีสมาชิกเหล่านั้น ตัวอย่างของคุณแสดงว่าคุณไม่สามารถเข้าถึงสมาชิกที่ไม่มีในคลาสได้
Stefan Fabian

-2

ฉันเพิ่งเจอปัญหาการแบ่งและลงจอดที่นี่ทันที ขอผมเพิ่มสองเซนต์ลงไปในนี้

ขอตัวอย่างจาก "รหัสการผลิต" (หรือบางอย่างที่ใกล้เคียง):


สมมติว่าเรามีสิ่งที่ยื้อการกระทำ UI ของศูนย์ควบคุมเป็นตัวอย่าง
UI นี้ต้องได้รับรายการสิ่งต่าง ๆ ที่สามารถส่งได้ในปัจจุบัน ดังนั้นเราจึงกำหนดคลาสที่มีข้อมูลการจัดส่ง Actionขอเรียกว่า ดังนั้น a จึงActionมีตัวแปรสมาชิกบางตัว เพื่อความง่ายเราก็มี 2 เป็นและstd::string name std::function<void()> fจากนั้นก็มีสิ่งvoid activate()ที่เพิ่งดำเนินการfสมาชิก

ดังนั้น UI จึงได้รับstd::vector<Action>มา ลองนึกภาพฟังก์ชั่นบางอย่างเช่น:

void push_back(Action toAdd);

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

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

แล้วเกิดอะไรขึ้น?
อย่างที่คุณอาจเดาได้: วัตถุถูกตัดออก

ข้อมูลพิเศษจากอินสแตนซ์หายไปและfขณะนี้มีแนวโน้มที่จะมีพฤติกรรมที่ไม่ได้กำหนด


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

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