Số nguyên rất tốt để sử dụng, nhưng đôi khi chúng ta cần lưu trữ số rất lớn hoặc số có thành phần phân số. Biến kiểu dấu phẩy động là biến có thể chứa một số thực, chẳng hạn như 4320.0, -3.33 hoặc 0.01226. Có nghĩa là biến đó có thể chứa một số có phần thập phân.

Có ba kiểu dữ liệu dấu phẩy động khác nhau: float, double và long double. Cũng như các số nguyên, C ++ không xác định kích thước thực tế của các loại này (nhưng nó đảm bảo kích thước tối thiểu). Trên các kiến ​​trúc máy hiện đại, biểu diễn dấu phẩy động hầu như luôn tuân theo định dạng nhị phân IEEE 754. Trong định dạng này, float là 4 byte, double là 8 và double double có thể tương đương với double (8 byte), 80 bit (thường được đệm thành 12 byte) hoặc 16 byte.

Các kiểu dữ liệu dấu phẩy động luôn có ký hiệu (có dấu) (có thể giữ các giá trị dương và âm).

CategoryTypeMinimum SizeTypical Size
floating pointfloat4 bytes4 bytes
double8 bytes8 bytes
long double8 bytes8, 12, or 16 bytes

Dưới đây là một số định nghĩa về số dấu phẩy động:

float fValue;
double dValue;
long double ldValue;

Khi sử dụng float literals(literals là gì?), luôn luôn bao gồm ít nhất một chữ số thập phân (ngay cả khi số thập phân bằng 0). Điều này giúp trình biên dịch hiểu rằng số này là số kiểu dấu phẩy động chứ không phải là số nguyên.

int x{5}; // 5 means integer
double y{5.0}; // 5.0 is a floating point literal (no suffix means double type by default)
float z{5.0f}; // 5.0 is a floating point literal, f suffix means float type

Lưu ý rằng theo mặc định, dấu phẩy động mặc định sẽ là kiểu double. Một hậu tố f được sử dụng để biểu thị cho kiểu float.

1. In số dấu phẩy động(floating point)

Bây giờ hãy xem xét chương trình đơn giản này:

#include <iostream>
 
int main()
{
	std::cout << 5.0 << '\n';
	std::cout << 6.7f << '\n';
	std::cout << 9876543.21 << '\n';
}

Kết quả của chương trình tưởng chừng đơn giản này có thể làm bạn ngạc nhiên:

5
6.7
9.87654e+06

Trong trường hợp đầu tiên, std :: cout đã in 5, mặc dù chúng ta đã gõ là 5.0. Theo mặc định, std :: cout sẽ không in phần phân số của số nếu phần phân số bằng 0.

Trong trường hợp thứ hai, count in ra như chúng ta mong đợi.

Trong trường hợp thứ ba, nó đã in số theo ký hiệu khoa học (vì số này quá lớn).

2. Phạm vi, kích thước của số floating(Số dấu phẩy động)

Giả sử ta có IEEE 754:

SizePhạm viĐộ chính xác
4 bytes±1.18 x 10-38 to ±3.4 x 1038Từ 6-9 chữ số, thông thường là 7
8 bytes±2.23 x 10-308 to ±1.80 x 10308Từ 15-18 chữ số, thông thường thì 16
80-bits (thông thường sử dụng 12 or 16 bytes)±3.36 x 10-4932 to ±1.18 x 104932Từ 18-21 chữ số
16 bytes±3.36 x 10-4932 to ±1.18 x 104932Từ 33-36 chữ số

Loại dấu phẩy động 80 bit có một chút bất thường. Trên các bộ xử lý hiện đại, nó thường được triển khai bằng cách sử dụng 12 hoặc 16 byte (đây là kích thước tự nhiên để xử lý).

Có vẻ hơi lạ khi 80 bit có cùng phạm vi với số dấu phẩy động 16 byte. Điều này là do chúng có cùng số bit dành riêng cho số mũ – tuy nhiên, số 16 byte có thể lưu trữ các chữ số nhiều hơn

3. Độ chính xác của dấu phẩy động

Xét phân số 1/3. Số thập phân của số này là 0,3333333333333 với 3 đi ra vô cùng. Nếu bạn đang viết số này trên một tờ giấy, đôi lúc cánh tay của bạn sẽ mệt mỏi và cuối cùng bạn sẽ dừng viết. Và số bạn còn lại sẽ ở gần 0,333333333. Nhưng không chính xác.

Trên máy tính, số lượng chiều dài vô hạn sẽ cần bộ nhớ vô hạn để lưu trữ và thông thường chúng ta chỉ có 4 hoặc 8 byte. Bộ nhớ hạn chế này có nghĩa là số dấu phẩy động chỉ có thể lưu trữ một số chữ số có nghĩa nhất định – và bất kỳ chữ số có nghĩa bổ sung nào cũng bị mất. Số thực sự được lưu trữ sẽ gần với số mong muốn, nhưng không chính xác.

Độ chính xác của một số dấu phẩy động xác định có bao nhiêu chữ số có thể biểu thị mà không mất thông tin.

Khi xuất các số dấu phẩy động, std :: cout có độ chính xác mặc định là 6 – nghĩa là, nó giả sử tất cả các biến dấu phẩy động chỉ in ra được 6 chữ số (độ chính xác tối thiểu của một dấu phẩy) và do đó nó sẽ cắt bất cứ thứ gì sau đó .

Chương trình sau đây hiển thị std :: cout cắt ngắn thành 6 chữ số:

#include <iostream>
int main()
{
    std::cout << 9.87654321f << '\n';
    std::cout << 987.654321f << '\n';
    std::cout << 987654.321f << '\n';
    std::cout << 9876543.21f << '\n';
    std::cout << 0.0000987654321f << '\n';
 
    return 0;
}

Chương trình này xuất ra:

9.87654
987.654
987654
9.87654e+006
9.87654e-005

Lưu ý rằng mỗi trong số này chỉ có 6 chữ số có nghĩa hay chỉ có 6 chữ số được in ra.

Cũng lưu ý rằng std :: cout sẽ chuyển sang xuất số theo ký hiệu khoa học trong một số trường hợp(số đó quá lớn). Tùy thuộc vào trình biên dịch, số mũ thường sẽ được đệm đến một số chữ số tối thiểu. Đừng sợ, 9,87654e + 006 giống như 9,87654e6. Số chữ số mũ tối thiểu được hiển thị là dành riêng cho trình biên dịch (Visual Studio sử dụng 3, một số khác sử dụng 2 theo tiêu chuẩn C99).

Số chữ số của độ chính xác của biến số dấu phẩy động phụ thuộc vào cả kích thước (số float có độ chính xác thấp hơn gấp đôi) và giá trị cụ thể được lưu trữ (một số giá trị có độ chính xác cao hơn các giá trị khác). Giá trị float có độ chính xác từ 6 đến 9 chữ số, với hầu hết các giá trị float có ít nhất 7 chữ số có nghĩa. Giá trị double có độ chính xác từ 15 đến 18 chữ số, với hầu hết các giá trị double có ít nhất 16 chữ số có nghĩa. Long double có độ chính xác tối thiểu là 15, 18 hoặc 33 chữ số có nghĩa tùy thuộc vào số lượng byte chiếm.

Chúng ta có thể ghi đè độ chính xác mặc định mà std :: cout hiển thị bằng cách sử dụng hàm std :: setprecision () được xác định trong tiêu đề iomanip.

#include <iostream>
#include <iomanip> // for std::setprecision()
int main()
{
    std::cout << std::setprecision(16); // show 16 digits of precision
    std::cout << 3.33333333333333333333333333333333333333f <<'\n'; // f suffix means float
    std::cout << 3.33333333333333333333333333333333333333 << '\n'; // no suffix means double
    return 0;
}

Outputs:

3.333333253860474
3.333333333333334

Vì chúng ta đặt độ chính xác thành 16 chữ số, mỗi số ở trên được in 16 chữ số. Nhưng, như bạn có thể thấy, những con số chắc chắn có thể chính xác đến 16 chữ số!

Các vấn đề chính xác không chỉ có tác động đến các số phân số, chúng còn tác động đến bất kỳ số nào có quá nhiều chữ số có nghĩa. Hãy xem xét một số lượng lớn:

#include <iostream>
#include <iomanip> // for std::setprecision()
 
int main()
{
    float f { 123456789.0f }; // f has 10 significant digits
    std::cout << std::setprecision(9); // to show 9 digits in f
    std::cout << f << '\n';
    return 0;
}

Output:

123456792

123456792 lớn hơn 123456789. Giá trị 123456789.0 có 10 chữ số có nghĩa, nhưng giá trị float thường có 7 chữ số chính xác (và kết quả của 123456792 chỉ chính xác đến 7 chữ số có nghĩa). Chúng ta đã mất độ chính xác của một số! Khi độ chính xác bị mất vì một số chỉ có thể được lưu trữ được như vậy thôi thì đây được gọi là lỗi làm tròn.

Do đó, người ta phải cẩn thận khi sử dụng các số dấu phẩy động đòi hỏi độ chính xác cao.

Thực hành tốt nhất ở đây là

Ưu tiên sử dụng double so với float khi cần không gian ở mức cao, vì sự thiếu chính xác trong float thường sẽ dẫn đến kết quả không chính xác.

4. Lỗi làm tròn trong các phép so sánh dấu phẩy động

Số dấu phẩy động rất khó để làm việc do sự khác biệt không rõ ràng giữa số nhị phân (cách lưu trữ dữ liệu) và số thập phân (cách chúng ta thấy). Xét phân số 1/10. Trong phần thập phân, số này dễ dàng được biểu thị bằng 0,1 và chúng ta thường nghĩ 0,1 là một số dễ biểu thị với 1 chữ số có nghĩa. Tuy nhiên, trong hệ nhị phân, 0,1 được biểu thị bằng chuỗi vô hạn: 0,00011001100110011 Bằng cách này, khi chúng ta gán 0,1 cho một số dấu phẩy động, chúng ta sẽ gặp vấn đề về độ chính xác.

Bạn có thể thấy tác dụng của việc này trong chương trình sau:

#include <iostream>
#include <iomanip> // for std::setprecision()
 
int main()
{
    double d{0.1};
    std::cout << d << '\n'; // use default cout precision of 6
    std::cout << std::setprecision(17);
    std::cout << d << '\n';
    return 0;
}

Kết quả:

0.1
0.10000000000000001

Trên dòng trên cùng, std :: cout in 0.1, như chúng ta mong đợi.

Ở dòng dưới cùng, nơi chúng ta có std :: cout cho chúng ta thấy 17 chữ số chính xác, chúng ta thấy rằng d thực sự không hoàn toàn 0,1! Điều này là do double phải cắt ngắn xấp xỉ do bộ nhớ hạn chế của nó. Kết quả là một số chính xác đến 16 chữ số, nhưng số này không chính xác 0,1. Lỗi làm tròn có thể làm cho một số nhỏ hơn hoặc lớn hơn một chút, tùy thuộc vào nơi xảy ra cắt ngắn.

Lỗi làm tròn có thể có hậu quả không mong muốn:

#include <iostream>
#include <iomanip> // for std::setprecision()
 
int main()
{
    std::cout << std::setprecision(17);
 
    double d1(1.0);
    std::cout << d1 << std::endl;
	
    double d2(0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1); // should equal 1.0
    std::cout << d2 << std::endl;
}
1
0.99999999999999989

Mặc dù chúng ta có thể mong đợi rằng d1 và d2 bằng nhau, nhưng chúng ta thấy rằng chúng không như vậy. Nếu chúng ta so sánh d1 và d2 trong một chương trình, chương trình có thể sẽ không hoạt động như mong đợi. Do các số dấu phẩy động có xu hướng không chính xác, nên việc so sánh các số dấu phẩy động nói chung là có vấn đề – chúng ta thảo luận về chủ đề này nhiều hơn (và các giải pháp) trong tương lại – Toán tử quan hệ và so sánh số dấu phẩy động.

Một lưu ý cuối cùng về lỗi làm tròn: các phép toán (như cộng và nhân) có xu hướng làm cho lỗi làm tròn tăng lên. Vì vậy, mặc dù 0,1 có lỗi làm tròn ở chữ số có nghĩa thứ 17, nhưng khi chúng ta thêm 0,1 mười lần, lỗi làm tròn đã len lỏi vào chữ số có nghĩa thứ 16. Các hoạt động liên tục sẽ khiến lỗi này ngày càng trở nên nghiêm trọng hơn.

Một hệ quả của quy tắc này là: không bao giờ sử dụng số dấu phẩy động cho dữ liệu tài chính hoặc tiền tệ.

5. NaN and Inf

Có hai loại số dấu phẩy động đặc biệt. Đầu tiên là Inf, đại diện cho vô cùng. Inf có thể tích cực hoặc tiêu cực. Thứ hai là NaN, viết tắt của cụm từ “Not a Number”. Có một số loại NaN khác nhau.

Ở đây, một chương trình hiển thị cả ba:

#include <iostream>
 
int main()
{
    double zero {0.0};
    double posinf { 5.0 / zero }; // positive infinity
    std::cout << posinf << std::endl;
 
    double neginf { -5.0 / zero }; // negative infinity
    std::cout << neginf << std::endl;
 
    double nan { zero / zero }; // not a number (mathematically invalid)
    std::cout << nan << std::endl;
 
    return 0;
}

Và kết quả khi sử dụng Visual Studio 2008 trên Windows:

1.#INF
-1.#INF
1.#IND

INF là viết tắt của infinity(vô cùng), và IND là viết tắt của indeterminate(không xác định). Lưu ý rằng kết quả in ra Inf và NaN tuỳ vào từng nền tảng cụ thể, vì vậy kết quả của bạn có thể thay đổi.

Tóm lại, hai điều bạn nên nhớ về số dấu phẩy động:

1) Số dấu phẩy động rất hữu ích để lưu trữ các số rất lớn hoặc rất nhỏ, bao gồm cả các số có thành phần phân số.

2) Số dấu phẩy động thường có sai số làm tròn nhưng khá nhỏ, ngay cả khi số có ít chữ số có nghĩa(ít chữ số được hiển thị). Thường thì chúng ta không chú ý tới nó vì chúng quá nhỏ và vì các số bị cắt bớt khi được in ra. Tuy nhiên, so sánh các số dấu phẩy động có thể không cho ra kết quả như mong đợi. Thực hiện các phép toán trên các giá trị này sẽ khiến các lỗi làm tròn ngày càng lớn hơn.

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!