Số dấu phẩy động
Chúng ta sử dụng các số có dấu thập phân, hoặc số thực, trong cuộc sống hàng ngày cũng thường xuyên như số nguyên. Bản thân tên 'thực' chỉ ra rằng khi sử dụng các số như vậy, bạn có thể biểu thị một thứ hữu hình từ thế giới thực, chẳng hạn như cân nặng, chiều dài, nhiệt độ cơ thể, tức là mọi thứ có thể đo được bằng một lượng đơn vị không phải số nguyên, nhưng "nhiều hơn một chút".
INFO
Chúng ta cũng thường sử dụng số thực trong giao dịch. Ví dụ, chúng được sử dụng để thể hiện giá ký hiệu hoặc khối lượng trong lệnh giao dịch (thường cho phép các phần lẻ của một lô đầy đủ).
Có 2 kiểu dữ liệu thực được cung cấp trong MQL5: float
cho độ chính xác thông thường và double
cho độ chính xác gấp đôi.
Trong mã nguồn, các giá trị hằng số của kiểu float
và double
thường được ghi lại dưới dạng một số nguyên và một phần phân số (mỗi phần là một chuỗi các chữ số), được phân tách bằng ký tự .
, chẳng hạn như 1.23
hoặc -789.01
. Không thể có số nguyên hoặc phân số (nhưng không phải cả hai cùng một lúc), nhưng dấu chấm là bắt buộc. Ví dụ, .123
có nghĩa là 0.123
, trong khi 123.
có nghĩa là 123.0
. Chỉ cần 123
sẽ tạo ra một hằng số kiểu số nguyên.
Tuy nhiên, có một dạng khác để ghi lại hằng số thực, dạng mũ. Trong đó, phần nguyên và phần phân số được theo sau bởi E
hoặc e
(không phân biệt chữ hoa chữ thường) và một số nguyên biểu diễn lũy thừa, trong đó 10 phải được nâng lên để có được một hệ số bổ sung. Ví dụ, các biểu diễn sau đây hiển thị cùng một số, 0,57
, ở dạng mũ:
.0057e2
0.057e1
.057e1
57e-2
2
3
4
Khi ghi các hằng số thực, các hằng số sau được định nghĩa theo mặc định là kiểu double
(chúng chiếm 8 byte). Để đặt kiểu float
, hậu tố F
(hoặc f
) phải được thêm vào hằng số bên phải.
Các kiểu float
và double
khác nhau về kích thước, phạm vi giá trị và độ chính xác biểu diễn số. Tất cả những điều này được thể hiện trong bảng bên dưới.
Kiểu | Kích thước (bytes) | Tối thiểu | Tối đa | Độ chính xác (thứ tự chữ số) |
---|---|---|---|---|
float | 4 | ±1,18 * 10-38 | ±3,4 * 10 38 | 6-9, thường là 7 |
double | 8 | ±2,23 * 10-308 | ±1,80 * 10 308 | 15-18, thường là 16 |
Phạm vi giá trị được hiển thị cho chúng theo các điều khoản tuyệt đối: Tối thiểu và tối đa xác định biên độ của các giá trị được phép trong cả vùng dương và vùng âm. Tương tự như các kiểu số nguyên, có các hằng số được đặt tên nhúng cho các giá trị giới hạn này: FLT_MIN
, FLT_MAX
, DBL_MIN
, DBL_MAX
.
Xin lưu ý rằng số thực luôn có dấu, nghĩa là không có số tương tự không dấu nào cho chúng.
Độ chính xác có nghĩa là số lượng chữ số có nghĩa (chữ số thập phân) mà số thực của loại liên quan có thể lưu trữ mà không bị biến dạng.
Thật vậy, số lượng các kiểu số thực không chính xác bằng số lượng các kiểu số nguyên. Đây là cái giá phải trả cho tính phổ quát của chúng và phạm vi giá trị tiềm năng rộng hơn nhiều. Ví dụ, nếu một số nguyên 4 byte không dấu (uint
) có giá trị cao nhất là 4294967295
, tức là khoảng 4 triệu, hoặc 4,29*10^9
, thì số thực 4 byte (float
) có giá trị là 3,4*10^38
, cao hơn 29 bậc độ lớn. Đối với các kiểu 8 byte, sự khác biệt thậm chí còn dễ nhận thấy hơn: ulong
có thể chứa 18446744073709551615
(18,44*10^18
, hoặc ~18
nghìn tỷ), trong khi double có thể
chứa 1,80*10^308
, tức là cao hơn 289 bậc độ lớn. Việc chèn cung cấp thêm chi tiết về độ chính xác.
INFO
Mantissa và Exponent
Biểu diễn nội bộ của các số thực trong bộ nhớ (theo các byte được phân bổ cho chúng) khá phức tạp. Bit bậc cao hơn được sử dụng làm dấu hiệu của dấu âm (chúng ta cũng đã thấy điều đó trong các kiểu số nguyên). Tất cả các bit khác được chia thành hai nhóm. Nhóm lớn hơn chứa mantissa
của số, tức là các chữ số có nghĩa (ý chúng ta là các chữ số nhị phân, tức là các bit). Nhóm nhỏ hơn lưu trữ lũy thừa (mũ), trong đó 10 phải được nâng lên để có được số được lưu trữ khi nhân nó với mantissa
. Đặc biệt, đối với kiểu float
, mantissa
có kích thước 24 bit (FLT_MANT_DIG
), trong khi đối với double
là 53 (DBL_MANT_DIG
). Về mặt các chữ số thập phân thông thường (chữ số), chúng ta sẽ có được độ chính xác giống như đã được hiển thị trong bảng trên: 6 (FLT_DIG
) là số lượng chữ số có nghĩa thấp nhất đối với float , trong khi 15 (DBL_DIG
) là số lượng chữ số có nghĩa đối với double . Tuy nhiên, tùy thuộc vào số cụ thể, nó có thể có các tổ hợp bit "may mắn", tương ứng với số lượng chữ số thập phân lớn hơn. Kích thước của các tham số lần lượt là 8 và 11 bit cho float
và double
.
Do số mũ, các số thực có phạm vi giá trị lớn hơn nhiều. Đồng thời, với sự gia tăng của số mũ, "trọng số riêng" của chữ số bậc thấp của mantissa
cũng tăng theo. Điều này có nghĩa là hai số thực lân cận có thể được biểu diễn trong bộ nhớ máy tính là khác nhau đáng kể. Ví dụ, đối với số 1.0, "trọng số riêng" của bit bậc thấp là 1.192092896e—07
(FLT_EPSILON
) trong trường hợp float
và 2.2204460492503131e-016
(DBL_EPSILON
) trong trường hợp double
. Nói cách khác, 1.0 không thể phân biệt được với bất kỳ số nào gần nó nếu số đó nhỏ hơn 1.192092896e—07
. Điều này có vẻ không quan trọng lắm hoặc "không phải là vấn đề lớn", nhưng vùng không chắc chắn này sẽ lớn hơn đối với các số lớn hơn. Nếu bạn lưu trữ trong float
một số khoảng 1 tỷ (1*10^9
), 2 chữ số cuối sẽ ngừng được lưu trữ an toàn hoặc khôi phục từ bộ nhớ (xem mã bên dưới). Tuy nhiên, về cơ bản, vấn đề không phải là giá trị tuyệt đối của một số, mà là số lượng chữ số tối đa trong đó, có thể được nhớ lại mà không bị mất. "Tốt" như vậy, chúng ta có thể thử đưa một số được biểu diễn là 1234.56789
(về mặt cấu trúc rất giống với giá của một công cụ tài chính) vào float
; và hai chữ số cuối của nó sẽ "float" do thiếu độ chính xác trong biểu diễn bên trong của chúng.
Đối với double
, một tình huống tương tự sẽ bắt đầu hiển thị đối với các số lớn hơn nhiều (hoặc đối với số lượng chữ số có nghĩa lớn hơn nhiều), nhưng vẫn có thể xảy ra và thường xảy ra trong thực tế. Bạn nên cân nhắc điều này khi vận hành các số thực rất lớn hoặc rất nhỏ và viết chương trình của mình với các kiểm tra bổ sung để phát hiện khả năng mất độ chính xác. Đặc biệt, bạn nên so sánh một số thực với số không theo một cách đặc biệt. Chúng ta sẽ giải quyết vấn đề này trong phần về toán tử so sánh. Đối với người đọc cẩn thận, có thể thấy rằng kích thước của mantissa và số mũ ở trên được chỉ định sai. Hãy giải thích ví dụ về float
. Nó được lưu trữ trong ô nhớ có kích thước 4 byte, tức là sử dụng 32 bit. Đồng thời, kích thước của mantissa (24) và số mũ (8) đã bằng 32. Vậy thì bit có dấu ở đâu? Vấn đề là các chuyên gia IT đã sắp xếp để lưu trữ mantissa ở dạng 'chuẩn hóa'. Sẽ dễ hiểu hơn về nó nếu chúng ta xem xét dạng mũ của việc ghi lại một số thập phân bình thường trước. Giả sử số 123.0
có thể được biểu diễn là 1.23E2
, 12.3E1
hoặc 0.123E3
. Một ký hiệu được coi là dạng chuẩn hóa, trong đó chỉ có một chữ số có nghĩa (tức là không phải số 0) được đặt trước dấu phẩy. Đối với số này, đó là 1.23E2
. Theo định nghĩa, các chữ số từ 1 đến 9 được coi là các chữ số có nghĩa trong ký hiệu thập phân. Bây giờ chúng ta sẽ chuyển sang ký hiệu nhị phân một cách trôi chảy. Chỉ có một chữ số có nghĩa trong đó, 1. Có vẻ như dạng chuẩn hóa trong ký hiệu nhị phân luôn bắt đầu bằng 1 và có thể bỏ qua (để không tốn bộ nhớ). Theo cách này, một bit có thể được lưu trong mantissa. Trên thực tế, nó chứa 23 bit (một đơn vị bậc cao hơn nữa được ngầm định và tự động thêm vào khi tái tạo số và truy xuất nó từ bộ nhớ). Giảm mantissa đi 1 bit sẽ tạo chỗ cho bit có dấu.
Chủ yếu, khi cần sử dụng kiểu dấu phẩy động, chúng ta chọn double
vì đây là kiểu chính xác hơn. Kiểu float
chỉ được sử dụng để tiết kiệm bộ nhớ, chẳng hạn như khi làm việc với các mảng dữ liệu rất lớn.
Một số ví dụ về việc sử dụng hằng số của kiểu thực được hiển thị trong tập lệnh MQL5/Scripts/MQL5Book/p2/TypeFloat.mq5
.
void OnStart()
{
double a0 = 123; // ok, a0 = 123.0
double a1 = 123.0; // ok, a1 = 123.0
double a2 = 0.123E3; // ok, a2 = 123.0
double a3 = 12300E-2; // ok, a3 = 123.0
double b = -.75; // ok, b = -0.75
double q = LONG_MAX; // cảnh báo, bị cắt bớt, q = 9.223372036854776e+18
// LONG_MAX = 9223372036854775807
double d = 9007199254740992; // ok, maximal stable long in double
double z = 0.12345678901234567890123456789; // ok, nhưng bị cắt bớt
// to 16 digits: z = 0.1234567890123457
double y1 = 1234.56789; // ok, y1 = 1234.56789
double y2 = 1234.56789f; // mất độ chính xác, y2 = 1234.56787109375
float m = 1000000000.0; // ok, được lưu trữ như vậy
float n = 999999975.0; // cảnh báo, bị cắt bớt, n = 1000000000.0
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Các biến a0 , a1 , a2 và a3 chứa các số giống nhau (123,0) được viết theo các phương pháp khác nhau.
Trong hằng số của biến b, số không không đáng kể được bỏ qua trước dấu chấm. Hơn nữa, đây là minh họa về việc ghi lại số âm bằng cách sử dụng dấu trừ, '-'.
Có một nỗ lực được thực hiện để lưu trữ số nguyên lớn nhất trong biến q. Tại đây, trình biên dịch đưa ra cảnh báo, vì double
không thể biểu diễn chính xác LONG_MAX
: Thay vì 9223372036854775807
, sẽ có 9223372036854776000
. Rõ ràng là, mặc dù phạm vi của các giá trị double
vượt xa phạm vi của các số nguyên, nhưng điều này đạt được là do mất các chữ số bậc thấp.
Để so sánh, số nguyên lớn nhất mà kiểu double
có thể lưu trữ mà không bị biến dạng được đưa ra dưới dạng giá trị của biến d. Trong chuỗi số nguyên, nó sẽ được theo sau bởi các lần bỏ qua ngẫu nhiên, nếu chúng ta sử dụng double
cho chúng.
Biến z một lần nữa nhắc nhở chúng ta về giới hạn về số lượng chữ số có nghĩa tối đa (16) – một hằng số dài hơn sẽ bị cắt bớt.
Các biến y1 và y2, trong đó cùng một số được ghi ở các định dạng khác nhau (double
và float
), cho phép thấy được sự mất độ chính xác do chuyển đổi sang float .
Trên thực tế, các biến m và n sẽ bằng nhau, vì 999999975.0 được lưu trữ gần đúng trong biểu diễn bên trong và chuyển thành 1000000000.0.
Các kiểu số thường được sử dụng để tính toán bằng công thức; một tập hợp rộng các phép toán được định nghĩa cho chúng (xem Biểu thức ).
Đôi khi, các phép tính có thể dẫn đến kết quả không chính xác, tức là chúng không thể được biểu diễn dưới dạng số. Ví dụ, căn bậc hai của một số âm hoặc logarit của số không không thể được xác định. Trong những trường hợp như vậy, các kiểu số thực có thể lưu trữ một giá trị đặc biệt có tên là NaN
(Not A Number). Trên thực tế, có một số kiểu giá trị khác nhau như vậy cho phép, ví dụ, phân biệt giữa cộng vô cực và trừ vô cực. MQL5 cung cấp một hàm đặc biệt, MathIsValidNumber
, để kiểm tra xem giá trị double có phải là số hay một trong các giá trị NaN không.