Mảng Động
Mảng động có thể thay đổi kích thước trong quá trình thực thi chương trình theo yêu cầu của lập trình viên. Hãy nhớ rằng để mô tả một mảng động, bạn nên để cặp dấu ngoặc đầu tiên sau định danh mảng trống rỗng. MQL5 yêu cầu tất cả các chiều tiếp theo (nếu có hơn một chiều) phải có kích thước cố định được chỉ định bằng một hằng số.
Không thể tăng số lượng phần tử một cách động cho bất kỳ chiều nào "cũ hơn" chiều đầu tiên. Ngoài ra, do mô tả kích thước nghiêm ngặt, các mảng có hình dạng "vuông", tức là, ví dụ, không thể xây dựng một mảng hai chiều với các cột hoặc hàng có độ dài khác nhau. Nếu bất kỳ hạn chế nào trong số này là quan trọng đối với việc thực hiện thuật toán, bạn nên sử dụng không phải mảng chuẩn MQL5, mà là các cấu trúc hoặc lớp của riêng bạn được viết bằng MQL5.
Lưu ý rằng nếu một mảng không có kích thước ở chiều đầu tiên, nhưng có danh sách khởi tạo cho phép xác định kích thước, thì mảng đó là mảng có kích thước cố định, không phải mảng động.
Ví dụ, trong phần trước, chúng ta đã sử dụng mảng array1D
:
int array1D[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Do có danh sách khởi tạo, kích thước của nó được trình biên dịch biết, và do đó mảng này là cố định.
Không giống như ví dụ đơn giản này, không phải lúc nào cũng dễ xác định liệu một mảng cụ thể trong chương trình thực tế có phải là mảng động hay không. Đặc biệt, một mảng có thể được truyền dưới dạng tham số vào một hàm. Tuy nhiên, việc biết một mảng có phải là động hay không có thể quan trọng vì bộ nhớ chỉ có thể được cấp phát thủ công bằng cách gọi ArrayResize
cho các mảng như vậy.
Trong những trường hợp như vậy, hàm AllaIsDynamic
cho phép bạn xác định loại của mảng.
Hãy xem xét một số mô tả kỹ thuật của các hàm để làm việc với mảng động và sau đó thử nghiệm chúng bằng tập lệnh ArrayDynamic.mq5
.
bool ArrayIsDynamic(const void &array[])
Hàm này kiểm tra xem mảng được truyền vào có phải là mảng động hay không. Mảng có thể có bất kỳ chiều nào được phép từ 1 đến 4. Các phần tử mảng có thể thuộc bất kỳ loại nào.
Hàm trả về true
cho mảng động, hoặc false
trong các trường hợp khác (mảng cố định, hoặc mảng với chuỗi thời gian, được điều khiển bởi terminal hoặc bởi chỉ báo).
int ArrayResize(void &array[], int size, int reserve = 0)
Hàm này đặt kích thước mới size
ở chiều đầu tiên của mảng động array
. Mảng có thể có bất kỳ chiều nào được phép từ 1 đến 4. Các phần tử mảng có thể thuộc bất kỳ loại nào.
Nếu tham số reserve
lớn hơn không, bộ nhớ sẽ được cấp phát cho mảng với một khoản dự trữ cho số lượng phần tử được chỉ định. Điều này có thể tăng tốc độ của chương trình có nhiều lời gọi hàm liên tiếp. Cho đến khi kích thước mới được yêu cầu của mảng vượt quá kích thước hiện tại tính cả khoản dự trữ, sẽ không có sự tái phân bổ bộ nhớ vật lý và các phần tử mới sẽ được lấy từ khoản dự trữ.
Hàm trả về kích thước mới của mảng nếu việc sửa đổi thành công, hoặc -1 trong trường hợp có lỗi.
Nếu hàm được áp dụng cho một mảng cố định hoặc chuỗi thời gian, kích thước của nó không thay đổi. Trong những trường hợp này, nếu kích thước được yêu cầu nhỏ hơn hoặc bằng kích thước hiện tại của mảng, hàm sẽ trả về giá trị của tham số size
, nếu không, nó sẽ trả về -1.
Khi tăng kích thước của một mảng đã tồn tại, tất cả dữ liệu của các phần tử của nó được giữ nguyên. Các phần tử được thêm vào không được khởi tạo với bất kỳ giá trị nào và có thể chứa dữ liệu ngẫu nhiên không chính xác ("rác").
Việc đặt kích thước mảng về 0, ArrayResize(array, 0)
, không giải phóng bộ nhớ thực sự được cấp phát cho nó, bao gồm cả khoản dự trữ có thể có. Lời gọi như vậy chỉ đặt lại siêu dữ liệu cho mảng. Điều này được thực hiện nhằm mục đích tối ưu hóa các hoạt động trong tương lai với mảng. Để buộc giải phóng bộ nhớ, hãy sử dụng ArrayFree
(xem bên dưới).
Điều quan trọng là phải hiểu rằng tham số reserve
không được sử dụng mỗi khi hàm được gọi, mà chỉ ở những thời điểm khi việc tái phân bổ bộ nhớ thực sự được thực hiện, tức là khi kích thước được yêu cầu vượt quá dung lượng hiện tại của mảng bao gồm cả khoản dự trữ. Để hiển thị trực quan cách thức hoạt động này, chúng ta sẽ tạo một bản sao không đầy đủ của đối tượng mảng nội bộ và triển khai hàm song sinh ArrayResize
cho nó, cũng như các hàm tương tự ArrayFree
và ArraySize
, để có một bộ công cụ hoàn chỉnh.
template<typename T>
struct DynArray
{
int size;
int capacity;
T memory[];
};
template<typename T>
int DynArraySize(DynArray<T> &array)
{
return array.size;
}
template<typename T>
void DynArrayFree(DynArray<T> &array)
{
ArrayFree(array.memory);
ZeroMemory(array);
}
template<typename T>
int DynArrayResize(DynArray<T> &array, int size, int reserve = 0)
{
if(size > array.capacity)
{
static int temp;
temp = array.capacity;
long ul = (long)GetMicrosecondCount();
array.capacity = ArrayResize(array.memory, size + reserve);
array.size = MathMin(size, array.capacity);
ul -= (long)GetMicrosecondCount();
PrintFormat("Reallocation: [%d] -> [%d], done in %d µs",
temp, array.capacity, -ul);
}
else
{
array.size = size;
}
return array.size;
}
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
Ưu điểm của hàm DynArrayResize
so với hàm tích hợp ArrayResize
là ở chỗ chúng ta chèn một lệnh in gỡ lỗi cho những tình huống khi dung lượng nội bộ của mảng được tái phân bổ.
Bây giờ chúng ta có thể lấy ví dụ chuẩn cho hàm ArrayResize
từ tài liệu MQL5 và thay thế các lời gọi hàm tích hợp bằng các tương tự "tự chế" với tiền tố "Dyn". Kết quả đã sửa đổi được trình bày trong tập lệnh ArrayCapacity.mq5
.
void OnStart()
{
ulong start = GetTickCount();
ulong now;
int count = 0;
DynArray<double> a;
// tùy chọn nhanh với dự trữ bộ nhớ
Print("--- Test Fast: ArrayResize(arr,100000,100000)");
DynArrayResize(a, 100000, 100000);
for(int i = 1; i <= 300000 && !IsStopped(); i++)
{
// đặt kích thước mới và dự trữ 100000 phần tử
DynArrayResize(a, i, 100000);
// ở các lần lặp "tròn", hiển thị kích thước của mảng và thời gian đã trôi qua
if(DynArraySize(a) % 100000 == 0)
{
now = GetTickCount();
count++;
PrintFormat("%d. ArraySize(arr)=%d Time=%d ms",
count, DynArraySize(a), (now - start));
start = now;
}
}
DynArrayFree(a);
// bây giờ là tùy chọn chậm không có dư thừa (với ít dư thừa hơn)
count = 0;
start = GetTickCount();
Print("---- Test Slow: ArrayResize(slow,100000)");
DynArrayResize(a, 100000, 100000);
for(int i = 1; i <= 300000 && !IsStopped(); i++)
{
// đặt kích thước mới nhưng với lề nhỏ hơn 100 lần: 1000
DynArrayResize(a, i, 1000);
// ở các lần lặp "tròn", hiển thị kích thước của mảng và thời gian đã trôi qua
if(DynArraySize(a) % 100000 == 0)
{
now = GetTickCount();
count++;
PrintFormat("%d. ArraySize(arr)=%d Time=%d ms",
count, DynArraySize(a), (now - start));
start = now;
}
}
}
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
Sự khác biệt đáng kể duy nhất là như sau: trong phiên bản chậm, lời gọi ArrayResize(a, i)
được thay thế bằng một lời gọi vừa phải hơn DynArrayResize(a, i, 1000)
, tức là việc tái phân phối không được yêu cầu ở mỗi lần lặp, mà ở mỗi lần thứ 1000 (nếu không nhật ký sẽ đầy thông điệp).
Sau khi chạy tập lệnh, chúng ta sẽ thấy thời gian sau trong nhật ký (khoảng thời gian tuyệt đối phụ thuộc vào máy tính của bạn, nhưng chúng ta quan tâm đến sự khác biệt giữa các biến thể hiệu suất với và không có dự trữ):
--- Test Fast: ArrayResize(arr,100000,100000)
Reallocation: [0] -> [200000], done in 17 µs
1. ArraySize(arr)=100000 Time=0 ms
2. ArraySize(arr)=200000 Time=0 ms
Reallocation: [200000] -> [300001], done in 2296 µs
3. ArraySize(arr)=300000 Time=0 ms
---- Test Slow: ArrayResize(slow,100000)
Reallocation: [0] -> [200000], done in 21 µs
1. ArraySize(arr)=100000 Time=0 ms
2. ArraySize-porn(arr)=200000 Time=0 ms
Reallocation: [200000] -> [201001], done in 1838 µs
Reallocation: [201001] -> [202002], done in 1994 µs
Reallocation: [202002] -> [203003], done in 1677 µs
Reallocation: [203003] -> [204004], done in 1983 µs
Reallocation: [204004] -> [205005], done in 1637 µs
...
Reallocation: [295095] -> [296096], done in 2921 µs
Reallocation: [296096] -> [297097], done in 2189 µs
Reallocation: [297097] -> [298098], done in 2152 µs
Reallocation: [298098] -> [299099], done in 2767 µs
Reallocation: [299099] -> [300100], done in 2115 µs
3. ArraySize(arr)=300000 Time=219 ms
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Thời gian tiết kiệm là đáng kể. Ngoài ra, chúng ta thấy ở những lần lặp nào và cách dung lượng thực sự của mảng (dự trữ) được thay đổi.
void ArrayFree(void &array[])
Hàm này giải phóng toàn bộ bộ nhớ của mảng động được truyền vào (bao gồm cả khoản dự trữ có thể được đặt bằng tham số thứ ba của hàm ArrayResize
) và đặt kích thước của chiều đầu tiên của nó về không.
Về lý thuyết, các mảng trong MQL5 tự động giải phóng bộ nhớ khi việc thực thi thuật toán trong khối hiện tại kết thúc. Không quan trọng liệu mảng được định nghĩa cục bộ (trong các hàm) hay toàn cục, là cố định hay động, vì hệ thống sẽ tự giải phóng bộ nhớ trong mọi trường hợp, mà không yêu cầu hành động rõ ràng từ lập trình viên.
Do đó, không cần thiết phải gọi hàm này. Tuy nhiên, có những tình huống khi một mảng được sử dụng trong một thuật toán để điền lại từ đầu, tức là cần giải phóng nó trước mỗi lần điền. Khi đó tính năng này có thể hữu ích.
Hãy nhớ rằng nếu các phần tử mảng chứa con trỏ đến các đối tượng được cấp phát động, hàm không xóa chúng: lập trình viên phải gọi delete
cho chúng (xem bên dưới).
Hãy thử nghiệm các hàm đã thảo luận ở trên: ArrayIsDynamic
, ArrayResize
, ArrayFree
.
Trong tập lệnh ArrayDynamic.mq5
, hàm ArrayExtend
được viết, hàm này tăng kích thước của mảng động lên 1 và ghi giá trị được truyền vào phần tử mới.
template<typename T>
void ArrayExtend(T &array[], const T value)
{
if(ArrayIsDynamic(array))
{
const int n = ArraySize(array);
ArrayResize(array, n + 1);
array[n] = (T)value;
}
}
2
3
4
5
6
7
8
9
10
Hàm ArrayIsDynamic
được sử dụng để đảm bảo rằng mảng chỉ được cập nhật nếu nó là động. Điều này được thực hiện trong một câu lệnh điều kiện. Hàm ArrayResize
cho phép thay đổi kích thước của mảng, và hàm ArraySize
được sử dụng để tìm ra kích thước hiện tại (nó sẽ được thảo luận trong phần tiếp theo).
Trong hàm chính của tập lệnh, chúng ta sẽ áp dụng ArrayExtend
cho các mảng thuộc các danh mục khác nhau: động và cố định.
void OnStart()
{
int dynamic[];
int fixed[10] = {}; // điền bằng số không
PRT(ArrayResize(fixed, 0)); // cảnh báo: không áp dụng cho mảng cố định
for(int i = 0; i < 10; ++i)
{
ArrayExtend(dynamic, (i + 1) * (i + 1));
ArrayExtend(fixed, (i + 1) * (i + 1));
}
Print("Filled");
ArrayPrint(dynamic);
ArrayPrint(fixed);
ArrayFree(dynamic);
ArrayFree(fixed); // cảnh báo: không áp dụng cho mảng cố định
Print("Free Up");
ArrayPrint(dynamic); // không xuất ra gì
ArrayPrint(fixed);
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Trong các dòng mã gọi các hàm không thể sử dụng cho mảng cố định, trình biên dịch tạo ra cảnh báo "cannot be used for static allocated array". Điều quan trọng cần lưu ý là không có cảnh báo như vậy bên trong hàm ArrayExtend
vì một mảng thuộc bất kỳ danh mục nào cũng có thể được truyền vào hàm. Đó là lý do tại sao chúng ta kiểm tra điều này bằng ArrayIsDynamic
.
Sau một vòng lặp trong OnStart,
mảng dynamic
sẽ mở rộng lên 10 và nhận các phần tử bằng bình phương của các chỉ số. Mảng fixed
sẽ vẫn được điền bằng số không và không thay đổi kích thước.
Việc giải phóng một mảng cố định bằng ArrayFree
sẽ không có hiệu quả, và mảng động sẽ thực sự bị xóa. Trong trường hợp này, nỗ lực cuối cùng để in nó sẽ không tạo ra bất kỳ dòng nào trong nhật ký.
Hãy xem kết quả thực thi tập lệnh.
ArrayResize(fixed,0)=0
Filled
1 4 9 16 25 36 49 64 81 100
0 0 0 0 0 0 0 0 0 0
Free Up
0 0 0 0 0 0 0 0 0 0
2
3
4
5
6
Đặc biệt quan tâm là các mảng động với con trỏ đến các đối tượng. Hãy định nghĩa một lớp giả đơn giản Dummy
và tạo một mảng các con trỏ đến các đối tượng như vậy.
class Dummy
{
};
void OnStart()
{
...
Dummy *dummies[] = {};
ArrayExtend(dummies, new Dummy());
ArrayFree(dummies);
}
2
3
4
5
6
7
8
9
10
11
Sau khi mở rộng mảng dummy
với một con trỏ mới, chúng ta giải phóng nó bằng ArrayFree
, nhưng có các mục trong nhật ký terminal chỉ ra rằng đối tượng vẫn còn trong bộ nhớ.
1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory
2
3
Sự thật là hàm chỉ quản lý bộ nhớ được cấp phát cho mảng. Trong trường hợp này, bộ nhớ này chứa một con trỏ, nhưng những gì nó trỏ tới không thuộc về mảng. Nói cách khác, nếu mảng chứa các con trỏ đến các đối tượng "bên ngoài", thì bạn cần tự mình chăm sóc chúng. Ví dụ:
for(int i = 0; i < ArraySize(dummies); ++i)
{
delete dummies[i];
}
2
3
4
Việc xóa này phải được bắt đầu trước khi gọi ArrayFree
.
Để rút ngắn mục nhập, bạn có thể sử dụng các macro sau (lặp qua các phần tử, gọi delete
cho từng phần tử trong số chúng):
#define FORALL(A) for(int _iterator_ = 0; _iterator_ < ArraySize(A); ++_iterator_)
#define FREE(P) { if(CheckPointer(P) == POINTER_DYNAMIC) delete (P); }
#define CALLALL(A, CALL) FORALL(A) { CALL(A[_iterator_]) }
2
3
Sau đó, việc xóa các con trỏ được đơn giản hóa thành ký hiệu sau:
...
CALLALL(dummies, FREE);
ArrayFree(dummies);
2
3
Như một giải pháp thay thế, bạn có thể sử dụng một lớp bao bọc con trỏ như AutoPtr
, mà chúng ta đã thảo luận trong phần Mẫu loại đối tượng. Khi đó mảng nên được khai báo với loại AutoPtr
. Vì mảng sẽ lưu trữ các đối tượng bao bọc, không phải con trỏ, khi mảng được xóa, các hàm hủy cho mỗi "bao bọc" sẽ được tự động gọi, và bộ nhớ con trỏ sẽ lần lượt được giải phóng từ chúng.