Nội dung
1. Tính kế thừa trong C++
Kế thừa là một trong những tính năng chính của lập trình hướng đối tượng trong C++. Nó cho phép chúng ta tạo một lớp mới (lớp con – derived class) từ một lớp hiện có (lớp cơ sở – base class).
Lớp con kế thừa các tính năng từ lớp cơ sở và có thể có các tính năng bổ sung của riêng nó. Ví dụ:
class Animal {
// eat() function
// sleep() function
};
class Dog : public Animal {
// bark() function
};
Ở đây, lớp Dog có nguồn gốc từ lớp Animal. Vì Dog có nguồn gốc từ Aminal nên các thành phần của Animal đều có thể truy cập vào Dog.
https://cdn.programiz.com/sites/tutorial2program/files/cpp-inheritance.png
Lưu ý về việc sử dụng từ khóa public trong kế thừa Dog từ Animal.
class Dog : public Animal {…};
Ta cũng có thể sử dụng các từ khóa private và protected thay vì public.
Chúng ta sẽ tìm hiểu về sự khác biệt giữa việc sử dụng private, public và protected sau trong bài viết này.
1.1. Quan hệ is-a
Kế thừa là một quan hệ is-a. Ta sử dụng kế thừa chỉ khi có một mối quan hệ is-a giữa hai lớp.
Dưới đây là một số ví dụ:
- A car is a vehicle
- Orange is a fruit
- A surgeon is a doctor
- A dog is an animal
Ví dụ 1: Ví dụ đơn giản về tính kế thừa trong C++
// C++ program to demonstrate inheritance
#include <iostream>
using namespace std;
// base class
class Animal {
public:
void eat() {
cout << “I can eat!” << endl;
}
void sleep() {
cout << “I can sleep!” << endl;
}
};
// derived class
class Dog : public Animal {
public:
void bark() {
cout << “I can bark! Woof woof!!” << endl;
}
};
int main() {
// Create object of the Dog class
Dog dog1;
// Calling members of the base class
dog1.eat();
dog1.sleep();
// Calling member of the derived class
dog1.bark();
return 0;
}
Đầu ra
I can eat!
I can sleep!
I can bark! Woof woof!!
Ở đây, dog1 (đối tượng của lớp con Dog) có thể truy cập các thành phần của lớp cơ sở Animal. Đó là vì Dog được kế thừa từ Animal.
// Calling members of the Animal class
dog1.eat();
dog1.sleep();
1.2. Protected trong C++
Công cụ sửa đổi truy cập protected đặc biệt liên quan đến tính kế thừa trong C++.
Giống như private, protected không thể truy cập được ở bên ngoài lớp. Tuy nhiên, chúng có thể được truy cập bởi các lớp con và các lớp/ hàm friend.
Chúng ta cần có protected nếu ta muốn ẩn dữ liệu của một lớp nhưng vẫn muốn dữ liệu đó được kế thừa bởi các lớp con của nó.
Ví dụ 2: Protected trong C++
// C++ program to demonstrate protected members
#include <iostream>
#include <string>
using namespace std;
// base class
class Animal {
private:
string color;
protected:
string type;
public:
void eat() {
cout << “I can eat!” << endl;
}
void sleep() {
cout << “I can sleep!” << endl;
}
void setColor(string clr) {
color = clr;
}
string getColor() {
return color;
}
};
// derived class
class Dog : public Animal {
public:
void setType(string tp) {
type = tp;
}
void displayInfo(string c) {
cout << “I am a ” << type << endl;
cout << “My color is ” << c << endl;
}
void bark() {
cout << “I can bark! Woof woof!!” << endl;
}
};
int main() {
// Create object of the Dog class
Dog dog1;
// Calling members of the base class
dog1.eat();
dog1.sleep();
dog1.setColor(“black”);
// Calling member of the derived class
dog1.bark();
dog1.setType(“mammal”);
// Using getColor() of dog1 as argument
// getColor() returns string data
dog1.displayInfo(dog1.getColor());
return 0;
}
Đầu ra
I can eat!
I can sleep!
I can bark! Woof woof!!
I am a mammal
My color is black
Ở đây, kiểu biến là protected, do đó có thể truy cập được từ lớp con Dog. Ta có thể thấy điều này khi khởi tạo kiểu trong lớp Dog bằng cách sử dụng hàm setType().
Mặt khác, biến private color không thể được khởi tạo trong Dog.
class Dog : public Animal {
public:
void setColor(string clr) {
// Error: member “Animal::color” is inaccessible
color = clr;
}
};
Ngoài ra, vì từ khóa protected ẩn dữ liệu nên ta không thể truy cập kiểu trực tiếp từ một đối tượng của lớp Dog hoặc Animal.
// Error: member “Animal::type” is inaccessible
dog1.type = “mammal”;
1.3. Chế độ truy cập trong kế thừa C++
Trong các nội dung trước, chúng ta đã tìm hiểu về các truy cập như public, private và protected.
Tôi đã sử dụng từ khóa public để kế thừa một lớp từ một lớp cơ sở đã tồn tại trước đó. Tuy nhiên, chúng ta cũng có thể sử dụng các từ khóa private và protected để kế thừa các lớp. Ví dụ:
class Animal {
// code
};
class Dog : private Animal {
// code
};
class Cat : protected Animal {
// code
};
Cách mà chúng ta có thể lấy được các lớp gọi là các chế độ truy cập. Các chế độ truy cập này có tác dụng:
- Public: nếu một lớp con được khai báo ở chế độ public thì các thành phần của lớp cơ sở được kế thừa bởi lớp con giống như chúng.
- Private: trong trường hợp này, tất cả các thành phần của lớp cơ sở đều trở thành private trong lớp con.
- Protected: các thành phần public của lớp cơ sở trở thành protected trong lớp con.
Các thành phần private của lớp cơ sở luôn là private trong lớp con.
1.4. Ghi đè chức năng trong kế thừa C++
Giả sử, lớp cơ sở và lớp con có các hàm thành phần có cùng tên và các đối số.
Nếu chúng ta tạo một đối tượng của lớp con và cố gắng truy cập hàm thành phần đó, thì hàm thành phần trong lớp con được gọi thay vì hàm trong lớp cơ sở.
Hàm thành phần của lớp con ghi đè hàm thành phần của lớp cơ sở.
2. Kiểm soát truy cập kế thừa trong C++
Trong kế thừa C++, chúng ta có thể dẫn xuất một lớp con từ lớp cơ sở trong các chế độ truy cập khác nhau. Ví dụ:
class Base {
…. … ….
};
class Derived : public Base {
…. … ….
};
Chú ý từ khóa public trong code:
class Derived : public Base
Điều này có nghĩa là ta đã tạo một lớp con từ lớp cơ sở ở chế độ public. Ngoài ra, chúng ta cũng có thể laays các lớp trong các chế độ protected hoặc private.
Ba từ khóa này (public, protected và private) được gọi là chỉ định truy cập trong kế thừa C++.
Kế thừa public, protected và private có các tính năng sau:
- Kế thừa public làm cho các thành phần public của lớp cơ sở là public trong lớp con và các protected của lớp cơ sở cũng là protected trong lớp con.
- Kế thừa protected làm cho các thành phần public và protected của lớp cơ sở là protected trong lớp con.
- Kế thừa private làm cho các thành phần public và protected của lớp cơ sở là private trong lớp con.
Lưu ý: private của lớp cơ sở không thể truy cập được vào lớp con.
class Base {
public:
int x;
protected:
int y;
private:
int z;
};
class PublicDerived: public Base {
// x is public
// y is protected
// z is not accessible from PublicDerived
};
class ProtectedDerived: protected Base {
// x is protected
// y is protected
// z is not accessible from ProtectedDerived
};
class PrivateDerived: private Base {
// x is private
// y is private
// z is not accessible from PrivateDerived
}
Ví dụ 1: Kế thừa public trong C++
// C++ program to demonstrate the working of public inheritance
#include <iostream>
using namespace std;
class Base {
private:
int pvt = 1;
protected:
int prot = 2;
public:
int pub = 3;
// function to access private member
int getPVT() {
return pvt;
}
};
class PublicDerived : public Base {
public:
// function to access protected member from Base
int getProt() {
return prot;
}
};
int main() {
PublicDerived object1;
cout << “Private = ” << object1.getPVT() << endl;
cout << “Protected = ” << object1.getProt() << endl;
cout << “Public = ” << object1.pub << endl;
return 0;
}
Đầu ra
Private = 1
Protected = 2
Public = 3
Ở đây, tôi đã dẫn xuất PublicDerived từ Base ở chế độ public.
Do đó, tròn PublicDervied:
- prot được kế thừa như protected
- pub và getPVT() được kế thừa như public
- pvt không thể truy cập được vì nó là private trong lớp cơ sở (Base).
Vì private và protected không thể truy cập được, nên tôi cần tạo các hàm public getPVT() và getProt() để truy cập chúng:
// Error: member “Base::pvt” is inaccessible
cout << “Private = ” << object1.pvt;
// Error: member “Base::prot” is inaccessible
cout << “Protected = ” << object1.prot;
Khả năng tiếp cận trong kế thừa Public
Khả năng tiếp cận | Private | Protected | Public |
Lớp cơ sở | Có | Có | Có |
Lớp con | Không | Có | Có |
Ví dụ 2: Kế thừa protected trong C++
// C++ program to demonstrate the working of protected inheritance
#include <iostream>
using namespace std;
class Base {
private:
int pvt = 1;
protected:
int prot = 2;
public:
int pub = 3;
// function to access private member
int getPVT() {
return pvt;
}
};
class ProtectedDerived : protected Base {
public:
// function to access protected member from Base
int getProt() {
return prot;
}
// function to access public member from Base
int getPub() {
return pub;
}
};
int main() {
ProtectedDerived object1;
cout << “Private cannot be accessed.” << endl;
cout << “Protected = ” << object1.getProt() << endl;
cout << “Public = ” << object1.getPub() << endl;
return 0;
}
Đầu ra
Private cannot be accessed.
Protected = 2
Public = 3
Ở đây, tôi đã dẫn xuất ProtectedDerived từ Base ở chế độ protected.
Kết quả là, trong ProtectedDerived:
- Prot, pub và getPVT() được kế thừa như protected
- Pvt không thể truy cập vì nó là private trong lớp cơ sở (Base).
Như chúng ta biết, các protected không thể truy cập trực tiếp. Do đó, tôi không thể sử dụng getPVT() từ ProtectedDerived. Đó cũng là lý do tại sao tôi cần tạo hàm getPub() trong ProtectedDerived để truy cập biến pub.
// Error: member “Base::getPVT()” is inaccessible
cout << “Private = ” << object1.getPVT();
// Error: member “Base::pub” is inaccessible
cout << “Public = ” << object1.pub;
Khả năng tiếp cận trong kế thừa protected
Khả năng tiếp cận | Private | Protected | Public |
Lớp cơ sở | Có | Có | Có |
Lớp con | Không | Có | Có (kế thừa dưới dạng biến protected) |
Ví dụ 3: Kế thừa private trong C++
// C++ program to demonstrate the working of private inheritance
#include <iostream>
using namespace std;
class Base {
private:
int pvt = 1;
protected:
int prot = 2;
public:
int pub = 3;
// function to access private member
int getPVT() {
return pvt;
}
};
class PrivateDerived : private Base {
public:
// function to access protected member from Base
int getProt() {
return prot;
}
// function to access private member
int getPub() {
return pub;
}
};
int main() {
PrivateDerived object1;
cout << “Private cannot be accessed.” << endl;
cout << “Protected = ” << object1.getProt() << endl;
cout << “Public = ” << object1.getPub() << endl;
return 0;
}
Đầu ra
Private cannot be accessed.
Protected = 2
Public = 3
Ở đây, tôi đã dẫn xuất PrivateDerived từ Base ở chế độ private.
Do đó, trong PricvateDerived:
- Prot, pub và getPVT() được kế thừa dưới dạng private.
- Pvt không thể truy cập được vì nó là private trong lớp cơ sở.
Như chúng ta biết. Không thể truy cập trực tiếp từ private. Do đó, tôi không thể sử dụng getPVT() từ PrivateDerived. Đó cũng là lý do tại sao tôi cần tạo hàm getPub() trong PrivateDerived để truy cập biến pub.
// Error: member “Base::getPVT()” is inaccessible
cout << “Private = ” << object1.getPVT();
// Error: member “Base::pub” is inaccessible
cout << “Public = ” << object1.pub;
Khả năng tiếp cận trong kế thừa private
Khả năng tiếp cận | Private | Protected | Public |
Lớp cơ sở | Có | Có | Có |
Lớp con | Không | Có (kế thừa dưới dạng biến private) | Có (kế thừa dưới dạng biến private) |
3. Ghi đè chức năng trong C++
Như chúng ta đã biết, kế thừa là một tính năng của OOP cho phép chúng ta tạo các lớp con từ một lớp cơ sở. Các lớp con kế thừa các tính năng của lớp cơ sở.
Giả sử, khi một hàm cùng được định nghĩa trong cả lớp con và lớp cơ sở, nếu tôi gọi hàm này bằng cách sử dụng đối tượng của lớp con, thì hàm của lớp con sẽ được thực thi.
Đây được gọi là ghi đè hàm trong C++. Hàm trong lớp con sẽ ghi đè hàm trong lớp cơ sở.
Ví dụ: Ghi đè hàm trong C++
// C++ program to demonstrate function overriding
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << “Base Function” << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
}
};
int main() {
Derived derived1;
derived1.print();
return 0;
}
Đầu ra
Derived Function
Ở đây, hàm print() cùng được định nghĩa trong cả lớp cơ sở và lớp con.
Vì vậy, khi tôi gọi print() từ đối tượng con derived1, print() từ Derived được thực thi bằng cách ghi đè hàm trong Base.
https://cdn.programiz.com/sites/tutorial2program/files/cpp-function-overriding.png
Truy cập hàm ghi đè trong C++
Để truy cập hàm ghi đè của lớp cơ sở, chúng ta sử dụng toán tử phân giải phạm vi ::.
Ta có thể truy cập hàm được ghi đè bằng cách sử dụng một pointer của lớp cơ sở để trỏ đến một đối tượng của lớp con và sau đó gọi hàm từ pointer đó.
Ví dụ 2: Truy cập hàm ghi đè vào lớp cơ sở trong C++
// C++ program to access overridden function
// in main() using the scope resolution operator ::
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << “Base Function” << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
}
};
int main() {
Derived derived1, derived2;
derived1.print();
// access print() function of the Base class
derived2.Base::print();
return 0;
}
Đầu ra
Derived Function
Base Function
Ở đây, câu lệnh
derived2.Base::print();
Truy cập hàm print() của lớp cơ sở.
Ví dụ 3: Gọi hàm ghi đè từ lớp con trong C++
// C++ program to call the overridden function
// from a member function of the derived class
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << “Base Function” << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
// call overridden function
Base::print();
}
};
int main() {
Derived derived1;
derived1.print();
return 0;
}
Đầu ra
Derived Function
Base Function
Trong chương trình này, tôi đã gọi hàm ghi đè trong lớp Derived
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
Base::print();
}
};
Cần lưu ý rằng code Base::print(); gọi hàm ghi đè bên trong lớp Derived.
Ví dụ 4: Gọi hàm ghi đè bằng cách dùng Pointer trong C++
// C++ program to access overridden function using pointer
// of Base type that points to an object of Derived class
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << “Base Function” << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
}
};
int main() {
Derived derived1;
// pointer of Base type that points to derived1
Base* ptr = &derived1;
// call function of Base class using ptr
ptr->print();
return 0;
}
Đầu ra
Base Function
Trong chương trình này, tôi đã tạo một pointer loại Base tên ptr. Pointer này trỏ đến đối tượng derived1.
// pointer of Base type that points to derived1
Base* ptr = &derived1;
Khi tôi gọi hàm print() bằng ptr, nó sẽ gọi hàm ghi đè từ Base.
// call function of Base class using ptr
ptr->print();
Mặc dù ptr trỏ đến một đối tượng con nhưng thuộc kiểu cơ sở nên nó đã gọi hàm thành viên của Base.
Để ghi đè hàm Base thay vì truy cập nó, chúng ta cần sử dụng các hàm ảo trong lớp cơ sở.
4. Kế thừa đa cấp và đa kế thừa trong C++ (multiple và multilevel)
Kế thừa là một trong những tính năng cốt lõi của ngôn ngữ lập trình hướng đối tượng. Nó cho phép các nhà phát triển phần mềm lấy ra một lớp mới từ lớp hiện có. Trong đó lớp con lại kế thừa các tính năng của lớp cơ sở (lớp hiện có).
Trong lập trình C++, có nhiều mô hình tính kế thừa trong C++ khác nhau.
4.1. Kế thừa đa cấp trong C++
Trong lập trình C++, bạn không chỉ có thể dẫn xuất một lớp con từ lớp cơ sở mà bạn còn có thể dẫn xuất một lớp khác từ lớp con. Hình thức kế thừa này được gọi là kế thừa đa cấp.
class A
{
… .. …
};
class B: public A
{
… .. …
};
class C: public B
{
… … …
};
Ở đây, lớp B có nguồn gốc từ lớp cơ sở A và lớp C được dẫn xuất từ lớp con B.
Ví dụ 1: Kế thừa đa cấp trong C++
#include <iostream>
using namespace std;
class A
{
public:
void display()
{
cout<<“Base class content.”;
}
};
class B : public A
{
};
class C : public B
{
};
int main()
{
C obj;
obj.display();
return 0;
}
Đầu ra
Base class content.
Trong chương trình này, lớp C có nguồn gốc từ lớp B (được dẫn xuất từ lớp cơ sở A).
Đối tượng obj của lớp C được định nghĩa trong hàm main().
Khi hàm display() được gọi, hàm display() trong lớp A được thực thi. Đó là vì không có hàm display() trong lớp C và lớp B.
Đầu tiên, trình biên dịch tìm kiếm hàm display() trong lớp C. Vì hàm không tồn tại ở đó nên nó sẽ tìm hàm trong lớp B (vì C có nguồn gốc từ B).
Hàm cũng không tồn tại trong lớp B, vì vậy trình biên dịch sẽ tìm kiếm nó trong lớp A (vì B có nguồn gốc từ A).
Nếu hàm display() tồn tại trong C, trình biên dịch sẽ ghi đè hàm display() của lớp A (chức năng ghi đè hàm thành viên)
4.2. Đa kế thừa trong C++
Trong lập trình C++, một lớp có thể được bắt nguồn từ nhiều hơn một lớp gốc. Ví dụ: một lớp Bat có nguồn gốc từ các lớp cơ sở Mammal và WingedAnimal. Nó hợp lý bởi vì dơi là động vật có vú và cũng là động vật có cánh.
https://cdn.programiz.com/sites/tutorial2program/files/multiple-inheritance-example.jpg
Ví dụ: Đa kế thừa trong C++
#include <iostream>
using namespace std;
class Mammal {
public:
Mammal()
{
cout << “Mammals can give direct birth.” << endl;
}
};
class WingedAnimal {
public:
WingedAnimal()
{
cout << “Winged animal can flap.” << endl;
}
};
class Bat: public Mammal, public WingedAnimal {
};
int main()
{
Bat b1;
return 0;
}
Đầu ra
Mammals can give direct birth.
Winged animal can flap.
Sự không rõ ràng trong đa kế thừa C++
Vấn đề rõ ràng nhất trong đa kế thừa diễn ra trong quá trình ghi đè hàm.
Giả sử, hai lớp cơ sở có cùng một chức năng không bị ghi đè trong lớp con.
Nếu bạn cố gắng gọi hàm bằng cách sử dụng đối tượng của lớp con, trình biên dịch sẽ hiển thị lỗi. Đó là vì trình biên dịch không biết gọi hàm nào. Ví dụ:
class base1
{
public:
void someFunction( )
{ …. … …. }
};
class base2
{
void someFunction( )
{ …. … …. }
};
class derived : public base1, public base2
{
};
int main()
{
derived obj;
obj.someFunction() // Error!
}
Vấn đề này có thể được giải quyết bằng cách sử dụng hàm phân giải phạm vi để chỉ định hàm nào sẽ phân loại, là base1 hoặc base2.
int main()
{
obj.base1::someFunction( ); // Function of base1 class is called
obj.base2::someFunction(); // Function of base2 class is called.
}
5. Hàm Friend trong C++
Ẩn dữ liệu là một khái niệm cơ bản của lập trình hướng đối tượng. Nó hạn chế quyền truy cập của các thành phần private từ bên ngoài lớp.
Tương tự, các protected chỉ có thể được truy cập bởi cá lớp con và không thể truy cập từ bên ngoài. Ví dụ:
class MyClass {
private:
int member1;
}
int main() {
MyClass obj;
// Error! Cannot access private members from here.
obj.member1 = 5;
}
Tuy nhiên, có một tính năng trong C++ được gọi là hàm friend giúp phá vỡ quy tắc này và cho phép chúng ta truy cập các hàm thành phần từ bên ngoài lớp.
Tương tự, cũng có một lớp friend mà chúng ta sẽ tìm hiểu ở phần sau của bài.
5.1. Hàm friend trong C++
Hàm friend có thể truy cập dữ liệu private và protected của một lớp. Ta có thể khai báo hàm friend bằng cách sử dụng từ khóa friend bên trong phần thân của lớp.
class className {
… .. …
friend returnType functionName(arguments);
… .. …
}
Ví dụ 1: Hoạt động của hàm friend
// C++ program to demonstrate the working of friend function
#include <iostream>
using namespace std;
class Distance {
private:
int meter;
// friend function
friend int addFive(Distance);
public:
Distance() : meter(0) {}
};
// friend function definition
int addFive(Distance d) {
//accessing private members from the friend function
d.meter += 5;
return d.meter;
}
int main() {
Distance D;
cout << “Distance: ” << addFive(D);
return 0;
}
Đầu ra
Distance: 5
Ở đây, addFive() là một hàm friend có thể truy cập cả dữ liệu private và public.
Mặc dù ví dụ này giúp chúng ta hiểu về khái niệm hàm friend nhưng không cho ta thấy cách sử dụng có ý nghĩa nào của hàm.
Hàm friend sẽ có ý nghĩa hơn khi hoạt động trên các đối tượng của hai lớp khác nhau.
Ví dụ 2: Thêm thành phần của hai lớp khác nhau
// Add members of two different classes using friend functions
#include <iostream>
using namespace std;
// forward declaration
class ClassB;
class ClassA {
public:
// constructor to initialize numA to 12
ClassA() : numA(12) {}
private:
int numA;
// friend function declaration
friend int add(ClassA, ClassB);
};
class ClassB {
public:
// constructor to initialize numB to 1
ClassB() : numB(1) {}
private:
int numB;
// friend function declaration
friend int add(ClassA, ClassB);
};
// access members of both classes
int add(ClassA objectA, ClassB objectB) {
return (objectA.numA + objectB.numB);
}
int main() {
ClassA objectA;
ClassB objectB;
cout << “Sum: ” << add(objectA, objectB);
return 0;
}
Đầu ra
Sum: 13
Trong chương trình này, ClassA và ClassB đã khai báo add() như một hàm friend. Do đó, hàm này có thể truy cập dữ liệu private của cả hai lớp.
Một điều cần lưu ý ở đây là hàm friend bên trong ClassA đang sử dụng ClassB. Tuy nhiên, tôi chưa xác định ClassB ở thời điểm này.
// inside classA
friend int add(ClassA, ClassB);
Để thao tác này hoạt động, tôi cần khai báo ClassB trước trong chương trình.
// forward declaration
class ClassB;
5.2. Lớp friend trong C++
Ta cũng có thể dùng lớp friend trong C++ bằng cách dùng từ khóa friend. Ví dụ:
class ClassB;
class ClassA {
// ClassB is a friend class of ClassA
friend class ClassB;
… .. …
}
class ClassB {
… .. …
}
Khi một lớp friend được khai báo, tất cả các hàm thành phần của lớp friend đều trở thành hàm friend.
Vì ClassB là một lớp friend nên tôi có thể truy cập tất cả các thành phần của classA từ bên trong classB.
Tuy nhiên, tôi không thể truy cập các thành phần của classB từ bên trong classA. Đó là vì quan hệ friend trong C++ chỉ được cấp, không được lấy.
Ví dụ 3: Lớp friend trong C++
// C++ program to demonstrate the working of friend class
#include <iostream>
using namespace std;
// forward declaration
class ClassB;
class ClassA {
private:
int numA;
// friend class declaration
friend class ClassB;
public:
// constructor to initialize numA to 12
ClassA() : numA(12) {}
};
class ClassB {
private:
int numB;
public:
// constructor to initialize numB to 1
ClassB() : numB(1) {}
// member function to add numA
// from ClassA and numB from ClassB
int add() {
ClassA objectA;
return objectA.numA + numB;
}
};
int main() {
ClassB objectB;
cout << “Sum: ” << objectB.add();
return 0;
}
Đầu ra
Sum: 13
Ở đây, ClassB là một lớp friend của ClassA. Vì vậy, ClassB có quyền truy cập vào classA.
Trong ClassB, tôi đã tạo một hàm add() trả về tổng của numA và numB. Vì classB là một lớp friend nên tôi có thể tạo các đối tượng của classA bên trong classB.
6. Hàm ảo (virtual) trong C++
Hàm ảo là một hàm thành viên trong lớp cơ sở mà ta muốn xác định lại trong các lớp con.
Về cơ bản, một hàm ảo được sử dụng trong lớp cơ sở để đảm bảo rằng hàm được ghi đè. Điều này đặc biệt áp dụng cho các trường hợp một pointer của lớp cơ sở trỏ đến một đối tượng của lớp con.
Ví dụ:
class Base {
public:
void print() {
// code
}
};
class Derived : public Base {
public:
void print() {
// code
}
};
Sau đó, nếu tôi tạo một pointer kiểu Base để trỏ đến một đối tượng của lớp con và gọi làm print(), nó sẽ gọi hàm print() của lớp cơ sở.
Nói cách khác, hàm thành viên của Base không bị ghi đè.
int main() {
Derived derived1;
Base* base1 = &derived1;
// calls function of Base class
base1->print();
return 0;
}
Để tránh điều này, ta cần khai báo hàm print() của lớp cơ sở là hàm ảo bằng cách sử dụng từ khóa virtual.
class Base {
public:
virtual void print() {
// code
}
};
Hàm ảo là một phần không thể thiếu của tính đa hình trong C++.
Ví dụ 1: Hàm ảo trong C++
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() {
cout << “Base Function” << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << “Derived Function” << endl;
}
};
int main() {
Derived derived1;
// pointer of Base type that points to derived1
Base* base1 = &derived1;
// calls member function of Derived class
base1->print();
return 0;
}
Đầu ra
Derived Function
Ở đây, tôi đã khai báo hàm print() của Base là ảo.
Vì vậy, hàm này bị ghi đè ngay cả khi tôi sử dụng pointer kiểu cơ sở trỏ đến đến đối tượng con derived1.
https://cdn.programiz.com/sites/tutorial2program/files/cpp-virtual-function.png
6.1. Ghi đè mã định danh trong C++
C++11 đã cung cấp cho người dùng chức năng mới rất hữu ích là ghi đè mã định danh để tránh lỗi khi sử dụng các hàm ảo.
Định danh này chỉ định các hàm thành viên của các lớp con ghi đè hàm thành viên của lớp cơ sở.
Ví dụ:
class Base {
public:
virtual void print() {
// code
}
};
class Derived : public Base {
public:
void print() override {
// code
}
};
Nếu chúng ta sử dụng một nguyên mẫu hàm trong lớp con và xác định hàm đó bên ngoài lớp thì ta dùng đoạn code sau:
class Derived : public Base {
public:
// function prototype
void print() override;
};
// function definition
void Derived::print() {
// code
}
Sử dụng ghi đè trong C++
Khi sử dụng các hàm ảo, ta có thể gặp lỗi khi khai báo các hàm thành viên của các lớp con.
Việc sử dụng mã định danh ghi đè sẽ nhắc trình biên dịch hiển thị thông báo lỗi khi những lỗi này xảy ra.
Nếu không, chương trình sẽ biên dịch đơn giản nhưng hàm ảo sẽ không bị ghi đè.
Một số lỗi có thể xảy ra bao gồm:
- Hàm sai tên: ví dụ, nếu hàm ảo trong lớp cơ sở được đặt tên là print(), nhưng chúng ta vô tình đặt tên cho hàm ghi đè trong lớp con là pint().
- Hàm có kiểu trả về khác nhau: nếu hàm ảo là kiểu void nhưng hàm trong lớp con là kiểu int.
- Hàm với các tham số khác nhau: nếu các tham số của hàm ảo và các hàm trong lớp con không khớp.
- Không có hàm ảo nào được khai báo trong lớp cơ sở.
6.2. Sử dụng hàm ảo trong C++
Giả sử ta có một lớp cơ sở Animal và lớp con là Dog và Cat, mỗi lớp có dữ liệu thành viên tên là kiểu và các biến này được khởi tạo thông qua các hàm khởi tạo tương ứng.
class Animal {
private:
string type;
… .. …
public:
Animal(): type(“Animal”) {}
… .. …
};
class Dog : public Animal {
private:
string type;
… .. …
public:
Animal(): type(“Dog”) {}
… .. …
};
class Cat : public Animal {
private:
string type;
… .. …
public:
Animal(): type(“Cat”) {}
… .. …
};
Bây giờ, tôi giả sử rằng chương trình yêu cầu ta tạo hai hàm public cho mỗi lớp:
- getType() để trả về giá trị của kiểu.
- print() để xuất giá trị của kiểu
Tôi có thể tạo cả hai hàm này trong mỗi lớp riêng biệt và ghi đè chúng nhưng sẽ rất lâu và nhàm chán.
Hoặc tôi có thể làm cho getType() ảo trong lớp Animal, sau đó tạo một hàm print() riêng lẻ chấp nhận một pointer kiểu Animal làm đối số của nó. Sau đó, tôi có thể sử dụng hàm đơn này để ghi đè hàm ảo.
class Animal {
… .. …
public:
… .. …
virtual string getType {…}
};
… .. …
… .. …
void print(Animal* ani) {
cout << “Animal: ” << ani->getType() << endl;
}
Điều này sẽ làm cho code ngắn hơn, rõ ràng hơn và ít lặp lại hơn.
Ví dụ 2: Hàm ảo C++
// C++ program to demonstrate the use of virtual function
#include <iostream>
#include <string>
using namespace std;
class Animal {
private:
string type;
public:
// constructor to initialize type
Animal() : type(“Animal”) {}
// declare virtual function
virtual string getType() {
return type;
}
};
class Dog : public Animal {
private:
string type;
public:
// constructor to initialize type
Dog() : type(“Dog”) {}
string getType() override {
return type;
}
};
class Cat : public Animal {
private:
string type;
public:
// constructor to initialize type
Cat() : type(“Cat”) {}
string getType() override {
return type;
}
};
void print(Animal* ani) {
cout << “Animal: ” << ani->getType() << endl;
}
int main() {
Animal* animal1 = new Animal();
Animal* dog1 = new Dog();
Animal* cat1 = new Cat();
print(animal1);
print(dog1);
print(cat1);
return 0;
}
Đầu ra
Animal: Animal
Animal: Dog
Animal: Cat
Ở đây, tôi đã sử dụng hàm ảo getType() và một pointer Animal là ani để tránh lặp lại hàm print() trong mỗi lớp.
void print(Animal* ani) {
cout << “Animal: ” << ani->getType() << endl;
}
Trong main(), tôi đã tạo 3 pointer Animal để tạo các đối tượng của các lớp Animal, Dog và Cat.
// dynamically create objects using Animal pointers
Animal* animal1 = new Animal();
Animal* dog1 = new Dog();
Animal* cat1 = new Cat();
Sau đó, tôi gọi hàm print() bằng cách sử dụng các pointer sau:
- Khi print(animal1) được gọi, pointer trỏ đến một đối tượng Animal. Vì vậy, hàm ảo trong lớp Animal được thực thi bên trong print().
- Khi print(dog1) được gọi, pointer trỏ đến một đối tượng Dog. Vì vậy, hàm ảo bị ghi đè và hàm của Dog được thực thi bên trong print().
- Khi print(cat1) được gọi, pointer trỏ đến một đối tượng Cat. Vì vậy, hàm ảo bị ghi đè và hàm của Cat được được thi bên trong print().
7. Templates trong C++
Templates là tính năng quyền lực trong C++ cho phép bạn viết các chương trình chung. Nói một cách dễ hiểu, bạn có thể tạo một hàm hoặc môt lớp để làm việc với các kiểu dữ liệu khác nhau bằng cách sử dụng templates.
Templates thường được sử dụng trong cơ sở mã lớn nhằm mục đích tái sử dụng mã và sự linh hoạt của chương trình.
Khái niệm về templates có thể được chia theo hai cách:
- Function templates – Khuôn mẫu hàm
- Class templates – Khuôn mẫu lớp
7.1. Function templates – Khuôn mẫu hàm
Một khuôn mẫu hàm hoạt động tương tự như một hàm bình thường nhưng có một khác biệt chính.
Một khuôn mẫu hàm duy nhất có thể hoạt động với các kiểu dữ liệu khác nhau cùng một lúc, nhưng một hàm bình thường chỉ có thể hoạt động với một nhóm kiểu dữ liệu.
Thông thường, nếu bạn cần thực hiện các thao tác giống hệt nhau trên hai hoặc nhiều kiểu dữ liệu, bạn sử dụng tính năng nạp chồng hàm để tạo hai hàm với khai báo hàm được yêu cầu.
Tuy nhiên, bạn có thể sử dụng các khuôn mẫu hàm vì nó có thể thực hiện cùng một tác vụ với code ít hơn và có thể duy trì.
Cách khai báo khuôn mẫu hàm
Một khuôn mẫu hàm bắt đầu với từ khóa template, theo sau là tham số/ s bên trong ngoặc nhọn < > , và sau đó là khai báo hàm.
template <class T>
T someFunction(T arg)
{
… .. …
}
Trong đoạn mã trên, T là một đối số khuôn mẫu chấp nhận các kiểu dữ liệu khác nhau (int, float) và class là một từ khóa.
Bạn cũng có thể sử dụng từ khóa typename thay vì class trong ví dụ trên.
Khi một đối số của một kiểu dữ liệu được chuyển đến someFunction(), trình biên dịch sẽ tạo một phiên bản mới của someFunction() cho kiểu dữ liệu đã cho.
Ví dụ 1: Khuôn mẫu hàm tìm kiếm số lớn nhất.
Chương trình hiển thị số lớn nhất trong hai số bằng cách sử dụng khuôn mẫu hàm
// If two characters are passed to function template, character with larger ASCII value is displayed.
#include <iostream>
using namespace std;
// template function
template <class T>
T Large(T n1, T n2)
{
return (n1 > n2) ? n1 : n2;
}
int main()
{
int i1, i2;
float f1, f2;
char c1, c2;
cout << “Enter two integers:\n”;
cin >> i1 >> i2;
cout << Large(i1, i2) <<” is larger.” << endl;
cout << “\nEnter two floating-point numbers:\n”;
cin >> f1 >> f2;
cout << Large(f1, f2) <<” is larger.” << endl;
cout << “\nEnter two characters:\n”;
cin >> c1 >> c2;
cout << Large(c1, c2) << ” has larger ASCII value.”;
return 0;
}
Đầu ra
Enter two integers:
5
10
10 is larger.
Enter two floating-point numbers:
12.4
10.2
12.4 is larger.
Enter two characters:
z
Z
z has larger ASCII value.
Trong chương trình trên, một khuôn mẫu hàm Large() được định nghĩa, chấp nhận hai đối số n1 và n2 của kiểu dữ liệu T. T biểu thị rằng đối số có thể thuộc bất kỳ kiểu dữ liệu nào.
Hàm Large() trả về giá trị lớn nhất trong số hai đối số bằng cách sử dụng một phép toán có điều kiện đơn giản.
Bên trong hàm main(), các biến của ba kiểu dữ liệu khác nhau: int, float và char được khai báo. Các biến sau đó được chuyển đến khuôn mẫu hàm Large() như các hàm bình thường.
Trong thời gian chạy, khi một số nguyên được chuyển đến khuôn mẫu hàm, trình biên dịch biết nó phải tạo ra một hàm Large() để nhận các đối số int nên nó đã làm như vậy.
Tương tự, khi dữ liệu dấu chấm động và dữ liệu char được truyền, nó sẽ biết các kiểu dữ liệu đối số và tạo ra hàm Large() cho phù hợp.
Bằng cách này, chỉ sử dụng một khuôn mẫu hàm duy nhất đã thay thế ba hàm bình thường giống hệt nhau và làm cho mã của bạn được duy trì.
Ví dụ 2: Hoán đổi dữ liệu bằng khuôn mẫu hàm
Chương trình hoán đổi dữ liệu sử dụng khuôn mẫu hàm
#include <iostream>
using namespace std;
template <typename T>
void Swap(T &n1, T &n2)
{
T temp;
temp = n1;
n1 = n2;
n2 = temp;
}
int main()
{
int i1 = 1, i2 = 2;
float f1 = 1.1, f2 = 2.2;
char c1 = ‘a’, c2 = ‘b’;
cout << “Before passing data to function template.\n”;
cout << “i1 = ” << i1 << “\ni2 = ” << i2;
cout << “\nf1 = ” << f1 << “\nf2 = ” << f2;
cout << “\nc1 = ” << c1 << “\nc2 = ” << c2;
Swap(i1, i2);
Swap(f1, f2);
Swap(c1, c2);
cout << “\n\nAfter passing data to function template.\n”;
cout << “i1 = ” << i1 << “\ni2 = ” << i2;
cout << “\nf1 = ” << f1 << “\nf2 = ” << f2;
cout << “\nc1 = ” << c1 << “\nc2 = ” << c2;
return 0;
}
Đầu ra
Before passing data to function template.
i1 = 1
i2 = 2
f1 = 1.1
f2 = 2.2
c1 = a
c2 = b
After passing data to function template.
i1 = 2
i2 = 1
f1 = 2.2
f2 = 1.1
c1 = b
c2 = a
Trong chương trình này, thay vì gọi một hàm bằng cách truyền một giá trị, một lệnh gọi bằng tham chiếu đã được thực hiện.
Khuôn mẫu hàm swap() đã nhận hai đối số và hoán đổi chúng bằng cách tham chiếu.
7.2. Class templates – Khuôn mẫu lớp
Giống với khuôn mẫu hàm, bạn cũng có thể tạo các khuôn mẫu lớp cho các hoạt động chung của lớp.
Đôi khi, bạn cần triển khai lớp giống nhau cho tất cả các lớp, chỉ có kiểu dữ liệu được sử dụng là khác nhau.
Thông thường, bạn sẽ cần tạo một lớp khác nhau cho từng kiểu dữ liệu, hoặc tạo các biến và các hàm thành viên khác nhau trong một lớp duy nhất.
Việc này sẽ khiến cho code của bạn phức tạp một cách không cần thiết và khó duy trì, vì việc thay đổi một lớp/ hàm nên được thực hiện trên tất cả các lớp/ hàm.
Tuy nhiên, các khuôn mẫu lớp sẽ giúp bạn dễ dàng sử dụng lại cùng một code cho tất cả các kiểu dữ liệu.
Cách khai báo khuôn mẫu lớp
template <class T>
class className
{
… .. …
public:
T var;
T someOperation(T arg);
… .. …
};
Trong phần khai báo trên, T là đối số khuôn mẫu, là phần giữ chỗ cho kiểu dữ liệu được sử dụng.
Bên trong phần thân của lớp, một biến var và một biến someOperation() đều thuộc kiểu T.
Cách tạo một đối tượng khuôn mẫu lớp
Để tạo một đối tượng khuôn mẫu lớp, bạn cần xác định kiểu dữ liệu bên trong một dấu ngoặc nhọn < > khi tạo.
className<dataType> classObject;
Ví dụ:
className<int> classObject;
className<float> classObject;
className<string> classObject;
Ví dụ 3: Phép tính đơn giản sử dụng khuôn mẫu lớp
Chương trình cộng, trừ, nhân, chia hai số sử dụng khuôn mẫu lớp.
#include <iostream>
using namespace std;
template <class T>
class Calculator
{
private:
T num1, num2;
public:
Calculator(T n1, T n2)
{
num1 = n1;
num2 = n2;
}
void displayResult()
{
cout << “Numbers are: ” << num1 << ” and ” << num2 << “.” << endl;
cout << “Addition is: ” << add() << endl;
cout << “Subtraction is: ” << subtract() << endl;
cout << “Product is: ” << multiply() << endl;
cout << “Division is: ” << divide() << endl;
}
T add() { return num1 + num2; }
T subtract() { return num1 – num2; }
T multiply() { return num1 * num2; }
T divide() { return num1 / num2; }
};
int main()
{
Calculator<int> intCalc(2, 1);
Calculator<float> floatCalc(2.4, 1.2);
cout << “Int results:” << endl;
intCalc.displayResult();
cout << endl << “Float results:” << endl;
floatCalc.displayResult();
return 0;
}
Đầu ra
Int results:
Numbers are: 2 and 1.
Addition is: 3
Subtraction is: 1
Product is: 2
Division is: 2
Float results:
Numbers are: 2.4 and 1.2.
Addition is: 3.6
Subtraction is: 1.2
Product is: 2.88
Division is: 2
Trong chương trình trên, một khuôn mẫu lớp Calculator được khai báo.
Lớp chứa hai thành phần private của kiểu T là num1 và num2, và một hàm khởi tạo để tạo các thành viên.
Nó cũng chứa các hàm public để tính cộng, trừ, nhân, chia các số trả về giá trị của kiểu dữ liệu do người dùng xác định. Tương tự vậy, hàm displayResult() để hiển thị kết quả cuối cùng ra màn hình.
Trong hàm main(), hai đối tượng Calculator khác nhau là intCalc và floatCalc được tạo cho kiểu dữ liệu int và float tương ứng. Các giá trị được khởi tạo bằng cách sử dụng hàm khởi tạo.
Lưu ý rằng tôi sử dụng <int> và <float> trong khi tạo các đối tượng. Chúng cho trình biên dịch biết kiểu dữ liệu được sử dụng để tạo lớp.
Việc này tạo ra định nghĩa lớp cho từng int và float và được sử dụng cho phù hợp. Sau đó, displayResult() của cả hai đối tượng được gọi để thực hiện các hoạt động tính toán và hiển thị kết quả đầu ra.
8. Lời kết
Trên đây là tất cả những kiến thức tổng quan về tính kế thừa trong C++ cùng các ví dụ bài tập kế thừa trong C++ cụ thể. Hy vọng các kiến thức bổ ích này giúp bạn có thể hiểu rõ hơn về kế thừa C++ trong lập trình.
Tham khảo thêm các bài viết về kiến thức lập trình của Ironhack Việt Nam để cập nhập thêm mindset cho bản thân. Hoặc nếu bạn muốn trở thành một lập trình viên chuyên nghiệp, đừng ngần ngại tham khảo các khóa học của Ironhack Việt Nam. Chúc bạn thành công.