השפה הבסיסית של C נותנת לכם את + - * / ואת %, ותו לא: אין אופרטור חזקה, אין שורש ריבועי, אין עיגול. כל מה שמעבר לאריתמטיקה נמצא בספרייה המתמטית הסטנדרטית, שמוצהרת ב-<math.h>.
כל הפונקציות האלה עובדות בדיוק של double: הן מקבלות ארגומנטים מסוג double ומחזירות double. זה הדבר הראשון שצריך להפנים, כי העברה של int עובדת בשקט (הוא מומר), בעוד שהשמת התוצאה ל-int משליכה בשקט את השבר.
צעדים ראשונים, והדגל -lm
על המחשב שלכם, קומפילציה של זה עלולה להיכשל:
/usr/bin/ld: /tmp/ccXYZ.o: in function `main':
program.c:(.text+0x1a): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
זו המעידה הנפוצה ביותר עם math.h, והיא לא טעות בקוד שלכם. ה-header סיפק את ההצהרות, אז הקומפיילר היה מרוצה; המימושים נמצאים בספרייה נפרדת (libm) שה-linker לא כולל כברירת מחדל ב-Linux. הוסיפו -lm:
gcc program.c -o program -lm
הדגל חייב לבוא אחרי קובצי המקור: linkers מעבדים את הקלטים שלהם לפי הסדר, וספרייה שמופיעה לפני הקוד שצריך אותה לא פותרת כלום. ב-macOS וב-MinGW ב-Windows, השגרות המתמטיות כבר חלק מספריית C הסטנדרטית, כך ש--lm מיותר (אם כי לא מזיק ב-macOS).
חזקות ושורשים
sqrt(x) של מספר שלילי מחזיר NaN במקום לקרוס; הגנו על הארגומנט אם הוא יכול להיות שלילי. כדאי להכיר את hypot, כי הוא מחשב את אותו ערך כמו sqrt(x*x + y*y) בלי לגלוש כש-x או y גדולים מאוד.
עבור חזקות שלמות קטנות, כפל רגיל גם מהיר יותר וגם מדויק: x * x עדיף על pow(x, 2.0), שעובר דרך לוגריתמים ועלול להחזיר 8.999999999999998 כשציפיתם ל-9.
עיגול: floor, ceil, round, trunc
ארבע פונקציות, ארבע תשובות שונות עבור מספרים שליליים. הטבלה הזו היא החלק ששווה לזכור:
floor(x): המספר השלם הגדול ביותר שאינו גדול מ-x. תמיד זז לכיוון מינוס אינסוף:floor(-2.3)הוא-3.ceil(x): המספר השלם הקטן ביותר שאינו קטן מ-x. תמיד זז לכיוון פלוס אינסוף:ceil(-2.3)הוא-2.round(x): המספר השלם הקרוב ביותר, חצאים מתעגלים הרחק מאפס:round(2.5)הוא3,round(-2.5)הוא-3.trunc(x): חותך את החלק השברי, זז לכיוון אפס:trunc(-2.7)הוא-2.
כל הארבע מחזירות double. המרה ל-int כשרוצים מספר שלם היא בסדר, אבל שימו לב ש-(int)x לבדו מבצע חיתוך, כך ש-(int)(x + 0.5) הוא ביטוי ה"עיגול" הישן, והוא שגוי עבור מספרים שליליים. השתמשו ב-round().
fabs ו-fmod
fabs הוא ערך מוחלט עבור נקודה צפה. שימוש ב-abs במקומו הוא טעות קלאסית, כי abs מקבלת int וההמרה קורית בשקט:
% לא עובד בכלל על double: הוא אופרטור של מספרים שלמים והקומפיילר דוחה אותו. fmod(a, b) הוא המקביל שלו לנקודה צפה, והוא שומר על הסימן של a.
fabs הוא גם הדרך הנכונה להשוות שני double, כי == על נקודה צפה הוא מלכודת:
אי אפשר לייצג את 0.1 ואת 0.2 בדיוק בבינארי, ולכן הסכום שלהם רחוק כחוט השערה מ-0.3. השוו עם סבולת, אף פעם לא עם ==.
טריגונומטריה
הפונקציות הטריגונומטריות עובדות ברדיאנים, לא במעלות, וזה המקור האמין השני לתשובות שגויות.
הסט המלא: sin, cos, tan, ההופכיות שלהן asin, acos, atan, הגרסה עם שני ארגומנטים atan2(y, x), והמשפחה ההיפרבולית sinh, cosh, tanh. העדיפו את atan2(y, x) על פני atan(y / x) עבור זוויות: היא מטפלת ב-x == 0 ומזהה נכון את הרביע.
math.h מגדיר את M_PI ברוב המערכות, אבל זו הרחבה של POSIX ולא C תקני, כך שהוא עלול להיות חסר תחת -std=c17. הגדרת קבוע משלכם, כמו למעלה, היא ניידת.
לוגריתמים ואקספוננטים
שימו לב למלכודת השמות שעברה מהמתמטיקה: log() הוא הלוגריתם הטבעי (בסיס e), לא בסיס 10. בסיס 10 הוא log10().
log(0.0) מחזיר מינוס אינסוף ו-log(-1.0) מחזיר NaN: אף אחד מהם לא קורס, מה שאומר שארגומנט גרוע עובר בשקט דרך כל החישוב עד שמשהו מדפיס nan.
INFINITY, NAN ואיך בודקים אותם
לנקודה צפה יש ערכים שאינם מספרים, ו-math.h נותן להם שמות:
השורה האחרונה היא הקריטית. אי אפשר לכתוב if (x == NAN): זה תמיד שקר. השתמשו ב-isnan(x). (הזהות x != x היא אמת רק עבור NaN, וכך isnan ממומש לעתים קרובות, וזה גיבוי שימושי אם תיתקלו פעם בכלי פיתוח בלעדיו.)
אינסופים מגיעים מגלישה (exp(1000.0)) ומחילוק של double שאינו אפס באפס, שבניגוד לחילוק שלמים באפס מוגדר ולא קורס.
גרסאות float ו-long double
לכל פונקציה יש שלוש צורות: גרסת ה-double, גרסת float עם הסיומת f, וגרסת long double עם הסיומת l.
float sqrtf(float x);
double sqrt(double x);
long double sqrtl(long double x);
השתמשו בגרסאות ה-double הרגילות אלא אם יש לכם סיבה מדודה שלא: double הוא מה ש-C מקדמת אליו כברירת מחדל, וערבוב של גרסאות float בדרך כלל עולה בהמרות יותר ממה שהוא חוסך. המקום היחיד שבו הסיומת חשובה הוא כשחישוב שכולו float קריטי לביצועים על חומרה בלי תמיכה ב-double.
דוגמה מעשית
שילוב של כמה מהן יחד: המרחק בין שתי נקודות והשורשים של משוואה ריבועית:
שימו לב ל-fabs(disc) < 1e-12 במקום disc == 0.0: אותו כלל סבולת כמו קודם, מיושם במקום שבו דיסקרימיננטה שחושבה מערכים מדודים כמעט אף פעם לא תנחת בדיוק על אפס.
<math.h> הוא header אחד בארגז כלים גדול יותר; העמוד על הספרייה הסטנדרטית ממפה את השאר, ומספרים אקראיים מכסה את rand, שנמצא ב-<stdlib.h> ולא כאן.
שאלות נפוצות
איך משתמשים בפונקציות מתמטיות ב-C?
הוסיפו #include <math.h> בראש הקובץ וקראו להן: sqrt(16.0), pow(2.0, 10.0), fabs(-3.5). הן מקבלות ומחזירות double. ב-Linux צריך גם לקשר את הספרייה המתמטית עם -lm: gcc program.c -o program -lm.
למה מתקבלת השגיאה 'undefined reference to sqrt' בקומפילציה?
ה-header נתן לקומפיילר את ההצהרה, אבל המימוש נמצא בספרייה נפרדת שה-linker לא מושך אוטומטית ב-Linux. הוסיפו -lm בסוף הפקודה: gcc program.c -o program -lm. ב-macOS וב-MinGW ב-Windows הקוד המתמטי הוא חלק מספריית C הסטנדרטית, כך שאין צורך בדגל.
מה ההבדל בין abs ל-fabs ב-C?
abs() מ-<stdlib.h> מקבלת int ומחזירה int. fabs() מ-<math.h> מקבלת double ומחזירה double. קריאה ל-abs(-3.7) ממירה קודם את הארגומנט ל-int ונותנת 3, ומשליכה בשקט את השבר: השתמשו ב-fabs עבור ערכי נקודה צפה.
איך בודקים NaN ב-C?
השתמשו ב-isnan(x) מ-<math.h>. אי אפשר לבדוק עם x == NAN, כי NaN לא שווה לשום דבר, כולל לעצמו, והמוזרות הזו היא בעצם בדיקת הגיבוי: x != x הוא אמת רק עבור NaN. השתמשו ב-isinf(x) עבור אינסופים וב-isfinite(x) כדי לבדוק שמדובר במספר רגיל.