Cấu trúc, Lớp và Giao diện
Cấu trúc
Cấu trúc là một tập hợp các phần tử thuộc bất kỳ loại nào (ngoại trừ loại void). Do đó, cấu trúc kết hợp các dữ liệu liên quan logic thuộc các loại khác nhau.
Khai báo cấu trúc
Loại dữ liệu cấu trúc được xác định bởi mô tả sau:
struct structure_name
{
elements_description
};
2
3
4
Tên cấu trúc không thể được sử dụng làm định danh (tên của biến hoặc hàm). Cần lưu ý rằng trong MQL5, các phần tử cấu trúc được sắp xếp liền kề nhau mà không có căn chỉnh. Trong C++, thứ tự như vậy được thực hiện cho trình biên dịch bằng cách sử dụng lệnh sau:
#pragma pack(1)
Nếu bạn muốn thực hiện căn chỉnh khác trong cấu trúc, hãy sử dụng các thành viên phụ trợ, "phần đệm" để đạt kích thước mong muốn.
Ví dụ:
struct trade_settings
{
uchar slippage; // giá trị của độ trượt giá cho phép - kích thước 1 byte
char reserved1; // bỏ qua 1 byte
short reserved2; // bỏ qua 2 byte
int reserved4; // bỏ qua thêm 4 byte, đảm bảo căn chỉnh ranh giới 8 byte
double take; // giá trị của giá cố định lợi nhuận
double stop; // giá trị của mức dừng bảo vệ
};
2
3
4
5
6
7
8
9
Mô tả như vậy về các cấu trúc được căn chỉnh chỉ cần thiết khi truyền sang các hàm dll nhập khẩu.
Chú ý: Ví dụ này minh họa dữ liệu được thiết kế không chính xác. Sẽ tốt hơn nếu khai báo trước các dữ liệu lớn take
và stop
thuộc loại double, sau đó khai báo thành viên slippage
thuộc loại uchar
. Trong trường hợp này, biểu diễn nội bộ của dữ liệu sẽ luôn giống nhau bất kể giá trị được chỉ định trong #pragma pack()
.
Nếu một cấu trúc chứa các biến thuộc loại string và/hoặc đối tượng của mảng động, trình biên dịch sẽ gán một hàm tạo ẩn cho cấu trúc đó. Hàm tạo này đặt lại tất cả các thành viên cấu trúc thuộc loại string
và khởi tạo chính xác các đối tượng của mảng động.
Cấu trúc đơn giản
Các cấu trúc không chứa chuỗi, đối tượng lớp, con trỏ và đối tượng của mảng động được gọi là cấu trúc đơn giản. Các biến của cấu trúc đơn giản, cũng như mảng của chúng, có thể được truyền làm tham số cho các hàm nhập khẩu từ DLL.
Việc sao chép các cấu trúc đơn giản chỉ được phép trong hai trường hợp:
- Nếu các đối tượng thuộc cùng một loại cấu trúc
- Nếu các đối tượng được liên kết bởi dòng dõi, nghĩa là một cấu trúc là hậu duệ của cấu trúc khác.
Để cung cấp một ví dụ, hãy phát triển cấu trúc tùy chỉnh CustomMqlTick
với nội dung giống hệt cấu trúc tích hợp MqlTick. Trình biên dịch không cho phép sao chép giá trị đối tượng MqlTick sang đối tượng loại CustomMqlTick. Ép kiểu trực tiếp sang loại cần thiết cũng gây ra lỗi biên dịch:
//--- sao chép các cấu trúc đơn giản của các loại khác nhau là bị cấm
my_tick1=last_tick; // trình biên dịch trả về lỗi tại đây
//--- ép kiểu các cấu trúc của các loại khác nhau sang nhau cũng bị cấm
my_tick1=(CustomMqlTick)last_tick;// trình biên dịch trả về lỗi tại đây
2
3
4
5
Do đó, chỉ còn một lựa chọn – sao chép từng giá trị của các phần tử cấu trúc một cách riêng lẻ. Việc sao chép các giá trị của cùng loại CustomMqlTick vẫn được phép.
CustomMqlTick my_tick1,my_tick2;
//--- được phép sao chép các đối tượng cùng loại CustomMqlTick theo cách sau
my_tick2=my_tick1;
//--- tạo một mảng từ các đối tượng của cấu trúc đơn giản CustomMqlTick và ghi giá trị vào đó
CustomMqlTick arr[2];
arr[0]=my_tick1;
arr[1]=my_tick2;
2
3
4
5
6
7
8
Hàm ArrayPrint() được gọi để kiểm tra nhằm hiển thị giá trị mảng arr[]
trong nhật ký.
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- phát triển cấu trúc tương tự như MqlTick tích hợp
struct CustomMqlTick
{
datetime time; // Thời gian cập nhật giá cuối cùng
double bid; // Giá Bid hiện tại
double ask; // Giá Ask hiện tại
double last; // Giá hiện tại của giao dịch cuối cùng (Last)
ulong volume; // Khối lượng cho giá Last hiện tại
long time_msc; // Thời gian cập nhật giá cuối cùng tính bằng mili giây
uint flags; // Cờ tick
};
//--- lấy giá trị tick cuối cùng
MqlTick last_tick;
CustomMqlTick my_tick1,my_tick2;
//--- cố gắng sao chép dữ liệu từ MqlTick sang CustomMqlTick
if(SymbolInfoTick(Symbol(),last_tick))
{
//--- sao chép các cấu trúc đơn giản không liên quan là bị cấm
//1. my_tick1=last_tick; // trình biên dịch trả về lỗi tại đây
//--- ép kiểu các cấu trúc không liên quan sang nhau cũng bị cấm
//2. my_tick1=(CustomMqlTick)last_tick;// trình biên dịch trả về lỗi tại đây
//--- do đó, sao chép từng thành viên cấu trúc một cách riêng lẻ
my_tick1.time=last_tick.time;
my_tick1.bid=last_tick.bid;
my_tick1.ask=last_tick.ask;
my_tick1.volume=last_tick.volume;
my_tick1.time_msc=last_tick.time_msc;
my_tick1.flags=last_tick.flags;
//--- được phép sao chép các đối tượng cùng loại CustomMqlTick theo cách sau
my_tick2=my_tick1;
//--- tạo một mảng từ các đối tượng của cấu trúc đơn giản CustomMqlTick và ghi giá trị vào đó
CustomMqlTick arr[2];
arr[0]=my_tick1;
arr[1]=my_tick2;
ArrayPrint(arr);
//--- ví dụ về hiển thị giá trị của mảng chứa các đối tượng loại CustomMqlTick
/*
[time] [bid] [ask] [last] [volume] [time_msc] [flags]
[0] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000 1450000 1496070277157 2
[1] 2017.05.29 15:04:37 1.11854 1.11863 +0.00000 1450000 1496070277157 2
*/
}
else
Print("SymbolInfoTick() failed, error = ",GetLastError());
}
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
Ví dụ thứ hai cho thấy các đặc điểm của việc sao chép cấu trúc đơn giản theo dòng dõi. Giả sử chúng ta có cấu trúc cơ bản Animal
, từ đó các cấu trúc Cat và Dog được dẫn xuất. Chúng ta có thể sao chép các đối tượng Animal và Cat, cũng như Animal và Dog sang nhau, nhưng không thể sao chép Cat và Dog sang nhau, mặc dù cả hai đều là hậu duệ của cấu trúc Animal.
//--- cấu trúc mô tả chó
struct Dog: Animal
{
bool hunting; // giống săn
};
//--- cấu trúc mô tả mèo
struct Cat: Animal
{
bool home; // giống nhà
};
//--- tạo các đối tượng của cấu trúc con
Dog dog;
Cat cat;
//--- có thể sao chép từ tổ tiên sang hậu duệ (Animal ==> Dog)
dog=some_animal;
dog.swim=true; // chó có thể bơi
//--- không thể sao chép các đối tượng của cấu trúc con (Dog != Cat)
cat=dog; // trình biên dịch trả về lỗi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Mã ví dụ hoàn chỉnh:
//--- cấu trúc cơ bản mô tả động vật
struct Animal
{
int head; // số đầu
int legs; // số chân
int wings; // số cánh
bool tail; // đuôi
bool fly; // bay
bool swim; // bơi
bool run; // chạy
};
//--- cấu trúc mô tả chó
struct Dog: Animal
{
bool hunting; // giống săn
};
//--- cấu trúc mô tả mèo
struct Cat: Animal
{
bool home; // giống nhà
};
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- tạo và mô tả một đối tượng của loại Animal cơ bản
Animal some_animal;
some_animal.head=1;
some_animal.legs=4;
some_animal.wings=0;
some_animal.tail=true;
some_animal.fly=false;
some_animal.swim=false;
some_animal.run=true;
//--- tạo các đối tượng của loại con
Dog dog;
Cat cat;
//--- có thể sao chép từ tổ tiên sang hậu duệ (Animal ==> Dog)
dog=some_animal;
dog.swim=true; // chó có thể bơi
//--- không thể sao chép các đối tượng của cấu trúc con (Dog != Cat)
//cat=dog; // trình biên dịch trả về lỗi tại đây
//--- do đó, chỉ có thể sao chép từng phần tử một cách riêng lẻ
cat.head=dog.head;
cat.legs=dog.legs;
cat.wings=dog.wings;
cat.tail=dog.tail;
cat.fly=dog.fly;
cat.swim=false; // mèo không thể bơi
//--- có thể sao chép giá trị từ hậu duệ sang tổ tiên
Animal elephant;
elephant=cat;
elephant.run=false;// voi không thể chạy
elephant.swim=true;// voi có thể bơi
//--- tạo một mảng
Animal animals[4];
animals[0]=some_animal;
animals[1]=dog;
animals[2]=cat;
animals[3]=elephant;
//--- in ra
ArrayPrint(animals);
//--- kết quả thực thi
/*
[head] [legs] [wings
] [tail] [fly] [swim] [run]
[0] 1 4 0 true false false true
[1] 1 4 0 true false true true
[2] 1 4 0 true false false false
[3] 1 4 0 true false true false
*/
}
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Một cách khác để sao chép các loại đơn giản là sử dụng union. Các đối tượng của cấu trúc phải là thành viên của cùng một union – xem ví dụ trong union.
Truy cập vào các thành viên cấu trúc
Tên của một cấu trúc trở thành một loại dữ liệu mới, vì vậy bạn có thể khai báo các biến thuộc loại này. Cấu trúc chỉ có thể được khai báo một lần trong một dự án. Các thành viên cấu trúc được truy cập bằng toán tử chấm (.).
Ví dụ:
struct trade_settings
{
double take; // giá trị của giá cố định lợi nhuận
double stop; // giá trị của giá dừng bảo vệ
uchar slippage; // giá trị của độ trượt giá chấp nhận được
};
//--- tạo và khởi tạo một biến của loại trade_settings
trade_settings my_set={0.0,0.0,5};
if (input_TP>0) my_set.take=input_TP;
2
3
4
5
6
7
8
9
'pack' để căn chỉnh các trường của cấu trúc và lớp
Thuộc tính đặc biệt pack
cho phép thiết lập căn chỉnh của các trường cấu trúc hoặc lớp.
pack([n])
nơi n là một trong các giá trị sau: 1, 2, 4, 8 hoặc 16. Nó có thể không có.
Ví dụ:
struct pack(sizeof(long)) MyStruct
{
// các thành viên cấu trúc sẽ được căn chỉnh theo ranh giới 8 byte
};
or
struct MyStruct pack(sizeof(long))
{
// các thành viên cấu trúc sẽ được căn chỉnh theo ranh giới 8 byte
};
2
3
4
5
6
7
8
9
pack(1)
được áp dụng mặc định cho các cấu trúc. Điều này có nghĩa là các thành viên cấu trúc được đặt liền kề nhau trong bộ nhớ, và kích thước cấu trúc bằng tổng kích thước của các thành viên của nó.
Ví dụ:
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- cấu trúc đơn giản không có căn chỉnh
struct Simple_Structure
{
char c; // sizeof(char)=1
short s; // sizeof(short)=2
int i; // sizeof(int)=4
double d; // sizeof(double)=8
};
//--- khai báo một thể hiện của cấu trúc đơn giản
Simple_Structure s;
//--- hiển thị kích thước của từng thành viên cấu trúc
Print("sizeof(s.c)=",sizeof(s.c));
Print("sizeof(s.s)=",sizeof(s.s));
Print("sizeof(s.i)=",sizeof(s.i));
Print("sizeof(s.d)=",sizeof(s.d));
//--- đảm bảo kích thước của cấu trúc POD bằng tổng kích thước của các thành viên
Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
Kết quả:
sizeof(s.c)=1
sizeof(s.s)=2
sizeof(s.i)=4
sizeof(s.d)=8
sizeof(simple_structure)=15
*/
}
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
Việc căn chỉnh các trường của cấu trúc có thể cần thiết khi trao đổi dữ liệu với các thư viện bên thứ ba (*.DLL) nơi áp dụng căn chỉnh như vậy.
Hãy sử dụng một số ví dụ để thể hiện cách hoạt động của căn chỉnh. Chúng ta sẽ áp dụng một cấu trúc gồm bốn thành viên mà không có căn chỉnh.
//--- cấu trúc đơn giản không có căn chỉnh
struct Simple_Structure pack() // không chỉ định kích thước, sẽ đặt căn chỉnh theo ranh giới 1 byte
{
char c; // sizeof(char)=1
short s; // sizeof(short)=2
int i; // sizeof(int)=4
double d; // sizeof(double)=8
};
//--- khai báo một thể hiện của cấu trúc đơn giản
Simple_Structure s;
2
3
4
5
6
7
8
9
10
Các trường cấu trúc sẽ được đặt trong bộ nhớ liên tục theo thứ tự khai báo và kích thước loại. Kích thước của cấu trúc là 15, trong khi độ lệch đến các trường cấu trúc trong mảng là không xác định.
Bây giờ hãy khai báo cùng cấu trúc với căn chỉnh 4 byte và chạy mã.
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- cấu trúc đơn giản với căn chỉnh 4 byte
struct Simple_Structure pack(4)
{
char c; // sizeof(char)=1
short s; // sizeof(short)=2
int i; // sizeof(int)=4
double d; // sizeof(double)=8
};
//--- khai báo một thể hiện của cấu trúc đơn giản
Simple_Structure s;
//--- hiển thị kích thước của từng thành viên cấu trúc
Print("sizeof(s.c)=",sizeof(s.c));
Print("sizeof(s.s)=",sizeof(s.s));
Print("sizeof(s.i)=",sizeof(s.i));
Print("sizeof(s.d)=",sizeof(s.d));
//--- đảm bảo kích thước của cấu trúc POD giờ không bằng tổng kích thước của các thành viên
Print("sizeof(simple_structure)=",sizeof(simple_structure));
/*
Kết quả:
sizeof(s.c)=1
sizeof(s.s)=2
sizeof(s.i)=4
sizeof(s.d)=8
sizeof(simple_structure)=16 // kích thước cấu trúc đã thay đổ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
Kích thước cấu trúc đã thay đổi để tất cả các thành viên từ 4 byte trở lên có độ lệch từ đầu cấu trúc là bội số của 4 byte. Các thành viên nhỏ hơn được căn chỉnh theo ranh giới kích thước của chính chúng (ví dụ, 2 cho short
). Đây là cách nó trông như thế nào (byte được thêm vào được hiển thị bằng màu xám).
Trong trường hợp này, 1 byte được thêm vào sau thành viên s.c
, để trường s.s
(sizeof(short)==2) có ranh giới 2 byte (căn chỉnh cho loại short
).
Độ lệch đến đầu cấu trúc trong mảng cũng được căn chỉnh theo ranh giới 4 byte, tức là địa chỉ của các phần tử a[0], a[1] và a[n] phải là bội số của 4 byte cho Simple_Structure arr[].
Hãy xem xét hai cấu trúc nữa gồm các loại tương tự với căn chỉnh 4 byte nhưng thứ tự thành viên khác nhau. Trong cấu trúc đầu tiên, các thành viên được sắp xếp theo thứ tự tăng dần của kích thước loại.
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- cấu trúc đơn giản căn chỉnh theo ranh giới 4 byte
struct CharShortInt pack(4)
{
char c; // sizeof(char)=1
short s; // sizeof(short)=2
int i; // sizeof(double)=4
};
//--- khai báo một thể hiện của cấu trúc đơn giản
CharShortInt ch_sh_in;
//--- hiển thị kích thước của từng thành viên cấu trúc
Print("sizeof(ch_sh_in.c)=",sizeof(ch_sh_in.c));
Print("sizeof(ch_sh_in.s)=",sizeof(ch_sh_in.s));
Print("sizeof(ch_sh_in.i)=",sizeof(ch_sh_in.i));
//--- đảm bảo kích thước của cấu trúc POD bằng tổng kích thước của các thành viên
Print("sizeof(CharShortInt)=",sizeof(CharShortInt));
/*
**Kết quả:**
**sizeof(ch_sh_in.c)=1**
**sizeof(ch_sh_in.s)=2**
**sizeof(ch_sh_in.i)=4**
**sizeof(CharShortInt)=8**
*/
}
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
Như chúng ta thấy, kích thước cấu trúc là 8 và gồm hai khối 4 byte. Khối đầu tiên chứa các trường với loại char
và short
, trong khi khối thứ hai chứa trường với loại int
.
Bây giờ hãy biến cấu trúc đầu tiên thành cấu trúc thứ hai, chỉ khác ở thứ tự trường, bằng cách di chuyển thành viên loại short
xuống cuối.
//+------------------------------------------------------------------+
//| Script program start function |
//+------------------------------------------------------------------+
void OnStart()
{
//--- cấu trúc đơn giản căn chỉnh theo ranh giới 4 byte
struct CharIntShort pack(4)
{
char c; // sizeof(char)=1
int i; // sizeof(double)=4
short s; // sizeof(short)=2
};
//--- khai báo một thể hiện của cấu trúc đơn giản
CharIntShort ch_in_sh;
//--- hiển thị kích thước của từng thành viên cấu trúc
Print("sizeof(ch_in_sh.c)=",sizeof(ch_in_sh.c));
Print("sizeof(ch_in_sh.i)=",sizeof(ch_in_sh.i));
Print("sizeof(ch_in_sh.s)=",sizeof(ch_in_sh.s));
//--- đảm bảo kích thước của cấu trúc POD bằng tổng kích thước của các thành viên
Print("sizeof(CharIntShort)=",sizeof(CharIntShort));
/*
**Kết quả:**
**sizeof(ch_in_sh.c)=1**
**sizeof(ch_in_sh.i)=4**
**sizeof(ch_in_sh.s)=2**
**sizeof(CharIntShort)=12**
*/
}
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
Mặc dù nội dung cấu trúc không thay đổi, việc thay đổi thứ tự thành viên đã làm tăng kích thước của nó.
Căn chỉnh cũng nên được xem xét khi kế thừa. Hãy thể hiện điều này bằng cấu trúc Parent đơn giản có một thành viên loại char
. Kích thước cấu trúc không có căn chỉnh là 1.
struct Parent
{
char c; // sizeof(char)=1
};
2
3
4
Hãy tạo lớp con Children với thành viên loại short
(sizeof(short)=2).
struct Children pack(2) : Parent
{
short s; // sizeof(short)=2
};
2
3
4
Kết quả là, khi đặt căn chỉnh thành 2 byte, kích thước cấu trúc bằng 4, mặc dù kích thước của các thành viên là 3. Trong ví dụ này, 2 byte được cấp phát cho lớp Parent, để truy cập vào trường short
của lớp con được căn chỉnh theo 2 byte.
Việc hiểu cách bộ nhớ được cấp phát cho các thành viên cấu trúc là cần thiết nếu một ứng dụng MQL5 tương tác với dữ liệu bên thứ ba bằng cách ghi/đọc ở cấp độ tệp hoặc luồng.
Thư mục MQL5\Include\WinAPI của Thư viện Chuẩn chứa các hàm để làm việc với các hàm WinAPI. Các hàm này áp dụng các cấu trúc với căn chỉnh được chỉ định cho các trường hợp khi cần thiết để làm việc với WinAPI.
offsetof
là một lệnh đặc biệt liên quan trực tiếp đến thuộc tính pack. Nó cho phép chúng ta lấy độ lệch của một thành viên từ đầu cấu trúc.
//--- khai báo biến loại Children
Children child;
//--- phát hiện độ lệch từ đầu cấu trúc
Print("offsetof(Children,c)=",offsetof(Children,c));
Print("offsetof(Children,s)=",offsetof(Children,s));
/*
Kết quả:
offsetof(Children,c)=0
offsetof(Children,s)=2
*/
2
3
4
5
6
7
8
9
10
Chỉ định final
Việc sử dụng chỉ định final
trong khi khai báo cấu trúc sẽ cấm kế thừa thêm từ cấu trúc này. Nếu một cấu trúc không yêu cầu sửa đổi thêm, hoặc sửa đổi không được phép vì lý do bảo mật, hãy khai báo cấu trúc này với bộ sửa đổi final
. Ngoài ra, tất cả các thành viên của cấu trúc cũng sẽ được coi là final một cách ẩn.
struct settings final
{
//--- Thân cấu trúc
};
struct trade_settings : public settings
{
//--- Thân cấu trúc
};
2
3
4
5
6
7
8
9
Nếu bạn cố gắng kế thừa từ một cấu trúc có bộ sửa đổi final
như trong ví dụ trên, trình biên dịch sẽ trả về lỗi:
cannot inherit from 'settings' as it has been declared as 'final'
see declaration of 'settings'
2
Lớp
Lớp khác với cấu trúc ở các điểm sau:
- Từ khóa
class
được sử dụng trong khai báo; - Mặc định, tất cả các thành viên lớp có chỉ định truy cập
private
, trừ khi được chỉ định khác. Các thành viên dữ liệu của cấu trúc có loại truy cập mặc định làpublic
, trừ khi được chỉ định khác; - Các đối tượng lớp luôn có bảng hàm ảo, ngay cả khi không có hàm ảo nào được khai báo trong lớp. Cấu trúc không thể có hàm ảo;
- Toán tử new có thể được áp dụng cho các đối tượng lớp; toán tử này không thể áp dụng cho cấu trúc;
- Lớp chỉ có thể được kế thừa từ lớp, cấu trúc chỉ có thể được kế thừa từ cấu trúc.
Lớp và cấu trúc có thể có hàm tạo và hàm hủy rõ ràng. Nếu hàm tạo của bạn được định nghĩa rõ ràng, việc khởi tạo một biến cấu trúc hoặc lớp bằng chuỗi khởi tạo là không thể.
Ví dụ:
struct trade_settings
{
double take; // giá trị của giá cố định lợi nhuận
double stop; // giá trị của giá dừng bảo vệ
uchar slippage; // giá trị của độ trượt giá chấp nhận được
//--- Hàm tạo
trade_settings() { take=0.0; stop=0.0; slippage=5; }
//--- Hàm hủy
~trade_settings() { Print("This is the end"); }
};
//--- Trình biên dịch sẽ tạo thông báo lỗi rằng khởi tạo là không thể
trade_settings my_set={0.0,0.0,5};
2
3
4
5
6
7
8
9
10
11
12
Hàm tạo và Hàm hủy
Hàm tạo là một hàm đặc biệt, được gọi tự động khi tạo một đối tượng của cấu trúc hoặc lớp và thường được sử dụng để khởi tạo các thành viên lớp. Tiếp theo chúng ta sẽ chỉ nói về lớp, trong khi điều tương tự áp dụng cho cấu trúc, trừ khi được chỉ định khác. Tên của hàm tạo phải khớp với tên lớp. Hàm tạo không có loại trả về (bạn có thể chỉ định loại void).
Các thành viên lớp đã định nghĩa – chuỗi, mảng động và các đối tượng yêu cầu khởi tạo – sẽ được khởi tạo trong mọi trường hợp, bất kể có hàm tạo hay không.
Mỗi lớp có thể có nhiều hàm tạo, khác nhau bởi số lượng tham số và danh sách khởi tạo. Hàm tạo yêu cầu chỉ định tham số được gọi là hàm tạo có tham số.
Hàm tạo không có tham số được gọi là hàm tạo mặc định. Nếu không có hàm tạo nào được khai báo trong lớp, trình biên dịch sẽ tạo một hàm tạo mặc định trong quá trình biên dịch.
//+------------------------------------------------------------------+
//| Một lớp để làm việc với ngày tháng |
//+------------------------------------------------------------------+
class MyDateClass
{
private:
int m_year; // Năm
int m_month; // Tháng
int m_day; // Ngày trong tháng
int m_hour; // Giờ trong ngày
int m_minute; // Phút
int m_second; // Giây
public:
//--- Hàm tạo mặc định
MyDateClass(void);
//--- Hàm tạo có tham số
MyDateClass(int h,int m,int s);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hàm tạo có thể được khai báo trong mô tả lớp và sau đó thân của nó có thể được định nghĩa. Ví dụ, hai hàm tạo của MyDateClass có thể được định nghĩa như sau:
//+------------------------------------------------------------------+
//| Hàm tạo mặc định |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(void)
{
//---
MqlDateTime mdt;
datetime t=TimeCurrent(mdt);
m_year=mdt.year;
m_month=mdt.mon;
m_day=mdt.day;
m_hour=mdt.hour;
m_minute=mdt.min;
m_second=mdt.sec;
Print(__FUNCTION__);
}
//+------------------------------------------------------------------+
//| Hàm tạo có tham số |
//+------------------------------------------------------------------+
MyDateClass::MyDateClass(int h,int m,int s)
{
MqlDateTime mdt;
datetime t=TimeCurrent(mdt);
m_year=mdt.year;
m_month=mdt.mon;
m_day=mdt.day;
m_hour=h;
m_minute=m;
m_second=s;
Print(__FUNCTION__);
}
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
Trong hàm tạo mặc định, tất cả các thành viên của lớp được điền bằng hàm TimeCurrent()
. Trong hàm tạo có tham số, chỉ các giá trị giờ được điền. Các thành viên khác của lớp (m_year, m_month và m_day) sẽ được tự động khởi tạo với ngày hiện tại.
Hàm tạo mặc định có mục đích đặc biệt khi khởi tạo một mảng các đối tượng của lớp đó. Hàm tạo mà tất cả các tham số đều có giá trị mặc định không phải là hàm tạo mặc định. Dưới đây là một ví dụ:
//+------------------------------------------------------------------+
//| Một lớp với hàm tạo mặc định |
//+------------------------------------------------------------------+
class CFoo
{
datetime m_call_time; // Thời gian gọi cuối cùng của đối tượng
public:
//--- Hàm tạo với tham số có giá trị mặc định không phải là hàm tạo mặc định
CFoo(const datetime t=0){m_call_time=t;};
//--- Hàm tạo sao chép
CFoo(const CFoo &foo){m_call_time=foo.m_call_time;};
string ToString(){return(TimeToString(m_call_time,TIME_DATE|TIME_SECONDS));};
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
// CFoo foo; // Biến thể này không thể sử dụng - hàm tạo mặc định không được đặt
//--- Các lựa chọn khả thi để tạo đối tượng CFoo
CFoo foo1(TimeCurrent()); // Gọi rõ ràng hàm tạo có tham số
CFoo foo2(); // Gọi rõ ràng hàm tạo có tham số với tham số mặc định
CFoo foo3=D'2009.09.09'; // Gọi ngầm hàm tạo có tham số
CFoo foo40(foo1); // Gọi rõ ràng hàm tạo sao chép
CFoo foo41=foo1; // Gọi ngầm hàm tạo sao chép
CFoo foo5; // Gọi rõ ràng hàm tạo mặc định (nếu không có hàm tạo mặc định,
// thì hàm tạo có tham số với giá trị mặc định sẽ được gọi)
//--- Các lựa chọn khả thi để nhận con trỏ CFoo
CFoo *pfoo6=new CFoo(); // Tạo động đối tượng và nhận con trỏ tới nó
CFoo *pfoo7=new CFoo(TimeCurrent()); // Một lựa chọn khác để tạo đối tượng động
CFoo *pfoo8=GetPointer(foo1); // Bây giờ pfoo8 trỏ tới đối tượng foo1
CFoo *pfoo9=pfoo7; // pfoo9 và pfoo7 trỏ tới cùng một đối tượng
// CFoo foo_array[3]; // Lựa chọn này không thể sử dụng - hàm tạo mặc định không được chỉ định
//--- Hiển thị giá trị của m_call_time
Print("foo1.m_call_time=",foo1.ToString());
Print("foo2.m_call_time=",foo2.ToString());
Print("foo3.m_call_time=",foo3.ToString());
Print("foo4.m_call_time=",foo4.ToString());
Print("foo5.m_call_time=",foo5.ToString());
Print("pfoo6.m_call_time=",pfoo6.ToString());
Print("pfoo7.m_call_time=",pfoo7.ToString());
Print("pfoo8.m_call_time=",pfoo8.ToString());
Print("pfoo9.m_call_time=",pfoo9.ToString());
//--- Xóa các mảng được tạo động
delete pfoo6;
delete pfoo7;
//delete pfoo8; // Không cần xóa pfoo8 rõ ràng, vì nó trỏ tới đối tượng foo1 được tạo tự động
//delete pfoo9; // Không cần xóa pfoo9 rõ ràng, vì nó trỏ tới cùng đối tượng với pfoo7
}
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
Nếu bạn bỏ ghi chú các chuỗi này
//CFoo foo_array[3]; // Biến thể này không thể sử dụng - hàm tạo mặc định không được đặt
hoặc
//CFoo foo_dyn_array[]; // Biến thể này không thể sử dụng - hàm tạo mặc định không được đặt
thì trình biên dịch sẽ trả về lỗi cho chúng "hàm tạo mặc định không được định nghĩa".
Nếu một lớp có hàm tạo do người dùng định nghĩa, hàm tạo mặc định sẽ không được tạo tự động bởi trình biên dịch. Điều này có nghĩa là nếu một hàm tạo có tham số được khai báo trong lớp, nhưng hàm tạo mặc định không được khai báo, bạn không thể khai báo mảng các đối tượng của lớp này. Trình biên dịch sẽ trả về lỗi cho script này:
//+------------------------------------------------------------------+
//| Một lớp không có hàm tạo mặc định |
//+------------------------------------------------------------------+
class CFoo
{
string m_name;
public:
CFoo(string name) { m_name=name;}
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- Nhận lỗi "hàm tạo mặc định không được định nghĩa" trong quá trình biên dịch
CFoo badFoo[5];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Trong ví dụ này, lớp CFoo có một hàm tạo có tham số được khai báo - trong trường hợp như vậy, trình biên dịch không tự động tạo hàm tạo mặc định trong quá trình biên dịch. Đồng thời, khi bạn khai báo một mảng các đối tượng,假定 tất cả các đối tượng sẽ được tạo và khởi tạo tự động. Trong quá trình tự động khởi tạo một đối tượng, cần gọi hàm tạo mặc định, nhưng vì hàm tạo mặc định không được khai báo rõ ràng và không được tạo tự động bởi trình biên dịch, việc tạo đối tượng như vậy là không thể. Vì lý do này, trình biên dịch tạo ra lỗi ở giai đoạn biên dịch.
Có một cú pháp đặc biệt để khởi tạo một đối tượng bằng hàm tạo. Các khởi tạo viên (cấu trúc đặc biệt để khởi tạo) cho các thành viên của một cấu trúc hoặc lớp có thể được chỉ định trong danh sách khởi tạo.
Danh sách khởi tạo là danh sách các khởi tạo viên được phân tách bằng dấu phẩy, xuất hiện sau dấu hai chấm sau danh sách tham số của hàm tạo và đứng trước thân hàm (đi trước dấu ngoặc mở). Có một số yêu cầu:
- Danh sách khởi tạo chỉ có thể được sử dụng trong hàm tạo;
- Thành viên cha mẹ không thể được khởi tạo trong danh sách khởi tạo;
- Danh sách khởi tạo phải được theo sau bởi một định nghĩa (triển khai) của hàm.
Dưới đây là ví dụ về một số hàm tạo để khởi tạo các thành viên lớp.
//+------------------------------------------------------------------+
//| Một lớp để lưu trữ tên của một nhân vật |
//+------------------------------------------------------------------+
class CPerson
{
string m_first_name; // Tên
string m_second_name; // Họ
public:
//--- Hàm tạo mặc định rỗng
CPerson() {Print(__FUNCTION__);};
//--- Hàm tạo có tham số
CPerson(string full_name);
//--- Hàm tạo với danh sách khởi tạo
CPerson(string surname,string name): m_second_name(surname), m_first_name(name) {};
void PrintName(){PrintFormat("Name=%s Surname=%s",m_first_name,m_second_name);};
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CPerson::CPerson(string full_name)
{
int pos=StringFind(full_name," ");
if(pos>=0)
{
m_first_name=StringSubstr(full_name,0,pos);
m_second_name=StringSubstr(full_name,pos+1);
}
}
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- Nhận lỗi "hàm tạo mặc định không được định nghĩa"
CPerson people[5];
CPerson Tom="Tom Sawyer"; // Tom Sawyer
CPerson Huck("Huckleberry","Finn"); // Huckleberry Finn
CPerson *Pooh = new CPerson("Winnie","Pooh"); // Winnie the Pooh
//--- Xuất giá trị
Tom.PrintName();
Huck.PrintName();
Pooh.PrintName();
//--- Xóa đối tượng được tạo động
delete Pooh;
}
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
Trong trường hợp này, lớp CPerson có ba hàm tạo:
- Một hàm tạo mặc định rõ ràng, cho phép tạo một mảng các đối tượng của lớp này;
- Một hàm tạo với một tham số, nhận tên đầy đủ làm tham số và chia nó thành tên và họ theo khoảng trắng tìm thấy;
- Một hàm tạo với hai tham số chứa danh sách khởi tạo. Các khởi tạo viên - m_second_name(surname) và m_first_name(name).
Lưu ý rằng việc khởi tạo bằng danh sách đã thay thế việc gán. Các thành viên riêng lẻ phải được khởi tạo như sau:
class_member (danh sách các biểu thức)
Trong danh sách khởi tạo, các thành viên có thể xuất hiện theo bất kỳ thứ tự nào, nhưng tất cả các thành viên của lớp sẽ được khởi tạo theo thứ tự khai báo của chúng. Điều này có nghĩa là trong hàm tạo thứ ba, đầu tiên thành viên m_first_name sẽ được khởi tạo, vì nó được khai báo trước, và chỉ sau đó m_second_name được khởi tạo. Điều này cần được xem xét trong các trường hợp mà việc khởi tạo một số thành viên của lớp phụ thuộc vào giá trị trong các thành viên lớp khác.
Nếu hàm tạo mặc định không được khai báo trong lớp cơ sở, và đồng thời một hoặc nhiều hàm tạo với tham số được khai báo, bạn phải luôn gọi một trong các hàm tạo của lớp cơ sở trong danh sách khởi tạo. Nó được liệt kê qua dấu phẩy như các thành viên thông thường của danh sách và sẽ được gọi đầu tiên trong quá trình khởi tạo đối tượng, bất kể vị trí của nó trong danh sách khởi tạo.
//+------------------------------------------------------------------+
//| Lớp cơ sở |
//+------------------------------------------------------------------+
class CFoo
{
string m_name;
public:
//--- Hàm tạo với danh sách khởi tạo
CFoo(string name) : m_name(name) { Print(m_name);}
};
//+------------------------------------------------------------------+
//| Lớp kế thừa từ CFoo |
//+------------------------------------------------------------------+
class CBar : CFoo
{
CFoo m_member; // Thành viên lớp là một đối tượng của lớp cha
public:
//--- Hàm tạo mặc định trong danh sách khởi tạo gọi hàm tạo của lớp cha
CBar(): m_member(_Symbol), CFoo("CBAR") {Print(__FUNCTION__);}
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
CBar bar;
}
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
Trong ví dụ này, khi tạo đối tượng bar, hàm tạo mặc định CBar() sẽ được gọi, trong đó đầu tiên hàm tạo cho lớp cha CFoo được gọi, sau đó đến hàm tạo cho thành viên lớp m_member.
Hàm hủy là một hàm đặc biệt được gọi tự động khi một đối tượng lớp bị hủy. Tên của hàm hủy được viết dưới dạng tên lớp với dấu ngã (~). Chuỗi, mảng động và các đối tượng yêu cầu giải khởi tạo sẽ được giải khởi tạo dù có hay không có hàm hủy. Nếu có hàm hủy, các hành động này sẽ được thực hiện sau khi gọi hàm hủy.
Hàm hủy luôn là ảo, bất kể chúng có được khai báo với từ khóa virtual
hay không.
Định nghĩa phương thức lớp
Phương thức hàm của lớp có thể được định nghĩa cả bên trong lớp và bên ngoài khai báo lớp. Nếu phương thức được định nghĩa trong lớp, thì thân của nó xuất hiện ngay sau khai báo phương thức.
Ví dụ:
class CTetrisShape
{
protected:
int m_type;
int m_xpos;
int m_ypos;
int m_xsize;
int m_ysize;
int m_prev_turn;
int m_turn;
int m_right_border;
public:
void CTetrisShape();
void SetRightBorder(int border) { m_right_border=border; }
void SetYPos(int ypos) { m_ypos=ypos; }
void SetXPos(int xpos) { m_xpos=xpos; }
int GetYPos() { return(m_ypos); }
int GetXPos() { return(m_xpos); }
int GetYSize() { return(m_ysize); }
int GetXSize() { return(m_xsize); }
int GetType() { return(m_type); }
void Left() { m_xpos-=SHAPE_SIZE; }
void Right() { m_xpos+=SHAPE_SIZE; }
void Rotate() { m_prev_turn=m_turn; if(++m_turn>3) m_turn=0; }
virtual void Draw() { return; }
virtual bool CheckDown(int& pad_array[]);
virtual bool CheckLeft(int& side_row[]);
virtual bool CheckRight(int& side_row[]);
};
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
Các hàm từ SetRightBorder(int border) đến Draw() được khai báo và định nghĩa trực tiếp bên trong lớp CTetrisShape.
Hàm tạo CTetrisShape() và các phương thức CheckDown(int& pad_array[]), CheckLeft(int& side_row[]) và CheckRight(int& side_row[]) chỉ được khai báo bên trong lớp, nhưng chưa được định nghĩa. Định nghĩa của các hàm này sẽ xuất hiện sau trong mã. Để định nghĩa phương thức bên ngoài lớp, toán tử phân giải phạm vi (scope resolution operator) được sử dụng, tên lớp được dùng làm phạm vi.
Ví dụ:
//+------------------------------------------------------------------+
//| Hàm tạo của lớp cơ sở |
//+------------------------------------------------------------------+
void CTetrisShape::CTetrisShape()
{
m_type=0;
m_ypos=0;
m_xpos=0;
m_xsize=SHAPE_SIZE;
m_ysize=SHAPE_SIZE;
m_prev_turn=0;
m_turn=0;
m_right_border=0;
}
//+------------------------------------------------------------------+
//| Kiểm tra khả năng di chuyển xuống (cho thanh và khối) |
//+------------------------------------------------------------------+
bool CTetrisShape::CheckDown(int& pad_array[])
{
int i,xsize=m_xsize/SHAPE_SIZE;
//---
for(i=0; i<xsize; i++)
{
if(m_ypos+m_ysize>=pad_array[i]) return(false);
}
//---
return(true);
}
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
Chỉ định truy cập Public, Protected và Private
Khi phát triển một lớp mới, nên hạn chế truy cập vào các thành viên từ bên ngoài. Để làm điều này, từ khóa private
hoặc protected
được sử dụng. Trong trường hợp này, dữ liệu ẩn chỉ có thể được truy cập từ các phương thức hàm của cùng lớp. Nếu từ khóa protected
được sử dụng, dữ liệu ẩn cũng có thể được truy cập từ các phương thức của các lớp - kế thừa từ lớp này. Phương pháp tương tự có thể được sử dụng để hạn chế truy cập vào các phương thức hàm của lớp.
Nếu bạn cần mở hoàn toàn quyền truy cập vào các thành viên và/hoặc phương thức của một lớp, hãy sử dụng từ khóa public
.
Ví dụ:
class CTetrisField
{
private:
int m_score; // Điểm số
int m_ypos; // Vị trí hiện tại của các hình
int m_field[FIELD_HEIGHT][FIELD_WIDTH]; // Ma trận của giếng
int m_rows[FIELD_HEIGHT]; // Đánh số các hàng của giếng
int m_last_row; // Hàng trống cuối cùng
CTetrisShape *m_shape; // Hình Tetris
bool m_bover; // Kết thúc trò chơi
public:
void CTetrisField() { m_shape=NULL; m_bover=false; }
void Init();
void Deinit();
void Down();
void Left();
void Right();
void Rotate();
void Drop();
private:
void NewShape();
void CheckAndDeleteRows();
void LabelOver();
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Bất kỳ thành viên và phương thức nào của lớp được khai báo sau chỉ định public:
(và trước chỉ định truy cập tiếp theo) đều có thể được chương trình truy cập thông qua bất kỳ tham chiếu nào đến đối tượng lớp. Trong ví dụ này, đó là các thành viên sau: các hàm CTetrisField()
, Init()
, Deinit()
, Down()
, Left()
, Right()
, Rotate()
và Drop()
.
Bất kỳ thành viên nào được khai báo sau chỉ định truy cập private:
(và trước chỉ định truy cập tiếp theo) chỉ có thể được truy cập bởi các hàm thành viên của lớp này. Các chỉ định truy cập vào các phần tử luôn kết thúc bằng dấu hai chấm (😃 và có thể xuất hiện nhiều lần trong định nghĩa lớp.
Bất kỳ thành viên nào của lớp được khai báo sau chỉ định truy cập protected:
(và cho đến chỉ định truy cập tiếp theo) chỉ có thể được truy cập bởi các hàm thành viên của lớp này và các hàm thành viên của lớp con. Khi cố gắng tham chiếu đến các thành viên có chỉ định private
và protected
từ bên ngoài, chúng ta sẽ nhận được lỗi ở giai đoạn biên dịch. Ví dụ:
class A
{
protected:
//--- toán tử sao chép chỉ khả dụng bên trong lớp A và các lớp con của nó
void operator=(const A &)
{
}
};
class B
{
//--- khai báo đối tượng lớp A
A a;
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- khai báo hai biến kiểu B
B b1, b2;
//--- cố gắng sao chép một đối tượng vào đối tượng khác
b2=b1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Khi biên dịch mã này, thông báo lỗi sẽ xuất hiện — cố gắng gọi toán tử sao chép từ xa:
attempting to reference deleted function 'void B::operator=(const B&)' trash3.mq5 32 6
Chuỗi thứ hai dưới đây cung cấp mô tả chi tiết hơn — toán tử sao chép trong lớp B
đã bị xóa ngầm, vì toán tử sao chép không khả dụng của lớp A
được gọi:
function 'void B::operator=(const B&)' was implicitly deleted because it invokes inaccessible function 'void A::operator=(const A&)'
Quyền truy cập vào các thành viên của lớp cơ sở có thể được định nghĩa lại trong quá trình kế thừa trong các lớp dẫn xuất.
Chỉ định delete
Chỉ định delete
đánh dấu các hàm thành viên của lớp không thể sử dụng. Điều này có nghĩa là nếu chương trình tham chiếu đến hàm đó một cách rõ ràng hoặc ngầm định, lỗi sẽ xuất hiện ngay ở giai đoạn biên dịch. Ví dụ, chỉ định này cho phép làm cho các phương thức cha không khả dụng trong lớp con. Kết quả tương tự có thể đạt được nếu chúng ta khai báo hàm trong vùng private
của lớp cha (khai báo trong phần private
). Ở đây, việc sử dụng delete
giúp mã dễ đọc và quản lý hơn ở cấp độ các lớp con.
class A
{
public:
A(void) {value=5;};
double GetValue(void) {return(value);}
private:
double value;
};
class B: public A
{
double GetValue(void)=delete;
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- khai báo biến kiểu A
A a;
Print("a.GetValue()=", a.GetValue());
//--- cố gắng lấy giá trị từ biến kiểu B
B b;
Print("b.GetValue()=", b.GetValue()); // trình biên dịch hiển thị lỗi tại dòng này
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Thông báo của trình biên dịch:
attempting to reference deleted function 'double B::GetValue()'
function 'double B::GetValue()' was explicitly deleted here
2
Chỉ định delete
cho phép vô hiệu hóa ép kiểu tự động hoặc hàm tạo sao chép, vốn nếu không sẽ phải được ẩn trong phần private
. Ví dụ:
class A
{
public:
void SetValue(double v) {value=v;}
//--- vô hiệu hóa gọi kiểu int
void SetValue(int) = delete;
//--- vô hiệu hóa toán tử sao chép
void operator=(const A&) = delete;
private:
double value;
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- khai báo hai biến kiểu A
A a1, a2;
a1.SetValue(3); // lỗi!
a1.SetValue(3.14); // OK
a2=a1; // lỗi!
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Trong quá trình biên dịch, chúng ta nhận được các thông báo lỗi:
attempting to reference deleted function 'void A::SetValue(int)'
function 'void A::SetValue(int)' was explicitly deleted here
attempting to reference deleted function 'void A::operator=(const A&)'
function 'void A::operator=(const A&)' was explicitly deleted here
2
3
4
Chỉ định final
Việc sử dụng chỉ định final
trong khai báo lớp sẽ cấm kế thừa thêm từ lớp này. Nếu giao diện lớp không cần sửa đổi thêm hoặc không được phép sửa đổi vì lý do bảo mật, hãy khai báo lớp này với sửa đổi final
. Ngoài ra, tất cả các thành viên của lớp cũng sẽ được coi là final
một cách ngầm định.
class CFoo final
{
//--- Thân lớp
};
class CBar : public CFoo
{
//--- Thân lớp
};
2
3
4
5
6
7
8
9
Nếu bạn cố gắng kế thừa từ một lớp có chỉ định final
như trong ví dụ trên, trình biên dịch sẽ trả về lỗi:
cannot inherit from 'CFoo' as it has been declared as 'final'
see declaration of 'CFoo'
2
Liên kết (union)
Liên kết là một kiểu dữ liệu đặc biệt bao gồm nhiều biến chia sẻ cùng một vùng bộ nhớ. Do đó, liên kết cung cấp khả năng diễn giải cùng một chuỗi bit theo hai (hoặc nhiều) cách khác nhau. Khai báo liên kết tương tự như khai báo cấu trúc và bắt đầu bằng từ khóa union
.
union LongDouble
{
long long_value;
double double_value;
};
2
3
4
5
Không giống như cấu trúc, các thành viên khác nhau của liên kết thuộc cùng một vùng bộ nhớ. Trong ví dụ này, liên kết LongDouble
được khai báo với các giá trị kiểu long và double chia sẻ cùng một vùng bộ nhớ. Vui lòng lưu ý rằng không thể làm cho liên kết lưu trữ đồng thời một giá trị nguyên long
và một giá trị thực double
(khác với cấu trúc), vì các biến long_value
và double_value
chồng lấp nhau (trong bộ nhớ). Mặt khác, một chương trình MQL5 có thể xử lý dữ liệu chứa trong liên kết như một giá trị nguyên (long
) hoặc thực (double
) bất kỳ lúc nào. Do đó, liên kết cho phép nhận được hai (hoặc nhiều) lựa chọn để biểu diễn cùng một chuỗi dữ liệu.
Trong quá trình khai báo liên kết, trình biên dịch tự động cấp phát vùng bộ nhớ đủ để lưu trữ kiểu dữ liệu lớn nhất (theo dung lượng) trong liên kết biến. Cú pháp tương tự được sử dụng để truy cập phần tử liên kết như đối với cấu trúc – toán tử chấm.
union LongDouble
{
long long_value;
double double_value;
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//---
LongDouble lb;
//--- lấy và hiển thị số không hợp lệ -nan(ind)
lb.double_value=MathArcsin(2.0);
printf("1. double=%f integer=%I64X",lb.double_value,lb.long_value);
//--- giá trị chuẩn hóa lớn nhất (DBL_MAX)
lb.long_value=0x7FEFFFFFFFFFFFFF;
printf("2. double=%.16e integer=%I64X",lb.double_value,lb.long_value);
//--- giá trị chuẩn hóa dương nhỏ nhất (DBL_MIN)
lb.long_value=0x0010000000000000;
printf("3. double=%.16e integer=%.16I64X",lb.double_value,lb.long_value);
}
/* Kết quả thực thi
1. double=-nan(ind) integer=FFF8000000000000
2. double=1.7976931348623157e+308 integer=7FEFFFFFFFFFFFFF
3. double=2.2250738585072014e-308 integer=0010000000000000
*/
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
Vì các liên kết cho phép chương trình diễn giải cùng một dữ liệu bộ nhớ theo các cách khác nhau, chúng thường được sử dụng khi cần một chuyển đổi kiểu bất thường.
Các liên kết không thể tham gia vào kế thừa, và chúng cũng không thể có thành viên tĩnh do bản chất của chúng. Về mọi khía cạnh khác, union
hoạt động như một cấu trúc với tất cả các thành viên có độ lệch bằng không. Các kiểu sau không thể là thành viên của liên kết:
- Mảng động
- Chuỗi
- Con trỏ đến đối tượng và hàm
- Đối tượng lớp
- Đối tượng cấu trúc có hàm tạo hoặc hàm hủy
- Đối tượng cấu trúc có thành viên từ các điểm 1-5
Tương tự như các lớp, liên kết có khả năng có hàm tạo và hàm hủy, cũng như các phương thức. Theo mặc định, các thành viên của liên kết thuộc kiểu truy cập public. Để tạo các phần tử riêng tư, sử dụng từ khóa private. Tất cả các khả năng này được hiển thị trong ví dụ minh họa cách chuyển đổi màu của kiểu color sang ARGB như hàm ColorToARGB() thực hiện.
//+------------------------------------------------------------------+
//| Liên kết để chuyển đổi màu (BGR) sang ARGB |
//+------------------------------------------------------------------+
union ARGB
{
uchar argb[4];
color clr;
//--- hàm tạo
ARGB(color col,uchar a=0){Color(col,a);};
~ARGB(){};
//--- phương thức công khai
public:
uchar Alpha(){return(argb[3]);};
void Alpha(const uchar alpha){argb[3]=alpha;};
color Color(){ return(color(clr));};
//--- phương thức riêng tư
private:
//+------------------------------------------------------------------+
//| đặt giá trị kênh alpha và màu |
//+------------------------------------------------------------------+
void Color(color col,uchar alpha)
{
//--- đặt màu cho thành viên clr
clr=col;
//--- đặt giá trị thành phần Alpha - mức độ mờ
argb[3]=alpha;
//--- hoán đổi các byte của thành phần R và B (Đỏ và Xanh)
uchar t=argb[0];argb[0]=argb[2];argb[2]=t;
};
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- 0x55 nghĩa là 55/255=21.6 % (0% - hoàn toàn trong suốt)
uchar alpha=0x55;
//--- kiểu color được biểu diễn dưới dạng 0x00BBGGRR
color test_color=clrDarkOrange;
//--- giá trị của các byte từ liên kết ARGB được chấp nhận tại đây
uchar argb[];
PrintFormat("0x%.8X - đây là cách kiểu 'color' trông như thế nào đối với %s, BGR=(%s)",
test_color,ColorToString(test_color,true),ColorToString(test_color));
//--- kiểu ARGB được biểu diễn dưới dạng 0x00RRGGBB, các thành phần RR và BB được hoán đổi
ARGB argb_color(test_color);
//--- sao chép mảng byte
ArrayCopy(argb,argb_color.argb);
//--- đây là cách nó trông trong biểu diễn ARGB
PrintFormat("0x%.8X - biểu diễn ARGB với kênh alpha=0x%.2x, ARGB=(%d,%d,%d,%d)",
argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- thêm mức độ mờ
argb_color.Alpha(alpha);
//--- thử định nghĩa ARGB như kiểu 'color'
Print("ARGB như color=(",argb_color.clr,") kênh alpha=",argb_color.Alpha());
//--- sao chép mảng byte
ArrayCopy(argb,argb_color.argb);
//--- đây là cách nó trông trong biểu diễn ARGB
PrintFormat("0x%.8X - biểu diễn ARGB với kênh alpha=0x%.2x, ARGB=(%d,%d,%d,%d)",
argb_color.clr,argb_color.Alpha(),argb[3],argb[2],argb[1],argb[0]);
//--- kiểm tra với kết quả của hàm ColorToARGB()
PrintFormat("0x%.8X - kết quả của ColorToARGB(%s,0x%.2x)",ColorToARGB(test_color,alpha),
ColorToString(test_color,true),alpha);
}
/* Kết quả thực thi
0x00008CFF - đây là cách kiểu color trông cho clrDarkOrange, BGR=(255,140,0)
0x00FF8C00 - biểu diễn ARGB với kênh alpha=0x00, ARGB=(0,255,140,0)
ARGB như color=(0,140,255) kênh alpha=85
0x55FF8C00 - biểu diễn ARGB với kênh alpha=0x55, ARGB=(85,255,140,0)
0x55FF8C00 - kết quả của ColorToARGB(clrDarkOrange,0x55)
*/
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
Giao diện
Giao diện cho phép xác định chức năng cụ thể mà một lớp có thể triển khai. Trên thực tế, giao diện là một lớp không thể chứa bất kỳ thành viên nào, và có thể không có hàm tạo và/hoặc hàm hủy. Tất cả các phương thức được khai báo trong giao diện đều là ảo thuần túy, ngay cả khi không có định nghĩa rõ ràng.
Giao diện được định nghĩa bằng từ khóa "interface". Ví dụ:
//--- Giao diện cơ bản để mô tả động vật
interface IAnimal
{
//--- Các phương thức của giao diện có quyền truy cập công khai theo mặc định
void Sound(); // Âm thanh do động vật phát ra
};
//+------------------------------------------------------------------+
//| Lớp CCat được kế thừa từ giao diện IAnimal |
//+------------------------------------------------------------------+
class CCat : public IAnimal
{
public:
CCat() { Print("Mèo được sinh ra"); }
~CCat() { Print("Mèo đã chết"); }
//--- Triển khai phương thức Sound của giao diện IAnimal
void Sound(){ Print("meou"); }
};
//+------------------------------------------------------------------+
//| Lớp CDog được kế thừa từ giao diện IAnimal |
//+------------------------------------------------------------------+
class CDog : public IAnimal
{
public:
CDog() { Print("Chó được sinh ra"); }
~CDog() { Print("Chó đã chết"); }
//--- Triển khai phương thức Sound của giao diện IAnimal
void Sound(){ Print("guaf"); }
};
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script |
//+------------------------------------------------------------------+
void OnStart()
{
//--- Mảng các con trỏ đến các đối tượng kiểu IAnimal
IAnimal *animals[2];
//--- Tạo các lớp con của IAnimal và lưu con trỏ đến chúng vào mảng
animals[0]=new CCat;
animals[1]=new CDog;
//--- Gọi phương thức Sound() của giao diện cơ bản IAnimal cho mỗi lớp con
for(int i=0;i<ArraySize(animals);++i)
animals[i].Sound();
//--- Xóa các đối tượng
for(int i=0;i<ArraySize(animals);++i)
delete animals[i];
//--- Kết quả thực thi
/*
Mèo được sinh ra
Chó được sinh ra
meou
guaf
Mèo đã chết
Chó đã chết
*/
}
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
Giống như với lớp trừu tượng, không thể tạo đối tượng giao diện mà không có kế thừa. Giao diện chỉ có thể được kế thừa từ các giao diện khác và có thể là cha của một lớp. Giao diện luôn công khai.
Giao diện không thể được khai báo trong khai báo lớp hoặc cấu trúc, nhưng con trỏ đến giao diện có thể được lưu trong biến kiểu void *. Nói chung, con trỏ đến đối tượng của bất kỳ lớp nào cũng có thể được lưu vào biến kiểu void *. Để chuyển đổi con trỏ void * thành con trỏ đến đối tượng của một lớp cụ thể, sử dụng toán tử dynamic_cast. Nếu không thể chuyển đổi, kết quả của thao tác dynamic_cast sẽ là NULL.