Con trỏ
Như chúng ta đã nói trong phần Định nghĩa lớp, con trỏ trong MQL5 là một số mô tả (số duy nhất) của các đối tượng, chứ không phải địa chỉ trong bộ nhớ như trong C++. Đối với một đối tượng tự động, chúng ta lấy được con trỏ bằng cách đặt ký tự & trước tên của nó (trong ngữ cảnh này, ký tự & là toán tử "lấy địa chỉ"). Vì vậy, trong ví dụ sau, biến p
trỏ đến đối tượng tự động s
.
Shape s; // đối tượng tự động
Shape *p = &s; // con trỏ đến cùng đối tượng
s.draw(); // gọi phương thức của đối tượng
p.draw(); // thực hiện tương tự
2
3
4
Trong các phần trước, chúng ta đã học cách lấy con trỏ đến một đối tượng như kết quả của việc tạo nó một cách động với new
. Lúc này, không cần ký tự & để lấy mô tả: giá trị của con trỏ chính là mô tả.
API MQL5 cung cấp hàm GetPointer
thực hiện hành động tương tự như toán tử &, tức là trả về một con trỏ đến một đối tượng:
void *GetPointer(Class object);
Việc sử dụng một trong hai tùy chọn này là vấn đề sở thích cá nhân.
Con trỏ thường được sử dụng để liên kết các đối tượng với nhau. Hãy minh họa ý tưởng tạo các đối tượng phụ thuộc nhận một con trỏ đến this
của đối tượng tạo ra nó (ThisCallback.mq5
). Chúng ta đã đề cập đến kỹ thuật này trong phần về từ khóa this
.
Hãy thử sử dụng nó để triển khai một cơ chế thông báo cho "người tạo" định kỳ về phần trăm tính toán được thực hiện trong đối tượng phụ thuộc: chúng ta đã tạo một bản tương tự bằng con trỏ hàm. Lớp Manager
kiểm soát các phép tính, và bản thân các phép tính (có thể sử dụng các công thức khác nhau) được thực hiện trong các lớp riêng biệt - trong ví dụ này, một trong số đó là lớp Element
.
class Manager; // khai báo trước
class Element
{
Manager *owner; // con trỏ
public:
Element(Manager &t): owner(&t) { }
void doMath()
{
const int N = 1000000;
for(int i = 0; i < N; ++i)
{
if(i % (N / 20) == 0)
{
// chúng ta truyền chính mình đến phương thức của lớp kiểm soát
owner.progressNotify(&this, i * 100.0f / N);
}
// ... các phép tính lớn
}
}
string getMyName() const
{
return typename(this);
}
};
class Manager
{
Element *elements[1]; // mảng con trỏ (1 để demo)
public:
Element *addElement()
{
// tìm kiếm một vị trí trống trong mảng
// ...
// truyền vào hàm tạo của lớp con
elements[0] = new Element(this); // tạo đối tượng động
return elements[0];
}
void progressNotify(Element *e, const float percent)
{
// Manager quyết định cách thông báo cho người dùng:
// hiển thị, in, gửi qua Internet
Print(e.getMyName(), "=", percent);
}
};
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
38
39
40
41
42
43
44
45
46
47
48
49
50
Một đối tượng phụ thuộc có thể sử dụng liên kết nhận được để thông báo cho "ông chủ" về tiến độ công việc. Khi hoàn thành phép tính, nó gửi tín hiệu đến đối tượng kiểm soát rằng có thể xóa đối tượng tính toán hoặc để một đối tượng khác hoạt động. Tất nhiên, mảng một phần tử cố định trong lớp Manager
không quá ấn tượng, nhưng như một bản demo, nó truyền tải được ý tưởng. Manager không chỉ quản lý việc phân phối các nhiệm vụ tính toán mà còn cung cấp một lớp trừu tượng để thông báo cho người dùng: thay vì xuất ra log, nó có thể ghi thông điệp vào một tệp riêng, hiển thị trên màn hình hoặc gửi qua Internet.
Nhân tiện, hãy chú ý đến khai báo trước của lớp Manager
trước khi định nghĩa lớp Element
. Điều này cần thiết để mô tả trong lớp Element
một con trỏ đến lớp Manager
, được định nghĩa sau trong mã. Nếu bỏ qua khai báo trước, chúng ta sẽ gặp lỗi "'Manager' - unexpected token, probably type is missing?".
Nhu cầu khai báo trước xuất hiện khi hai lớp tham chiếu lẫn nhau thông qua các thành viên của chúng: trong trường hợp này, bất kể chúng ta sắp xếp các lớp theo thứ tự nào, không thể định nghĩa đầy đủ cả hai. Khai báo trước cho phép đặt trước một tên kiểu mà không cần định nghĩa đầy đủ.
Một đặc tính cơ bản của con trỏ là con trỏ đến lớp cơ sở có thể được sử dụng để trỏ đến đối tượng của bất kỳ lớp dẫn xuất nào. Đây là một trong những biểu hiện của đa hình. Hành vi này khả thi vì các đối tượng dẫn xuất chứa các "đối tượng con" tích hợp của các lớp cha, giống như búp bê lồng nhau Matryoshka.
Đặc biệt, đối với nhiệm vụ của chúng ta với các hình dạng, dễ dàng mô tả một mảng động các con trỏ Shape
và thêm các đối tượng thuộc các loại khác nhau vào đó theo yêu cầu của người dùng.
Số lượng lớp sẽ được mở rộng lên năm (Shapes2.mq5
). Ngoài Rectangle
và Ellipse
, hãy thêm Triangle
, đồng thời tạo một lớp dẫn xuất từ Rectangle
cho hình vuông (Square
), và một lớp dẫn xuất từ Ellipse
cho hình tròn (Circle
). Rõ ràng, hình vuông là hình chữ nhật với các cạnh bằng nhau, và hình tròn là hình elip với bán kính lớn và nhỏ bằng nhau.
Để truyền tên lớp chuỗi dọc theo chuỗi kế thừa, hãy thêm vào phần protected
của các lớp Rectangle
và Ellipse
các hàm tạo đặc biệt với một tham số chuỗi bổ sung t
:
class Rectangle : public Shape
{
protected:
Rectangle(int px, int py, int sx, int sy, color back, string t) :
Shape(px, py, back, t), dx(sx), dy(sy)
{
}
...
};
2
3
4
5
6
7
8
9
Sau đó, khi tạo hình vuông, chúng ta không chỉ đặt kích thước các cạnh bằng nhau mà còn truyền typename(this)
từ lớp Square
:
class Square : public Rectangle
{
public:
Square(int px, int py, int sx, color back) :
Rectangle(px, py, sx, sx, back, typename(this))
{
}
};
2
3
4
5
6
7
8
Ngoài ra, chúng ta sẽ chuyển các hàm tạo trong lớp Shape
sang phần protected
: điều này sẽ cấm tạo đối tượng Shape
độc lập - nó chỉ có thể đóng vai trò là cơ sở cho các lớp con của nó.
Hãy chỉ định hàm addRandomShape
để tạo các hình dạng, trả về một con trỏ đến đối tượng vừa được tạo. Để minh họa, hiện tại nó sẽ thực hiện việc tạo ngẫu nhiên các hình dạng: loại, vị trí, kích thước và màu sắc của chúng.
Các loại hình dạng được hỗ trợ được tóm tắt trong liệt kê SHAPES: chúng tương ứng với năm lớp đã triển khai.
Số ngẫu nhiên trong một phạm vi cho trước được trả về bởi hàm random
(nó sử dụng hàm tích hợp rand
, trả về một số nguyên ngẫu nhiên trong phạm vi từ 0 đến 32767 mỗi khi được gọi). Tâm của các hình dạng được tạo trong phạm vi từ 0 đến 500 pixel, kích thước của các hình dạng nằm trong phạm vi tối đa 200. Màu sắc được hình thành từ ba thành phần RGB (xem phần Màu sắc), mỗi thành phần từ 0 đến 255.
int random(int range)
{
return (int)(rand() / 32767.0 * range);
}
Shape *addRandomShape()
{
enum SHAPES
{
RECTANGLE,
ELLIPSE,
TRIANGLE,
SQUARE,
CIRCLE,
NUMBER_OF_SHAPES
};
SHAPES type = (SHAPES)random(NUMBER_OF_SHAPES);
int cx = random(500), cy = random(500), dx = random(200), dy = random(200);
color clr = (color)((random(256) << 16) | (random(256) << 8) | random(256));
switch(type)
{
case RECTANGLE:
return new Rectangle(cx, cy, dx, dy, clr);
case ELLIPSE:
return new Ellipse(cx, cy, dx, dy, clr);
case TRIANGLE:
return new Triangle(cx, cy, dx, clr);
case SQUARE:
return new Square(cx, cy, dx, clr);
case CIRCLE:
return new Circle(cx, cy, dx, clr);
}
return NULL;
}
void OnStart()
{
Shape *shapes[];
// mô phỏng việc tạo các hình dạng tùy ý bởi người dùng
ArrayResize(shapes, 10);
for(int i = 0; i < 10; ++i)
{
shapes[i] = addRandomShape();
}
// xử lý các hình dạng: hiện tại chỉ xuất ra log
for(int i = 0; i < 10; ++i)
{
Print(i, ": ", shapes[i].toString());
delete shapes[i];
}
}
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Chúng ta tạo 10 hình dạng và xuất chúng ra log (kết quả có thể khác nhau do tính ngẫu nhiên của việc chọn loại và thuộc tính). Đừng quên xóa các đối tượng bằng delete
vì chúng được tạo động (ở đây điều này được thực hiện trong cùng vòng lặp vì các hình dạng không được sử dụng thêm; trong một chương trình thực tế, mảng các hình dạng có thể sẽ được lưu trữ vào một tệp để tải lại sau và tiếp tục làm việc với hình ảnh).
0: Ellipse 241 38
1: Rectangle 10 420
2: Circle 186 38
3: Triangle 27 225
4: Circle 271 193
5: Circle 293 57
6: Rectangle 71 424
7: Square 477 46
8: Square 366 27
9: Ellipse 489 105
2
3
4
5
6
7
8
9
10
Các hình dạng được tạo thành công và thông báo về thuộc tính của chúng.
Bây giờ chúng ta đã sẵn sàng để truy cập API của các lớp của mình, tức là phương thức draw
.