Hàm tạo: Mặc định, có tham số và sao chép
Chúng ta đã gặp các hàm tạo trong chương về cấu trúc (xem phần Hàm tạo và hàm hủy). Đối với các lớp, chúng hoạt động gần giống như vậy. Hãy quay lại các điểm chính và xem xét thêm các tính năng khác.
Hàm tạo là một phương thức có cùng tên với lớp và thuộc kiểu void
, nghĩa là nó không trả về giá trị. Thông thường, từ khóa void
được bỏ qua trước tên hàm tạo. Một lớp có thể có nhiều hàm tạo: chúng phải khác nhau về số lượng hoặc kiểu của các tham số. Khi một đối tượng mới được tạo, chương trình gọi hàm tạo để nó có thể đặt các giá trị ban đầu cho các trường.
Một trong những cách chúng ta đã sử dụng để tạo đối tượng là mô tả biến của lớp tương ứng trong mã. Hàm tạo sẽ được gọi trên dòng này. Việc này diễn ra tự động.
Tùy thuộc vào sự hiện diện và kiểu của các tham số, các hàm tạo được chia thành:
- Hàm tạo mặc định: không có tham số;
- Hàm tạo sao chép: với một tham số duy nhất là kiểu tham chiếu đến một đối tượng cùng lớp;
- Hàm tạo có tham số: với một tập hợp tham số tùy ý, ngoại trừ một tham chiếu duy nhất để sao chép như đã nêu ở trên.
Hàm tạo mặc định
Hàm tạo đơn giản nhất, không có tham số, được gọi là hàm tạo mặc định. Không giống như C++, MQL5 không coi hàm tạo mặc định là hàm tạo có tham số mà tất cả đều có giá trị mặc định (tức là tất cả các tham số là tùy chọn, xem phần Tham số tùy chọn).
Hãy định nghĩa một hàm tạo mặc định cho lớp Shape
.
class Shape
{
...
public:
Shape()
{
...
}
};
2
3
4
5
6
7
8
9
Tất nhiên, điều này nên được thực hiện trong phần public
của lớp.
Các hàm tạo đôi khi được cố ý đặt ở chế độ protected
hoặc private
để kiểm soát cách tạo đối tượng, ví dụ, thông qua các phương thức nhà máy. Nhưng trong trường hợp này, chúng ta đang xem xét phiên bản tiêu chuẩn của việc tổ hợp lớp.
Để đặt giá trị ban đầu cho các biến của đối tượng, chúng ta có thể sử dụng các câu lệnh gán thông thường:
public:
Shape()
{
x = 0;
y = 0;
...
}
2
3
4
5
6
7
Tuy nhiên, cú pháp hàm tạo cung cấp một lựa chọn khác. Nó được gọi là danh sách khởi tạo và được viết sau tiêu đề hàm, cách nhau bằng dấu hai chấm. Danh sách này là một chuỗi các tên trường được phân tách bằng dấu phẩy, với giá trị ban đầu mong muốn trong dấu ngoặc đơn ở bên phải mỗi tên.
Ví dụ, đối với hàm tạo Shape
, nó có thể được viết như sau:
public:
Shape() :
x(0), y(0),
backgroundColor(clrNONE)
{
}
2
3
4
5
6
Cú pháp này được ưu tiên hơn việc gán biến trong thân hàm tạo vì một số lý do.
Thứ nhất, việc gán trong thân hàm được thực hiện sau khi biến tương ứng đã được tạo. Tùy thuộc vào kiểu của biến, điều này có thể có nghĩa là hàm tạo mặc định đã được gọi cho nó trước, sau đó giá trị mới được ghi đè (và điều này đồng nghĩa với chi phí bổ sung). Trong trường hợp danh sách khởi tạo, biến được tạo ngay lập tức với giá trị mong muốn. Có khả năng trình biên dịch sẽ tối ưu hóa việc gán khi không có danh sách khởi tạo, nhưng trong trường hợp chung, điều này không được đảm bảo.
Thứ hai, một số trường của lớp có thể được khai báo với bộ điều chỉnh const
. Khi đó, chúng chỉ có thể được thiết lập trong danh sách khởi tạo.
Thứ ba, các biến trường của kiểu do người dùng định nghĩa có thể không có hàm tạo mặc định (tức là tất cả các hàm tạo có sẵn trong lớp của chúng đều có tham số). Điều này có nghĩa là khi tạo một biến, bạn cần truyền các tham số thực tế cho nó, và danh sách khởi tạo cho phép thực hiện điều này: các giá trị đối số được chỉ định trong dấu ngoặc đơn, như trong một lời gọi hàm tạo rõ ràng. Danh sách khởi tạo có thể được sử dụng trong định nghĩa hàm tạo, nhưng không dùng trong các phương thức khác.
Hàm tạo có tham số
Hàm tạo có tham số, theo định nghĩa, có nhiều tham số (một hoặc nhiều hơn).
Ví dụ, hãy tưởng tượng rằng cho các tọa độ x
và y
, một cấu trúc đặc biệt với hàm tạo có tham số được mô tả:
struct Pair
{
int x, y;
Pair(int a, int b): x(a), y(b) { }
};
2
3
4
5
Sau đó, chúng ta có thể sử dụng trường coordinates
của kiểu mới Pair
thay vì hai trường số nguyên x
và y
trong lớp Shape
. Việc xây dựng đối tượng này được gọi là bao gồm hoặc tổng hợp thành phần. Đối tượng Pair
là một phần không thể tách rời của đối tượng Shape
. Một cặp tọa độ được tự động tạo và hủy cùng với đối tượng "chủ".
Vì Pair
không có hàm tạo không tham số, trường coordinates
phải được chỉ định trong danh sách khởi tạo của hàm tạo Shape
, với hai tham số (int, int)
:
class Shape
{
protected:
// int x, y;
Pair coordinates; // tọa độ tâm (bao gồm đối tượng)
...
public:
Shape() :
// x(0), y(0),
coordinates(0, 0), // khởi tạo đối tượng
backgroundColor(clrNONE)
{
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
Nếu không có danh sách khởi tạo, các đối tượng tự động như vậy không thể được tạo.
Do thay đổi cách lưu trữ tọa độ trong đối tượng, chúng ta cần cập nhật phương thức toString
:
string toString() const
{
return (string)coordinates.x + " " + (string)coordinates.y;
}
2
3
4
Nhưng đây không phải là phiên bản cuối cùng: chúng ta sẽ thực hiện thêm một số thay đổi sau.
Hãy nhớ rằng các biến tự động đã được mô tả trong phần Hướng dẫn khai báo/định nghĩa. Chúng được gọi là tự động vì trình biên dịch tạo chúng (cấp phát bộ nhớ) tự động, và cũng tự động xóa chúng khi quá trình thực thi chương trình rời khỏi ngữ cảnh (khối mã) nơi biến được tạo.
Trong trường hợp các biến đối tượng, việc tạo tự động không chỉ có nghĩa là cấp phát bộ nhớ mà còn là một lời gọi hàm tạo. Việc xóa tự động một đối tượng đi kèm với lời gọi hàm hủy của nó (xem phần dưới Hàm hủy). Hơn nữa, nếu đối tượng là một phần của một đối tượng khác, thì vòng đời của nó trùng với vòng đời của "chủ sở hữu", như trong trường hợp của trường
coordinates
– một thể hiện củaPair
trong đối tượngShape
.Các đối tượng tĩnh (bao gồm toàn cục) cũng được quản lý tự động bởi trình biên dịch.
Một lựa chọn thay thế cho việc cấp phát tự động là tạo và thao tác đối tượng động thông qua con trỏ.
Trong phần Thừa kế, chúng ta sẽ tìm hiểu cách một lớp có thể được thừa kế từ một lớp khác. Trong trường hợp này, danh sách khởi tạo là cách duy nhất để gọi hàm tạo có tham số của lớp cơ sở (trình biên dịch không thể tự động tạo một lời gọi hàm tạo với các tham số, như nó làm ngầm định cho hàm tạo mặc định).
Hãy thêm một hàm tạo khác vào lớp Shape
cho phép đặt các giá trị cụ thể cho các biến. Nó sẽ chỉ là một hàm tạo có tham số (bạn có thể tạo bao nhiêu tùy thích: cho các mục đích khác nhau và với tập hợp tham số khác nhau).
Shape(int px, int py, color back) :
coordinates(px, py),
backgroundColor(back)
{
}
2
3
4
5
Danh sách khởi tạo đảm bảo rằng khi thân của hàm tạo được thực thi, tất cả các trường bên trong (bao gồm cả các đối tượng lồng nhau, nếu có) đã được tạo và khởi tạo.
Thứ tự khởi tạo các thành viên của lớp không tương ứng với danh sách khởi tạo mà theo thứ tự khai báo của chúng trong lớp.
Nếu một hàm tạo có tham số được khai báo trong một lớp và cần cho phép tạo đối tượng mà không có đối số, lập trình viên phải tự triển khai hàm tạo mặc định.
Trong trường hợp không có hàm tạo nào trong lớp, trình biên dịch ngầm cung cấp một hàm tạo mặc định dưới dạng một phần giữ chỗ, chịu trách nhiệm khởi tạo các trường của các kiểu sau: chuỗi, mảng động, và các đối tượng tự động với hàm tạo mặc định. Nếu không có các trường như vậy, hàm tạo mặc định ngầm không làm gì cả. Các trường của các kiểu khác không bị ảnh hưởng bởi hàm tạo ngầm, vì vậy chúng sẽ chứa "rác" ngẫu nhiên. Để tránh điều này, lập trình viên phải khai báo rõ ràng hàm tạo và đặt các giá trị ban đầu.
Hàm tạo sao chép
Hàm tạo sao chép cho phép tạo một đối tượng dựa trên một đối tượng khác được truyền bằng tham chiếu làm tham số duy nhất.
Ví dụ, đối với lớp Shape
, hàm tạo sao chép có thể trông như sau:
class Shape
{
...
Shape(const Shape &source) :
coordinates(source.coordinates.x, source.coordinates.y),
backgroundColor(source.backgroundColor)
{
}
...
};
2
3
4
5
6
7
8
9
10
Lưu ý rằng các thành viên protected
và private
của một đối tượng khác có thể truy cập được trong đối tượng hiện tại vì quyền hoạt động ở cấp độ lớp. Nói cách khác, hai đối tượng của cùng một lớp có thể truy cập dữ liệu của nhau khi được cung cấp một tham chiếu (hoặc con trỏ).
Nếu có một hàm tạo như vậy, bạn có thể tạo đối tượng bằng một trong hai kiểu cú pháp:
void OnStart()
{
Shape s;
...
Shape s2(s); // ok: cú pháp 1 - sao chép
Shape s3 = s; // ok: cú pháp 2 - sao chép qua khởi tạo
// (nếu có hàm tạo sao chép)
// - hoặc gán
// (nếu không có hàm tạo sao chép,
// nhưng có hàm tạo mặc định)
Shape s4; // định nghĩa
s4 = s; // gán, không phải hàm tạo sao chép!
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Cần phân biệt giữa việc khởi tạo một đối tượng trong quá trình tạo và việc gán.
Tùy chọn thứ hai (được đánh dấu bằng bình luận "cú pháp 2") sẽ hoạt động ngay cả khi không có hàm tạo sao chép, nhưng có hàm tạo mặc định. Trong trường hợp này, trình biên dịch sẽ tạo mã kém hiệu quả hơn: đầu tiên, sử dụng hàm tạo mặc định, nó sẽ tạo một thể hiện trống của biến nhận (s3
, trong trường hợp này), sau đó sao chép từng phần tử của mẫu (s
, trong trường hợp này). Thực tế, trường hợp này sẽ giống như với biến s4
, mà định nghĩa và gán được thực hiện bằng các câu lệnh riêng biệt.
Nếu không có hàm tạo sao chép, việc cố gắng sử dụng cú pháp đầu tiên sẽ dẫn đến lỗi "chuyển đổi tham số không được phép", vì trình biên dịch sẽ cố gắng sử dụng một hàm tạo khác có sẵn với tập hợp tham số khác.
Hãy nhớ rằng nếu lớp có các trường với bộ điều chỉnh const
, việc gán các đối tượng như vậy bị cấm vì lý do rõ ràng: một trường hằng không thể thay đổi, nó chỉ có thể được đặt một lần khi tạo đối tượng. Do đó, hàm tạo sao chép trở thành cách duy nhất để sao chép một đối tượng.
Cụ thể, trong các phần sau, chúng ta sẽ hoàn thiện ví dụ Shape1.mq5
, và trường sau sẽ xuất hiện trong lớp Shape
(với chuỗi mô tả type
). Khi đó, toán tử gán sẽ tạo ra lỗi (đặc biệt, cho các dòng như với biến s4
):
attempting to reference deleted function
'void Shape::operator=(const Shape&)'
function 'void Shape::operator=(const Shape&)' was implicitly deleted
because member 'type' has 'const' modifier
2
3
4
Nhờ cách diễn đạt chi tiết của trình biên dịch, bạn có thể hiểu bản chất và lý do của những gì đang xảy ra: đầu tiên, toán tử gán ('=') được đề cập, không phải hàm tạo sao chép; thứ hai, được báo cáo rằng toán tử gán đã bị xóa ngầm do sự hiện diện của bộ điều chỉnh const
. Ở đây chúng ta gặp phải các khái niệm chưa biết, mà chúng ta sẽ nghiên cứu sau: nạp chồng toán tử trong lớp, chuyển đổi kiểu đối tượng, và khả năng đánh dấu các phương thức là đã xóa.
Trong phần Thừa kế, sau khi chúng ta học cách mô tả các lớp dẫn xuất, chúng ta cần làm rõ một số điều về hàm tạo sao chép trong hệ thống phân cấp lớp.