ฉันจะตั้งค่าคลาสที่แสดงถึงส่วนต่อประสานได้อย่างไร นี่เป็นเพียงระดับฐานนามธรรมหรือไม่
ฉันจะตั้งค่าคลาสที่แสดงถึงส่วนต่อประสานได้อย่างไร นี่เป็นเพียงระดับฐานนามธรรมหรือไม่
คำตอบ:
หากต้องการขยายคำตอบโดยbradtgmurrayคุณอาจต้องการยกเว้นหนึ่งรายการเมธอดเสมือนจริงของอินเตอร์เฟสของคุณโดยการเพิ่ม destructor เสมือน สิ่งนี้ช่วยให้คุณสามารถส่งผ่านความเป็นเจ้าของตัวชี้ไปยังบุคคลอื่นโดยไม่ต้องเปิดเผยคลาสที่ได้มาอย่างเป็นรูปธรรม destructor ไม่จำเป็นต้องทำอะไรเลยเนื่องจากส่วนต่อประสานไม่มีสมาชิกที่เป็นรูปธรรม ดูเหมือนว่าอาจขัดแย้งกันในการกำหนดฟังก์ชั่นเป็นทั้งเสมือนและอินไลน์ แต่เชื่อฉัน - ไม่ใช่
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Parent
{
public:
virtual ~Parent();
};
class Child : public Parent, public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
คุณไม่จำเป็นต้องรวมเนื้อหาสำหรับ destructor เสมือน - ปรากฏว่าคอมไพเลอร์บางตัวมีปัญหาในการปรับแก้ destructor ที่ว่างเปล่าและคุณจะดีขึ้นเมื่อใช้ค่าเริ่มต้น
=0
destructor เสมือนจริงบริสุทธิ์กับร่างกาย ข้อได้เปรียบตรงนี้คือคอมไพเลอร์สามารถตามหลักทฤษฎีดูว่า vtable ไม่มีสมาชิกที่ถูกต้องในตอนนี้และละทิ้งมันไปเลย ด้วย destructor เสมือนจริงที่มีร่างกายกล่าวว่า destructor สามารถเรียกได้ (จริง) เช่นในช่วงกลางของการก่อสร้างผ่านthis
ตัวชี้ (เมื่อวัตถุที่สร้างยังคงเป็นParent
ประเภท) และดังนั้นคอมไพเลอร์จะต้องให้ vtable ที่ถูกต้อง ดังนั้นหากคุณไม่ได้เรียก destructors เสมือนผ่านทางthis
ระหว่างการสร้าง :) คุณสามารถบันทึกขนาดรหัสได้
override
คำหลักเพื่ออนุญาตให้มีการรวบรวมอาร์กิวเมนต์เวลาและการตรวจสอบประเภทมูลค่า ตัวอย่างเช่นในการประกาศของเด็กvirtual void OverrideMe() override;
สร้างคลาสด้วยวิธีเสมือนบริสุทธิ์ ใช้อินเทอร์เฟซโดยการสร้างคลาสอื่นที่แทนที่วิธีเสมือนเหล่านั้น
วิธีเสมือนบริสุทธิ์เป็นวิธีการเรียนที่กำหนดเป็นเสมือนและได้รับมอบหมายให้เป็น 0
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
ใน C ++ 11
เหตุผลทั้งหมดที่คุณมีประเภทอินเตอร์เฟสพิเศษนอกเหนือจากคลาสพื้นฐานแบบนามธรรมใน C # / Javaเป็นเพราะ C # / Java ไม่สนับสนุนการสืบทอดหลายรายการ
C ++ รองรับการสืบทอดหลายแบบดังนั้นจึงไม่จำเป็นต้องใช้ชนิดพิเศษ คลาสพื้นฐานแบบ abstract ที่ไม่มีเมธอด (virtual virtual) ที่ไม่ใช่แบบนามธรรมจะเทียบเท่ากับอินเตอร์เฟส C # / Java
Thread
ตัวอย่าง การสืบทอดหลายอย่างอาจเป็นการออกแบบที่ไม่ดีเช่นเดียวกับองค์ประกอบ ทุกอย่างขึ้นอยู่กับกรณี
ไม่มีแนวคิด "อินเตอร์เฟส" ต่อ se ใน C ++ AFAIK มีการใช้งานอินเตอร์เฟสเป็นครั้งแรกใน Java เพื่อหลีกเลี่ยงการขาดการสืบทอดหลายอย่าง แนวคิดนี้มีประโยชน์มากและสามารถใช้เอฟเฟกต์เดียวกันใน C ++ ได้โดยใช้คลาสฐานนามธรรม
คลาสฐานนามธรรมเป็นคลาสที่ฟังก์ชันสมาชิกอย่างน้อยหนึ่งฟังก์ชัน (เมธอดใน Java lingo) เป็นฟังก์ชันเสมือนจริงที่ประกาศโดยใช้ไวยากรณ์ต่อไปนี้:
class A
{
virtual void foo() = 0;
};
คลาสพื้นฐานที่เป็นนามธรรมไม่สามารถสร้างอินสแตนซ์ได้เช่นคุณไม่สามารถประกาศวัตถุของคลาส A ได้คุณสามารถสืบทอดคลาสจาก A foo()
เท่านั้น เพื่อหยุดการเป็นนามธรรมคลาสที่ได้รับต้องจัดเตรียมการใช้งานสำหรับฟังก์ชันเสมือนบริสุทธิ์ทั้งหมดที่สืบทอด
โปรดทราบว่าคลาสฐานนามธรรมอาจเป็นมากกว่าอินเตอร์เฟสเนื่องจากสามารถประกอบด้วยข้อมูลสมาชิกและฟังก์ชันสมาชิกที่ไม่ได้เป็นเสมือนจริง อินเทอร์เฟซที่เทียบเท่ากันจะเป็นคลาสพื้นฐานที่เป็นนามธรรมโดยไม่มีข้อมูลใด ๆ ที่มีฟังก์ชันเสมือนบริสุทธิ์เท่านั้น
และดังที่มาร์คแรนซัมชี้ให้เห็นคลาสฐานนามธรรมควรจัดเตรียม destructor เสมือนเช่นเดียวกับคลาสพื้นฐานใด ๆ สำหรับเรื่องนั้น
เท่าที่ฉันสามารถทดสอบได้สิ่งสำคัญคือการเพิ่มตัวทำลายเสมือน ฉันใช้วัตถุที่สร้างขึ้นnew
และทำลายด้วยdelete
และถูกทำลายด้วย
ถ้าคุณไม่เพิ่ม destructor เสมือนในอินเทอร์เฟซแล้ว destructor ของคลาสที่สืบทอดมาจะไม่ถูกเรียก
class IBase {
public:
virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
virtual void Describe() = 0; // pure virtual method
};
class Tester : public IBase {
public:
Tester(std::string name);
virtual ~Tester();
virtual void Describe();
private:
std::string privatename;
};
Tester::Tester(std::string name) {
std::cout << "Tester constructor" << std::endl;
this->privatename = name;
}
Tester::~Tester() {
std::cout << "Tester destructor" << std::endl;
}
void Tester::Describe() {
std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}
void descriptor(IBase * obj) {
obj->Describe();
}
int main(int argc, char** argv) {
std::cout << std::endl << "Tester Testing..." << std::endl;
Tester * obj1 = new Tester("Declared with Tester");
descriptor(obj1);
delete obj1;
std::cout << std::endl << "IBase Testing..." << std::endl;
IBase * obj2 = new Tester("Declared with IBase");
descriptor(obj2);
delete obj2;
// this is a bad usage of the object since it is created with "new" but there are no "delete"
std::cout << std::endl << "Tester not defined..." << std::endl;
descriptor(new Tester("Not defined"));
return 0;
}
หากคุณเรียกใช้รหัสก่อนหน้าโดยไม่มีvirtual ~IBase() {};
คุณจะเห็นว่า destructor Tester::~Tester()
ไม่เคยถูกเรียก
คำตอบของฉันนั้นเหมือนกับคนอื่น ๆ แต่ฉันคิดว่ามีอีกสองสิ่งที่สำคัญที่ต้องทำ:
ประกาศ destructor IDemo
เสมือนในอินเตอร์เฟซของคุณหรือให้ได้รับการป้องกันที่ไม่เสมือนหนึ่งเพื่อหลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนดถ้ามีคนพยายามที่จะลบวัตถุของการพิมพ์
ใช้การสืบทอดเสมือนเพื่อหลีกเลี่ยงปัญหาที่มีหลายการสืบทอด (บ่อยครั้งที่มีการสืบทอดหลายอย่างเมื่อเราใช้ส่วนต่อประสาน)
และเช่นเดียวกับคำตอบอื่น ๆ :
ใช้อินเทอร์เฟซโดยการสร้างคลาสอื่นที่แทนที่วิธีเสมือนเหล่านั้น
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
หรือ
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
และ
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
ใน C ++ 11 คุณสามารถหลีกเลี่ยงการสืบทอดอย่างง่ายดาย:
struct Interface {
explicit Interface(SomeType& other)
: foo([=](){ return other.my_foo(); }),
bar([=](){ return other.my_bar(); }), /*...*/ {}
explicit Interface(SomeOtherType& other)
: foo([=](){ return other.some_foo(); }),
bar([=](){ return other.some_bar(); }), /*...*/ {}
// you can add more types here...
// or use a generic constructor:
template<class T>
explicit Interface(T& other)
: foo([=](){ return other.foo(); }),
bar([=](){ return other.bar(); }), /*...*/ {}
const std::function<void(std::string)> foo;
const std::function<void(std::string)> bar;
// ...
};
ในกรณีนี้อินเทอร์เฟซที่มีการอ้างอิงความหมายคือคุณต้องตรวจสอบให้แน่ใจว่าวัตถุที่อยู่เหนืออินเทอร์เฟซ
อินเตอร์เฟสประเภทนี้มีข้อดีข้อเสีย:
ในที่สุดมรดกคือรากเหง้าของความชั่วร้ายทั้งหมดในการออกแบบซอฟต์แวร์ที่ซับซ้อน ในซีแมนทิกส์คุณค่าของความหมายของผู้ปกครองและความหลากหลายตามแนวคิด (แนะนำอย่างยิ่งให้อธิบายเทคนิคที่ดีกว่าในรุ่นนี้) กรณีศึกษาต่อไปนี้:
บอกว่าฉันมีแอปพลิเคชันที่ฉันจัดการกับรูปร่างของฉัน polymorphically โดยใช้MyShape
อินเทอร์เฟซ:
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
ในแอปพลิเคชันของคุณคุณทำสิ่งเดียวกันกับรูปร่างที่แตกต่างกันโดยใช้YourShape
อินเตอร์เฟส:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
ตอนนี้พูดว่าคุณต้องการใช้รูปร่างบางอย่างที่ฉันได้พัฒนาในแอปพลิเคชันของคุณ โดยทั่วไปแล้วรูปร่างของเรามีอินเทอร์เฟซเหมือนกัน แต่เพื่อให้รูปร่างของฉันทำงานในแอปพลิเคชันของคุณคุณจะต้องขยายรูปร่างของฉันดังนี้:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
ก่อนอื่นการแก้ไขรูปร่างของฉันอาจไม่สามารถทำได้เลย ยิ่งกว่านั้นการสืบทอดหลายอย่างจะนำพาให้ถนนไปสู่สปาเก็ตตี้โค้ด (ลองนึกภาพว่าโครงการที่สามเข้ามาในTheirShape
อินเทอร์เฟซ ... เกิดอะไรขึ้นถ้าพวกเขาเรียกฟังก์ชั่นการวาดด้วยmy_draw
)
อัปเดต: มีการอ้างอิงใหม่สองสามรายการเกี่ยวกับความหลากหลายที่มีลักษณะไม่สืบทอด:
Circle
คลาสเป็นงานออกแบบที่ไม่ดี คุณควรใช้Adapter
รูปแบบในกรณีเช่นนี้ ขออภัยถ้ามันฟังดูรุนแรงนิดหน่อย แต่ลองใช้ห้องสมุดชีวิตจริงบางอย่างQt
ก่อนตัดสินใจเกี่ยวกับการรับมรดก มรดกทำให้ชีวิตง่ายขึ้นมาก
Adapter
รูปแบบได้ไหม ฉันสนใจที่จะดูข้อดีของมัน
Square
ยังไม่ได้อยู่ที่นั่น? ญาณ? นั่นเป็นเหตุผลว่าทำไมมันถึงแยกออกมาจากความเป็นจริง และในความเป็นจริงหากคุณเลือกที่จะใช้ไลบรารี่ "MyShape" คุณสามารถปรับใช้อินเทอร์เฟซของมันได้ตั้งแต่เริ่มต้น ในตัวอย่างรูปร่างมีเรื่องไร้สาระมากมาย (หนึ่งในนั้นคือคุณมีสองCircle
โครงสร้าง) แต่อะแดปเตอร์จะมีลักษณะเช่นนั้น -> ideone.com/UogjWk
คำตอบที่ดีทั้งหมดข้างต้น สิ่งหนึ่งที่พิเศษที่คุณควรจำไว้ - คุณสามารถมี destructor เสมือนแท้ ข้อแตกต่างเพียงอย่างเดียวคือคุณยังต้องใช้งาน
สับสน?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
เหตุผลหลักที่คุณต้องการทำคือถ้าคุณต้องการให้วิธีการอินเทอร์เฟซอย่างที่ฉันมี แต่ให้เอาชนะพวกเขาไม่จำเป็น
ในการทำให้คลาสเป็นคลาสอินเตอร์เฟสต้องใช้วิธีเสมือนจริง แต่วิธีเสมือนทั้งหมดของคุณมีการนำไปใช้งานเริ่มต้นดังนั้นวิธีเดียวที่เหลืออยู่ในการสร้างเสมือนจริงจึงเป็น destructor
การปรับใช้ destructor ใหม่ในคลาสที่ได้รับไม่ใช่เรื่องใหญ่เลย - ฉันมักจะปรับใช้ destructor เสมือนหรือไม่ในคลาสที่ได้รับของฉันเสมอ
หากคุณใช้คอมไพเลอร์ C ++ ของ Microsoft คุณสามารถทำสิ่งต่อไปนี้:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
ฉันชอบวิธีการนี้เพราะมันส่งผลให้มีรหัสอินเตอร์เฟสขนาดเล็กลงมากและขนาดรหัสที่สร้างขึ้นอาจมีขนาดเล็กลงอย่างมาก การใช้ novtable จะลบการอ้างอิงทั้งหมดไปยังตัวชี้ vtable ในคลาสนั้นดังนั้นคุณจะไม่สามารถยกตัวอย่างได้โดยตรง ดูเอกสารที่นี่ - novtable
novtable
เกินมาตรฐานvirtual void Bar() = 0;
= 0;
ซึ่งฉันเพิ่มไว้) อ่านเอกสารหากคุณไม่เข้าใจ
= 0;
และคิดว่ามันเป็นวิธีที่ไม่ได้มาตรฐานในการทำสิ่งเดียวกัน
นอกเหนือจากสิ่งที่เขียนอยู่ที่นั่น:
ขั้นแรกให้แน่ใจว่า destructor ของคุณเป็นเวอร์ช่วลเสมือนจริง
ประการที่สองคุณอาจต้องการรับมรดกอย่างแท้จริง (มากกว่าปกติ) เมื่อคุณดำเนินการเพียงเพื่อมาตรการที่ดี
คุณยังสามารถพิจารณาคลาสสัญญาที่ใช้กับ NVI (รูปแบบอินเตอร์เฟสเสมือนจริง) ตัวอย่างเช่น
struct Contract1 : boost::noncopyable
{
virtual ~Contract1();
void f(Parameters p) {
assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
// + class invariants.
do_f(p);
// Check post-conditions + class invariants.
}
private:
virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
virtual void do_f(Parameters p); // From contract 1.
virtual void do_g(Parameters p); // From contract 2.
};
ฉันยังใหม่ในการพัฒนา C ++ ฉันเริ่มต้นด้วย Visual Studio (VS)
แต่ดูเหมือนจะไม่มีใครกล่าวถึง__interface
ใน VS (.NET) ฉันไม่แน่ใจว่านี่เป็นวิธีที่ดีในการประกาศอินเทอร์เฟซ แต่ดูเหมือนว่าจะมีการบังคับใช้เพิ่มเติม (กล่าวถึงในเอกสาร ) เช่นที่คุณไม่ต้องระบุอย่างชัดเจนvirtual TYPE Method() = 0;
เนื่องจากมันจะถูกแปลงโดยอัตโนมัติ
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
อย่างไรก็ตามฉันไม่ได้ใช้เพราะฉันกังวลเกี่ยวกับความเข้ากันได้ของการรวบรวมข้ามแพลตฟอร์มเนื่องจากมีเฉพาะใน. NET
หากใครมีสิ่งที่น่าสนใจเกี่ยวกับเรื่องนี้โปรดแบ่งปัน :-)
ขอบคุณ
ในขณะที่มันเป็นความจริงที่virtual
เป็นมาตรฐาน de-facto เพื่อกำหนดอินเทอร์เฟซ แต่อย่าลืมเกี่ยวกับรูปแบบ C-like แบบคลาสสิกซึ่งมาพร้อมกับ Constructor ใน C ++:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
นี่เป็นข้อดีที่คุณสามารถผูกรันไทม์เหตุการณ์ใหม่โดยไม่ต้องสร้างคลาสของคุณอีกครั้ง (เนื่องจาก C ++ ไม่มีไวยากรณ์สำหรับการเปลี่ยนประเภท polymorphic นี่เป็นวิธีแก้ปัญหาสำหรับคลาสกิ้งก่า)
เคล็ดลับ:
click
ของลูกหลานของคุณprotected
สมาชิกและมีpublic
อ้างอิงและ / หรือทะเยอทะยานif
การเปลี่ยนแปลงสถานะเมื่อเทียบกับรหัสของคุณนี่อาจเร็วกว่าswitch()
es หรือif
s (คาดว่าจะมีการพลิกกลับประมาณ 3-4 if
วินาที แต่ควรทำการวัดก่อนเสมอstd::function<>
มากกว่าคำแนะนำการทำงานคุณอาจIBase
จะสามารถจัดการข้อมูลของวัตถุทั้งหมดของคุณภายใน จากจุดนี้คุณสามารถมีแผนงานสำหรับIBase
(เช่นstd::vector<IBase>
จะทำงาน) โปรดทราบว่าสิ่งนี้อาจช้ากว่านี้ขึ้นอยู่กับคอมไพเลอร์และรหัส STL ของคุณ การใช้งานในปัจจุบันของstd::function<>
มักจะมีค่าใช้จ่ายเมื่อเทียบกับตัวชี้ฟังก์ชั่นหรือแม้กระทั่งฟังก์ชั่นเสมือน (อาจมีการเปลี่ยนแปลงในอนาคต)นี่คือคำจำกัดความของabstract class
มาตรฐาน c ++
n4687
13.4.2
คลาสนามธรรมเป็นคลาสที่สามารถใช้เป็นคลาสพื้นฐานของคลาสอื่นได้เท่านั้น ไม่มีวัตถุของคลาสนามธรรมที่สามารถสร้างยกเว้นเป็น subobjects ของคลาสที่ได้รับมาจากมัน คลาสเป็นนามธรรมถ้ามีฟังก์ชันเสมือนบริสุทธิ์อย่างน้อยหนึ่งฟังก์ชัน
class Shape
{
public:
// pure virtual function providing interface framework.
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
cout << "Triangle area: " << Tri.getArea() << endl;
return 0;
}
ผลลัพธ์: พื้นที่สี่เหลี่ยมผืนผ้า: 35 พื้นที่สามเหลี่ยม: 17
เราได้เห็นว่าคลาสนามธรรมนิยามอินเทอร์เฟซในแง่ของ getArea () และอีกสองคลาสใช้ฟังก์ชันเดียวกัน แต่มีอัลกอริทึมที่แตกต่างกันในการคำนวณพื้นที่เฉพาะกับรูปร่าง