Làm thế nào để bạn khai báo một giao diện trong C ++?


Câu trả lời:


686

Để 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.


106
Bộ giải mã ảo ++! Cái này rất quan trọng. Bạn cũng có thể muốn bao gồm các khai báo ảo thuần túy của toán tử = và sao chép các định nghĩa hàm tạo để ngăn trình biên dịch tự động tạo các định nghĩa cho bạn.
xan

33
Một thay thế cho một hàm hủy ảo là một hàm hủy được bảo vệ. Điều này vô hiệu hóa sự phá hủy đa hình, có thể phù hợp hơn trong một số trường hợp. Tìm "Hướng dẫn số 4" trong gotw.ca/publications/mill18.htm .
Fred Larson

9
Một tùy chọn khác là định nghĩa một hàm =0hủ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 thiscon trỏ (khi đối tượng được xây dựng vẫn là Parentkiể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 thistrong khi xây dựng :) bạn có thể tiết kiệm kích thước mã.
Pavel Minaev

51
Làm thế nào điển hình của một câu trả lời C ++ mà câu trả lời hàng đầu không trả lời trực tiếp câu hỏi (mặc dù rõ ràng mã là hoàn hảo), thay vào đó nó tối ưu hóa câu trả lời đơn giản.
Tim

18
Đừng quên rằng trong C ++ 11, bạn có thể chỉ định overridetừ 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;
Sean

245

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
        }
};

29
bạn nên có một hàm hủy không làm gì trong IDemo để nó được xác định là hành vi cần làm: IDemo * p = new Child; / * sao cũng được * / xóa p;
Evan Teran

11
Tại sao phương thức OverrideMe trong lớp Con là ảo? Điều đó có cần thiết không?
Cemre

9
@Cemre - không cần thiết, nhưng nó cũng không đau.
PowerApp101

11
Nói chung, nên giữ từ khóa 'ảo' bất cứ khi nào ghi đè phương thức ảo. Mặc dù không bắt buộc, nó có thể làm cho mã rõ ràng hơn - nếu không, bạn không có dấu hiệu nào cho thấy phương thức đó có thể được sử dụng đa hình, hoặc thậm chí tồn tại trong lớp cơ sở.
Kevin

27
@Kevin Ngoại trừ overridetrong C ++ 11
keyer

146

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.


17
Sẽ vẫn tốt nếu có thể tạo giao diện, để cứu chúng tôi khỏi việc gõ quá nhiều (ảo, = 0, hàm hủy ảo). Ngoài ra nhiều kế thừa có vẻ như là một ý tưởng thực sự tồi tệ đối với tôi và tôi chưa bao giờ thấy nó được sử dụng trong thực tế, nhưng giao diện là cần thiết mọi lúc. Thật tệ, sự đồng cảm của C ++ sẽ không giới thiệu giao diện chỉ vì tôi muốn chúng.
Ha11owed

9
Ha11owed: Nó có giao diện. Chúng được gọi là các lớp với các phương thức ảo thuần túy và không có triển khai phương thức.
Miles Rout

6
@doc: java.lang.Thread có các phương thức và hằng số mà bạn có thể không muốn có trong đối tượng của mình. Trình biên dịch nên làm gì nếu bạn mở rộng từ Thread và một lớp khác với phương thức công khai checkAccess ()? Bạn có thực sự thích sử dụng các con trỏ cơ sở được đặt tên mạnh như trong C ++ không? Điều này có vẻ như thiết kế xấu, bạn thường cần thành phần mà bạn nghĩ rằng bạn cần nhiều sự kế thừa.
Ha11owed

4
@ Ha11owed đã lâu rồi nên tôi không nhớ chi tiết, nhưng nó có các phương thức và các đối tượng mà tôi muốn có trong lớp và quan trọng hơn là tôi muốn đối tượng lớp dẫn xuất của mình là một Threadví 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.
doc

2
@Dave: Thật sao? Objective-C có đánh giá thời gian biên dịch và các mẫu không?
Ded repeatator

51

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 đề đó.


13
Nhiều hơn là "thiếu nhiều thừa kế" tôi muốn nói, để thay thế nhiều thừa kế. Java đã được thiết kế như thế này ngay từ đầu vì nhiều kế thừa tạo ra nhiều vấn đề hơn những gì nó giải quyết. Câu trả lời hay
OscarRyz

11
Oscar, điều đó phụ thuộc vào việc bạn là lập trình viên C ++, người đã học Java hay ngược lại. :) IMHO, nếu được sử dụng một cách thận trọng, giống như hầu hết mọi thứ trong C ++, nhiều kế thừa sẽ giải quyết được các vấn đề. Một lớp cơ sở trừu tượng "giao diện" là một ví dụ về việc sử dụng nhiều kế thừa rất hợp lý.
Dima

8
@OscarRyz Sai. MI chỉ tạo ra vấn đề khi sử dụng sai. Hầu hết các vấn đề được cho là với MI cũng sẽ xuất hiện với các thiết kế thay thế (không có MI). Khi mọi người gặp vấn đề với thiết kế của họ với MI, đó là lỗi của MI; nếu họ có vấn đề về thiết kế với SI, thì đó là lỗi của chính họ. "Diamond of death" (kế thừa lặp đi lặp lại) là một ví dụ điển hình. MI bashing không phải là đạo đức giả thuần túy, mà là gần gũi.
tò mò

4
Về mặt ngữ nghĩa, các giao diện khác với các lớp trừu tượng, vì vậy các giao diện của Java không chỉ là một cách giải quyết kỹ thuật. Sự lựa chọn giữa việc xác định một giao diện hoặc một lớp trừu tượng được điều khiển bởi ngữ nghĩa, không phải là những cân nhắc kỹ thuật. Chúng ta hãy tưởng tượng một số giao diện "HasEngine": đó là một khía cạnh, một tính năng và nó có thể được áp dụng cho / thực hiện bởi các loại rất khác nhau (cho dù các lớp hoặc các lớp trừu tượng), vì vậy chúng ta sẽ định nghĩa một giao diện cho đó, không phải là một lớp trừu tượng.
Marek Stanley

2
@MarekStanley, bạn có thể đúng, nhưng tôi ước bạn đã chọn một ví dụ tốt hơn. Tôi thích nghĩ về nó trong việc kế thừa một giao diện so với kế thừa một triển khai. Trong C ++, bạn có thể kế thừa cả giao diện và triển khai cùng nhau (kế thừa công khai) hoặc bạn chỉ có thể kế thừa việc thực hiện (kế thừa riêng). Trong Java, bạn có tùy chọn kế thừa giao diện mà không cần triển khai.
Dima

43

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 newvà 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.


3
Câu trả lời tốt nhất trên trang này vì nó đưa ra quan điểm bằng cách cung cấp một ví dụ thực tế, có thể biên dịch được. Chúc mừng!
Lumi

1
Testet :: ~ Tester () chỉ chạy khi obj là "Khai báo với Tester".
Alessandro L.

Trên thực tế, hàm hủy của chuỗi tên riêng sẽ được gọi và trong bộ nhớ, đó là tất cả những gì sẽ được phân bổ cho. Theo như thời gian chạy, khi tất cả các thành viên cụ thể của một lớp bị hủy, thì thể hiện của lớp cũng vậy. Tôi đã thử một thí nghiệm tương tự với lớp Line có hai cấu trúc Điểm và thấy cả hai cấu trúc đều bị hủy (Ha!) Khi có lệnh gọi xóa hoặc trả về từ hàm bao quanh. valgrind xác nhận 0 rò rỉ.
Chris Reid

33

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:

  1. 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.

  2. 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:

  • 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 đó.

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }

    Hoặc là

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }

2
không cần thừa kế ảo vì bạn không có bất kỳ thành viên dữ liệu nào trong giao diện.
Robocide

3
Kế thừa ảo cũng quan trọng đối với các phương thức. Không có nó, bạn sẽ gặp phải sự mơ hồ với OverrideMe (), ngay cả khi một trong những 'trường hợp' của nó là ảo thuần túy (chỉ cần tự mình thử điều này).
Knarf Navillus

5
@Avishay_ " không cần thừa kế ảo vì bạn không có bất kỳ thành viên dữ liệu nào trong một giao diện. " Sai.
tò mò

Lưu ý rằng kế thừa ảo có thể không hoạt động trên một số phiên bản gcc, như phiên bản 4.3.3 được phân phối với WinAVR 2010: gcc.gnu.org/ormszilla/show_orms.cgi?id=35067
mMontu

-1 vì có một kẻ hủy diệt được bảo vệ không ảo, xin lỗi
Wolf

10

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:

  • Chúng đòi hỏi nhiều bộ nhớ hơn đa hình dựa trên kế thừa.
  • Chúng nói chung nhanh hơn đa hình dựa trên thừa kế.
  • Trong những trường hợp mà bạn biết loại cuối cùng, chúng nhanh hơn nhiều! (một số trình biên dịch như gcc và clang thực hiện tối ưu hóa nhiều hơn trong các loại không có / kế thừa từ các loại có chức năng ảo).

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 MyShapegiao 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 YourShapegiao 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 TheirShapegiao 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ế:


5
Kế thừa TBH rõ ràng hơn nhiều so với điều C ++ 11, giả vờ là một giao diện, nhưng đúng hơn là một chất keo để ràng buộc một số thiết kế không nhất quán. Ví dụ về hình dạng được tách ra khỏi thực tế và Circleđẳng cấp là một thiết kế kém. Bạn nên sử dụng Adaptermô 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ư Qttrướ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.
doc

2
Nó không có vẻ khắc nghiệt cả. Làm thế nào là ví dụ hình dạng tách ra khỏi thực tế? Bạn có thể đưa ra một ví dụ (có thể trên ideone) về việc sửa Circle bằng cách sử dụng Adaptermẫu không? Tôi quan tâm để thấy lợi thế của nó.
gnzlbg

OK tôi sẽ cố gắng để phù hợp với trong hộp nhỏ này. Trước hết, bạn thường chọn các thư viện như "MyShape" trước khi bạn bắt đầu viết ứng dụng của riêng mình, để đảm bảo an toàn cho công việc của bạn. Nếu không thì làm sao bạn biết Squarelà 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 Circlecấu trúc), nhưng bộ điều hợp sẽ trông giống như thế -> ideone.com/UogjWk
doc

2
Nó không tách rời khỏi thực tế sau đó. Khi công ty A mua công ty B và muốn tích hợp cơ sở mã của công ty B vào A, bạn có hai cơ sở mã hoàn toàn độc lập. Hãy tưởng tượng mỗi cái có một hệ thống phân cấp Hình dạng của các loại khác nhau. Bạn không thể kết hợp chúng dễ dàng với thừa kế, và thêm công ty C và bạn có một mớ hỗn độn rất lớn. Tôi nghĩ bạn nên xem bài nói chuyện này: youtube.com/watch?v=0I0FD3N5cgM Câu trả lời của tôi đã cũ hơn, nhưng bạn sẽ thấy những điểm tương đồng. Bạn không cần phải thực hiện lại mọi thứ mọi lúc, bạn có thể cung cấp triển khai trong giao diện và chọn chức năng thành viên nếu có.
gnzlbg

1
Tôi đã xem một phần của video và điều này là hoàn toàn sai. Tôi không bao giờ sử dụng Dynamic_cast trừ mục đích gỡ lỗi. Diễn viên động có nghĩa là có gì đó không đúng với thiết kế và thiết kế của bạn trong video này là sai bởi thiết kế :). Guy thậm chí còn đề cập đến Qt, nhưng ngay cả ở đây, anh ta đã sai - QLayout không được thừa hưởng từ QWidget cũng như cách khác!
doc

9

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.


4
Tại sao, oh tại sao, bất cứ ai cũng muốn làm cho dtor trong trường hợp này là thuần ảo? Điều gì sẽ đạt được điều đó? Bạn chỉ cần buộc một cái gì đó lên các lớp dẫn xuất mà chúng có thể không cần phải bao gồm - một dtor.
Johann Gerell

6
Cập nhật câu trả lời của tôi để trả lời câu hỏi của bạn. Hàm hủy ảo thuần túy là một cách hợp lệ để đạt được (cách duy nhất để đạt được?) Một lớp giao diện trong đó tất cả các phương thức đều có các cài đặt mặc định.
Rodyland

7

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 .


4
Tôi hoàn toàn không thấy lý do tại sao bạn sử dụng novtabletheo tiêu chuẩnvirtual void Bar() = 0;
Flexo

2
Ngoài ra (tôi mới nhận thấy sự thiếu sót = 0;mà tôi đã thêm). Đọc tài liệu nếu bạn không hiểu nó.
Đánh dấu Ingram

Tôi đọc nó mà không có = 0;và cho rằng đó chỉ là một cách làm không chuẩn giống hệt nhau.
Flexo

4

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.


Tôi thích thừa kế ảo vì về mặt khái niệm nó có nghĩa là chỉ có một thể hiện của lớp kế thừa. Phải thừa nhận rằng, lớp học ở đây không có bất kỳ yêu cầu không gian nào nên nó có thể là thừa. Tôi đã không thực hiện MI trong C ++ trong một thời gian, nhưng việc thừa kế không ảo có làm phức tạp hóa việc phát sóng không?
Uri

Tại sao, oh tại sao, bất cứ ai cũng muốn làm cho dtor trong trường hợp này là thuần ảo? Điều gì sẽ đạt được điều đó? Bạn chỉ cần buộc một cái gì đó lên các lớp dẫn xuất mà chúng có thể không cần phải bao gồm - một dtor.
Johann Gerell

2
Nếu có một tình huống một đối tượng sẽ bị phá hủy thông qua một con trỏ tới giao diện, bạn nên đảm bảo rằng hàm hủy là ảo ...
Uri

Không có gì sai với một hàm hủy ảo thuần. Điều đó là không cần thiết, nhưng không có gì sai với nó. Việc thực hiện một hàm hủy trong một lớp dẫn xuất hầu như không phải là một gánh nặng lớn đối với người thực hiện của lớp đó. Xem câu trả lời của tôi dưới đây để biết lý do tại sao bạn làm điều này.
Rodyland

+1 cho kế thừa ảo, vì với các giao diện, nhiều khả năng lớp đó sẽ lấy được giao diện từ hai hoặc nhiều đường dẫn. Tôi chọn cho các hàm hủy được bảo vệ trong giao diện tho.
doc

4

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.
};

Đối với những độc giả khác, bài viết "Cuộc trò chuyện: Chính mình" của Tiến sĩ Dobbs của Jim Hyslop và Herb Sutter nói thêm một chút về lý do tại sao một người có thể muốn sử dụng NVI.
2067021

bài viết này "Virtuality" của Herb Sutter.
2067021

1

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 __interfacetrong 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.


0

Mặc dù đúng virtuallà 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:

  • Bạn có thể thừa kế từ đây dưới dạng một lớp cơ sở (cả ảo và không ảo đều được phép) và điền clickvào hàm tạo của con cháu bạn.
  • Bạn có thể có con trỏ hàm là protectedthành viên và có một publictham chiếu và / hoặc getter.
  • Như đã đề cập ở trên, điều này cho phép bạn chuyển đổi việc thực hiện trong thời gian chạy. Vì vậy, đó là một cách để quản lý nhà nước là tốt. Tùy thuộc vào số ifs so với thay đổi trạng thái trong mã của bạn, tốc độ này thể nhanh hơn switch()es hoặc ifs (vòng quay dự kiến ​​khoảng 3-4 ifgiây, nhưng luôn luôn đo trước.
  • Nếu bạn chọn std::function<>các con trỏ hàm, bạn 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 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).

0

Đâ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.


-2
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.


5
Đây không phải là những gì được coi là một giao diện! Đó chỉ là một lớp cơ sở trừu tượng với một phương thức cần được ghi đè! Các giao diện thường là các đối tượng chỉ chứa các định nghĩa phương thức - một "hợp đồng" các lớp khác phải thực hiện khi chúng thực hiện giao diện.
guitarflow
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.