Kế thừa
Khi định nghĩa một lớp, nhà phát triển có thể kế thừa nó từ một lớp khác, từ đó thể hiện khái niệm kế thừa. Để làm điều này, tên lớp được theo sau bởi dấu hai chấm, một bộ điều chỉnh quyền truy cập tùy chọn (một trong các từ khóa public
, protected
, private
), và tên của lớp cha. Ví dụ, dưới đây là cách chúng ta có thể định nghĩa một lớp Rectangle
kế thừa từ Shape
:
class Rectangle : public Shape
{
};
2
3
Các bộ điều chỉnh quyền truy cập trong tiêu đề lớp kiểm soát "khả năng hiển thị" của các thành viên của lớp cha được bao gồm trong lớp con:
public
— tất cả các thành viên được kế thừa giữ nguyên quyền và giới hạn của chúng.protected
— thay đổi quyền của các thành viênpublic
được kế thừa thànhprotected
.private
— làm cho tất cả các thành viên được kế thừa trở thành riêng tư (private
).
Bộ điều chỉnh public
được sử dụng trong phần lớn các định nghĩa. Hai lựa chọn còn lại chỉ có ý nghĩa trong các trường hợp đặc biệt vì chúng vi phạm nguyên tắc cơ bản của kế thừa: các đối tượng của lớp dẫn xuất nên là "is a" – đại diện đầy đủ của họ lớp cha, và nếu chúng ta "cắt bớt" quyền của chúng, chúng sẽ mất đi một phần đặc điểm. Các cấu trúc cũng có thể kế thừa lẫn nhau theo cách tương tự. Việc kế thừa lớp từ cấu trúc hoặc cấu trúc từ lớp là không được phép.
Không giống như C++, MQL5 không hỗ trợ kế thừa đa cấp. Một lớp chỉ có thể có tối đa một lớp cha.
Một đối tượng của lớp dẫn xuất có một đối tượng của lớp cơ sở được tích hợp vào nó. Xét rằng lớp cơ sở cũng có thể được kế thừa từ một lớp cha khác, đối tượng được tạo ra có thể được so sánh với búp bê Matryoshka lồng vào nhau.
Trong lớp mới, chúng ta cần một hàm tạo để điền vào các trường của đối tượng theo cách tương tự như đã làm trong lớp cơ sở.
class Rectangle : public Shape
{
public:
Rectangle(int px, int py, color back) :
Shape(px, py, back)
{
Print(__FUNCSIG__, " ", &this);
}
};
2
3
4
5
6
7
8
9
Trong trường hợp này, danh sách khởi tạo đã trở thành một lời gọi duy nhất đến hàm tạo Shape
. Bạn không thể trực tiếp đặt các biến của lớp cơ sở trong danh sách khởi tạo, vì hàm tạo cơ sở chịu trách nhiệm khởi tạo chúng. Tuy nhiên, nếu cần, chúng ta có thể thay đổi các trường protected
của lớp cơ sở từ phần thân của hàm tạo Rectangle
(các câu lệnh trong thân hàm được thực thi sau khi hàm tạo cơ sở hoàn tất lời gọi của nó trong danh sách khởi tạo).
Hình chữ nhật có hai chiều, vì vậy hãy thêm chúng dưới dạng các trường protected
là dx
và dy
. Để đặt giá trị của chúng, bạn cần bổ sung danh sách các tham số của hàm tạo.
class Rectangle : public Shape
{
protected:
int dx, dy; // kích thước (chiều rộng, chiều cao)
public:
Rectangle(int px, int py, int sx, int sy, color back) :
Shape(px, py, back), dx(sx), dy(sy)
{
}
};
2
3
4
5
6
7
8
9
10
11
Điều quan trọng cần lưu ý là các đối tượng Rectangle
ngầm chứa hàm toString
được kế thừa từ Shape
(tuy nhiên, draw
cũng có mặt ở đó, nhưng nó vẫn trống). Do đó, đoạn mã sau là đúng:
void OnStart()
{
Rectangle r(100, 200, 50, 75, clrBlue);
Print(r.toString());
};
2
3
4
5
Điều này không chỉ thể hiện việc gọi toString
mà còn tạo một đối tượng hình chữ nhật bằng hàm tạo mới của chúng ta.
Không có hàm tạo mặc định (không có tham số) trong lớp Rectangle
. Điều này có nghĩa là người dùng của lớp không thể tạo các đối tượng hình chữ nhật một cách đơn giản, không có đối số:
Rectangle r; // 'Rectangle' - số lượng tham số sai
Trình biên dịch sẽ hiển thị lỗi "Số lượng đối số không hợp lệ".
Hãy tạo một lớp con khác – Ellipse
. Hiện tại, nó sẽ không khác gì so với Rectangle
, ngoại trừ tên. Sau này chúng ta sẽ giới thiệu sự khác biệt giữa chúng.
class Ellipse : public Shape
{
protected:
int dx, dy; // kích thước (bán kính lớn và nhỏ)
public:
Ellipse(int px, int py, int rx, int ry, color back) :
Shape(px, py, back), dx(rx), dy(ry)
{
Print(__FUNCSIG__, " ", &this);
}
};
2
3
4
5
6
7
8
9
10
11
12
Khi số lượng lớp tăng lên, sẽ rất tuyệt nếu hiển thị tên lớp trong phương thức toString
. Trong phần Toán tử sizeof và typename đặc biệt, chúng ta đã mô tả toán tử typename
. Hãy thử sử dụng nó.
Hãy nhớ rằng typename
mong đợi một tham số, mà từ đó tên kiểu được trả về. Ví dụ, nếu chúng ta tạo một cặp đối tượng s
và r
của các lớp Shape
và Rectangle
, chúng ta có thể tìm hiểu kiểu của chúng theo cách sau:
void OnStart()
{
Shape s;
Rectangle r(100, 200, 75, 50, clrRed);
Print(typename(s), " ", typename(r)); // Shape Rectangle
}
2
3
4
5
6
Nhưng chúng ta cần lấy tên này bên trong lớp bằng cách nào đó. Để làm điều này, hãy thêm một tham số chuỗi vào hàm tạo có tham số của Shape
và lưu trữ nó trong một trường chuỗi mới type
(chú ý đến phần protected
và bộ điều chỉnh const
: trường này được ẩn khỏi thế giới bên ngoài và không thể chỉnh sửa sau khi đối tượng được tạo):
class Shape
{
protected:
...
const string type;
public:
Shape(int px, int py, color back, string t) :
coordinates(px, py),
backgroundColor(back),
type(t)
{
Print(__FUNCSIG__, " ", &this);
}
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Trong các hàm tạo của các lớp dẫn xuất, chúng ta điền tham số này của hàm tạo cơ sở bằng cách sử dụng typename(this)
:
class Rectangle : public Shape
{
...
public:
Rectangle(int px, int py, int sx, int sy, color back) :
Shape(px, py, back, typename(this)), dx(sx), dy(sy)
{
Print(__FUNCSIG__, " ", &this);
}
};
2
3
4
5
6
7
8
9
10
Bây giờ chúng ta có thể cải thiện phương thức toString
bằng cách sử dụng trường type
.
class Shape
{
...
public:
string toString() const
{
return type + " " + (string)coordinates.x + " " + (string)coordinates.y;
}
};
2
3
4
5
6
7
8
9
Hãy đảm bảo rằng hệ thống phân cấp lớp nhỏ của chúng ta tạo ra các đối tượng như dự định và in các mục log thử nghiệm khi các hàm tạo và hàm hủy được gọi.
void OnStart()
{
Shape s;
// thiết lập một đối tượng bằng cách nối chuỗi các lời gọi qua 'this'
s.setColor(clrWhite).moveX(80).moveY(-50);
Rectangle r(100, 200, 75, 50, clrBlue);
Ellipse e(200, 300, 100, 150, clrRed);
Print(s.toString());
Print(r.toString());
Print(e.toString());
}
2
3
4
5
6
7
8
9
10
11
Kết quả là, chúng ta nhận được các mục log gần giống như sau (các dòng trống được thêm vào cố ý để phân tách đầu ra từ các đối tượng khác nhau):
Pair::Pair(int,int) 0 0
Shape::Shape() 1048576
Pair::Pair(int,int) 100 200
Shape::Shape(int,int,color,string) 2097152
Rectangle::Rectangle(int,int,int,int,color) 2097152
Pair::Pair(int,int) 200 300
Shape::Shape(int,int,color,string) 3145728
Ellipse::Ellipse(int,int,int,int,color) 3145728
Shape 80 -50
Rectangle 100 200
Ellipse 200 300
Ellipse::~Ellipse() 3145728
Shape::~Shape() 3145728
Pair::~Pair() 200 300
Rectangle::~Rectangle() 2097152
Shape::~Shape() 2097152
Pair::~Pair() 100 200
Shape::~Shape() 1048576
Pair::~Pair() 80 -50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Log cho thấy rõ ràng thứ tự gọi các hàm tạo và hàm hủy.
Đối với mỗi đối tượng, đầu tiên, các trường đối tượng được mô tả trong nó được tạo (nếu có), sau đó hàm tạo cơ sở và tất cả các hàm tạo của các lớp dẫn xuất dọc theo chuỗi kế thừa được gọi. Nếu có các trường riêng (được thêm vào) của một số kiểu đối tượng trong lớp dẫn xuất, các hàm tạo cho chúng sẽ được gọi ngay trước hàm tạo của lớp dẫn xuất này. Khi có nhiều trường đối tượng, chúng được tạo theo thứ tự được mô tả trong lớp.
Các hàm hủy được gọi theo thứ tự hoàn toàn ngược lại.
Trong các lớp dẫn xuất, có thể định nghĩa các hàm tạo sao chép, mà chúng ta đã tìm hiểu trong Hàm tạo: Mặc định, Có tham số, Sao chép. Đối với các kiểu hình cụ thể, chẳng hạn như hình chữ nhật, cú pháp của chúng tương tự:
class Rectangle : public Shape
{
...
Rectangle(const Rectangle &other) :
Shape(other), dx(other.dx), dy(other.dy)
{
}
...
};
2
3
4
5
6
7
8
9
Phạm vi đang mở rộng một chút. Một đối tượng của lớp dẫn xuất có thể được sử dụng để sao chép vào lớp cơ sở (vì lớp dẫn xuất chứa tất cả dữ liệu cho lớp cơ sở). Tuy nhiên, trong trường hợp này, các trường được thêm vào trong lớp dẫn xuất đương nhiên bị bỏ qua.
void OnStart()
{
Rectangle r(100, 200, 75, 50, clrBlue);
Shape s2(r); // ok: sao chép từ dẫn xuất sang cơ sở
Shape s;
Rectangle r4(s); // lỗi: không có overload nào có thể áp dụng
// yêu cầu nạp chồng hàm tạo rõ ràng
}
2
3
4
5
6
7
8
9
Để sao chép theo hướng ngược lại, bạn cần cung cấp một phiên bản hàm tạo với tham chiếu đến lớp dẫn xuất trong lớp cơ sở (về lý thuyết, điều này mâu thuẫn với các nguyên tắc của OOP), nếu không, lỗi biên dịch "không có overload nào có thể áp dụng cho lời gọi hàm" sẽ xảy ra.
Bây giờ chúng ta có thể lập trình một vài biến hình để sau đó "yêu cầu" chúng tự vẽ bằng phương thức draw
.
void OnStart()
{
Rectangle r(100, 200, 50, 75, clrBlue);
Ellipse e(100, 200, 50, 75, clrGreen);
r.draw();
e.draw();
};
2
3
4
5
6
7
Tuy nhiên, cách ghi như vậy có nghĩa là số lượng hình, loại hình và tham số của chúng được cố định trong chương trình, trong khi người dùng nên có thể chọn cái gì và vẽ ở đâu. Do đó, cần tạo các hình một cách động.