Ngôn ngữ lõi của C chỉ cho bạn + - * / và %, ngoài ra không có gì: không có toán tử lũy thừa, không có căn bậc hai, không có làm tròn. Mọi thứ vượt ra ngoài số học cơ bản đều nằm trong thư viện toán học chuẩn, được khai báo trong <math.h>.
Các hàm này đều làm việc với độ chính xác double: chúng nhận đối số double và trả về double. Đó là điều đầu tiên cần thấm, vì truyền vào một int vẫn chạy lặng lẽ (nó được chuyển kiểu), trong khi gán kết quả cho một int lại lặng lẽ bỏ mất phần thập phân.
Bắt đầu, và cờ -lm
Trên máy của bạn, biên dịch đoạn này có thể bị lỗi:
/usr/bin/ld: /tmp/ccXYZ.o: in function `main':
program.c:(.text+0x1a): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
Đây là chỗ vấp phổ biến nhất với math.h, và nó không phải lỗi trong code của bạn. Header đã cung cấp các khai báo nên trình biên dịch hài lòng; còn phần cài đặt nằm trong một thư viện riêng (libm) mà trên Linux trình liên kết không đưa vào theo mặc định. Hãy thêm -lm:
gcc program.c -o program -lm
Cờ này phải đứng sau các tệp nguồn: trình liên kết xử lý đầu vào theo thứ tự, và một thư viện liệt kê trước phần code cần đến nó sẽ không giải quyết được gì. Trên macOS và với MinGW trên Windows, các hàm toán học đã nằm sẵn trong thư viện C chuẩn, nên không cần -lm (dù trên macOS thêm vào cũng không sao).
Lũy thừa và căn
sqrt(x) với số âm trả về NaN chứ không làm chương trình crash; hãy kiểm tra trước nếu đối số có thể âm. hypot đáng biết vì nó tính cùng giá trị với sqrt(x*x + y*y) mà không bị tràn số khi x hoặc y rất lớn.
Với lũy thừa nguyên nhỏ, phép nhân thường vừa nhanh hơn vừa chính xác: x * x tốt hơn pow(x, 2.0), vốn đi qua logarit và có thể trả về 8.999999999999998 khi bạn mong đợi 9.
Làm tròn: floor, ceil, round, trunc
Bốn hàm, bốn đáp án khác nhau với số âm. Bảng đó là phần đáng học thuộc:
floor(x): số nguyên lớn nhất không vượt quáx. Luôn đi về phía âm vô cực:floor(-2.3)là-3.ceil(x): số nguyên nhỏ nhất không nhỏ hơnx. Luôn đi về phía dương vô cực:ceil(-2.3)là-2.round(x): số nguyên gần nhất, giá trị nửa thì làm tròn ra xa số 0:round(2.5)là3,round(-2.5)là-3.trunc(x): cắt bỏ phần thập phân, đi về phía số 0:trunc(-2.7)là-2.
Cả bốn hàm đều trả về double. Ép kiểu sang int khi bạn cần số nguyên là được, nhưng lưu ý bản thân (int)x đã là phép cắt bỏ phần thập phân, nên (int)(x + 0.5) là cách "làm tròn" kiểu cũ, và nó sai với số âm. Hãy dùng round().
fabs và fmod
fabs là giá trị tuyệt đối cho số thực. Dùng nhầm abs là lỗi kinh điển, vì abs nhận int và việc chuyển kiểu diễn ra lặng lẽ:
% hoàn toàn không dùng được với double: đó là toán tử số nguyên và trình biên dịch sẽ từ chối. fmod(a, b) là phiên bản cho số thực, và nó giữ dấu của a.
fabs cũng là cách đúng để so sánh hai số double, vì == trên số thực là một cái bẫy:
0.1 và 0.2 không biểu diễn chính xác được ở hệ nhị phân, nên tổng của chúng lệch khỏi 0.3 một chút xíu. Hãy so sánh với một sai số cho phép, đừng bao giờ dùng ==.
Lượng giác
Các hàm lượng giác làm việc với radian, không phải độ: đây là một nguồn đáp án sai khác rất đáng tin cậy.
Bộ đầy đủ gồm: sin, cos, tan, các hàm ngược asin, acos, atan, hàm hai đối số atan2(y, x), và nhóm hàm hyperbolic sinh, cosh, tanh. Khi tính góc, hãy ưu tiên atan2(y, x) thay vì atan(y / x): nó xử lý được x == 0 và xác định đúng góc phần tư.
math.h định nghĩa M_PI trên phần lớn hệ thống, nhưng đó là phần mở rộng POSIX chứ không phải C chuẩn, nên có thể thiếu khi dùng -std=c17. Tự định nghĩa hằng số của bạn, như ở trên, là cách khả chuyển.
Logarit và hàm mũ
Để ý cái bẫy đặt tên kế thừa từ toán học: log() là logarit tự nhiên (cơ số e), không phải cơ số 10. Cơ số 10 là log10().
log(0.0) trả về âm vô cực và log(-1.0) trả về NaN: cả hai đều không làm crash, nghĩa là một đối số sai sẽ lặng lẽ đi xuyên qua phép tính của bạn cho tới khi có chỗ in ra nan.
INFINITY, NAN và cách kiểm tra chúng
Số thực có những giá trị không phải là số, và math.h đặt tên cho chúng:
Dòng cuối cùng là dòng quan trọng nhất. Bạn không thể viết if (x == NAN): điều kiện này luôn sai. Hãy dùng isnan(x). (Đẳng thức x != x chỉ đúng với NaN, đó cũng là cách isnan thường được cài đặt và là phương án dự phòng tiện lợi nếu bạn gặp một bộ công cụ không có nó.)
Giá trị vô cực xuất hiện do tràn số (exp(1000.0)) và khi chia một số double khác 0 cho 0, phép chia mà, khác với chia số nguyên cho 0, được định nghĩa rõ ràng và không làm crash.
Các biến thể float và long double
Mỗi hàm có ba dạng: phiên bản double, phiên bản float có hậu tố f, và phiên bản long double có hậu tố l.
float sqrtf(float x);
double sqrt(double x);
long double sqrtl(long double x);
Hãy dùng các phiên bản double thường trừ khi bạn có lý do đã đo đạc để không dùng: double là kiểu C mặc định nâng lên, và trộn các biến thể float vào thường tốn cho việc chuyển kiểu nhiều hơn phần tiết kiệm được. Chỗ duy nhất hậu tố này quan trọng là khi một phép tính chỉ dùng float cần hiệu năng cao trên phần cứng không hỗ trợ double.
Một ví dụ thực tế
Kết hợp vài hàm ở trên: khoảng cách giữa hai điểm và nghiệm của phương trình bậc hai:
Để ý fabs(disc) < 1e-12 thay vì disc == 0.0: vẫn là quy tắc sai số cho phép như trước, áp dụng ở chỗ một biệt thức tính từ giá trị đo đạc gần như không bao giờ rơi đúng vào 0.
<math.h> chỉ là một header trong bộ công cụ lớn hơn; trang thư viện chuẩn trình bày phần còn lại, và số ngẫu nhiên nói về rand, hàm nằm trong <stdlib.h> chứ không phải ở đây.
Câu hỏi thường gặp
Dùng hàm toán học trong C như thế nào?
Thêm #include <math.h> ở đầu tệp rồi gọi hàm: sqrt(16.0), pow(2.0, 10.0), fabs(-3.5). Các hàm này nhận và trả về double. Trên Linux bạn còn phải liên kết thư viện toán học bằng -lm: gcc program.c -o program -lm.
Vì sao biên dịch bị lỗi 'undefined reference to sqrt'?
Header đã cung cấp khai báo cho trình biên dịch, nhưng phần cài đặt nằm trong một thư viện riêng mà trên Linux trình liên kết không tự động kéo vào. Thêm -lm vào cuối lệnh: gcc program.c -o program -lm. Trên macOS và với MinGW trên Windows, code toán học đã nằm trong thư viện C chuẩn nên không cần cờ này.
abs và fabs trong C khác nhau thế nào?
abs() trong <stdlib.h> nhận một int và trả về một int. fabs() trong <math.h> nhận một double và trả về một double. Gọi abs(-3.7) sẽ chuyển đối số sang int trước và cho ra 3, lặng lẽ bỏ mất phần thập phân: hãy dùng fabs cho số thực.
Kiểm tra NaN trong C như thế nào?
Dùng isnan(x) trong <math.h>. Bạn không thể kiểm tra bằng x == NAN, vì NaN không bằng bất cứ thứ gì, kể cả chính nó; chính điểm kỳ quặc đó lại là cách kiểm tra dự phòng: x != x chỉ đúng với NaN. Dùng isinf(x) cho giá trị vô cực và isfinite(x) để kiểm tra một số bình thường.