לדלג לתוכן

4.9 - משתני ארגומנטים ומימוש printf - פתרון

תרגיל 1 - פתרון: המקסימום מבין כמה מספרים

#include <stdio.h>
#include <stdarg.h>

int max_of(int count, ...) {
    va_list ap;
    va_start(ap, count);

    int max = va_arg(ap, int);
    for (int i = 1; i < count; i++) {
        int current = va_arg(ap, int);
        if (current > max) {
            max = current;
        }
    }

    va_end(ap);
    return max;
}

int main() {
    printf("max_of(3, 10, 45, 22) = %d\n", max_of(3, 10, 45, 22));
    printf("max_of(5, -7, -2, -99, -1, -50) = %d\n", max_of(5, -7, -2, -99, -1, -50));

    return 0;
}

שימו לב שאתחלנו את max בארגומנט הראשון ולא באפס או ב-INT_MIN. אתחול לאפס היה שובר את המקרה השני, שבו כל המספרים שליליים.

התרגיל הזה גם מדגים את הבעיה של הפונקציות האלה: אם נקרא ל-max_of(5, 1, 2) הפונקציה תקרא חמישה ארגומנטים למרות שהועברו רק שניים, ותחזיר זבל. אין שום בדיקה שתתפוס את זה - החוזה הוא באחריות הקורא בלבד.

תרגיל 2 - פתרון: כתיבת my_printf

#include <stdio.h>
#include <stdarg.h>

static void print_unsigned(unsigned int n, int base) {
    char digits[] = "0123456789abcdef";
    char buf[32];
    int i = 0;

    if (n == 0) {
        putchar('0');
        return;
    }

    // the digits are produced from the least significant to the most
    while (n > 0) {
        buf[i++] = digits[n % base];
        n /= base;
    }

    // so we print the buffer backwards
    while (i > 0) {
        putchar(buf[--i]);
    }
}

static void print_int(int n) {
    if (n < 0) {
        putchar('-');
        print_unsigned(-(unsigned int)n, 10);
    } else {
        print_unsigned(n, 10);
    }
}

void my_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    for (const char *p = fmt; *p != '\0'; p++) {
        if (*p != '%') {
            putchar(*p);
            continue;
        }

        p++;
        switch (*p) {
            case 'd':
                print_int(va_arg(ap, int));
                break;
            case 'x':
                print_unsigned(va_arg(ap, unsigned int), 16);
                break;
            case 'c':
                // char is promoted to int when passed through '...'
                putchar(va_arg(ap, int));
                break;
            case 's': {
                const char *s = va_arg(ap, const char *);
                while (*s) {
                    putchar(*s++);
                }
                break;
            }
            case '%':
                putchar('%');
                break;
            case '\0':
                // the format string ended with a stray '%'
                putchar('%');
                p--;
                break;
            default:
                putchar('%');
                putchar(*p);
                break;
        }
    }

    va_end(ap);
}

int main() {
    my_printf("hello %s, you are %d years old\n", "dan", 25);
    my_printf("hex: %x, char: %c, percent: %%\n", 255, 'A');
    my_printf("negative: %d, zero: %d\n", -42, 0);

    return 0;
}

הפרדנו את ההמרה לשתי פונקציות, ויש לכך סיבה. %d מתייחס למספר כאל מספר מסומן ולכן צריך לטפל במינוס, אבל %x מתייחס אליו כאל לא מסומן - printf("%x", -1) האמיתי ידפיס ffffffff ולא -1. הפרדה ל-print_int ו-print_unsigned נותנת לנו את שתי ההתנהגויות בלי לשכפל את לולאת החילוק.

שימו לב גם ל--(unsigned int)n. אם היינו כותבים -n על INT_MIN היינו נופלים להתנהגות לא מוגדרת, כי הערך החיובי של INT_MIN לא נכנס ב-int. ההמרה ל-unsigned לפני השלילה פותרת את זה. נרחיב על מלכודות מהסוג הזה בפרק 10.2.

המקרה של case '\0' מטפל במחרוזת פורמט שנגמרת ב-% בודד. בלעדיו הלולאה הייתה מדלגת מעל סוף המחרוזת וממשיכה לקרוא זיכרון שלא שייך לנו.

תרגיל 3 - פתרון: פונקציית לוג עם רמות

#include <stdio.h>
#include <stdarg.h>

void log_msg(const char *level, const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    printf("[%s] ", level);
    vprintf(fmt, ap);
    printf("\n");

    va_end(ap);
}

int main() {
    log_msg("INFO", "server started on port %d", 8080);
    log_msg("ERROR", "cannot open file %s", "config.txt");
    log_msg("DEBUG", "%d items, %d%% done", 42, 75);

    return 0;
}

הניסיון הטבעי הוא לכתוב printf(fmt, ...) בתוך הפונקציה, אבל אין סינטקס כזה בשפה - אי אפשר "לפרוס" את שלוש הנקודות בקריאה לפונקציה אחרת.

הפתרון הוא vprintf, שמקבלת va_list מוכן במקום .... אנחנו מכינים את ה-va_list בעצמנו עם va_start ומעבירים אותו הלאה. אותו רעיון קיים לכל משפחת ה-printf: vfprintf לכתיבה לקובץ ו-vsnprintf לכתיבה למאגר.

אם היינו רוצים לכתוב את ההודעה למאגר במקום למסך, למשל כדי לשלוח אותה לרשת או לשמור אותה לקובץ לוג, זה היה נראה כך:

void log_to_buffer(char *buf, size_t size, const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);

    vsnprintf(buf, size, fmt, ap);

    va_end(ap);
}

vsnprintf היא הבחירה הנכונה כאן ולא vsprintf, כי היא מקבלת את גודל המאגר ולא תחרוג ממנו. זו אותה הבחנה שראינו בפרק 4.5 בין sprintf ל-snprintf.