Menu

פונקציות מתמטיות ב-C: math.h, sqrt, pow וקישור עם -lm

סיור ב-math.h: sqrt, pow, fabs, floor ו-ceil, round, fmod, משפחות הטריגונומטריה והלוגריתמים, INFINITY ו-NAN, ובנוסף דגל הקישור -lm שגורם לשגיאה הקלאסית undefined reference.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

השפה הבסיסית של 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) כדי לבדוק שמדובר במספר רגיל.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל