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.