Lập trình C++ - Thừa kế C++

KHÁI NIỆM KẾ THỪA 

Lập trình hướng đối tượng có hai đặc trưng cơ bản: 

  • Đóng gói dữ liệu, được thể hiện bằng cách dùng khái niệm lớp để biểu diễn đối tượng với các thuộc tính private, chỉ cho phép bên ngoài truy nhập vào thông qua các phương thức get/set. 
  • Dùng lại mã, thể hiện bằng việc thừa kế giữa các lớp. Việc thừa kế cho phép các lớp thừa kế (gọi là lớp dẫn xuất) sử dụng lại các phương thức đã được định nghĩa trong các lớp gốc (gọi là lớp cơ sở). 

 


Khai báo thừa kế 
Cú pháp khai báo một lớp kế thừa từ một lớp khác như sau: 

class <Tên lớp dẫn xuất>: <Từ khóa dẫn xuất> <Tên lớp cơ sở>{ 
…  // Khai báo các thành phần lớp 
}; 

Trong đó: 

  • Tên lớp dẫn xuất: là tên lớp được cho kế thừa từ lớp khác. Tên lớp này tuân thủ theo quy tắc đặt tên biến trong C++. 
  • Tên lớp cở sở: là tên lớp đã được định nghĩa trước đó để cho lớp khác kế thừa. Tên lớp này cũng tuân thủ theo quy tắc đặt tên biến của C++. 
  • Từ khóa dẫn xuất: là từ khóa quy định tính chất của sự kế thừa. Có ba từ khóa dẫn xuất là private, protected và public. Mục tiếp theo sẽ trình bày ý nghĩa của các từ khóa dẫn xuất này. 

Ví dụ: 

class Bus: public Car{ 
…  // Khai báo các thành phần 
}; 

 Khai báo một lớp Bus (xe buýt) kế thừa từ lớp Car (xe ô tô) với tính chất kế thừa là public. 


Tính chất dẫn xuất 
Sự kế thừa cho phép trong lớp dẫn xuất có thể sử dụng lại một số mã nguồn của các phương thức và thuộc tính đã được định nghĩa trong lớp cơ sở. Nghĩa là lớp dẫn xuất có thể truy nhập trực tiếp đến một số thành phần của lớp cơ sở. Tuy nhiên, phạm vi truy nhập từ lớp dẫn xuất đến lớp cơ sở không phải bao giờ cũng giống nhau: chúng được quy định bởi các từ khóa dẫn xuất private, protected và public. 
Dẫn xuất private 
Dẫn xuất private quy định phạm vi truy nhập như sau: 

  • Các thành phần private của lớp cơ sở thì không thể truy nhập được từ lớp dẫn xuất. 
  • Các thành phần protected của lớp cơ sở trở thành các thành phần private của lớp dẫn xuất 
  • Các thành phần public của lớp cơ sở cũng trở thành các thành phần private của lớp dẫn xuất. 
  • Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất được tuân thủ như quy tắc phạm vi lớp thông thường. 

Dẫn xuất protected 
Dẫn xuất protected quy định phạm vi truy nhập như sau: 

  • Các thành phần private của lớp cơ sở thì không thể truy nhập được từ lớp dẫn xuất. 
  • Các thành phần protected của lớp cơ sở trở thành các thành phần protected của lớp dẫn xuất 
  • Các thành phần public của lớp cơ sở cũng trở thành các thành phần protected của lớp dẫn xuất. 
  • Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất được tuân thủ như quy tắc phạm vi lớp thông thường. 

Dẫn xuất public 
Dẫn xuất public quy định phạm vi truy nhập như sau: 

  • Các thành phần private của lớp cơ sở thì không thể truy nhập được từ lớp dẫn xuất. 
  • Các thành phần protected của lớp cơ sở trở thành các thành phần protected của lớp dẫn xuất. 
  • Các thành phần public của lớp cơ sở vẫn là các thành phần public của lớp dẫn xuất. 
  • Phạm vi truy nhập từ bên ngoài vào lớp dẫn xuất được tuân thủ như quy tắc phạm vi lớp thông thường. 

Chúng ta tổng kết các kiểu truy cập khác nhau, tương ứng với ai đó có thể truy cập chúng như sau:

Truy cập public protected private
Trong cùng lớp
Lớp kế thừa Không
Bên ngoài lớp Không Không

Ví dụ: Chúng ta xây dựng lớp Car có 2 thuộc tính và 2 phương thức input() show(), lớp Bus kế thừa từ lớp Car có 1 thuộc tính lable và 2 phương thức input() show()

#include<iostream>
#include<string>

using namespace std;
/* Định nghĩa lớp */
class Car {
	private:
		int  speed;               // Tốc độ  
		string  mark;           // Nhãn hiệu
		float price;               // Giá xe 
	public:
		void	input();
		void	show();               // Hiển thị thông tin về xe
};
// khai báo xe Bus kế thừa từ xe
class Bus: public Car{
	private:
		string lable;	// số xe
	public:
		void	input();
		void	show();
};

void Car::input() {
	cout << "Nhap toc do ";
	cin >> speed;
	cout << "Nhap nhan hieu ";
	cin >> mark;
	cout << "Nhap gia xe ";
	cin >> price;		
}
void Car::show() {
	cout << "Toc do xe: " << speed << ", Nhan hieu: " << mark << ", Gia xe:" << price;
}
void Bus::input() {
	Car::input();
	cout << "Nhap so xe ";
	cin >> lable;
}
void Bus::show() {
	Car::show();
	cout << ", So xe: " << lable;
}
int  main() {
	Bus myBus;
	myBus.input();
	myBus.show();
	system("pause");
	return 0;
}

Kết quả:

Nhap toc do 120
Nhap nhan hieu Toyota
Nhap gia xe 2300
Nhap so xe 9999
Toc do xe: 120, Nhan hieu: Toyota, Gia xe:2300, So xe: 9999

Hàm khởi tạo trong kế thừa 
Khi khai báo một đối tượng có kiểu lớp được dẫn xuất từ một lớp cơ sở khác. Chương trình sẽ tự động gọi tới hàm khởi tạo của lớp dẫn xuất. Tuy nhiên, thứ tự được gọi sẽ bắt đầu từ hàm khởi tạo tương ứng của lớp cơ sở, sau đó đến hàm khởi tạo của lớp dẫn xuất. Do đó, thông thường, trong hàm khởi tạo của lớp dẫn xuất phải có hàm khởi tạo của lớp cơ sở. 
Cú pháp khai báo hàm khởi tạo như sau: 

<Tên hàm khởi tạo dẫn xuất>([<Các tham số>]): <Tên hàm khởi tạo cơ sở>([<Các đối số>]){ 
… // Khởi tạo các thuộc tính mới bổ sung của lớp dẫn xuất 
};  

Vì tên hàm khởi tạo là trùng với tên lớp, nên có thể viết lại thành: 

<Tên lớp dẫn xuất>([<Các tham số>]): <Tên lớp cơ sở>([<Các đối số>]){ 
… // Khởi tạo các thuộc tính mới bổ sung của lớp dẫn xuất 
};  

Ví dụ:  

Bus():Car(){ 
    …     // Khởi tạo các thuộc tính mới bổ sung của lớp Bus 
} 

 Định nghĩa một hàm khởi tạo của lớp Bus kế thừa từ lớp Car. Định nghĩa này được thược hiện trong phạm vi khai báo lớp Bus. Đây là một hàm khởi tạo không tham số, nó gọi tới hàm khởi tạo không tham số của lớp Car. 
Lưu ý: 

  • Nếu định nghĩa hàm khởi tạo bên ngoài phạm vi lớp thì phải thêm tên lớp dẫn xuất và toán tử phạm vi “::” trước tên hàm khởi tạo. 
  • Giữa tên hàm khởi tạo của lớp dẫn xuất và hàm khởi tạo của lớp cơ sở, chỉ có môt dấu hai chấm “:”, nếu là hai dấu “::” thì trở thành toán tử phạm vi lớp. 
  • Nếu không chỉ rõ hàm khởi tạo của lớp cơ sở sau dấu hai chấm “:” chương trình sẽ tự động gọi hàm khởi tạo ngầm định hoặc hàm khởi tạo không có tham số của lớp cơ sở nếu hàm đó được định nghĩa tường minh trong lớp cơ sở. 

Ví dụ, định nghĩa hàm khởi tạo: 

Bus():Car(){ 
… 
};  

Có thể thay bằng:     // Khởi tạo các thuộc tính mới bổ sung của lớp Bus 

Bus(){     // Gọi hàm khởi tạo không tham số của lớp Car 
…     // Khởi tạo các thuộc tính mới bổ sung của lớp Bus 
};  

Ví dụ: Định nghĩa lớp Car có 3 thuộc tính với  hàm khởi tạo và phương thức show(), sau đó định nghĩa lớp Bus có thêm thuộc tính label là số hiệu của tuyến xe buýt. Lớp Bus sẽ được cài đặt  phương thức show() và hàm khởi tạo tường minh, gọi đến hai hàm khởi tạo tương ứng của lớp Car. 

#include<iostream> 
#include<string> 
using namespace std;
/* Định nghĩa lớp */
class Car {
private:
	int  speed;               // Tốc độ  
	string  mark;           // Nhãn hiệu
	float price;               // Giá xe 						
							   // Khởi tạo với các giá trị ngầm định cho các tham số 
public:
	Car(int speed, string mark, float price);
	void show(); // Giới thiệu xe 
};

/* Khai báo phương thức bên ngoài lớp */
Car::Car(int speed, string mark, float price) {
	this->speed = speed;
	this->mark = mark;
	this->price = price;
}

void Car::show() { // Phương thức giới thiệu xe 
	cout << "This is a " << mark << " having a speed of "
		<< speed << "km / h and its price is $" << price << endl;
	return;
}
// Khai báo lớp Bus kế thừa lớp Car
class Bus : public Car {
	private:	
		int lable;
	public:	
		Bus(int speed, string mark, float price, int lable);
		void show();
};
// Cài đặt lớp Buss
Bus::Bus(int speed, string mark, float price, int lable) :Car(speed, mark, price) {
	this->lable = lable;
}

void Bus::show() {
	Car::show();
	cout << "So xe la: " << lable;
}

// Hàm main, chương trình chính 
int  main() {

	Car c(120, "Honda civic", 20000);
	c.show();//phuong thuc cua lop car

	Bus b(100, "\Huyndai", 15000, 26);
	
	b.show();//phuong thuc cua lop bus

	cout << "\nchuyen doi kieu giua lop dan xuat va lop co so" << endl;
	c = b; // phải kế thừa public
	c.show();
	system("pause");
	return 0;
}

Kết quả:

This is a Honda civic having a speed of 120km / h and its price is $20000
This is a Huyndai having a speed of 100km / h and its price is $15000
So xe la: 26
chuyen doi kieu giua lop dan xuat va lop co so
This is a Huyndai having a speed of 100km / h and its price is $15000


Trong hàm khởi tạo của lớp Bus, muốn khởi tạo các thuộc tính của lớp Car, ta phải khởi tạo gián tiếp thông qua hàm khởi tạo của lớp Car mà không thể gán giá trị trực tiếp cho các thuộc tính speed, mark và price. Lí do là các thuộc tính này có tính chất private, nên lớp dẫn xuất không thể truy nhập trực tiếp đến chúng. 


Hàm hủy bỏ trong kế thừa 
Khi một đối tượng lớp dẫn xuất bị giải phóng khỏi bộ nhớ, thứ tự gọi các hàm hủy bỏ ngược với thứ tự gọi hàm thiết lập: gọi hàm hủy bỏ của lớp dẫn xuất trước khi gọi hàm hủy bỏ của lớp cơ sở. 
Vì mỗi lớp chỉ có nhiều nhất là một hàm hủy bỏ, nên ta không cần phải chỉ ra hàm hủy bỏ nào của lớp cơ sở sẽ được gọi sau khi hủy bỏ lớp dẫn xuất. Do vậy, hàm hủy bỏ trong lớp dẫn xuất được khai báo và định nghĩa hoàn toàn giống với các lớp thông thường: 

<Tên lớp>::~<Tên lớp>([<Các tham số>]){ 
         … // giải phóng phần bộ nhớ cấp phát cho các thuộc tính bổ sung 
}

Lưu ý: 

  • Hàm hủy bỏ của lớp dẫn xuất chỉ giải phóng phần bộ nhớ được cấp phát động cho các thuộc tính mới bổ sung trong lớp dẫn xuất, nếu có, mà không được giải phóng bộ nhớ được cấp cho các thuộc tính trong lớp cơ sở (phần này là do hàm hủy bỏ của lớp cơ sở đảm nhiệm). 
  • Không phải gọi tường minh hàm hủy bỏ của lớp cơ sở trong hàm hủy bỏ của lớp dẫn xuất. 
  • Ngay cả khi lớp dẫn xuất không định nghĩa tường minh hàm hủy bỏ (do không cần thiết) mà lớp cơ sở lại có định nghĩa tường minh. Chương trình vẫn gọi hàm hủy bỏ ngầm định của lớp dẫn xuất, sau đó vẫn gọi hàm hủy bỏ tường minh của lớp cơ sở. 

Ví dụ: cài đặt lớp Bus kế thừa từ lớp Car: lớp Car có một thuộc tính có dạng con trỏ nên cần giải phóng bằng hàm hủy bỏ tường minh. Lớp Bus có thêm một thuộc tính có dạng con trỏ là danh sách các đường phố mà xe buýt đi qua (mảng động các chuỗi kí tự *char[]) nên cũng cần giải phóng bằng hàm hủy bỏ tường minh. 


#include<string> 

/* Định nghĩa lớp Car */ 
class Car {
	char  *mark; 	 	 	// Nhãn hiệu xe
public: 
	~Car(); 	 	 	 	// Hủy bỏ tường minh 
};

Car::~Car() { 	 	 	 	 	// Hủy bỏ tường minh  	
	delete [] mark; 
}

/* Định nghĩa lớp Bus kế thừa từ lớp Car */ 
class Bus : public Car {
	char *voyage[];  	 	// Hành trình tuyến xe  	
public: 
	~Bus(); 	 	 	 	// Hủy bỏ tường minh  
};

Bus::~Bus() { 	// Hủy bỏ tường minh  	
	delete [] voyage; 
}