Làm cách nào để thiết lập một lớp đại diện cho một giao diện? Đây chỉ là một lớp cơ sở trừu tượng?
Làm cách nào để thiết lập một lớp đại diện cho một giao diện? Đây chỉ là một lớp cơ sở trừu tượng?
Câu trả lời:
Để mở rộng câu trả lời của bradtgm bồ , bạn có thể muốn tạo một ngoại lệ cho danh sách phương thức ảo thuần túy của giao diện của mình bằng cách thêm một hàm hủy ảo. Điều này cho phép bạn chuyển quyền sở hữu con trỏ cho một bên khác mà không để lộ lớp dẫn xuất cụ thể. Hàm hủy không phải làm gì cả, vì giao diện không có thành viên cụ thể nào. Có vẻ như mâu thuẫn khi định nghĩa một chức năng là cả ảo và nội tuyến, nhưng tin tôi đi - không phải vậy.
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
}
};
Bạn không cần phải bao gồm phần thân cho hàm hủy ảo - hóa ra một số trình biên dịch gặp khó khăn khi tối ưu hóa một hàm hủy trống và tốt hơn hết là bạn nên sử dụng mặc định.
=0
hủy ảo ( ) thuần túy với phần thân. Ưu điểm ở đây là về mặt lý thuyết, trình biên dịch có thể thấy rằng vtable hiện không có thành viên hợp lệ và loại bỏ nó hoàn toàn. Với một hàm hủy ảo có thân, hàm hủy có thể được gọi (hầu như), ví dụ ở giữa công trình thông qua this
con trỏ (khi đối tượng được xây dựng vẫn là Parent
kiểu), và do đó trình biên dịch phải cung cấp một vtable hợp lệ. Vì vậy, nếu bạn không gọi một cách rõ ràng các hàm hủy ảo this
trong khi xây dựng :) bạn có thể tiết kiệm kích thước mã.
override
từ khóa để cho phép đối số thời gian biên dịch và kiểm tra loại giá trị trả về. Ví dụ: trong tuyên bố của Trẻ emvirtual void OverrideMe() override;
Tạo một lớp với các phương thức ảo thuần túy. Sử dụng giao diện bằng cách tạo một lớp khác ghi đè các phương thức ảo đó.
Một phương thức ảo thuần túy là một phương thức lớp được định nghĩa là ảo và được gán cho 0.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
trong C ++ 11
Toàn bộ lý do bạn có một loại Giao diện đặc biệt ngoài các lớp cơ sở trừu tượng trong C # / Java là vì C # / Java không hỗ trợ nhiều kế thừa.
C ++ hỗ trợ nhiều kế thừa và do đó không cần một loại đặc biệt. Một lớp cơ sở trừu tượng không có các phương thức không trừu tượng (thuần ảo) có chức năng tương đương với giao diện C # / Java.
Thread
ví dụ. Nhiều kế thừa có thể là thiết kế xấu cũng như thành phần. Tất cả phụ thuộc vào trường hợp.
Không có khái niệm về "giao diện" mỗi se trong C ++. AFAIK, các giao diện được giới thiệu lần đầu tiên trong Java để giải quyết vấn đề thiếu tính kế thừa. Khái niệm này đã trở nên khá hữu ích và hiệu quả tương tự có thể đạt được trong C ++ bằng cách sử dụng một lớp cơ sở trừu tượng.
Một lớp cơ sở trừu tượng là một lớp trong đó có ít nhất một hàm thành viên (phương thức trong biệt ngữ Java) là một hàm ảo thuần được khai báo bằng cú pháp sau:
class A
{
virtual void foo() = 0;
};
Một lớp cơ sở trừu tượng không thể được khởi tạo, tức là bạn không thể khai báo một đối tượng của lớp A. Bạn chỉ có thể lấy được các lớp từ A, nhưng bất kỳ lớp dẫn xuất nào không cung cấp triển khai foo()
cũng sẽ trừu tượng. Để ngừng trừu tượng, một lớp dẫn xuất phải cung cấp các triển khai cho tất cả các hàm ảo thuần mà nó kế thừa.
Lưu ý rằng một lớp cơ sở trừu tượng có thể nhiều hơn một giao diện, bởi vì nó có thể chứa các thành viên dữ liệu và các hàm thành viên không phải là ảo thuần túy. Một giao diện tương đương sẽ là một lớp cơ sở trừu tượng không có bất kỳ dữ liệu nào chỉ có các hàm ảo thuần túy.
Và, như Mark Ransom đã chỉ ra, một lớp cơ sở trừu tượng sẽ cung cấp một hàm hủy ảo, giống như bất kỳ lớp cơ sở nào, cho vấn đề đó.
Theo tôi có thể kiểm tra, việc thêm bộ hủy ảo là rất quan trọng. Tôi đang sử dụng các đối tượng được tạo new
và hủy với delete
.
Nếu bạn không thêm hàm hủy ảo trong giao diện, thì hàm hủy của lớp kế thừa sẽ không được gọi.
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;
}
Nếu bạn chạy mã trước đó mà không có virtual ~IBase() {};
, bạn sẽ thấy hàm hủy Tester::~Tester()
không bao giờ được gọi.
Câu trả lời của tôi về cơ bản giống như những câu hỏi khác nhưng tôi nghĩ có hai điều quan trọng khác phải làm:
Khai báo một hàm hủy ảo trong giao diện của bạn hoặc tạo một hàm không ảo được bảo vệ để tránh các hành vi không xác định nếu ai đó cố gắng xóa một đối tượng thuộc loại IDemo
.
Sử dụng thừa kế ảo để tránh các vấn đề với nhiều kế thừa. (Thường có nhiều kế thừa hơn khi chúng ta sử dụng giao diện.)
Và giống như các câu trả lời khác:
Sử dụng giao diện bằng cách tạo một lớp khác ghi đè các phương thức ảo đó.
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
Hoặc là
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
Và
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
Trong C ++ 11, bạn có thể dễ dàng tránh việc thừa kế hoàn toàn:
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;
// ...
};
Trong trường hợp này, Giao diện có ngữ nghĩa tham chiếu, tức là bạn phải đảm bảo rằng đối tượng tồn tại lâu hơn giao diện (cũng có thể tạo giao diện với ngữ nghĩa giá trị).
Những loại giao diện có ưu và nhược điểm của chúng:
Cuối cùng, sự kế thừa là gốc rễ của mọi tội lỗi trong thiết kế phần mềm phức tạp. Trong ngữ nghĩa giá trị của Sean Parent và đa hình dựa trên khái niệm (rất khuyến khích, các phiên bản tốt hơn của kỹ thuật này được giải thích ở đó) trường hợp sau đây được nghiên cứu:
Giả sử tôi có một ứng dụng mà tôi xử lý các hình dạng của mình một cách đa hình bằng MyShape
giao diện:
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
Trong ứng dụng của bạn, bạn làm tương tự với các hình dạng khác nhau bằng YourShape
giao diện:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
Bây giờ hãy nói rằng bạn muốn sử dụng một số hình dạng mà tôi đã phát triển trong ứng dụng của mình. Về mặt khái niệm, các hình dạng của chúng tôi có cùng giao diện, nhưng để làm cho hình dạng của tôi hoạt động trong ứng dụng của bạn, bạn sẽ cần mở rộng hình dạng của mình như sau:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
Đầu tiên, sửa đổi hình dạng của tôi có thể không thể thực hiện được. Hơn nữa, nhiều kế thừa dẫn đường đến mã spaghetti (hãy tưởng tượng một dự án thứ ba xuất hiện trong đó là sử dụng TheirShape
giao diện ... điều gì xảy ra nếu họ cũng gọi chức năng vẽ của mìnhmy_draw
?).
Cập nhật: Có một vài tài liệu tham khảo mới về đa hình dựa trên không thừa kế:
Circle
đẳng cấp là một thiết kế kém. Bạn nên sử dụng Adapter
mô hình trong những trường hợp như vậy. Xin lỗi nếu nó sẽ nghe hơi khó nghe, nhưng hãy thử sử dụng một số thư viện thực tế như Qt
trước khi đưa ra đánh giá về sự kế thừa. Kế thừa làm cho cuộc sống dễ dàng hơn nhiều.
Adapter
mẫu không? Tôi quan tâm để thấy lợi thế của nó.
Square
là chưa có? Tiên tri? Đó là lý do tại sao nó tách ra khỏi thực tế. Và trong thực tế nếu bạn chọn dựa vào thư viện "MyShape", bạn có thể chấp nhận giao diện của nó ngay từ đầu. Trong ví dụ về hình dạng có rất nhiều điều vô nghĩa (một trong số đó là bạn có hai Circle
cấu trúc), nhưng bộ điều hợp sẽ trông giống như thế -> ideone.com/UogjWk
Tất cả các câu trả lời tốt ở trên. Một điều nữa bạn nên ghi nhớ - bạn cũng có thể có một hàm hủy ảo thuần túy. Sự khác biệt duy nhất là bạn vẫn cần phải thực hiện nó.
Bối rối?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Lý do chính bạn muốn làm điều này là nếu bạn muốn cung cấp các phương thức giao diện, như tôi có, nhưng thực hiện ghi đè chúng tùy chọn.
Để làm cho lớp một lớp giao diện đòi hỏi một phương thức ảo thuần túy, nhưng tất cả các phương thức ảo của bạn đều có các cài đặt mặc định, vì vậy phương thức duy nhất còn lại để tạo ảo thuần là hàm hủy.
Việc thực hiện lại một hàm hủy trong lớp dẫn xuất không phải là vấn đề lớn - tôi luôn luôn thực hiện lại một hàm hủy, ảo hay không, trong các lớp dẫn xuất của mình.
Nếu bạn đang sử dụng trình biên dịch C ++ của Microsoft, thì bạn có thể làm như sau:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
Tôi thích cách tiếp cận này vì nó dẫn đến mã giao diện nhỏ hơn rất nhiều và kích thước mã được tạo có thể nhỏ hơn đáng kể. Việc sử dụng novtable sẽ loại bỏ tất cả các tham chiếu đến con trỏ vtable trong lớp đó, vì vậy bạn không bao giờ có thể khởi tạo nó trực tiếp. Xem tài liệu ở đây - novtable .
novtable
theo tiêu chuẩnvirtual void Bar() = 0;
= 0;
mà tôi đã thêm). Đọc tài liệu nếu bạn không hiểu nó.
= 0;
và cho rằng đó chỉ là một cách làm không chuẩn giống hệt nhau.
Một bổ sung nhỏ cho những gì được viết trên đó:
Đầu tiên, hãy chắc chắn rằng hàm hủy của bạn cũng là thuần ảo
Thứ hai, bạn có thể muốn kế thừa hầu như (chứ không phải bình thường) khi bạn thực hiện, chỉ cho các biện pháp tốt.
Bạn cũng có thể xem xét các lớp hợp đồng được triển khai với NVI (Mẫu giao diện không ảo). Ví dụ:
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.
};
Tôi vẫn còn mới trong phát triển C ++. Tôi bắt đầu với Visual Studio (VS).
Tuy nhiên, dường như không ai đề cập đến __interface
trong VS (.NET) . Tôi không chắc chắn nếu đây là một cách tốt để khai báo một giao diện. Nhưng nó dường như cung cấp một thực thi bổ sung (được đề cập trong các tài liệu ). Như vậy bạn không cần phải xác định rõ ràng virtual TYPE Method() = 0;
, vì nó sẽ được tự động chuyển đổi.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
Tuy nhiên, tôi không sử dụng nó vì tôi lo ngại về khả năng tương thích biên dịch đa nền tảng, vì nó chỉ có sẵn trong .NET.
Nếu bất cứ ai có bất cứ điều gì thú vị về nó, xin vui lòng chia sẻ. :-)
Cảm ơn.
Mặc dù đúng virtual
là tiêu chuẩn thực tế để xác định giao diện, nhưng đừng quên mẫu tương tự C cổ điển, đi kèm với hàm tạo trong C ++:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
Điều này có lợi thế là bạn có thể liên kết lại thời gian chạy sự kiện mà không phải xây dựng lại lớp của mình (vì C ++ không có cú pháp để thay đổi các loại đa hình, đây là một cách giải quyết cho các lớp tắc kè hoa).
Lời khuyên:
click
vào hàm tạo của con cháu bạn.protected
thành viên và có một public
tham chiếu và / hoặc getter.if
s so với thay đổi trạng thái trong mã của bạn, tốc độ này có thể nhanh hơn switch()
es hoặc if
s (vòng quay dự kiến khoảng 3-4 if
giây, nhưng luôn luôn đo trước.std::function<>
các con trỏ hàm, bạn có thể quản lý tất cả dữ liệu đối tượng của mình trong đó IBase
. Từ thời điểm này, bạn có thể có sơ đồ giá trị cho IBase
(ví dụ: std::vector<IBase>
sẽ hoạt động). Lưu ý rằng điều này có thể chậm hơn tùy thuộc vào trình biên dịch và mã STL của bạn; Ngoài ra, việc triển khai hiện tại std::function<>
có xu hướng có chi phí hoạt động khi so sánh với các con trỏ hàm hoặc thậm chí các hàm ảo (điều này có thể thay đổi trong tương lai).Đây là định nghĩa của abstract class
tiêu chuẩn c ++
n4687
13.4.2
Một lớp trừu tượng là một lớp chỉ có thể được sử dụng như một lớp cơ sở của một số lớp khác; không có đối tượng nào của một lớp trừu tượng có thể được tạo ra ngoại trừ các đối tượng con của một lớp xuất phát từ nó. Một lớp là trừu tượng nếu nó có ít nhất một hàm ảo thuần túy.
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;
}
Kết quả: Diện tích hình chữ nhật: 35 Diện tích tam giác: 17
Chúng ta đã thấy làm thế nào một lớp trừu tượng định nghĩa một giao diện theo getArea () và hai lớp khác thực hiện cùng một hàm nhưng với thuật toán khác nhau để tính diện tích cụ thể cho hình dạng.