Nếu tôi có khai báo sau:
float a = 3.0 ;
đó là một lỗi? Tôi đọc trong một cuốn sách đó 3.0
là một double
giá trị và tôi phải xác định nó là float a = 3.0f
. Có phải vậy không?
Nếu tôi có khai báo sau:
float a = 3.0 ;
đó là một lỗi? Tôi đọc trong một cuốn sách đó 3.0
là một double
giá trị và tôi phải xác định nó là float a = 3.0f
. Có phải vậy không?
;
sau.
Câu trả lời:
Nó không phải là một lỗi để khai báo float a = 3.0
: nếu bạn làm vậy, trình biên dịch sẽ chuyển đổi chữ kép 3.0 thành một float cho bạn.
Tuy nhiên, bạn nên sử dụng ký hiệu float trong các trường hợp cụ thể.
Vì lý do hiệu suất:
Cụ thể, hãy xem xét:
float foo(float x) { return x * 0.42; }
Ở đây trình biên dịch sẽ phát ra một chuyển đổi (mà bạn sẽ trả trong thời gian chạy) cho mỗi giá trị trả về. Để tránh nó, bạn nên khai báo:
float foo(float x) { return x * 0.42f; } // OK, no conversion required
Để tránh lỗi khi so sánh kết quả:
ví dụ: so sánh sau không thành công:
float x = 4.2;
if (x == 4.2)
std::cout << "oops"; // Not executed!
Chúng tôi có thể sửa nó bằng ký hiệu float:
if (x == 4.2f)
std::cout << "ok !"; // Executed!
(Lưu ý: tất nhiên, đây không phải là cách bạn nên so sánh các số thực hoặc số kép cho bằng nhau nói chung )
Để gọi đúng hàm quá tải (vì lý do tương tự):
Thí dụ:
void foo(float f) { std::cout << "\nfloat"; }
void foo(double d) { std::cout << "\ndouble"; }
int main()
{
foo(42.0); // calls double overload
foo(42.0f); // calls float overload
return 0;
}
Như đã lưu ý bởi Cyber , trong ngữ cảnh suy luận kiểu, cần phải giúp trình biên dịch suy ra float
:
Trong trường hợp auto
:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
Và tương tự, trong trường hợp loại trừ kiểu mẫu:
void foo(float f) { std::cout << "\nfloat"; }
void foo(double d) { std::cout << "\ndouble"; }
template<typename T>
void bar(T t)
{
foo(t);
}
int main()
{
bar(42.0); // Deduce double
bar(42.0f); // Deduce float
return 0;
}
42
là một số nguyên, được tự động thăng cấp float
(và nó sẽ xảy ra tại thời điểm biên dịch trong bất kỳ trình biên dịch tốt nào), vì vậy không có hình phạt về hiệu suất. Có lẽ bạn muốn nói một cái gì đó như thế 42.0
.
4.2
thành 4.2f
có thể có tác dụng phụ là đặt FE_INEXACT
cờ, tùy thuộc vào trình biên dịch và hệ thống, và một số (phải thừa nhận là một số chương trình) quan tâm đến hoạt động dấu phẩy động nào là chính xác và hoạt động nào không, và kiểm tra cờ đó . Điều này có nghĩa là chuyển đổi thời gian biên dịch hiển nhiên đơn giản sẽ thay đổi hành vi của chương trình.
float foo(float x) { return x*42.0; }
có thể được biên dịch thành một phép nhân chính xác duy nhất và đã được biên dịch bởi Clang vào lần cuối tôi thử. Tuy nhiên, float foo(float x) { return x*0.1; }
không thể được biên dịch thành một phép nhân chính xác duy nhất. Có thể hơi lạc quan trước bản vá này, nhưng sau bản vá, nó chỉ nên kết hợp convert-double_pre khít_op-convert thành single_pre khít_op khi kết quả luôn giống nhau. article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
someFloat
, thì biểu thức someFloat * 0.1
sẽ cho kết quả chính xác hơn someFloat * 0.1f
, trong khi trong nhiều trường hợp, nó rẻ hơn phép chia dấu phẩy động. Ví dụ: (float) (167772208.0f * 0.1) sẽ làm tròn chính xác thành 16777220 thay vì 16777222. Một số trình biên dịch có thể thay thế một double
phép nhân cho một phép chia dấu phẩy động, nhưng đối với những người đó thì không (nó an toàn cho nhiều người mặc dù không phải tất cả các giá trị ) phép nhân có thể là một cách tối ưu hóa hữu ích, nhưng chỉ khi được thực hiện với một phép double
đối ứng.
Trình biên dịch sẽ biến bất kỳ ký tự nào sau đây thành float, bởi vì bạn đã khai báo biến là float.
float a = 3; // converted to float
float b = 3.0; // converted to float
float c = 3.0f; // float
Điều quan trọng là nếu bạn sử dụng auto
(hoặc các phương pháp khấu trừ kiểu khác), ví dụ:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
auto
không phải là trường hợp duy nhất.
Các ký tự dấu chấm động không có hậu tố thuộc loại double , điều này được đề cập trong phần dự thảo tiêu chuẩn C ++ 2.14.4
Các ký tự nổi :
[...] Kiểu của một ký tự động là kép trừ khi được chỉ định rõ ràng bởi một hậu tố. [...]
vậy có phải lỗi khi gán 3.0
một ký tự kép cho float không ?:
float a = 3.0
Không, không phải vậy, nó sẽ được chuyển đổi, được đề cập trong phần 4.8
Chuyển đổi dấu chấm động :
Giá trị prvalue của kiểu dấu phẩy động có thể được chuyển đổi thành giá trị prvalue của kiểu dấu phẩy động khác. Nếu giá trị nguồn có thể được biểu thị chính xác trong kiểu đích, thì kết quả của chuyển đổi là biểu diễn chính xác đó. Nếu giá trị nguồn nằm giữa hai giá trị đích liền kề, thì kết quả của chuyển đổi là lựa chọn do triển khai xác định của một trong hai giá trị đó. Nếu không, hành vi là không xác định.
Chúng ta có thể đọc thêm chi tiết về hàm ý của điều này trong GotW # 67: double or nothing nói:
Điều này có nghĩa là hằng số kép có thể được chuyển đổi hoàn toàn (tức là âm thầm) thành hằng số float, ngay cả khi làm như vậy làm mất độ chính xác (tức là dữ liệu). Điều này được phép duy trì vì lý do khả năng tương thích và khả năng sử dụng của C, nhưng điều đáng lưu ý khi bạn thực hiện công việc dấu phẩy động.
Một trình biên dịch chất lượng sẽ cảnh báo bạn nếu bạn cố gắng thực hiện hành vi không xác định, cụ thể là đặt một số lượng gấp đôi vào một float nhỏ hơn giá trị tối thiểu hoặc lớn hơn giá trị tối đa mà float có thể đại diện. Một trình biên dịch thực sự tốt sẽ cung cấp một cảnh báo tùy chọn nếu bạn cố gắng làm điều gì đó có thể được xác định nhưng có thể mất thông tin, cụ thể là đặt một số lượng gấp đôi vào một float nằm giữa các giá trị tối thiểu và tối đa có thể biểu diễn bằng một float, nhưng không thể được biểu diễn chính xác như một phao.
Vì vậy, có những lưu ý đối với trường hợp chung mà bạn cần lưu ý.
Từ góc độ thực tế, trong trường hợp này, kết quả rất có thể sẽ giống nhau mặc dù về mặt kỹ thuật có sự chuyển đổi, chúng ta có thể thấy điều này bằng cách thử đoạn mã sau trên chốt thần :
#include <iostream>
float func1()
{
return 3.0; // a double literal
}
float func2()
{
return 3.0f ; // a float literal
}
int main()
{
std::cout << func1() << ":" << func2() << std::endl ;
return 0;
}
và chúng tôi thấy rằng kết quả cho func1
và func2
giống hệt nhau, sử dụng cả hai clang
và gcc
:
func1():
movss xmm0, DWORD PTR .LC0[rip]
ret
func2():
movss xmm0, DWORD PTR .LC0[rip]
ret
Như Pascal đã chỉ ra trong nhận xét này, bạn không phải lúc nào cũng có thể tin tưởng vào điều này. Việc sử dụng 0.1
và 0.1f
tương ứng làm cho hợp ngữ được tạo ra khác nhau vì việc chuyển đổi bây giờ phải được thực hiện rõ ràng. Đoạn mã sau:
float func1(float x )
{
return x*0.1; // a double literal
}
float func2(float x)
{
return x*0.1f ; // a float literal
}
kết quả trong hội đồng sau:
func1(float):
cvtss2sd %xmm0, %xmm0 # x, D.31147
mulsd .LC0(%rip), %xmm0 #, D.31147
cvtsd2ss %xmm0, %xmm0 # D.31147, D.31148
ret
func2(float):
mulss .LC2(%rip), %xmm0 #, D.31155
ret
Bất kể bạn có thể xác định xem chuyển đổi có tác động đến hiệu suất hay không, việc sử dụng đúng loại tài liệu tốt hơn ý định của bạn. Ví dụ: sử dụng một chuyển đổi rõ ràng static_cast
cũng giúp làm rõ chuyển đổi là nhằm mục đích thay vì ngẫu nhiên, điều này có thể biểu thị một lỗi hoặc lỗi tiềm ẩn.
Ghi chú
Như supercat đã chỉ ra, phép nhân với ví dụ 0.1
và 0.1f
không tương đương. Tôi chỉ định trích dẫn nhận xét vì nó quá xuất sắc và một bản tóm tắt có lẽ sẽ không công bằng:
Ví dụ: nếu f bằng 100000224 (có thể biểu diễn chính xác dưới dạng số thực), nhân nó với một phần mười sẽ mang lại kết quả làm tròn xuống 10000022, nhưng nhân với 0,1f thay vào đó sẽ mang lại kết quả làm tròn sai lên đến 10000023 Nếu mục đích là chia cho mười, thì phép nhân với hằng số kép 0,1 có thể sẽ nhanh hơn phép chia cho 10f và chính xác hơn phép nhân với 0,1f.
Quan điểm ban đầu của tôi là chứng minh một ví dụ sai được đưa ra trong một câu hỏi khác nhưng điều này chứng minh rõ ràng các vấn đề tế nhị có thể tồn tại trong các ví dụ đồ chơi.
f = f * 0.1;
và f = f * 0.1f;
làm những điều khác nhau . Ví dụ: nếu f
bằng 100000224 (có thể biểu diễn chính xác là a float
), nhân nó với một phần mười sẽ mang lại kết quả làm tròn xuống 10000022, nhưng nhân với 0,1f thay vào đó sẽ mang lại kết quả làm tròn sai lên đến 10000023. Nếu mục đích là chia cho mười, phép nhân với double
hằng số 0,1 có thể sẽ nhanh hơn phép chia cho 10f
và chính xác hơn phép nhân với 0.1f
.
Nó không phải là một lỗi mà trình biên dịch sẽ từ chối nó, nhưng nó là một lỗi theo nghĩa nó có thể không như những gì bạn muốn.
Như sách của bạn tuyên bố chính xác, 3.0
là một giá trị của loại double
. Có một chuyển đổi ngầm định từ double
thành float
, vì vậyfloat a = 3.0;
một định nghĩa hợp lệ về một biến.
Tuy nhiên, ít nhất về mặt khái niệm, điều này thực hiện một chuyển đổi không cần thiết. Tùy thuộc vào trình biên dịch, việc chuyển đổi có thể được thực hiện tại thời điểm biên dịch hoặc có thể được lưu trong thời gian chạy. Một lý do hợp lệ để tiết kiệm thời gian chạy là chuyển đổi dấu phẩy động rất khó và có thể có tác dụng phụ không mong muốn nếu giá trị không thể được biểu diễn chính xác và không phải lúc nào cũng dễ dàng xác minh xem giá trị có thể được biểu diễn chính xác hay không.
3.0f
tránh được vấn đề đó: mặc dù về mặt kỹ thuật, trình biên dịch vẫn được phép tính hằng số tại thời điểm chạy (nó luôn như vậy), ở đây, hoàn toàn không có lý do gì khiến bất kỳ trình biên dịch nào có thể làm được điều đó.
Khi bạn xác định một biến, nó sẽ được khởi tạo bằng bộ khởi tạo được cung cấp. Điều này có thể yêu cầu chuyển đổi giá trị của trình khởi tạo thành loại biến đang được khởi tạo. Đó là những gì đang xảy ra khi bạn nói float a = 3.0;
: Giá trị của trình khởi tạo được chuyển đổi thành float
và kết quả của chuyển đổi trở thành giá trị ban đầu củaa
.
Nói chung là ổn, nhưng không có hại gì khi viết 3.0f
để thể hiện rằng bạn nhận thức được những gì bạn đang làm, và đặc biệt là nếu bạn muốn viết auto a = 3.0f
.
Nếu bạn thử những cách sau:
std::cout << sizeof(3.2f) <<":" << sizeof(3.2) << std::endl;
bạn sẽ nhận được đầu ra là:
4:8
điều đó cho thấy, kích thước 3.2f được lấy bằng 4 byte trên máy 32 bit trong khi 3.2 được hiểu là giá trị kép lấy 8 byte trên máy 32 bit. Điều này sẽ cung cấp câu trả lời mà bạn đang tìm kiếm.
double
và float
là khác nhau, nó không trả lời cho dù bạn có thể khởi tạo một float
từ một đôi đen
Trình biên dịch suy ra loại phù hợp nhất từ các nghĩa đen hoặc theo nghĩa mà nó cho là phù hợp nhất. Điều đó khá mất hiệu quả so với độ chính xác, tức là sử dụng double thay vì float. Nếu nghi ngờ, hãy sử dụng dấu ngoặc nhọn để làm rõ ràng:
auto d = double{3}; // make a double
auto f = float{3}; // make a float
auto i = int{3}; // make a int
Câu chuyện trở nên thú vị hơn nếu bạn khởi tạo từ một biến khác có áp dụng quy tắc chuyển đổi kiểu: Mặc dù hợp pháp để hiểu dạng kép là một chữ, nhưng nó không thể được cấu trúc từ một int mà không thể thu hẹp:
auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double'
3.0
thành một float cho bạn. Kết quả cuối cùng là không thể phân biệt đượcfloat a = 3.0f
.