Con trỏ, tham chiếu và const
Sau khi tìm hiểu về các kiểu dữ liệu tích hợp sẵn và kiểu đối tượng, cùng với khái niệm về tham chiếu và con trỏ, có lẽ việc so sánh tất cả các sửa đổi kiểu có sẵn là điều hợp lý.
Tham chiếu trong MQL5 chỉ được sử dụng khi mô tả các tham số của hàm và phương thức. Hơn nữa, các tham số kiểu đối tượng phải được truyền qua tham chiếu.
cpp void function(ClassOrStruct &object) { } // OK void function(ClassOrStruct object) { } // sai void function(double &value) { } // OK void function(double value) { } // OK
Ở đây, ClassOrStruct
là tên của lớp hoặc cấu trúc.
Chỉ được phép truyền các biến (LValue) làm đối số cho tham số kiểu tham chiếu, nhưng không được truyền hằng số hoặc giá trị tạm thời thu được từ việc đánh giá biểu thức.
Bạn không thể tạo một biến kiểu tham chiếu hoặc trả về một tham chiếu từ một hàm.
cpp ClassOrStruct &function(void) { return Class(); } // sai ClassOrStruct &object; // sai double &value; // sai
Con trỏ trong MQL5 chỉ khả dụng cho các đối tượng lớp. Con trỏ tới các biến thuộc kiểu tích hợp sẵn hoặc cấu trúc không được hỗ trợ.
Bạn có thể khai báo một biến hoặc tham số hàm thuộc kiểu con trỏ tới một đối tượng, và cũng có thể trả về một con trỏ tới một đối tượng từ hàm.
cpp ClassOrStruct *pointer; // OK void function(ClassOrStruct *object) { } // OK ClassOrStruct *function() { return new ClassOrStruct(); } // OK
Tuy nhiên, bạn không thể trả về một con trỏ tới một đối tượng tự động cục bộ, vì đối tượng đó sẽ được giải phóng khi hàm kết thúc, và con trỏ sẽ trở nên không hợp lệ.
Nếu hàm trả về một con trỏ tới một đối tượng được cấp phát động trong hàm bằng new
, thì mã gọi phải "nhớ" giải phóng con trỏ bằng delete
.
Không giống như tham chiếu, một con trỏ có thể là NULL
. Các tham số con trỏ có thể có giá trị mặc định, nhưng tham chiếu thì không (lỗi "tham chiếu không thể được khởi tạo").
cpp void function(ClassOrStruct *object = NULL) { } // OK void function(ClassOrStruct &object = NULL) { } // sai
Tham chiếu và con trỏ có thể được kết hợp trong mô tả tham số. Vì vậy, một hàm có thể nhận một tham chiếu tới một con trỏ: và sau đó các thay đổi đối với con trỏ trong hàm sẽ khả dụng trong mã gọi. Đặc biệt, hàm factory, chịu trách nhiệm tạo đối tượng, có thể được triển khai theo cách này.
cpp void createObject(ClassName *&ref) { ref = new ClassName(); // tùy chỉnh thêm cho ref ... }
Thực ra, để trả về một con trỏ đơn từ một hàm, thường sử dụng câu lệnh return
, nên ví dụ này có phần hơi gượng ép. Tuy nhiên, trong những trường hợp cần truyền một mảng con trỏ ra ngoài, tham chiếu tới nó trong tham số trở thành lựa chọn ưu tiên. Ví dụ, trong một số lớp của thư viện chuẩn để làm việc với các lớp chứa kiểu bản đồ với cặp [khóa, giá trị] (MQL5/Include/Generic/SortedMap.mqh
, MQL5/Include/Generic/HashMap.mqh
) có các phương thức CopyTo
để lấy mảng với các phần tử CKeyValuePair
.
cpp int CopyTo(CKeyValuePair<TKey,TValue> *&dst_array[], const int dst_start = 0);
Kiểu tham số dst_array
có thể trông lạ lẫm: đó là một mẫu lớp. Chúng ta sẽ tìm hiểu về mẫu trong chương tiếp theo. Hiện tại, điều quan trọng duy nhất với chúng ta là đây là một tham chiếu tới một mảng con trỏ.
Bộ sửa đổi const
áp đặt hành vi đặc biệt cho tất cả các kiểu. Đối với các kiểu tích hợp sẵn, nó đã được thảo luận trong phần về Biến hằng số. Các kiểu đối tượng có những đặc điểm riêng.
Nếu một biến hoặc tham số hàm được khai báo là con trỏ hoặc tham chiếu tới một đối tượng (tham chiếu chỉ trong trường hợp tham số), thì sự hiện diện của bộ sửa đổi const
trên chúng giới hạn tập hợp các phương thức và thuộc tính có thể truy cập chỉ còn những cái cũng có bộ sửa đổi const
. Nói cách khác, chỉ các thuộc tính hằng mới có thể truy cập qua tham chiếu và con trỏ hằng.
Khi bạn cố gắng gọi một phương thức không phải const
hoặc thay đổi một trường không phải const
, trình biên dịch sẽ tạo ra lỗi: "gọi phương thức không phải const cho đối tượng hằng" hoặc "hằng không thể được sửa đổi".
Một tham số con trỏ không phải const
có thể nhận bất kỳ đối số nào (hằng hoặc không hằng).
Cần lưu ý rằng hai bộ sửa đổi const
có thể được đặt trong mô tả con trỏ: một sẽ liên quan đến đối tượng, và cái thứ hai liên quan đến con trỏ:
Class *pointer
là con trỏ tới một đối tượng; đối tượng và con trỏ hoạt động không giới hạn;const Class *pointer
là con trỏ tới một đối tượng hằng; đối với đối tượng, chỉ các phương thức hằng và đọc thuộc tính khả dụng, nhưng con trỏ có thể thay đổi (gán cho nó địa chỉ của đối tượng khác);const Class * const pointer
là con trỏ hằng tới một đối tượng hằng; đối với đối tượng, chỉ các phương thức hằng và đọc thuộc tính khả dụng; con trỏ không thể thay đổi;Class * const pointer
là con trỏ hằng tới một đối tượng; con trỏ không thể thay đổi, nhưng thuộc tính của đối tượng có thể thay đổi.
Hãy xem xét lớp Counter
(CounterConstPtr.mq5
) sau đây làm ví dụ.
``cpp class Counter { public: int counter;
Counter(const int n = 0) : counter(n)
void increment() { ++counter; }
Counter *clone() const { return new Counter(counter); } }; ``
Nó cố tình đặt biến công khai counter
. Lớp cũng có hai phương thức, một trong số đó là hằng (clone
), và cái còn lại không phải (increment
). Hãy nhớ rằng một phương thức hằng không có quyền thay đổi các trường của đối tượng.
Hàm sau với tham số kiểu Counter *ptr
có thể gọi tất cả các phương thức của lớp và thay đổi các trường của nó.
cpp void functionVolatile(Counter *ptr) { // OK: mọi thứ đều khả dụng ptr.increment(); ptr.counter += 2; // xóa bản sao ngay lập tức để không rò rỉ bộ nhớ // bản sao chỉ cần để thể hiện việc gọi phương thức hằng delete ptr.clone(); ptr = NULL; }
Hàm sau với tham số const Counter *ptr
sẽ gây ra một vài lỗi.
``cpp void functionConst(const Counter *ptr) { // LỖI: ptr.increment(); // gọi phương thức không phải const cho đối tượng hằng ptr.counter = 1; // hằng không thể được sửa đổi
// OK: chỉ các phương thức const khả dụng, các trường có thể được đọc Print(ptr.counter); // đọc một đối tượng hằng Counter *clone = ptr.clone(); // gọi một phương thức const ptr = clone; // thay đổi con trỏ không phải const ptr delete ptr; // dọn dẹp bộ nhớ } ``
Cuối cùng, hàm sau với tham số const Counter * const ptr
thậm chí còn làm được ít hơn.
``cpp void functionConstConst(const Counter * const ptr) { // OK: chỉ các phương thức const khả dụng, con trỏ ptr không thể thay đổi Print(ptr.counter); // đọc một đối tượng hằng delete ptr.clone(); // gọi một phương thức const
Counter local(0); // LỖI: ptr.increment(); // gọi phương thức không phải const cho đối tượng hằng ptr.counter = 1; // hằng không thể được sửa đổi ptr = &local; // hằng không thể được sửa đổi } ``
Trong hàm OnStart
, nơi chúng ta đã khai báo hai đối tượng Counter
(một là hằng và một không phải), bạn có thể gọi các hàm này với một số ngoại lệ:
``cpp void OnStart() { Counter counter; const Counter constCounter;
counter.increment();
// LỖI: // constCounter.increment(); // gọi phương thức không phải const cho đối tượng hằng Counter *ptr = (Counter *)&constCounter; // mẹo: ép kiểu mà không có const ptr.increment();
functionVolatile(&counter);
// LỖI: không thể chuyển đổi từ con trỏ const... // functionVolatile(&constCounter); // sang con trỏ không phải const
functionVolatile((Counter *)&constCounter); // ép kiểu mà không có const
functionConst(&counter); functionConst(&constCounter);
functionConstConst(&counter); functionConstConst(&constCounter); } ``
Đầu tiên, lưu ý rằng các biến cũng tạo ra lỗi khi cố gắng gọi phương thức const increment
trên một đối tượng không phải const.
Thứ hai, constCounter
không thể được truyền vào hàm functionVolatile
— chúng ta nhận được lỗi "không thể chuyển đổi từ con trỏ const sang con trỏ không phải const".
Tuy nhiên, cả hai lỗi này có thể được vượt qua bằng cách ép kiểu rõ ràng mà không có bộ sửa đổi const
. Mặc dù điều này không được khuyến khích.