Đặt lại về không cho các đối tượng và mảng
Thông thường, việc khởi tạo hoặc điền dữ liệu vào biến và mảng không gây ra vấn đề. Vì vậy, đối với các biến đơn giản, chúng ta có thể sử dụng toán tử =
trong câu lệnh định nghĩa cùng với khởi tạo, hoặc gán giá trị mong muốn vào bất kỳ thời điểm nào sau đó.
Khởi tạo dạng tổng hợp có sẵn cho các cấu trúc (xem phần Định nghĩa cấu trúc):
Struct struct = {value1, value2, ...};
Nhưng điều này chỉ có thể thực hiện nếu không có mảng động hoặc chuỗi trong cấu trúc. Hơn nữa, cú pháp khởi tạo tổng hợp không thể được sử dụng để làm sạch lại một cấu trúc. Thay vào đó, bạn phải gán giá trị cho từng trường riêng lẻ hoặc dự trữ một thực thể của cấu trúc rỗng trong chương trình và sao chép nó vào các thực thể cần làm sạch.
Nếu đồng thời chúng ta đang nói về một mảng của các cấu trúc, thì mã nguồn sẽ nhanh chóng tăng lên do các lệnh phụ trợ nhưng cần thiết.
Đối với mảng, có các hàm ArrayInitialize
và ArrayFill
, nhưng chúng chỉ hỗ trợ các loại số: một mảng chuỗi hoặc cấu trúc không thể được điền bằng các hàm này.
Trong những trường hợp như vậy, hàm ZeroMemory
có thể hữu ích. Nó không phải là giải pháp hoàn hảo, vì nó có những hạn chế đáng kể về phạm vi áp dụng, nhưng biết về nó là điều tốt.
void ZeroMemory(void &entity)
Hàm này có thể được áp dụng cho một loạt các thực thể khác nhau: biến của loại đơn giản hoặc đối tượng, cũng như các mảng của chúng (cố định, động hoặc đa chiều).
Các biến sẽ nhận giá trị 0 (đối với số) hoặc giá trị tương đương (NULL cho chuỗi và con trỏ).
Trong trường hợp của một mảng, tất cả các phần tử của nó được đặt về 0. Đừng quên rằng các phần tử có thể là đối tượng, và đến lượt chúng, chứa các đối tượng khác. Nói cách khác, hàm ZeroMemory
thực hiện việc làm sạch bộ nhớ sâu chỉ trong một lần gọi.
Tuy nhiên, có những hạn chế đối với các đối tượng hợp lệ. Bạn chỉ có thể điền số 0 vào các đối tượng của cấu trúc và lớp, mà:
- Chỉ chứa các trường công khai (tức là không chứa dữ liệu với loại truy cập
private
hoặcprotected
) - Không chứa các trường với bộ sửa đổi
const
- Không chứa con trỏ
Hai hạn chế đầu tiên được tích hợp trong trình biên dịch: việc cố gắng đặt về không các đối tượng có trường không đáp ứng các yêu cầu đã nêu sẽ gây ra lỗi (xem bên dưới).
Hạn chế thứ ba là một khuyến nghị: việc đặt về không bên ngoài của một con trỏ sẽ khiến việc kiểm tra tính toàn vẹn của dữ liệu trở nên khó khăn, điều này có thể dẫn đến mất đối tượng liên quan và gây rò rỉ bộ nhớ.
Nghiêm túc mà nói, yêu cầu về tính công khai của các trường trong các đối tượng có thể đặt về không vi phạm nguyên tắc đóng gói, vốn có trong các đối tượng lớp, và do đó ZeroMemory
chủ yếu được sử dụng với các đối tượng của cấu trúc đơn giản và mảng của chúng.
Ví dụ về cách làm việc với ZeroMemory
được đưa ra trong script ZeroMemory.mq5
.
Các vấn đề với danh sách khởi tạo tổng hợp được thể hiện bằng cấu trúc Simple
:
#define LIMIT 5
struct Simple
{
MqlDateTime data[]; // mảng động vô hiệu hóa danh sách khởi tạo,
// string s; // và một trường chuỗi cũng sẽ cấm,
// ClassType *ptr; // và một con trỏ cũng vậy
Simple()
{
// cấp phát bộ nhớ, nó sẽ chứa dữ liệu ngẫu nhiên
ArrayResize(data, LIMIT);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
Trong hàm OnStart
hoặc trong ngữ cảnh toàn cục, chúng ta không thể định nghĩa và ngay lập tức đặt về không một đối tượng của cấu trúc như vậy:
void OnStart()
{
Simple simple = {}; // lỗi: không thể khởi tạo bằng danh sách khởi tạo
...
}
2
3
4
5
Trình biên dịch báo lỗi "không thể sử dụng danh sách khởi tạo". Điều này đặc biệt liên quan đến các trường như mảng động, biến chuỗi và con trỏ. Cụ thể, nếu mảng data
có kích thước cố định, sẽ không có lỗi xảy ra.
Do đó, thay vì danh sách khởi tạo, chúng ta sử dụng ZeroMemory
:
void OnStart()
{
Simple simple;
ZeroMemory(simple);
...
}
2
3
4
5
6
Việc điền số 0 ban đầu cũng có thể được thực hiện trong hàm tạo của cấu trúc, nhưng việc thực hiện các lần làm sạch sau đó bên ngoài sẽ thuận tiện hơn (hoặc cung cấp một phương thức cho việc này với cùng hàm ZeroMemory
).
Lớp sau được định nghĩa trong Base
:
class Base
{
public: // công khai là bắt buộc cho ZeroMemory
// const cho bất kỳ trường nào sẽ gây lỗi biên dịch khi gọi ZeroMemory:
// "không cho phép đối với các đối tượng có thành viên được bảo vệ hoặc kế thừa"
/* const */ int x;
Simple t; // sử dụng một cấu trúc lồng: nó cũng sẽ được đặt về không
Base()
{
x = rand();
}
virtual void print() const
{
PrintFormat("%d %d", &this, x);
ArrayPrint(t.data);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vì lớp này sau đó được sử dụng trong các mảng của các đối tượng có thể đặt về không bằng ZeroMemory
, chúng ta buộc phải viết một phần truy cập public
cho các trường của nó (điều này về nguyên tắc không điển hình cho các lớp và được thực hiện để minh họa các yêu cầu do ZeroMemory
đặt ra). Cũng lưu ý rằng các trường không thể có bộ sửa đổi const
. Nếu không, chúng ta sẽ nhận được lỗi biên dịch với nội dung không thực sự phù hợp với vấn đề: "cấm đối với các đối tượng có thành viên được bảo vệ hoặc kế thừa".
Hàm tạo của lớp điền trường x
bằng một số ngẫu nhiên để sau đó bạn có thể thấy rõ việc làm sạch của nó bằng hàm ZeroMemory
. Phương thức print
hiển thị nội dung của tất cả các trường để phân tích, bao gồm số đối tượng duy nhất (mô tả) &this
.
MQL5 không ngăn ZeroMemory
được áp dụng cho một biến con trỏ:
Base *base = new Base();
ZeroMemory(base); // sẽ đặt con trỏ thành NULL nhưng để lại đối tượng
2
Tuy nhiên, điều này không nên thực hiện, vì hàm chỉ đặt lại chính biến base
, và nếu nó tham chiếu đến một đối tượng, đối tượng này sẽ vẫn "treo" trong bộ nhớ, không thể truy cập từ chương trình do mất con trỏ.
Bạn chỉ có thể đặt về không một con trỏ sau khi thực thể con trỏ đã được giải phóng bằng toán tử delete
. Hơn nữa, việc đặt lại một con trỏ riêng biệt từ ví dụ trên, giống như bất kỳ biến đơn giản nào khác (không phải tổng hợp), dễ dàng hơn bằng cách sử dụng toán tử gán. Việc sử dụng ZeroMemory
có ý nghĩa đối với các đối tượng tổng hợp và mảng.
Hàm cho phép làm việc với các đối tượng của hệ thống phân cấp lớp. Ví dụ, chúng ta có thể mô tả lớp dẫn xuất của lớp Dummy
kế thừa từ Base
:
class Dummy : public Base
{
public:
double data[]; // cũng có thể là đa chiều: ZeroMemory sẽ hoạt động
string s;
Base *pointer; // con trỏ công khai (nguy hiểm)
public:
Dummy()
{
ArrayResize(data, LIMIT);
// do việc áp dụng ZeroMemory sau đó cho đối tượng
// chúng ta sẽ mất 'pointer'
// và nhận được cảnh báo khi script kết thúc
// về các đối tượng chưa được xóa của loại Base
pointer = new Base();
}
~Dummy()
{
// do sử dụng ZeroMemory, con trỏ này sẽ bị mất
// và sẽ không được giải phóng
if(CheckPointer(pointer) != POINTER_INVALID) delete pointer;
}
virtual void print() const override
{
Base::print();
ArrayPrint(data);
Print(pointer);
if(CheckPointer(pointer) != POINTER_INVALID) pointer.print();
}
};
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
Nó bao gồm các trường với mảng động loại double
, chuỗi và con trỏ loại Base
(đây là cùng loại mà lớp được dẫn xuất, nhưng nó chỉ được sử dụng ở đây để thể hiện các vấn đề về con trỏ, để không phải mô tả một lớp giả khác). Khi hàm ZeroMemory
đặt về không đối tượng Dummy
, một đối tượng tại pointer
bị mất và không thể được giải phóng trong hàm hủy. Kết quả là, điều này dẫn đến các cảnh báo về rò rỉ bộ nhớ trong các đối tượng còn lại sau khi script kết thúc.
ZeroMemory
được sử dụng trong OnStart
để làm sạch mảng các đối tượng Dummy
:
void OnStart()
{
...
Print("Initial state");
Dummy array[];
ArrayResize(array, LIMIT);
for(int i = 0; i < LIMIT; ++i)
{
array[i].print();
}
ZeroMemory(array);
Print("ZeroMemory done");
for(int i = 0; i < LIMIT; ++i)
{
array[i].print();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Nhật ký sẽ xuất ra một cái gì đó như sau (trạng thái ban đầu sẽ khác nhau vì nó in nội dung của bộ nhớ "bẩn", mới được cấp phát; đây là một phần mã nhỏ):
Initial state
1048576 31539
[year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0] 0 65665 32 0 0 0 0 0
[1] 0 0 0 0 0 0 65624 8
[2] 0 0 0 0 0 0 0 0
[3] 0 0 0 0 0 0 0 0
[4] 5242880 531430129 51557552 0 0 65665 32 0
0.0 0.0 0.0 0.0 0.0
...
ZeroMemory done
1048576 0
[year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0] 0 0 0 0 0 0 0 0
[1] 0 0 0 0 0 0 0 0
[2] 0 0 0 0 0 0 0 0
[3] 0 0 0 0 0 0 0 0
[4] 0 0 0 0 0 0 0 0
0.0 0.0 0.0 0.0 0.0
...
5 undeleted objects left
5 objects of type Base left
3200 bytes of leaked memory
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Để so sánh trạng thái của các đối tượng trước và sau khi làm sạch, sử dụng các mô tả.
Vì vậy, một lần gọi duy nhất đến ZeroMemory
có thể đặt lại trạng thái của một cấu trúc dữ liệu phân nhánh bất kỳ (mảng, cấu trúc, mảng của cấu trúc với các trường cấu trúc lồng và mảng).
Cuối cùng, hãy xem cách ZeroMemory
có thể giải quyết vấn đề khởi tạo mảng chuỗi. Các hàm ArrayInitialize
và ArrayFill
không hoạt động với chuỗi.
string text[LIMIT] = {};
// một thuật toán điền và sử dụng 'text'
// ...
// sau đó bạn cần tái sử dụng mảng
// gọi các hàm gây lỗi:
// ArrayInitialize(text, NULL);
// `-> không có overload nào có thể áp dụng cho lệnh gọi hàm
// ArrayFill(text, 0, 10, NULL);
// `-> loại 'string' không thể sử dụng trong hàm ArrayFill
ZeroMemory(text); // ok
2
3
4
5
6
7
8
9
10
Trong các lệnh được chú thích, trình biên dịch sẽ tạo ra lỗi, nói rằng loại string
không được hỗ trợ trong các hàm này.
Cách giải quyết vấn đề này là hàm ZeroMemory
.