Các phương thức, thuộc tính và bộ chỉ mục ảo. Hàm ảo, hàm ảo thuần túy

Một sửa đổi khác của lớp cơ sở dẫn đến những hậu quả không mong muốn. Sửa đổi này bao gồm việc thay đổi bộ xác định hàm thành viên của lớp cơ sở. Chúng tôi (lần đầu tiên!) sử dụng công cụ xác định ảo trong khai báo hàm. Các hàm được khai báo bằng bộ xác định ảo được gọi là các hàm ảo. Giới thiệu hàm ảo trong khai báo lớp cơ sở (chỉ một từ xác định) ​​có ý nghĩa quan trọng đối với phương pháp lập trình hướng đối tượng nên một lần nữa chúng tôi sẽ cung cấp khai báo sửa đổi của lớp A:

Lớp A ( public: virtual int Fun1(int); );

Một chỉ định bổ sung trong khai báo hàm và không còn thay đổi nào (hiện tại) trong khai báo của các lớp dẫn xuất. Như mọi khi, rất chức năng đơn giản chủ yếu(). Trong đó, chúng ta định nghĩa một con trỏ tới một đối tượng của lớp cơ sở, thiết lập nó thành một đối tượng thuộc kiểu dẫn xuất, sau đó chúng ta gọi hàm Fun1() bằng con trỏ:

Void main () ( A *pObj; A MyA; AB MyAB; pObj = pObj->Fun1(1); AC MyAC; pObj = pObj->Fun1(1); )

Nếu không có trình xác định ảo, kết quả của việc thực hiện biểu thức cuộc gọi

PObj->Fun1(1);

sẽ hiển nhiên: như bạn biết, việc lựa chọn hàm được xác định bởi loại con trỏ.

Tuy nhiên, trình xác định ảo sẽ thay đổi mọi thứ. Việc lựa chọn hàm hiện được xác định bởi loại đối tượng mà con trỏ lớp cơ sở đang được đặt. Nếu một lớp dẫn xuất khai báo một hàm không tĩnh có tên, kiểu trả về và danh sách tham số giống với các hàm ảo của lớp cơ sở, thì biểu thức gọi kết quả sẽ gọi hàm thành viên của lớp dẫn xuất.

Cần lưu ý ngay rằng khả năng gọi một hàm thành viên của lớp dẫn xuất bằng con trỏ đến lớp cơ sở không có nghĩa là có thể quan sát một đối tượng “từ trên xuống” từ con trỏ đến đối tượng của lớp cơ sở. lớp học. Các chức năng và dữ liệu thành viên không ảo vẫn chưa có sẵn. Và điều này có thể được xác minh rất dễ dàng. Để làm được điều này, chỉ cần cố gắng làm những gì chúng ta đã làm một lần là đủ - gọi một ẩn số lớp cơ sở hàm thành viên của lớp dẫn xuất:

//pObj->Fun2(2); //pObj->AC::Fun1(2);

Kết quả là âm tính. Con trỏ, như trước đây, chỉ được cấu hình cho đoạn cơ sở của đối tượng lớp dẫn xuất. Tuy nhiên, việc gọi các hàm của lớp dẫn xuất vẫn có thể thực hiện được. Ngày xửa ngày xưa, trong các phần dành cho mô tả về hàm tạo, chúng tôi đã xem lại danh sách các hành động điều chỉnh được thực hiện bởi hàm tạo trong quá trình chuyển đổi một đoạn bộ nhớ được phân bổ thành một đối tượng lớp. Trong số các hoạt động này, việc khởi tạo các bảng chức năng ảo đã được đề cập.

Bạn có thể thử phát hiện sự hiện diện của các bảng chức năng ảo này bằng thao tác sizeof. Tất nhiên, tất cả điều này phụ thuộc vào việc triển khai cụ thể, nhưng ít nhất trong phiên bản Borland C++, đối tượng đại diện của lớp chứa các khai báo hàm ảo chiếm thêm bộ nhớ, chứ không phải là một đối tượng của một lớp tương tự trong đó các hàm giống nhau được khai báo mà không có bộ xác định ảo.

cout<< "Размеры объекта: " << sizeof(MyAC) << "…" << endl;

Vì vậy, đối tượng lớp dẫn xuất thu được một phần tử bổ sung - một con trỏ tới bảng hàm ảo. Sơ đồ của một đối tượng như vậy có thể được biểu diễn như sau (chúng ta biểu thị con trỏ tới bảng với mã định danh vptr, bảng các hàm ảo với mã định danh vtbl):

MyAC::= vptr A AC vtbl::= &AC::Fun1

Trong sơ đồ đối tượng mới của chúng tôi, không phải ngẫu nhiên mà con trỏ tới bảng (một mảng gồm một phần tử) của các hàm ảo được phân tách khỏi đoạn của đối tượng đại diện cho lớp cơ sở chỉ bằng một đường chấm. Nó nằm trong trường nhìn của mảnh vật thể này. Nhờ có sẵn con trỏ này, toán tử gọi hàm ảo Fun1

PObj->Fun1(1);

có thể được biểu diễn như sau:

(*(pObj->vptr)) (pObj,1);

Ở đây, thoạt nhìn, mọi thứ đều khó hiểu và khó hiểu. Trên thực tế, không có một biểu thức nào trong toán tử này mà chúng ta không biết.

Nó thực sự nói điều này:

GỌI CHỨC NĂNG NẰM Ở CHỈ SỐ 0 CỦA BẢNG CHỨC NĂNG ẢO vtbl (chúng ta chỉ có một phần tử trong bảng này), ĐỊA CHỈ BẮT ĐẦU CÓ THỂ TÌM THẤY BỞI INDEX vptr.

ĐẾN LƯỢT CỦA NÓ, CON TRỎ NÀY CÓ THỂ TRUY CẬP THÔNG QUA POINTER pObj, ĐƯỢC CẤU HÌNH CHO ĐỐI TƯỢNG MYAC. CHỨC NĂNG ĐƯỢC TRUYỀN HAI THAM SỐ (!), THAM SỐ ĐẦU TIÊN LÀ ĐỊA CHỈ CỦA ĐỐI TƯỢNG MyAC (giá trị cho con trỏ this!), THỨ HAI LÀ GIÁ TRỊ INTEGER BẰNG 1.

Lệnh gọi hàm thành viên của lớp cơ sở được cung cấp bởi một tên đủ điều kiện.

PObj->A::Fun1(1);

Trong tuyên bố này, chúng tôi loại bỏ các dịch vụ bảng chức năng ảo. Đồng thời, chúng tôi thông báo cho người dịch ý định gọi hàm thành viên của lớp cơ sở. Cơ chế hỗ trợ các chức năng ảo rất chặt chẽ và được quy định rất chặt chẽ. Một con trỏ tới bảng các hàm ảo nhất thiết phải được đưa vào đoạn cơ sở “trên cùng” của đối tượng lớp dẫn xuất. Bảng con trỏ bao gồm địa chỉ của các hàm thành viên của đoạn cấp thấp nhất chứa các khai báo của hàm này.

Chúng ta một lần nữa sửa đổi khai báo của lớp A, AB và khai báo một lớp mới ABC.

Việc sửa đổi các lớp A và AB nhằm khai báo các hàm thành viên mới trong chúng:

Lớp A ( public: virtual int Fun1(int key); virtual int Fun2(int key); ); ::::: int A::Fun2(int key) ( cout<< " Fun2(" << key << ") from A " << endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout << " Fun2(" << key << ") from AB " << endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC " << endl; return 0; }

Lớp này bao gồm một khai báo hàm thành viên Fun1, được khai báo trong lớp cơ sở gián tiếp A dưới dạng hàm ảo. Ngoài ra, lớp này kế thừa từ cơ sở trực tiếp hàm thành viên Fun2. Hàm này cũng được khai báo trong lớp cơ sở A là hàm ảo. Chúng ta khai báo một đối tượng đại diện của lớp ABC:

ABC MyABC;

Sơ đồ của nó có thể được biểu diễn như sau:

MyABC::= vptr A AB ABC vtbl::= &AB::Fun2 &ABC::Fun1

Bảng chức năng ảo hiện có hai phần tử. Chúng ta đặt con trỏ đối tượng của lớp cơ sở thành đối tượng MyABC, sau đó gọi các hàm thành viên:

PObj = pObj->Fun1(1); pObj->Fun2(2);

Trong trường hợp này, không thể gọi hàm thành viên AB::Fun1(), vì địa chỉ của nó không có trong danh sách các hàm ảo và đơn giản là nó không thể hiển thị từ cấp cao nhất của đối tượng MyABC mà pObj con trỏ được thiết lập. Bảng hàm ảo được xây dựng bởi hàm tạo tại thời điểm đối tượng của đối tượng tương ứng được tạo. Tất nhiên, người dịch đảm bảo rằng hàm tạo được mã hóa tương ứng. Nhưng người dịch không thể xác định nội dung của bảng hàm ảo cho một đối tượng cụ thể. Đây là một nhiệm vụ thời gian chạy. Cho đến khi bảng hàm ảo được xây dựng cho một đối tượng cụ thể thì hàm thành viên tương ứng của lớp dẫn xuất mới không thể gọi được. Điều này rất dễ dàng để xác minh sau một sửa đổi khác của khai báo lớp.

Chương trình này có dung lượng nhỏ nên việc cung cấp đầy đủ văn bản của nó là điều hợp lý. Bạn không nên bị đánh lừa bởi thao tác truy cập vào các thành phần của lớp::. Cuộc thảo luận về các vấn đề liên quan đến hoạt động này vẫn chưa được thực hiện.

#bao gồm lớp A ( public: virtual int Fun1(int key); ); int A::Fun1(int key) ( cout<< " Fun1(" << key << ") from A." << endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout << " Fun2(" << key << ") from AB." << endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC." << endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = &MyABC; // Определяем и настраиваем указатель. cout << "==========" << endl; pObj->Vui vẻ1(2); // Gọi ABC::Fun1(). //pObj->Fun2(2); // Hàm này không thể truy cập được thông qua con trỏ!!! pObj->A::Fun1(2); // Được gọi là A::Fun1(). )

Bây giờ tại thời điểm tạo đối tượng MyABC

ABC MyABC;

từ hàm tạo của lớp AB (và nó được gọi trước hàm tạo của lớp ABC), hàm A::Fun1() sẽ được gọi. Hàm này là thành viên của lớp A. Đối tượng MyABC chưa được hình thành đầy đủ, bảng hàm ảo chưa được điền và chưa biết gì về sự tồn tại của hàm ABC::Fun1(). Sau khi đối tượng MyABC cuối cùng được hình thành, bảng hàm ảo được điền và con trỏ pObj được đặt thành đối tượng MyABC, việc gọi hàm A::Fun1() thông qua con trỏ pObj sẽ chỉ có thể sử dụng tên đủ điều kiện của đối tượng này chức năng:

PObj->Fun1(1); // Đây là hàm gọi ABC::Fun1()! pObj->A::Fun1(1); // Rõ ràng đây là một hàm gọi A::Fun1()!

Lưu ý rằng việc gọi hàm thành viên Fun1 trực tiếp từ đối tượng MyABC sẽ tạo ra kết quả tương tự:

MyABC.Fun1(1); // Gọi hàm ABC::Fun1().

Và nỗ lực gọi hàm không ảo AB::Fun2() thông qua một con trỏ tới đối tượng lớp cơ sở không thành công. Không có địa chỉ cho hàm này trong bảng hàm ảo và không thể “nhìn xuống” từ cấp cao nhất của đối tượng.

//pObj->Fun2(2); // Bạn không thể làm theo cách này!

Kết quả thực hiện chương trình này thể hiện rõ ràng các chi tiết cụ thể của việc sử dụng các hàm ảo. Chỉ vài dòng thôi...

Fun1(125) từ A. Fun1(1) từ ABC. Fun1(5) từ ABC. Fun2(1) từ AB. Fun1(1) từ A. ========== Fun1(2) từ ABC. Fun1(2) từ A.

Con trỏ tương tự trong quá trình thực hiện chương trình có thể được điều chỉnh thành các đối tượng đại diện của các lớp dẫn xuất khác nhau. Kết quả là, theo nghĩa đen, cùng một biểu thức gọi hàm thành viên thực hiện các chức năng hoàn toàn khác nhau. Lần đầu tiên chúng ta phải đối mặt với cái gọi là Ràng buộc TRỄ hoặc TRÌ HOÃN.

Lưu ý rằng đặc tả ảo chỉ áp dụng cho các hàm. Không có thành viên dữ liệu ảo. Điều này có nghĩa là không có cách nào để truy cập các thành viên dữ liệu của đối tượng lớp dẫn xuất bằng con trỏ tới đối tượng lớp cơ sở được đặt thành đối tượng lớp dẫn xuất.

Mặt khác, rõ ràng là nếu bạn có thể gọi một hàm thay thế, thì trực tiếp “thông qua” hàm này bạn có quyền truy cập vào tất cả các hàm và thành viên dữ liệu của lớp dẫn xuất và sau đó “từ dưới lên” đến tất cả các hàm không riêng tư và các thành viên dữ liệu của các lớp cơ sở trực tiếp và gián tiếp. Trong trường hợp này, tất cả dữ liệu và hàm không riêng tư của các lớp cơ sở đều có sẵn trong hàm.

Và một ví dụ nhỏ nữa thể hiện sự thay đổi hành vi của một đối tượng đại diện của lớp dẫn xuất sau khi một trong các hàm của lớp cơ sở trở thành ảo.

#bao gồm lớp A ( public: void funA() (xFun();); /*virtual*/void xFun () (cout<<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<

Lúc đầu, bộ xác định ảo trong định nghĩa hàm A::xFun() được nhận xét. Quá trình thực hiện một chương trình bao gồm việc xác định một đối tượng đại diện objB của lớp dẫn xuất B và gọi hàm thành viên funA() trên đối tượng này. Hàm này được kế thừa từ lớp cơ sở, nó là một và rõ ràng là việc nhận dạng nó không gây ra bất kỳ vấn đề gì cho người dịch. Hàm này thuộc về lớp cơ sở, có nghĩa là tại thời điểm nó được gọi, quyền điều khiển sẽ được chuyển “lên cấp cao nhất” của đối tượng objB. Ở cùng cấp độ, có một trong các hàm có tên xFun() và chính hàm này điều khiển được chuyển giao trong quá trình thực hiện biểu thức cuộc gọi trong phần nội dung của hàm funA(). Hơn nữa, đơn giản là không thể gọi một hàm khác có cùng tên từ hàm funA(). Tại thời điểm phân tích cấu trúc của lớp A, người dịch hoàn toàn không biết gì về cấu trúc của lớp B. Hàm xFun(), một thành viên của lớp B, hóa ra là không thể truy cập được từ hàm funA().

Nhưng nếu bạn bỏ ghi chú bộ xác định ảo trong định nghĩa hàm A::xFun(), mối quan hệ thay thế sẽ được thiết lập giữa hai hàm cùng tên và việc tạo đối tượng objB sẽ đi kèm với việc tạo bảng của các hàm ảo, theo đó hàm thay thế, một thành viên của lớp B, sẽ được gọi. Bây giờ, để gọi hàm được thay thế, hàm phải sử dụng tên đủ điều kiện của nó:

Void A::funA () ( xFun(); A::xFun(); )

Sergey Malyshev (còn gọi là Mikhalych)

Phần 1. Lý thuyết chung về hàm ảo

Nhìn vào tiêu đề của bài viết này, bạn có thể nghĩ: “Hmm! Ai mà không biết chức năng ảo là gì nhỉ…” Nếu vậy thì bạn có thể yên tâm dừng đọc ngay tại đây.

Và đối với những người mới bắt đầu hiểu sự phức tạp của C++, nhưng đã có kiến ​​thức cơ bản về những thứ như tính kế thừa và đã nghe điều gì đó về tính đa hình, thì việc đọc tài liệu này là rất hợp lý. Nếu bạn hiểu các chức năng ảo, bạn sẽ có chìa khóa để mở khóa những bí mật của thiết kế hướng đối tượng thành công.

Nói chung tài liệu không khó lắm. Và mọi thứ sẽ được thảo luận ở đây chắc chắn có thể tìm thấy trong sách. Vấn đề duy nhất là bạn có thể sẽ không tìm được cách trình bày đầy đủ toàn bộ vấn đề trong một hoặc hai cuốn sách. Để viết về hàm ảo, tôi đã phải “nghiên cứu” 6 ấn phẩm khác nhau. Và ngay cả trong trường hợp này, tôi cũng không hề giả vờ là mình đã hoàn thiện. Trong danh sách tài liệu tham khảo, tôi chỉ nêu những tài liệu chính, những tài liệu đã truyền cảm hứng cho tôi về phong cách trình bày và nội dung.

Tôi quyết định chia tất cả tài liệu thành 3 phần.
Chúng ta hãy cố gắng tìm hiểu lý thuyết chung về hàm ảo trong phần đầu tiên. Trong phần thứ hai, chúng ta sẽ xem xét ứng dụng của chúng (cũng như sức mạnh và sức mạnh của chúng!) bằng cách sử dụng một số ví dụ thực tế ít nhiều. Chà, trong phần thứ ba chúng ta sẽ nói về một thứ như hàm hủy ảo.

Vậy đo la cai gi?

Trước tiên, hãy nhớ cách trong lập trình C cổ điển, bạn có thể truyền một đối tượng dữ liệu cho một hàm. Không có gì phức tạp về việc này, bạn chỉ cần đặt loại đối tượng được truyền vào thời điểm bạn viết mã hàm. Nghĩa là, để mô tả hành vi của các đối tượng, cần phải biết và mô tả trước loại của chúng. Sức mạnh của OOP trong trường hợp này là bạn có thể viết các hàm ảo để đối tượng tự xác định hàm nào nó cần gọi trong khi chương trình đang chạy.

Nói cách khác, với sự trợ giúp của các hàm ảo, đối tượng tự xác định hành vi của nó (hành động của chính nó). Kỹ thuật sử dụng hàm ảo được gọi là đa hình. Theo nghĩa đen, đa hình có nghĩa là có nhiều hình thức. Một đối tượng trong chương trình của bạn thực sự có thể đại diện không chỉ cho một lớp mà còn cho nhiều lớp khác nhau nếu chúng có quan hệ kế thừa với một lớp cơ sở chung. Tất nhiên, hành vi của các đối tượng của các lớp này trong hệ thống phân cấp sẽ khác nhau.

Vâng, bây giờ đến điểm!

Như bạn đã biết, theo các quy tắc của C++, một con trỏ tới một lớp cơ sở có thể tham chiếu đến một đối tượng của lớp này, cũng như một đối tượng của bất kỳ lớp nào khác dẫn xuất từ ​​lớp cơ sở. Hiểu quy tắc này là rất quan trọng. Chúng ta hãy xem xét một hệ thống phân cấp đơn giản của các lớp A, B và C nhất định. A sẽ là lớp cơ sở của chúng ta, B sẽ được dẫn xuất (được tạo) từ lớp A và C sẽ được dẫn xuất từ ​​B. Xem hình để biết giải thích.

Trong một chương trình, các đối tượng của các lớp này có thể được khai báo, chẳng hạn như theo cách này.

Một đối tượng_A; // khai báo một đối tượng kiểu A
đối tượng B_B; // khai báo một đối tượng kiểu B
đối tượng C_C; // khai báo một đối tượng kiểu C

Theo quy tắc này, một con trỏ loại A có thể tham chiếu đến bất kỳ đối tượng nào trong ba đối tượng này. Tức là điều này sẽ đúng:


point_to_Object=&object_C; //gán địa chỉ của đối tượng C cho con trỏ

Nhưng điều này không còn đúng nữa:

Trong *point_to_Object; // khai báo một con trỏ tới lớp dẫn xuất
point_to_Object=&object_A; // bạn không thể gán con trỏ tới địa chỉ của đối tượng cơ sở

Mặc dù con trỏ point_to_Object thuộc loại A* chứ không phải C* (hoặc B*), nó có thể tham chiếu đến các đối tượng thuộc loại C (hoặc B). Có lẽ quy luật sẽ rõ ràng hơn nếu bạn coi vật C là một loại vật thể A đặc biệt. Ví dụ, chim cánh cụt là một loại chim đặc biệt, nhưng nó vẫn là một con chim, mặc dù nó không bay. Tất nhiên, mối quan hệ giữa đối tượng và con trỏ chỉ hoạt động theo một hướng. Một vật thể loại C là một loại vật thể A đặc biệt, nhưng vật thể A không phải là một loại vật thể đặc biệt C. Quay trở lại với chim cánh cụt, chúng ta có thể nói một cách an toàn rằng nếu tất cả các loài chim đều là một loại chim cánh cụt đặc biệt, thì đơn giản là chúng sẽ không thể bay!

Nguyên tắc này trở nên đặc biệt quan trọng khi các hàm ảo được định nghĩa trong các lớp liên quan đến tính kế thừa. Các hàm ảo có hình thức giống hệt nhau và được lập trình giống như hầu hết các hàm thông thường. Chỉ quảng cáo của họ được thực hiện với một từ khóa ảo. Ví dụ: lớp cơ sở A của chúng tôi có thể khai báo một hàm ảo v_function().

hạng A
{
công cộng:
virtual void v_function(void);//function mô tả một số hành vi của lớp A
};

Một hàm ảo có thể được khai báo bằng các tham số và nó có thể trả về một giá trị, giống như bất kỳ hàm nào khác. Một lớp có thể khai báo bao nhiêu hàm ảo tùy theo nhu cầu của bạn. Và chúng có thể ở bất kỳ phần nào của lớp học - đóng, mở hoặc được bảo vệ.

Nếu trong lớp B, dẫn xuất từ ​​lớp A, bạn cần mô tả một số hành vi khác, thì bạn có thể khai báo một hàm ảo, lại được gọi là v_function().

hạng B: công cộng A
{
công cộng:
virtual void v_function(void);//hàm thay thế mô tả một cái gì đó
//hành vi mới của lớp B
};

Khi một lớp như B định nghĩa một hàm ảo có cùng tên với hàm ảo của lớp tổ tiên của nó, thì hàm đó được gọi là hàm ghi đè. Hàm ảo v_function() trong B thay thế hàm ảo có cùng tên trong lớp A. Trên thực tế, mọi thứ phức tạp hơn một chút và không chỉ đơn giản là trùng tên. Nhưng chúng ta sẽ nói thêm về điều này sau, trong phần “Một số điều tinh tế trong ứng dụng”.
Vâng, bây giờ là điều quan trọng nhất!

Hãy quay lại con trỏ point_to_Object loại A*, tham chiếu đến đối tượng object_B loại B*. Chúng ta hãy xem xét kỹ hơn câu lệnh gọi hàm ảo v_function() trên đối tượng được trỏ tới bởi điểm_to_Object.

Một *point_to_Object; // khai báo một con trỏ tới lớp cơ sở
point_to_Object=&object_B; //gán địa chỉ của đối tượng B cho con trỏ
point_to_Object->;v_function(); //gọi hàm

Con trỏ point_to_Object có thể lưu trữ địa chỉ của một đối tượng thuộc loại A hoặc B. Điều này có nghĩa là trong quá trình thực thi toán tử này point_to_Object-gt;v_function(); gọi một hàm ảo của lớp có đối tượng mà nó hiện đang đề cập đến. Nếu point_to_Object tham chiếu đến một đối tượng thuộc loại A, thì một hàm thuộc lớp A sẽ được gọi. Nếu point_to_Object tham chiếu đến một đối tượng thuộc loại B, thì một hàm thuộc lớp B sẽ được gọi. đối tượng đang được giải quyết. Đây là hành động được xác định trong quá trình thực hiện chương trình.

Vậy điều này mang lại cho chúng ta điều gì?

Đã đến lúc xem xét - các hàm ảo mang lại cho chúng ta những gì? Chúng ta đã có cái nhìn tổng quát về lý thuyết hàm ảo. Đã đến lúc xem xét một số tình huống thực tế mà bạn có thể hiểu được tầm quan trọng thực tế của chủ đề được đề cập trong thế giới lập trình thực tế.

Một ví dụ kinh điển (theo kinh nghiệm của tôi - trong 90% tài liệu về C++) được đưa ra cho mục đích này là viết một chương trình đồ họa. Một hệ thống phân cấp của các lớp được xây dựng, giống như “điểm -gt; đường -gt; hình phẳng -gt; Và chúng ta xem xét một hàm ảo, chẳng hạn như Draw(), để vẽ tất cả những thứ này... Chán quá!

Hãy xem xét một ví dụ ít mang tính học thuật hơn nhưng vẫn mang tính hình ảnh. (Cổ điển! Tôi có thể thoát khỏi nó ở đâu?). Chúng ta hãy thử xem xét một nguyên tắc giả định có thể được đưa vào trò chơi máy tính. Và không chỉ là một trò chơi, mà còn là nền tảng của bất kỳ game bắn súng nào (bất kể 3D hay 2D, hay hay tương tự). Bắn súng, nói một cách đơn giản. Ngoài đời tôi không khát máu, nhưng tội lỗi, đôi khi tôi thích bắn!

Vì vậy, chúng tôi quyết định tạo ra một game bắn súng thú vị. Bạn cần gì đầu tiên? Tất nhiên là vũ khí! (Chà, có lẽ không phải lần đầu tiên. Điều đó không thành vấn đề.) Tùy thuộc vào chủ đề chúng ta sẽ viết, những vũ khí như vậy sẽ cần thiết. Có thể đó sẽ là một bộ từ gậy đơn giản đến nỏ. Có thể từ súng hỏa mai đến súng phóng lựu. Hoặc thậm chí có thể từ một máy nổ trở thành một máy phân hủy. Chúng ta sẽ sớm thấy rằng đây chính xác là điều không quan trọng.

Chà, vì có rất nhiều khả năng nên chúng ta cần tạo một lớp cơ sở.

lớp vũ khí
{
công cộng:
... // sẽ có các thành viên dữ liệu có thể được mô tả, ví dụ như
//độ dày của gậy và số lượng lựu đạn trong súng phóng lựu
// phần này không quan trọng đối với chúng tôi

virtual void Use1(void);//thường - nút chuột trái
virtual void Use2(void);//thường - nút chuột phải

... // sẽ có thêm một số thành viên dữ liệu và phương thức
};

Không đi sâu vào chi tiết của lớp này, chúng ta có thể nói rằng điều quan trọng nhất có lẽ sẽ là các hàm Use1() và Use2(), mô tả hành vi (hoặc cách sử dụng) của loại vũ khí này. Lớp này có thể sinh ra bất kỳ loại vũ khí nào. Các thành phần dữ liệu mới (chẳng hạn như số lượng đạn, tốc độ bắn, mức năng lượng, chiều dài lưỡi kiếm, v.v.) và các chức năng mới sẽ được thêm vào. Và bằng cách xác định lại các hàm Use1() và Use2(), chúng ta sẽ mô tả sự khác biệt trong việc sử dụng vũ khí (đối với một con dao, nó có thể là đánh và ném, đối với súng máy, nó có thể là bắn một phát và bắn liên tục).

Bộ sưu tập vũ khí phải được cất giữ ở đâu đó. Rõ ràng, cách dễ nhất để làm điều này là tổ chức một mảng các con trỏ kiểu Weapon*. Để đơn giản, hãy giả sử rằng đây là một mảng Arms toàn cầu, với 10 loại vũ khí và tất cả các con trỏ đều được khởi tạo về 0 ngay từ đầu.

Vũ khí *Vũ khí; // mảng con trỏ tới các đối tượng thuộc loại Vũ khí

Bằng cách tạo các đối tượng động—các loại vũ khí—ở đầu chương trình, chúng ta sẽ thêm con trỏ tới chúng vào mảng.

Để cho biết loại vũ khí nào đang được sử dụng, chúng ta sẽ tạo một biến chỉ số mảng, giá trị của biến này sẽ thay đổi tùy thuộc vào loại vũ khí đã chọn.

int TypeOfWeapon;

Nhờ những nỗ lực này, mã mô tả việc sử dụng vũ khí trong trò chơi có thể trông như thế này:

if(LeftMouseClick) Arms-gt;Use1();
khác Cánh tay->Use2();

Tất cả! Chúng tôi đã tạo mã mô tả cuộc chiến bắn súng trước khi quyết định loại vũ khí nào sẽ được sử dụng. Hơn thế nữa. Chúng ta chưa có một loại vũ khí thực sự nào! Một lợi ích bổ sung (đôi khi rất quan trọng) là mã này có thể được biên dịch riêng và lưu trữ trong thư viện. Sau đó, bạn (hoặc một lập trình viên khác) có thể lấy các lớp mới từ Weapon, lưu trữ chúng trong mảng Arms và sử dụng chúng. Điều này sẽ không yêu cầu biên dịch lại mã của bạn.

Đặc biệt lưu ý rằng mã này không yêu cầu bạn chỉ định chính xác kiểu dữ liệu của các đối tượng được tham chiếu bởi con trỏ Arms, chỉ yêu cầu chúng bắt nguồn từ Weapon. Các đối tượng xác định trong thời gian chạy hàm Use() nào chúng nên gọi.

Một số sự tinh tế của ứng dụng

Chúng ta hãy dành một chút thời gian cho vấn đề thay thế các chức năng ảo.

Hãy quay lại từ đầu - với các lớp A, B và C nhàm chán. Lớp C hiện đang ở cuối cùng của hệ thống phân cấp, ở cuối dòng kế thừa. Trong lớp C, bạn có thể định nghĩa hàm ảo thay thế theo cách tương tự. Hơn nữa, không nhất thiết phải sử dụng từ khóa ảo vì đây là lớp cuối cùng trong dòng kế thừa. Chức năng này sẽ hoạt động và được chọn là ảo. Nhưng! Nhưng nếu bạn muốn loại bỏ một lớp D nhất định khỏi lớp C và thậm chí thay đổi hành vi của hàm v_function() thì sẽ không có kết quả gì. Để làm được điều này, trong lớp C, hàm v_function() phải được khai báo là ảo. Do đó, quy tắc có thể được xây dựng như sau: “một lần ảo, luôn ảo!” Nghĩa là, tốt hơn hết là không nên loại bỏ từ khóa ảo - nếu nó có ích thì sao?

Thêm một sự tinh tế nữa. Lớp dẫn xuất không thể định nghĩa một hàm có cùng tên và cùng bộ tham số nhưng có kiểu trả về khác với hàm ảo của lớp cơ sở. Trong trường hợp này, trình biên dịch sẽ chửi rủa ở giai đoạn biên dịch chương trình.

Hơn nữa. Nếu bạn đưa vào một hàm trong lớp dẫn xuất có cùng tên và kiểu trả về là hàm ảo của lớp cơ sở nhưng có tập tham số khác thì hàm này của lớp dẫn xuất sẽ không còn là hàm ảo nữa. Ngay cả khi bạn gắn thẻ nó với từ khóa ảo, nó sẽ không như bạn mong đợi. Trong trường hợp này, sử dụng một con trỏ tới lớp cơ sở, bất kỳ giá trị nào của con trỏ này sẽ gọi hàm của lớp cơ sở. Hãy nhớ quy tắc về nạp chồng hàm! Chúng chỉ là những chức năng khác nhau. Bạn sẽ kết thúc với một chức năng ảo hoàn toàn khác. Nói chung, những lỗi như vậy rất khó nắm bắt, vì cả hai dạng ký hiệu đều khá dễ chấp nhận và không có hy vọng chẩn đoán trình biên dịch trong trường hợp này.

Do đó có thêm một quy tắc nữa. Khi thay thế các hàm ảo, cần phải khớp hoàn toàn các loại tham số, tên hàm và loại giá trị trả về trong lớp cơ sở và lớp dẫn xuất.

Và xa hơn. Hàm ảo chỉ có thể là hàm lớp thành phần không tĩnh. Một chức năng toàn cầu không thể là ảo. Một hàm ảo có thể được khai báo là bạn trong một lớp khác. Nhưng chúng ta sẽ nói về các chức năng thân thiện trong một bài viết khác.

Đó là tất cả cho lần này.

Trong phần tiếp theo, bạn sẽ thấy một ví dụ đầy đủ chức năng của một chương trình đơn giản thể hiện tất cả những điểm chúng ta đã nói đến.

Nếu bạn có thắc mắc, hãy viết thư, chúng tôi sẽ giải quyết.

Hàm ảo- một loại hàm thành viên lớp đặc biệt. Hàm ảo khác với hàm thông thường ở chỗ đối với hàm thông thường, việc liên kết lệnh gọi hàm với định nghĩa của nó được thực hiện ở giai đoạn biên dịch. Đối với các hàm ảo, điều này xảy ra trong quá trình thực thi chương trình.

Để khai báo một hàm ảo, hãy sử dụng từ khóa ảo. Một hàm thành viên của lớp có thể được khai báo là ảo nếu

  • một lớp chứa hàm ảo, cơ sở trong hệ thống phân cấp thế hệ;
  • việc triển khai hàm là đặc thù của từng lớp và sẽ khác nhau ở mỗi lớp dẫn xuất.

Đó là một hàm được định nghĩa trong lớp cơ sở và bất kỳ lớp dẫn xuất nào cũng có thể ghi đè lên nó. Hàm ảo chỉ được gọi thông qua một con trỏ hoặc tham chiếu đến lớp cơ sở.

Việc xác định phiên bản nào của hàm ảo được gọi bằng biểu thức gọi hàm phụ thuộc vào lớp của đối tượng được đánh địa chỉ bởi con trỏ hoặc tham chiếu và được xác định tại thời điểm thực hiện chương trình. Cơ chế này được gọi là ràng buộc động (muộn) hoặc độ phân giải loại khi chạy.

Một con trỏ lớp cơ sở có thể trỏ tới một đối tượng của lớp cơ sở hoặc một đối tượng của lớp dẫn xuất. Việc lựa chọn hàm thành viên phụ thuộc vào đối tượng của lớp mà con trỏ trỏ tới trong khi thực hiện chương trình, chứ không phụ thuộc vào loại con trỏ. Nếu không có thành viên nào của lớp dẫn xuất thì hàm ảo mặc định của lớp cơ sở sẽ được sử dụng.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#bao gồm
sử dụng không gian tên std;
lớp X
{
được bảo vệ:
int tôi;
công cộng:
void seti(int c) ( i = c; )
in void ảo() ( cout<< endl << "lớp X: " << i; }
};
lớp Y: công khai X // kế thừa
{
công cộng:
void print() ( cout<< endl << "lớp Y: " << i; } // ghi đè hàm cơ bản
};
int chính()
{
X x;
X *px = // Con trỏ tới lớp cơ sở
Y y;
x.seti(10);
y.seti(15);
px->print(); // lớp X: 10
px =
px->print(); // lớp Y: 15
cin.get();
trả về 0;
}

Kết quả thực hiện

Trong mỗi trường hợp, một phiên bản khác nhau của hàm print() sẽ được thực thi. Việc lựa chọn có tính động tùy thuộc vào đối tượng mà con trỏ trỏ đến.

Nếu bạn loại bỏ từ khóa ảo ở dòng 9 (xem đoạn mã ở trên), thì kết quả thực hiện sẽ khác, bởi vì Liên kết chức năng sẽ xảy ra ở giai đoạn biên dịch:

Trong thuật ngữ OOP, "một đối tượng gửi thông báo in và chọn phiên bản riêng của phương thức tương ứng." Chỉ một hàm thành viên không tĩnh của một lớp mới có thể là hàm ảo. Đối với lớp dẫn xuất, hàm tự động trở thành hàm ảo nên có thể bỏ qua từ khóa virtual.

Ví dụ: lựa chọn chức năng ảo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#bao gồm
sử dụng không gian tên std;
nhân vật lớp học
{
được bảo vệ:
gấp đôi x, y;
công cộng:
hình(double a = 0, double b = 0) ( x = a; y = b; )
vùng kép ảo() ( return (0); ) // mặc định
};
lớp hình chữ nhật: nhân vật của công chúng
{
công cộng:
hình chữ nhật(double a = 0, double b = 0) : figure(a, b) ();
diện tích gấp đôi() ( return (x*y); )
};
vòng tròn lớp học: nhân vật của công chúng
{
công cộng:
hình tròn(double a = 0) : hình(a, 0) ();
diện tích gấp đôi() ( return (3.1415*x*x); )
};
int chính()
{
hình *f;
hình chữ nhật trực tràng(3, 4);
đường tròn cir(2);
tổng gấp đôi = 0;
f =
f =
tổng = f->diện tích();
cout<< total << endl;
tổng += f->diện tích();
cout<< total << endl;
cin.get();
trả về 0;
}

Kết quả thực hiện


Hàm ảo thuần túy

Lớp cơ sở của hệ thống phân cấp kiểu thường chứa một số hàm ảo cung cấp kiểu gõ động. Thông thường trong chính lớp cơ sở, bản thân các hàm ảo là giả và có phần thân trống. Chúng chỉ có một ý nghĩa nhất định trong các lớp được tạo ra. Những chức năng như vậy được gọi là hàm ảo thuần túy.

Hàm ảo thuần túy là một phương thức lớp có phần thân không được xác định.

Trong lớp cơ sở, hàm như vậy được viết như sau.

Các hàm ảo thuần túy

Cơ chế hàm ảo được sử dụng trong trường hợp cần đặt một hàm vào lớp cơ sở và hàm này phải được thực thi khác với các lớp dẫn xuất. Chính xác hơn, không chỉ hàm duy nhất từ ​​lớp cơ sở phải được thực thi khác nhau mà mỗi lớp sản xuất đều yêu cầu phiên bản riêng của hàm này.

Trước khi giải thích khả năng của các hàm ảo, hãy lưu ý rằng các lớp bao gồm các hàm như vậy đóng một vai trò đặc biệt trong lập trình hướng đối tượng. Đó là lý do tại sao chúng có một cái tên đặc biệt - đa hình. .

Không phải bất kỳ hàm nào cũng có thể là ảo mà chỉ có các hàm thành phần không tĩnh của một lớp. Khi một hàm được định nghĩa là ảo, việc xác định lại hàm đó trong lớp dẫn xuất (có cùng nguyên mẫu) sẽ tạo ra một hàm ảo mới trong lớp đó mà không cần sử dụng công cụ xác định ảo.

Lớp dẫn xuất không thể định nghĩa một hàm có cùng tên và cùng bộ tham số nhưng có kiểu trả về khác với hàm ảo của lớp cơ sở. Điều này dẫn đến lỗi khi biên dịch.

Nếu bạn giới thiệu một hàm trong lớp dẫn xuất có cùng tên và kiểu trả về như một hàm ảo trong lớp cơ sở, nhưng có tập tham số khác, thì hàm lớp dẫn xuất này sẽ không phải là hàm ảo. Trong trường hợp này, bằng cách sử dụng một con trỏ tới lớp cơ sở, đối với bất kỳ giá trị nào của con trỏ này, một lệnh gọi hàm của lớp cơ sở sẽ được thực hiện (bất chấp bộ xác định ảo và sự hiện diện của một hàm tương tự trong lớp dẫn xuất).

Phương thức (chức năng)

Các phương thức ảo được khai báo trong lớp cơ sở bằng từ khóa ảo và có thể được ghi đè trong lớp dẫn xuất. Nguyên mẫu của các phương thức ảo trong cả lớp cơ sở và lớp dẫn xuất phải giống nhau.

Việc sử dụng các phương thức ảo cho phép bạn triển khai cơ chế liên kết muộn, trong đó việc xác định phương thức được gọi xảy ra trong thời gian chạy chứ không phải ở giai đoạn biên dịch. Trong trường hợp này, phương thức ảo được gọi phụ thuộc vào loại đối tượng mà nó được gọi. Với liên kết sớm, được sử dụng cho các phương thức không ảo, việc xác định phương thức nào sẽ gọi xảy ra tại thời điểm biên dịch.

Ở giai đoạn biên dịch, một bảng các phương thức ảo được xây dựng và địa chỉ cụ thể được nhập ở giai đoạn thực thi.

Khi gọi một phương thức bằng con trỏ lớp, các quy tắc sau sẽ được áp dụng:

  • đối với một phương thức ảo, phương thức tương ứng với loại đối tượng được con trỏ trỏ tới sẽ được gọi.
  • đối với một phương thức không ảo, phương thức tương ứng với loại con trỏ sẽ được gọi.

Ví dụ sau minh họa việc gọi các phương thức ảo:

Lớp A // Khai báo lớp cơ sở( public: virtual void VirtMetod1(); // Phương thức ảo void Metod2(); // Phương thức không ảo void A::VirtMetod() ( cout);<< "Вызван A::VirtMetod1\n";} void A::Metod2() { cout << "Вызван A::Metod2\n"; } class B: public A // Объявление производного класса{public: void VirtMetod1(); // Виртуальный метод void Metod2(); // Не виртуальный метод};void B::VirtMetod1() { cout << "B::VirtMetod1\n";}void B::Metod2() { cout << "B::Metod2\n"; }void main() { B aB; // Объект класса B B *pB = &aB; // Указатель на объект класса B A *pA = &aB; // Указатель на объект класса A pA->VirtMetod1(); // Gọi phương thức VirtMetod của lớp B pB->VirtMetod1(); // Gọi phương thức VirtMetod của lớp B pA->Metod2(); // Gọi phương thức Metod2 của lớp A pB->Metod2(); // Gọi phương thức Method2 của lớp B)

Đầu ra của chương trình này sẽ là những dòng sau:

Đã gọi là B::VirtMetod1Được gọi là B::VirtMetod1Được gọi là A::Metod2Được gọi là B::Metod2

Hàm ảo thuần túy là hàm ảo được chỉ định bằng bộ khởi tạo

Ví dụ:

Khoảng trống ảo F1(int) =0;

Một khai báo lớp có thể chứa một hàm hủy ảo được sử dụng để xóa một đối tượng thuộc một kiểu cụ thể. Tuy nhiên, không có hàm tạo ảo trong C++. Một giải pháp thay thế cho phép bạn tạo các đối tượng thuộc một kiểu nhất định là các phương thức ảo, trong đó hàm tạo được gọi để tạo đối tượng của một lớp nhất định.

Tính đa hình trong thời gian chạy đạt được thông qua việc sử dụng các lớp dẫn xuất và các hàm ảo. Hàm ảo là hàm được khai báo bằng từ khóa virtual trong lớp cơ sở và được ghi đè trong một hoặc nhiều lớp dẫn xuất. Hàm ảo là các hàm đặc biệt vì khi bạn gọi một đối tượng của lớp dẫn xuất bằng cách sử dụng con trỏ hoặc tham chiếu đến nó, C++ sẽ xác định trong thời gian chạy hàm nào sẽ gọi dựa trên loại đối tượng. Các phiên bản khác nhau của cùng một hàm ảo được gọi cho các đối tượng khác nhau. Một lớp chứa một hoặc nhiều hàm ảo được gọi là lớp đa hình.

Một hàm ảo được khai báo trong lớp cơ sở bằng từ khóa ảo. Khi nó được ghi đè trong lớp dẫn xuất, không cần phải lặp lại từ khóa ảo, mặc dù sẽ không có lỗi nếu sử dụng lại.

Ví dụ đầu tiên về hàm ảo, hãy xem xét chương trình ngắn sau:

// một ví dụ nhỏ về việc sử dụng hàm ảo
#bao gồm
lớp cơ sở(
công cộng:

cout<< *Base\n";
}
};

công cộng:
void who() ( // định nghĩa của who() trong mối quan hệ với first_d
cout<< "First derivation\n";
}
};
lớp biệt phái: Cơ sở công cộng (
công cộng:

cout<< "Second derivation\n*";
}
};
int chính()
{
Cơ sở base_obj;
Cơ sở *p;
first_d first_obj;
giây_d giây_obj;
p = &base_obj;
p->
p = &first_obj;
p->
p = &giây_ob;
p->ai(); // truy cập vào ai của lớp thứ hai_d
trả về 0;
}

Chương trình sẽ cho ra kết quả sau:

Căn cứ
Đạo hàm đầu tiên
Đạo hàm thứ hai

Hãy phân tích chi tiết chương trình này để hiểu cách thức hoạt động của nó.

Như bạn có thể thấy, trong đối tượng Base, hàm who() được khai báo là ảo. Điều này có nghĩa là hàm này có thể bị ghi đè trong các lớp dẫn xuất. Trong mỗi lớp first_d và two_d, hàm who() sẽ bị ghi đè. Hàm main() xác định ba biến. Đầu tiên là đối tượng base_obj thuộc loại Base. Sau đó, một con trỏ p tới lớp Cơ sở được khai báo, sau đó là các đối tượng first_obj và two_obj, thuộc về hai lớp dẫn xuất. Tiếp theo, con trỏ p được gán địa chỉ của đối tượng base_obj và hàm who() được gọi. Vì hàm này được khai báo là ảo nên C++ xác định trong thời gian chạy phiên bản nào của hàm who() sẽ sử dụng, tùy thuộc vào đối tượng p trỏ tới. Trong trường hợp này, nó là một đối tượng thuộc loại Base, do đó phiên bản của hàm who() được khai báo trong lớp Base sẽ được thực thi. Con trỏ p sau đó được gán địa chỉ của đối tượng first_obj. (Như bạn đã biết, một con trỏ tới một lớp cơ sở có thể được sử dụng cho bất kỳ lớp dẫn xuất nào.) Sau khi who() được gọi, C++ lại kiểm tra kiểu của đối tượng được trỏ tới bởi p để xác định phiên bản của who() cần được gọi là. Vì p trỏ đến một đối tượng thuộc loại first_d nên phiên bản thích hợp của hàm who() sẽ được sử dụng. Tương tự, khi p được gán địa chỉ của Second_obj, phiên bản của hàm who() được khai báo trong Second_d sẽ được sử dụng.

Cách phổ biến nhất để gọi hàm ảo là sử dụng tham số hàm. Ví dụ: hãy xem xét sửa đổi sau đây của chương trình trước:

/* Ở đây tham chiếu lớp cơ sở được sử dụng để truy cập hàm ảo */
#bao gồm
lớp cơ sở(
công cộng:
virtual void who() ( // định nghĩa hàm ảo
cout<< "Base\n";
}
};
lớp first_d: Cơ sở công cộng (
công cộng:
void who () ( // định nghĩa của who() trong mối quan hệ với first_d
cout<< "First derivation\n";
}
};

công cộng:
void who() ( // định nghĩa của who() trong mối quan hệ với giây_d
cout<< "Second derivation\n*";
}
};
// sử dụng tham chiếu đến lớp cơ sở làm tham số
void show_who (Cơ sở &r) (
r.ai();
}
int chính()
{
Cơ sở base_obj;
first_d first_obj;
giây_d giây_obj;
show_who (base_ob j) ; // truy cập who của lớp Base
show_who(first_obj); // truy cập vào ai của lớp đầu tiên_d
show_who(second_obj); // truy cập vào ai của lớp thứ hai_d
trả về 0;
}

Chương trình này hiển thị dữ liệu giống như phiên bản trước. Trong ví dụ này, hàm show_who() có tham số kiểu tham chiếu đến lớp Cơ sở. Trong hàm main(), hàm ảo được gọi bằng cách sử dụng các đối tượng thuộc loại Base, first_d và two_d. Phiên bản của hàm who() được gọi trong hàm show_who() được xác định bởi loại đối tượng mà tham số tham chiếu đến khi hàm được gọi.

Chìa khóa để sử dụng hàm ảo nhằm cung cấp tính đa hình khi chạy là sử dụng một con trỏ tới lớp cơ sở. Tính đa hình thời gian chạy chỉ đạt được khi gọi hàm ảo bằng con trỏ hoặc tham chiếu đến lớp cơ sở. Tuy nhiên, không có gì ngăn cản bạn gọi các hàm ảo giống như bất kỳ hàm “bình thường” nào khác, nhưng không thể đạt được tính đa hình thời gian chạy theo cách này.

Thoạt nhìn, việc ghi đè một hàm ảo trong lớp dẫn xuất trông giống như một dạng nạp chồng hàm đặc biệt. Nhưng điều này không đúng và thuật ngữ nạp chồng hàm không áp dụng cho ghi đè hàm ảo, vì có sự khác biệt đáng kể giữa hai thuật ngữ này. Đầu tiên, chức năng phải phù hợp với nguyên mẫu. Như bạn đã biết, khi nạp chồng một hàm thông thường, số lượng và loại tham số phải khác nhau. Tuy nhiên, khi ghi đè một hàm ảo, giao diện của hàm đó phải khớp chính xác với nguyên mẫu. Nếu không có sự tương ứng như vậy thì hàm như vậy đơn giản được coi là quá tải và nó sẽ mất các thuộc tính ảo. Ngoài ra, nếu chỉ có kiểu trả về khác thì thông báo lỗi sẽ được đưa ra. (Các hàm chỉ khác nhau về kiểu trả về sẽ gây ra sự mơ hồ.) Một hạn chế khác là hàm ảo phải là thành viên chứ không phải là bạn của lớp mà nó được định nghĩa. Tuy nhiên, một hàm ảo có thể là bạn của một lớp khác. Mặc dù hàm hủy có thể là ảo nhưng hàm tạo không thể là ảo.

Do sự khác biệt giữa quá tải các hàm thông thường và ghi đè các hàm ảo, chúng tôi sẽ sử dụng thuật ngữ ghi đè cho hàm ảo.

Nếu một hàm đã được khai báo là ảo thì nó vẫn giữ nguyên như vậy, bất kể số cấp trong hệ thống phân cấp lớp mà nó đã vượt qua. Ví dụ: nếu lớp two_d có nguồn gốc từ lớp first_d chứ không phải lớp Base, thì hàm who() sẽ vẫn là hàm ảo và phiên bản chính xác của nó sẽ được gọi, như trong ví dụ sau:

// được tạo từ first_d, không phải từ Base
lớp thứ hai_d: công khai thứ nhất_d (
công cộng:
void who() ( // định nghĩa của who() trong mối quan hệ với giây_d
cout<< "Second derivation\n*";
}
};

Nếu một hàm ảo không bị ghi đè trong lớp dẫn xuất thì phiên bản của nó từ lớp cơ sở sẽ được sử dụng. Ví dụ: hãy chạy phiên bản sau của chương trình trước:

#bao gồm
lớp cơ sở(
công cộng:
khoảng trống ảo who() (
cout<< "Base\n";
}
};
lớp first_d: Cơ sở công cộng (
công cộng:
vô hiệu hóa ai() (
cout<< "First derivation\n";
}
};
lớp thứ hai_d: Cơ sở công cộng (
// who() chưa được xác định
};
int chính()
{
Cơ sở base_obj;
Cơ sở *p;
first_d first_obj; ,
giây_d giây_obj;
p = &base_obj;
p->ai(); // truy cập who của lớp Base
p = &đầu tiên;
p->ai(); // truy cập vào ai của lớp đầu tiên_d
p = &sepond_ob;
p->ai(); /* truy cập vào who() của Base vì two_d không ghi đè */
trả về 0;
}

Chương trình này sẽ tạo ra đầu ra sau:

Căn cứ
Đạo hàm đầu tiên
Căn cứ

Cần phải nhớ rằng đặc điểm của sự kế thừa là có tính chất thứ bậc. Để minh họa điều này, giả sử rằng trong ví dụ trước, lớp two_d được dẫn xuất từ ​​lớp first_d thay vì lớp Base. Khi who() được gọi bằng cách sử dụng một con trỏ tới một đối tượng thuộc loại two_d (trong đó who() chưa được xác định), phiên bản của who() được khai báo trong first_d sẽ được gọi, vì lớp đó là lớp gần nhất với two_d. Nói chung, khi một lớp không ghi đè một hàm ảo, C++ sử dụng định nghĩa đầu tiên mà nó tìm thấy, đi từ con cháu đến tổ tiên.