Câu lệnh khai báo/định nghĩa
Việc khai báo một biến, mảng, hàm hoặc bất kỳ thành phần nào có tên trong chương trình (bao gồm cấu trúc và lớp, sẽ được thảo luận trong Phần 3) là một câu lệnh.
Khai báo phải bao gồm kiểu và định danh của thành phần (xem Khai báo và định nghĩa biến), cũng như một giá trị khởi tạo tùy chọn cho việc khởi tạo. Ngoài ra, khi khai báo, có thể chỉ định các bộ sửa đổi bổ sung để thay đổi một số đặc tính của thành phần. Cụ thể, chúng ta đã biết đến các bộ sửa đổi static
(xem thêm) và const
(xem thêm), và sẽ có thêm nhiều bộ sửa đổi khác trong tương lai. Mảng yêu cầu thêm thông số về kích thước và số phần tử (xem Mô tả mảng), trong khi hàm yêu cầu danh sách tham số (xem chi tiết tại Hàm).
Câu lệnh khai báo biến có thể được tóm tắt như sau:
[modifiers] identifier type
[= initialization expressions];
2
Đối với mảng, nó trông như sau:
[modifiers] identifier type [ [size_1]ᵒᵖᵗ ] [ [size_N] ]ᵒᵖᵗ(3)
[ = { initialization_list } ]ᵒᵖᵗ;
2
Điểm khác biệt chính là sự hiện diện bắt buộc của ít nhất một cặp dấu ngoặc vuông (kích thước bên trong có thể được chỉ định hoặc không; tùy thuộc vào đó, ta có mảng cố định hoặc mảng phân bổ động). Tổng cộng, tối đa 4 cặp dấu ngoặc vuông được phép (4 là số chiều tối đa được hỗ trợ).
Trong nhiều trường hợp, một khai báo đồng thời cũng có thể là một định nghĩa, tức là nó cấp phát bộ nhớ cho thành phần, xác định hành vi của nó và cho phép sử dụng nó trong chương trình. Cụ thể, khai báo một biến hoặc mảng cũng là một định nghĩa. Từ quan điểm này, câu lệnh khai báo có thể được gọi là câu lệnh định nghĩa, nhưng điều này chưa trở thành thông lệ phổ biến.
Kiến thức cơ bản về hàm của chúng ta đủ để suy ra cách định nghĩa của chúng sẽ trông như thế nào:
type identifier ( [list_of_arguments] )
{
[statements]
}
2
3
4
Kiểu, định danh và danh sách tham số tạo thành tiêu đề hàm.
Lưu ý rằng đây là một định nghĩa, vì mô tả này chứa cả các thuộc tính bên ngoài của hàm (giao diện) và các câu lệnh xác định bản chất bên trong của nó (triển khai). Phần sau được thực hiện bằng một khối mã được tạo bởi cặp dấu ngoặc nhọn ngay sau tiêu đề hàm. Như bạn có thể đoán, đây là một ví dụ về câu lệnh ghép mà chúng ta đã đề cập trong phần trước. Trong trường hợp này, sự lặp lại thuật ngữ là không thể tránh khỏi, vì nó hoàn toàn hợp lý: câu lệnh ghép là một phần của câu lệnh định nghĩa hàm.
Một chút nữa, chúng ta sẽ tìm hiểu tại sao và làm thế nào để tách mô tả giao diện khỏi triển khai, từ đó đạt được khai báo hàm mà không định nghĩa nó. Chúng ta cũng sẽ chứng minh sự khác biệt giữa khai báo và định nghĩa bằng ví dụ về lớp.
Câu lệnh khai báo làm cho thành phần mới có thể truy cập bằng tên của nó trong ngữ cảnh của khối mã (xem Ngữ cảnh, phạm vi và vòng đời của biến) nơi câu lệnh đó nằm. Hãy nhớ rằng các khối tạo thành phạm vi cục bộ của các đối tượng (biến, mảng). Trong phần đầu của cuốn sách, chúng ta đã gặp điều này khi mô tả hàm chào hỏi.
Ngoài phạm vi cục bộ, luôn có một phạm vi toàn cục, trong đó bạn cũng có thể sử dụng các câu lệnh khai báo để tạo các thành phần có thể truy cập từ bất kỳ đâu trong chương trình.
Nếu không có bộ sửa đổi static
trong câu lệnh khai báo và nó nằm trong một khối cục bộ nào đó, thì thành phần tương ứng được tạo và khởi tạo tại thời điểm câu lệnh được thực thi (nói chính xác, bộ nhớ cho tất cả biến cục bộ trong hàm được cấp phát ngay khi vào hàm để đảm bảo hiệu quả, nhưng chúng chưa được hình thành tại thời điểm đó).
Ví dụ, khai báo biến i
sau đây ở đầu hàm OnStart
đảm bảo rằng biến này sẽ được tạo với giá trị ban đầu đã chỉ định (0) ngay khi hàm nhận được quyền điều khiển (tức là terminal sẽ gọi nó vì đây là hàm chính của script):
void OnStart()
{
int i = 0;
Print(i);
// error: 'j' - undeclared identifier
// Print(j);
int j = 1;
}
2
3
4
5
6
7
8
9
Nhờ khai báo trong câu lệnh đầu tiên, biến i
được biết đến và có sẵn trong các dòng tiếp theo của hàm, đặc biệt trong dòng thứ hai với lời gọi hàm Print
, hiển thị nội dung của biến trong nhật ký.
Biến j
được mô tả ở dòng cuối của hàm sẽ được tạo ngay trước khi hàm kết thúc (điều này tất nhiên là vô nghĩa, nhưng rõ ràng). Do đó, biến này không được biết đến trong tất cả các dòng trước đó của hàm. Một nỗ lực xuất j
ra nhật ký bằng lời gọi Print
đã bị chú thích sẽ dẫn đến lỗi biên dịch "undeclared identifier" (định danh chưa được khai báo).
Các thành phần được khai báo theo cách này (bên trong các khối mã và không có bộ sửa đổi static
) được gọi là tự động, vì chương trình tự động cấp phát bộ nhớ cho chúng khi vào khối và hủy chúng khi thoát khỏi khối (trong trường hợp của chúng ta, sau khi thoát hàm). Do đó, vùng bộ nhớ nơi điều này xảy ra được gọi là ngăn xếp ("vào sau, ra trước").
Các thành phần tự động được tạo theo thứ tự thực thi các câu lệnh khai báo (đầu tiên là i
, sau đó là j
). Việc hủy được thực hiện theo thứ tự ngược lại (đầu tiên là j
, sau đó là i
).
Nếu một biến được khai báo mà không khởi tạo và bắt đầu được sử dụng trong các câu lệnh tiếp theo (ví dụ, ở bên phải dấu '=') mà không ghi giá trị có ý nghĩa vào nó trước, trình biên dịch sẽ đưa ra cảnh báo: "possible use of uninitialized variable" (có thể sử dụng biến chưa được khởi tạo):
void OnStart()
{
int i, p;
i = p; // warning: possible use of uninitialized variable 'p'
}
2
3
4
5
Nếu một câu lệnh khai báo có bộ sửa đổi static, thành phần tương ứng chỉ được tạo một lần khi câu lệnh được thực thi lần đầu tiên và vẫn tồn tại trong bộ nhớ, bất kể việc thoát ra và có thể vào lại hoặc thoát ra khỏi cùng một khối mã. Tất cả các thành phần static như vậy chỉ bị xóa khi chương trình được gỡ bỏ.
Mặc dù có vòng đời tăng lên, phạm vi của các biến như vậy vẫn bị giới hạn trong ngữ cảnh cục bộ nơi chúng được định nghĩa, và chỉ có thể truy cập từ các câu lệnh sau đó (nằm dưới trong mã).
Ngược lại, các câu lệnh khai báo trong ngữ cảnh toàn cục tạo ra các thành phần của chúng theo thứ tự xuất hiện trong mã nguồn, ngay sau khi chương trình được tải (trước khi bất kỳ hàm khởi động tiêu chuẩn nào được gọi, chẳng hạn như OnStart
cho script). Các đối tượng toàn cục được xóa theo thứ tự ngược lại khi chương trình được gỡ bỏ.
Để minh họa những điều đã đề cập, hãy tạo một ví dụ "khéo léo" hơn (StmtDeclaration.mq5
). Nhớ lại các kỹ năng đã học ở phần đầu, ngoài OnStart
, chúng ta sẽ viết một hàm đơn giản Init
, sẽ được sử dụng trong các biểu thức khởi tạo biến và ghi lại chuỗi các lời gọi:
int Init(const int v)
{
Print("Init: ", v);
return v;
}
2
3
4
5
Hàm Init
nhận một tham số duy nhất v
kiểu int
, giá trị của nó được trả về mã gọi (câu lệnh return).
Điều này cho phép sử dụng nó như một bộ bao để đặt giá trị ban đầu của biến, ví dụ, cho hai biến toàn cục:
int k = Init(-1);
int m = Init(-2);
2
Giá trị của tham số được truyền vào các biến k
và m
bằng cách gọi hàm và trả về từ nó. Tuy nhiên, bên trong Init
, chúng ta còn xuất giá trị bằng Print
, và do đó có thể theo dõi cách các biến được tạo ra.
Lưu ý rằng chúng ta không thể sử dụng hàm Init
trong việc khởi tạo các biến toàn cục phía trên định nghĩa của nó. Nếu chúng ta cố gắng di chuyển khai báo biến k
lên trên khai báo Init
, chúng ta sẽ nhận được lỗi "'Init' is an unknown identifier" (định danh chưa biết). Hạn chế này chỉ áp dụng cho việc khởi tạo các biến toàn cục, vì các hàm cũng được định nghĩa toàn cục, và trình biên dịch xây dựng danh sách các định danh như vậy trong một lần. Trong tất cả các trường hợp khác, thứ tự định nghĩa hàm trong mã không quan trọng, vì trình biên dịch trước tiên đăng ký tất cả chúng trong danh sách nội bộ, rồi sau đó liên kết相互 các lời gọi của chúng từ các khối. Cụ thể, bạn có thể di chuyển toàn bộ hàm Init
và khai báo các biến toàn cục k
và m
xuống dưới hàm OnStart
- điều đó sẽ không làm hỏng gì.
Bên trong hàm OnStart
, chúng ta sẽ mô tả thêm một số biến bằng Init
: biến cục bộ i
và j
, cũng như biến static
n
. Để đơn giản, tất cả các biến được gán giá trị duy nhất để có thể phân biệt chúng:
void OnStart()
{
Print(k);
int i = Init(1);
Print(i);
// error: 'n' - undeclared identifier
// Print(n);
static int n = Init(0);
// error: 'j' - undeclared identifier
// Print(j);
int j = Init(2);
Print(j);
Print(n);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Các chú thích ở đây cho thấy những nỗ lực sai lầm khi gọi các biến liên quan trước khi chúng được định nghĩa.
Chạy script và nhận được nhật ký sau:
Init: -1
Init: -2
-1
Init: 1
1
Init: 0
Init: 2
2
0
2
3
4
5
6
7
8
9
Như chúng ta thấy, các biến toàn cục được khởi tạo trước khi hàm OnStart
được gọi, và chính xác theo thứ tự xuất hiện trong mã. Các biến bên trong được tạo theo cùng thứ tự mà các câu lệnh khai báo của chúng được viết.
Nếu một biến được định nghĩa nhưng không được sử dụng ở bất kỳ đâu, trình biên dịch sẽ đưa ra cảnh báo "variable 'name' not used" (biến 'tên' không được sử dụng). Đây là dấu hiệu của một lỗi tiềm ẩn của lập trình viên.
Nhìn trước, hãy nói rằng với sự trợ giúp của các câu lệnh khai báo/định nghĩa, không chỉ các thành phần dữ liệu (biến, mảng) hoặc hàm, mà còn các kiểu do người dùng định nghĩa mới (cấu trúc, lớp, mẫu, không gian tên) chưa được chúng ta biết đến có thể được đưa vào chương trình. Những câu lệnh như vậy chỉ có thể được thực hiện ở cấp toàn cục, tức là bên ngoài tất cả các hàm.
Cũng không thể định nghĩa một hàm bên trong một hàm. Mã sau sẽ không biên dịch được:
void OnStart()
{
int Init(const int v)
{
Print("Init: ", v);
return v;
}
int i = 0;
}
2
3
4
5
6
7
8
9
Trình biên dịch sẽ tạo lỗi: "function declarations are allowed on global, namespace, or class scope only" (khai báo hàm chỉ được phép ở phạm vi toàn cục, không gian tên hoặc lớp).