本檔收錄 5 題由易到難的練習。建議流程:先蓋住解答自己寫,卡住再看 「解題思路」,寫完對照「參考解答」。每題完整程式碼在 exercises/:

cc -std=c11 -Wall -Wextra -o /tmp/out exercises/ex1_max_n.c && /tmp/out

本單元三條鐵律,五題反覆出現:

  1. va_start/va_end 成對出現;
  2. va_arg 只能取「提升後」的型別(int/double/指標, 沒有 char、short、float);
  3. va_list 走過一遍後狀態不確定,要重掃先 va_copy。

練習 1:max_n(難度:基礎)

題目

寫 int max_n(int count, ...),回傳 count 個 int 引數中的最大值。 count <= 0 時印警告到 stderr 並回傳 INT_MIN。

測資:max_n(3, 1, 7, 3) → 7;全負數 max_n(5, -9, -2, -7, -1, -5) → -1;單一引數;全部相同。

解題思路

  1. 變參函式的資訊問題:... 裡有幾個引數、什麼型別, 函式自己無從得知——呼叫端必須用某種協定告知。 本題用「第一個固定參數當計數」(printf 用格式字串、 練習 3 用哨兵值,是另外兩種協定)。
  2. 標準骨架:
va_list ap;
va_start(ap, count);   /* count = 最後一個具名參數 */
... va_arg(ap, int) ...
va_end(ap);
  1. 求最大值的初始化:不要用 best = 0(全負數就錯了), 而是「第一個引數直接當目前最大」,從第二個開始比—— 這也順便處理了 count==1 的情況。
  2. 邊界情況:count <= 0 沒有「最大值」可言,定義一個明確的 失敗行為(回 INT_MIN + stderr 警告)比默默回 0 誠實。

參考解答

/*
 * ex1_max_n.c — 練習 1:max_n(count, ...) 回傳最大值
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex1_max_n.c
 * 執行:/tmp/out
 *
 * 可變引數函式的基本骨架:va_start -> va_arg 迴圈 -> va_end。
 * 引數個數由第一個固定參數 count 告知(printf 則是靠格式字串)。
 */
#include <stdio.h>
#include <stdarg.h>
#include <limits.h>

/*
 * 回傳 count 個 int 引數中的最大值。
 * count == 0 沒有意義:這裡定義回傳 INT_MIN 並印警告。
 */
static int max_n(int count, ...)
{
    va_list ap;

    if (count <= 0) {
        fprintf(stderr, "max_n: count must be positive\n");
        return INT_MIN;
    }

    va_start(ap, count);            /* ap 指向 count 後的第一個引數 */

    int best = va_arg(ap, int);     /* 第一個引數直接當目前最大 */
    for (int i = 1; i < count; i++) {
        int v = va_arg(ap, int);    /* 每呼叫一次前進一個引數 */
        if (v > best)
            best = v;
    }

    va_end(ap);                     /* 與 va_start 成對,必須呼叫 */
    return best;
}

int main(void)
{
    printf("max_n(3, 1, 7, 3)          = %d\n", max_n(3, 1, 7, 3));
    printf("max_n(5, -9, -2, -7, -1, -5)= %d\n",
           max_n(5, -9, -2, -7, -1, -5));
    printf("max_n(1, 42)               = %d\n", max_n(1, 42));
    printf("max_n(4, 5, 5, 5, 5)       = %d\n", max_n(4, 5, 5, 5, 5));
    return 0;
}

程式碼講解

  • va_start(ap, count) 的第二個引數必須是最後一個具名參數—— 巨集靠它定位 ... 的起點。
  • 早退路徑(count <= 0)放在 va_start 之前: 還沒 start 就不需要 end,控制流最單純。 若在 start 之後提前 return,必須先 va_end。
  • 讀取個數完全信任 count:多讀(count 比實際引數多)是未定義 行為——變參函式的安全完全建立在「呼叫端遵守協定」上, 這是它與陣列參數本質的差異。
  • 易錯處:best 初始化為 0——全負數測資 max_n(5, -9, ...) 就是抓這個 bug 的。

範例輸出

max_n(3, 1, 7, 3)          = 7
max_n(5, -9, -2, -7, -1, -5)= -1
max_n(1, 42)               = 42
max_n(4, 5, 5, 5, 5)       = 5

練習 2:product(難度:基礎)

題目

寫 double product(int count, ...),計算 count 個 double 引數的 乘積;count == 0 回傳 1.0(空乘積)。並回答:呼叫時寫 product(4, 0.5, 0.5, 2.0, 8)(最後一個是 int 的 8)會發生什麼事?

解題思路

  1. 骨架同練習 1,但初始值改為乘法單位元素 1.0, 空乘積自然成立(數學慣例:空積 = 1)。
  2. 本題真正的考點:預設引數提升。傳給 ... 的引數會做 default argument promotions: - char、short → int; - float → double。 所以 va_arg 的型別只能寫提升後的型別: va_arg(ap, float) 是未定義行為,一律取 double。
  3. 反過來的陷阱:呼叫端傳 8(int)但函式取 va_arg(ap, double)——int 不會自動升成 double(提升只在整數與 float 身上發生),讀出來是垃圾或當機,也是未定義行為。 變參函式沒有型別檢查,呼叫端寫 8.0 是自己的責任。
  4. 驗收:含負數(結果變號)、空乘積、0.5×0.5×2×8=4。

參考解答

/*
 * ex2_product.c — 練習 2:product(count, ...) 計算 double 乘積
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex2_product.c
 * 執行:/tmp/out
 *
 * 重點:預設引數提升(default argument promotions)——
 * 傳給 ... 的 float 會自動升成 double,所以 va_arg 一律取 double;
 * 寫 va_arg(ap, float) 是未定義行為。
 */
#include <stdio.h>
#include <stdarg.h>

/* 回傳 count 個 double 引數的乘積;count==0 回傳 1.0(空乘積) */
static double product(int count, ...)
{
    va_list ap;
    double prod = 1.0;              /* 乘法單位元素 */

    va_start(ap, count);
    for (int i = 0; i < count; i++)
        prod *= va_arg(ap, double); /* 只能取 double,不能取 float */
    va_end(ap);

    return prod;
}

int main(void)
{
    printf("product(3, 2.0, 3.0, 4.0)   = %g\n",
           product(3, 2.0, 3.0, 4.0));
    printf("product(2, 1.5, -2.0)       = %g\n", product(2, 1.5, -2.0));
    printf("product(0)                  = %g  (empty product)\n",
           product(0));
    printf("product(4, 0.5, 0.5, 2.0, 8)= %g\n",
           product(4, 0.5, 0.5, 2.0, 8.0));

    /*
     * 整數字面值 8 若不寫成 8.0 會怎樣?
     * 傳進去的是 int,但 va_arg 取 double —— 未定義行為!
     * (上面呼叫刻意寫 8.0;這是變參函式最常見的炸點。)
     */
    printf("\nrule: pass 8.0 not 8 -- va_arg(ap, double) on an int\n");
    printf("argument is undefined behavior.\n");
    return 0;
}

程式碼講解

  • prod = 1.0 讓 count == 0 不需要特判——選對初始值, 邊界情況自動正確。
  • 為什麼沒有 va_arg(ap, float):ABI 層面 float 引數早已在 呼叫端升成 double 放進暫存器/堆疊,函式端按 float 的大小去讀 必然錯位。編譯器對字面型別的 va_arg 甚至會直接警告。
  • printf 的 %g:自動選擇 %e 或 %f 並去掉多餘的 0, 適合展示「數值本身」。
  • 易錯處:測資最後一個引數寫 8 而非 8.0。這在 printf 家族 同樣成立——printf("%f", 1) 是同型的 UB,只是大家常忘記 自己寫的變參函式也一樣脆弱。

範例輸出

product(3, 2.0, 3.0, 4.0)   = 24
product(2, 1.5, -2.0)       = -3
product(0)                  = 1  (empty product)
product(4, 0.5, 0.5, 2.0, 8)= 4

rule: pass 8.0 not 8 -- va_arg(ap, double) on an int
argument is undefined behavior.

練習 3:concat——哨兵結尾的字串串接(難度:進階)

題目

實作 char *concat(const char *first, ...):以 (char *)NULL 為 哨兵,把所有字串串成一個新配置的字串(呼叫端負責 free)。 要求:先掃一遍算總長、只 malloc 一次,再掃第二遍複製—— 這需要 va_copy。

測資:concat("Hello", ", ", "world", "!", (char*)NULL)、 單一字串、含空字串的組合。

解題思路

  1. 協定換成哨兵:引數個數未知,改用「特殊值收尾」—— 對指標而言天然的哨兵就是 NULL。execl、gtk 的變參 API 都用這招。
  2. 為什麼掃兩遍:邊掃邊 realloc 也行但效率差(O(n²) 複製 風險);先算總長再一次 malloc 是標準的兩段式配置。
  3. va_copy 登場:va_list 被第一遍的 va_arg 消耗完後 狀態不確定,不能倒帶。要重掃就必須在消耗前 va_copy(ap2, ap) 留一份乾淨的副本,兩份各自 va_end。
  4. 複製迴圈:維護寫入游標 dst,每段 memcpy 後前進 strlen(s);最後補 '\0'。用 memcpy 而非 strcat 是因為 strcat 每次都要重找結尾,O(n²)。
  5. 哨兵的型別陷阱:呼叫端必須寫 (char *)NULL 而不是裸的 NULL 或 0——後者可能以 int 寬度入棧,在 64 位平台上 va_arg 讀指標時只讀到一半垃圾。
  6. 邊界情況:空字串是合法輸入(貢獻 0 長度);malloc 失敗 回 NULL 前記得 va_end(ap2)。

參考解答

/*
 * ex3_concat.c — 練習 3:concat(first, ...):哨兵結尾的字串串接
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex3_concat.c
 * 執行:/tmp/out
 *
 * 以 (char *)NULL 為哨兵,把所有字串串成一個新配置的字串。
 * 策略:掃兩遍——第一遍算總長(一次 malloc),第二遍複製。
 * 需要 va_copy:va_list 走過一遍後狀態不確定,不能重複使用。
 */
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

/*
 * 把 first 與後續字串(以 (char*)NULL 結尾)串成新字串。
 * 回傳 malloc 的緩衝區,呼叫端負責 free;失敗回 NULL。
 */
static char *concat(const char *first, ...)
{
    va_list ap, ap2;
    size_t total = 0;

    /* ---- 第一遍:算總長 ---- */
    va_start(ap, first);
    va_copy(ap2, ap);               /* 複製一份給第二遍用 */

    for (const char *s = first; s != NULL; s = va_arg(ap, const char *))
        total += strlen(s);
    va_end(ap);

    char *result = malloc(total + 1);       /* +1 給 '\0' */
    if (result == NULL) {
        va_end(ap2);
        return NULL;
    }

    /* ---- 第二遍:用 ap2 重掃一次,邊走邊複製 ---- */
    char *dst = result;
    for (const char *s = first; s != NULL; s = va_arg(ap2, const char *)) {
        size_t len = strlen(s);
        memcpy(dst, s, len);
        dst += len;
    }
    *dst = '\0';
    va_end(ap2);

    return result;
}

int main(void)
{
    /* 哨兵一定要寫 (char *)NULL:裸 NULL 可能是 int 0,寬度不對 */
    char *s1 = concat("Hello", ", ", "world", "!", (char *)NULL);
    char *s2 = concat("one", (char *)NULL);
    char *s3 = concat("", "empty", "", " pieces", (char *)NULL);

    printf("s1 = \"%s\"  (len=%zu)\n", s1, strlen(s1));
    printf("s2 = \"%s\"  (len=%zu)\n", s2, strlen(s2));
    printf("s3 = \"%s\"  (len=%zu)\n", s3, strlen(s3));

    free(s1);                       /* 呼叫端負責釋放 */
    free(s2);
    free(s3);
    return 0;
}

程式碼講解

  • va_copy(ap2, ap) 必須在第一個 va_arg 之前做: copy 複製的是「目前位置」,消耗一半才 copy 就只剩後半。
  • 兩個 for 迴圈的長相刻意一致(s = first; s != NULL; s = va_arg(...))——同一個協定掃兩遍,差別只在迴圈體。
  • dst += len 游標推進配 memcpy:整個串接是 O(總長); 新手常寫的 strcat(result, s) 版本是 O(總長²) (Shlemiel the painter 問題)。
  • malloc 失敗的路徑也要 va_end(ap2)——va_end 是資源釋放, 所有離開路徑都要經過。
  • 易錯處:忘記 +1 留 '\0' 的位置;或第二遍誤用已耗盡的 ap(讀到垃圾指標,多半當機)。

範例輸出

s1 = "Hello, world!"  (len=13)
s2 = "one"  (len=3)
s3 = "empty pieces"  (len=12)

練習 4:my_snprintf 與截斷偵測(難度:進階)

題目

實作 int my_snprintf(char *buf, size_t size, const char *fmt, ...): 內部用 vsnprintf,回傳「完整輸出所需長度」(不含 '\0')。 用同一條格式化需求(18 字元的輸出)在 size = 64、19、18、10、1、0 下各跑一次,展示截斷偵測的標準寫法;最後示範「先問需求量再配置」 的兩段式用法。

解題思路

  1. 三層包裝模式(本單元的核心模式):自己的函式收 ..., va_start 後把 va_list 轉交給 v 系列函式 (這裡是 vsnprintf),格式化邏輯完全不用自己寫。
  2. snprintf 的回傳值語意(很多人記錯):回傳的是 「如果緩衝區無限大,會寫出幾個字元」(不含 '\0'), 不是實際寫入的字元數。因此: - needed < size:完整寫入; - needed >= size:發生截斷,buf 裡是前 size-1 字元 + '\0'。 這個設計讓截斷偵測變成一個比較運算。
  3. 邊界枚舉:size=19 恰好裝下(18+1);size=18 差一格 (截斷,最後一個字元被 '\0' 擠掉);size=1 只裝 '\0'; size=0 完全不寫(此時 buf 甚至可以是 NULL)。
  4. 兩段式用法:my_snprintf(NULL, 0, fmt, ...) 只回需求量, 再 malloc(needed+1) 後重新格式化——動態組字串的標準流程 (asprintf 的手工版)。

參考解答

/*
 * ex4_my_snprintf.c — 練習 4:my_snprintf 與截斷偵測
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex4_my_snprintf.c
 * 執行:/tmp/out
 *
 * 實作 my_snprintf(buf, size, fmt, ...):內部用 vsnprintf,
 * 回傳「完整輸出所需長度」(不含 '\0')——與標準 snprintf 相同語意。
 * 回傳值 >= size 即表示發生截斷,示範標準的截斷偵測寫法。
 */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

/*
 * 三層包裝模式的教科書範例:
 *   1. 對外收 ...
 *   2. va_start 後把 va_list 轉交給 v 系列函式
 *   3. vsnprintf 完成真正的格式化
 */
static int my_snprintf(char *buf, size_t size, const char *fmt, ...)
{
    va_list ap;
    int needed;

    va_start(ap, fmt);
    needed = vsnprintf(buf, size, fmt, ap);   /* 交棒給 v 函式 */
    va_end(ap);

    return needed;      /* 完整輸出所需字元數(不含 '\0') */
}

static void try_size(size_t size)
{
    char buf[64];

    /* 只用 buf 的前 size 個位元組模擬小緩衝區 */
    int needed = my_snprintf(buf, size, "user=%s id=%d", "alice", 1234);

    printf("  size=%2zu  needed=%d  ", size, needed);
    if ((size_t)needed >= size)
        printf("TRUNCATED  buf=\"%s\"\n", size > 0 ? buf : "(no space)");
    else
        printf("ok         buf=\"%s\"\n", buf);
}

int main(void)
{
    printf("my_snprintf(\"user=%%s id=%%d\", \"alice\", 1234)\n");
    printf("full output is \"user=alice id=1234\" -> 18 chars\n\n");

    try_size(64);       /* 綽綽有餘 */
    try_size(19);       /* 剛好裝下 18 字元 + '\0' */
    try_size(18);       /* 差一格:截斷 */
    try_size(10);       /* 明顯不夠 */
    try_size(1);        /* 只放得下 '\0' */
    try_size(0);        /* 完全不寫入(buf 可為 NULL),只回需求量 */

    /* 「先問需求量再配置」的兩段式用法 */
    printf("\ntwo-pass usage: ask needed size first\n");
    int needed = my_snprintf(NULL, 0, "pi=%.10f", 3.1415926535);
    printf("  needed=%d -> allocate needed+1 bytes, then format again\n",
           needed);
    return 0;
}

程式碼講解

  • my_snprintf 本體只有四行——三層包裝模式把「變參的麻煩」 壓縮到最小。你自製的每個 printf 風格函式都應長這樣。
  • 截斷判斷 (size_t)needed >= size:cast 是因為 needed 是 int(vsnprintf 錯誤時可回負值,生產程式要先檢查 < 0)。
  • try_size 用同一個 64 位元組的 buf 模擬各種小緩衝區—— vsnprintf 保證絕不寫超過 size,所以這樣測試是安全的。
  • size=1 的結果 buf="":vsnprintf 永遠保證 NUL 結尾 (只要 size > 0),這是它比 strncpy 可靠的地方。
  • 易錯處:把回傳值當「實際寫入數」用來推進游標——截斷時游標 會衝出緩衝區。推進量應是 min(needed, size-1)。

範例輸出

my_snprintf("user=%s id=%d", "alice", 1234)
full output is "user=alice id=1234" -> 18 chars

  size=64  needed=18  ok         buf="user=alice id=1234"
  size=19  needed=18  ok         buf="user=alice id=1234"
  size=18  needed=18  TRUNCATED  buf="user=alice id=123"
  size=10  needed=18  TRUNCATED  buf="user=alic"
  size= 1  needed=18  TRUNCATED  buf=""
  size= 0  needed=18  TRUNCATED  buf="(no space)"

two-pass usage: ask needed size first
  needed=15 -> allocate needed+1 bytes, then format again

練習 5:log_msg——完整的 logging 外殼(難度:挑戰)

題目

寫 void log_msg(int level, const char *fmt, ...):

  • 加上 [HH:MM:SS] 時間戳與等級字首(DEBUG/INFO/WARN/ERROR)後 輸出到 stderr;
  • 內部拆成 vlog(level, fmt, va_list),格式化交給 vfprintf;
  • 支援執行期調整的等級門檻(低於門檻的訊息丟棄);
  • 用 format attribute 讓編譯器檢查呼叫端的格式字串與引數。

解題思路

  1. 架構:三層包裝的完整版。log_msg(收 ...)→ vlog(收 va_list,做門檻過濾、時間戳、字首)→ vfprintf(stderr, fmt, ap)(真正格式化)。 把 vlog 獨立出來的價值:日後要加「寫進檔案」「送網路」 的變體,都只是再寫一個薄外殼,格式化邏輯不重複。
  2. 時間戳:time() 取秒級時間、localtime_r 轉本地時間 (_r 版本執行緒安全,為第 13 單元鋪路)、strftime 格式化 成 HH:MM:SS。
  3. 等級門檻:一個全域 log_threshold,vlog 開頭 if (level < log_threshold) return;——工業 logger(syslog、 log4c)的最小可行版本。
  4. format attribute:
__attribute__((format(printf, 2, 3)))

告訴編譯器「第 2 參數是 printf 格式字串、變參從第 3 個起」。 之後呼叫端寫 log_msg(LOG_INFO, "count=%d", "oops") 會在 編譯期被 -Wall 抓出——把執行期 UB 提前成編譯警告, 這是自製變參函式必配的安全帶。 5. 細節:va_list 交給 vfprintf 之後狀態不確定, log_msg 不得再碰 ap,只能 va_end。

參考解答

/*
 * ex5_log_msg.c — 練習 5:log_msg——時間戳+等級+format attribute
 *
 * 編譯:cc -std=c11 -Wall -Wextra -o /tmp/out ex5_log_msg.c
 * 執行:/tmp/out
 *
 * 三層包裝模式的完整應用:
 *   log_msg(level, fmt, ...)   對外介面
 *     -> vlog(level, fmt, ap)  接受 va_list 的內部實作
 *       -> vfprintf(stderr)    真正的格式化輸出
 * 並用 GCC/Clang 的 format attribute 讓編譯器檢查呼叫端的格式字串。
 */
#include <stdio.h>
#include <stdarg.h>
#include <time.h>

enum log_level { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR };

static const char *level_name[] = { "DEBUG", "INFO ", "WARN ", "ERROR" };

/* 執行期可調的全域門檻:低於它的訊息直接丟棄 */
static enum log_level log_threshold = LOG_DEBUG;

/*
 * 內部實作:接受 va_list(v 系列慣例)。
 * 格式化邏輯只寫在這裡一次,所有外殼共用。
 */
static void vlog(enum log_level level, const char *fmt, va_list ap)
{
    if (level < log_threshold)
        return;                             /* 等級不夠:丟棄 */

    /* 時間戳:HH:MM:SS(logging 標配) */
    time_t now = time(NULL);
    struct tm tmv;
    localtime_r(&now, &tmv);                /* _r 版:執行緒安全 */

    char stamp[16];
    strftime(stamp, sizeof stamp, "%H:%M:%S", &tmv);

    fprintf(stderr, "[%s] %s ", stamp, level_name[level]);
    vfprintf(stderr, fmt, ap);              /* 轉交 va_list */
    fputc('\n', stderr);
}

/*
 * 對外介面。format attribute 告訴編譯器:
 * 「第 2 個參數是 printf 格式字串、可變引數從第 3 個開始」,
 * 呼叫端型別不符時 -Wall 會直接警告(編譯期抓 bug)。
 */
__attribute__((format(printf, 2, 3)))
static void log_msg(enum log_level level, const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    vlog(level, fmt, ap);                   /* 轉交後 ap 狀態不確定 */
    va_end(ap);
}

int main(void)
{
    printf("logging to stderr with timestamp + level prefix:\n\n");
    fflush(stdout);                         /* stdout/stderr 順序對齊 */

    log_msg(LOG_DEBUG, "cache warmed with %d entries", 128);
    log_msg(LOG_INFO,  "server listening on port %d", 8080);
    log_msg(LOG_WARN,  "disk usage at %.1f%%", 91.5);
    log_msg(LOG_ERROR, "cannot open %s: %s", "conf.yml", "no such file");

    /*
     * format attribute 的威力(取消註解會得到編譯警告):
     *   log_msg(LOG_INFO, "count=%d", "oops");  <- %d 對到字串
     *   log_msg(LOG_INFO, "%s %d", "only-one"); <- 引數不夠
     */

    fprintf(stderr, "\n");
    fflush(stderr);
    printf("raise threshold to WARN: DEBUG/INFO now dropped\n");
    fflush(stdout);

    log_threshold = LOG_WARN;
    log_msg(LOG_DEBUG, "this DEBUG message is dropped");
    log_msg(LOG_INFO,  "this INFO message is dropped");
    log_msg(LOG_WARN,  "warnings still visible");
    return 0;
}

程式碼講解

  • vlog 是系統的心臟:門檻過濾放最前(被丟棄的訊息連時間戳 都不用算)、字首用 fprintf、本文用 vfprintf、換行統一補—— 呼叫端的 fmt 不需要(也不應該)帶 \n。
  • level_name 的字串刻意等寬("INFO " 帶空白),輸出欄位 自然對齊,省掉格式化寬度的心力。
  • format attribute 放在函式宣告前;它是 GCC/Clang 擴充 (MSVC 用 SAL 註記),跨編譯器的程式可包進 #if defined(__GNUC__) 巨集。
  • fflush(stdout):stdout(可能全緩衝)與 stderr(不緩衝) 混用時,不沖掉會看到訊息順序錯亂。
  • 易錯處一:log_msg 在呼叫 vlog 之後又想用 ap—— 交棒後狀態不確定,要用必須事先 va_copy。
  • 易錯處二:忘了 format attribute 的參數是從 1 數起, 且成員函式/有隱藏參數時要位移;本題 fmt 是第 2 個、 ... 從第 3 個開始,寫 (printf, 2, 3)。

範例輸出

logging to stderr with timestamp + level prefix:

[21:17:39] DEBUG cache warmed with 128 entries
[21:17:39] INFO  server listening on port 8080
[21:17:39] WARN  disk usage at 91.5%
[21:17:39] ERROR cannot open conf.yml: no such file

raise threshold to WARN: DEBUG/INFO now dropped
[21:17:39] WARN  warnings still visible

(時間戳依執行時刻而異。)


常見錯誤解法對照

錯誤寫法 症狀 正解
va_arg(ap, float) / (ap, char) UB:引數已被提升 取 double/int(練習 2)
呼叫端傳 8 給期待 double 的 ... 讀到垃圾值 寫 8.0;用 format attribute 抓(練習 2、5)
用耗盡的 va_list 重掃 垃圾指標、當機 事先 va_copy(練習 3)
哨兵寫裸 NULL/0 64 位平台讀到半個垃圾指標 (char *)NULL(練習 3)
把 snprintf 回傳值當實際寫入數 截斷時游標衝出緩衝區 needed >= size 判截斷(練習 4)
轉交 va_list 後繼續用 狀態不確定 UB 交棒即結束,要用先 copy(練習 5)