0Pricing
C Academy · 课时

类型安全问题

可变参数的风险

类型安全问题 是 CoddyKit 上的免费 C Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。

可变参数绕过类型检查

普通 C 函数会检查参数是否与参数类型匹配。可变参数会完全跳过这项检查。

编译器无法验证您在 ... 后传入的内容,因此很容易出错,也很危险。

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

int add(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", add(2, 3, 4));
    return 0;
}

va_arg 中的错误类型

如果使用错误的类型读取参数,结果就是未定义行为。

传入 int 却将其读取为 double,会重新解释无关的字节,并产生垃圾值或导致崩溃。

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

int read_int(int n, ...) {
    va_list a; va_start(a, n);
    int v = va_arg(a, int);
    va_end(a);
    return v;
}

int main(void) {
    printf("%d\n", read_int(1, 100));
    return 0;
}

格式字符串不匹配

在 printf 风格函数中,错误的说明符是一个典型错误。printf("%d", 3.14) 会将 double 当作 int 读取。

现代编译器会针对字面量不匹配发出警告,但无法捕获运行时构建的格式字符串。

#include <stdio.h>

int main(void) {
    double pi = 3.14;
    printf("%f\n", pi);
    return 0;
}

整数提升带来的意外

在可变参数部分中,小类型会提升为 int。因此,即使您传入的是 char,也必须将它读取为 int。

使用 va_arg(a, char) 将其读取为 char 会导致未定义行为。

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

void show(int n, ...) {
    va_list a; va_start(a, n);
    int c = va_arg(a, int);
    printf("char is %c\n", c);
    va_end(a);
}

int main(void) {
    show(1, 'A');
    return 0;
}

Float 会变成 double

float 参数在可变参数中始终会提升为 double。您必须将其读取为 double,不能读取为 float。

这是最常见的可变参数错误之一。

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

void show(int n, ...) {
    va_list a; va_start(a, n);
    double d = va_arg(a, double);
    printf("%.2f\n", d);
    va_end(a);
}

int main(void) {
    float f = 1.5f;
    show(1, f);
    return 0;
}

读取过多参数

如果代码调用 va_arg 的次数超过调用者提供的参数数量,就会从栈中读取垃圾数据。

因此,准确的计数或哨兵值至关重要。

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

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum(3, 1, 2, 3));
    return 0;
}

NULL 指针哨兵需要谨慎处理

使用 NULL 作为哨兵存在风险:在某些平台上,NULL 是作为 int 的 0,其大小与指针不同。

请进行显式转换,例如 (char *)NULL,以确保压入的是正确类型。

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

void print_all(const char *first, ...) {
    va_list a; va_start(a, first);
    const char *s = first;
    while (s) { printf("%s\n", s); s = va_arg(a, const char *); }
    va_end(a);
}

int main(void) {
    print_all("a", "b", (char *)NULL);
    return 0;
}

编译器格式属性

GCC 和 Clang 提供了 __attribute__((format(printf, 1, 2))),用于告诉编译器您的函数使用 printf 风格的格式化。

这样,编译器就会在编译时检查格式字符串,捕获许多错误。

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

void logf(const char *fmt, ...)
    __attribute__((format(printf, 1, 2)));

void logf(const char *fmt, ...) {
    va_list a; va_start(a, fmt);
    vprintf(fmt, a);
    va_end(a);
}

int main(void) {
    logf("%d items\n", 3);
    return 0;
}

优先选择更安全的替代方案

由于可变参数容易出错,请尽可能选择类型安全的设计:传入显式数组、结构体或固定参数。

只有确实需要 printf 风格功能时,才使用可变参数。

#include <stdio.h>

int sum_array(const int *a, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += a[i];
    return s;
}

int main(void) {
    int nums[] = {1, 2, 3, 4};
    printf("%d\n", sum_array(nums, 4));
    return 0;
}

记录约定

由于编译器无法强制检查可变参数的类型,清晰的文档就是您的安全保障。

请明确说明调用者必须提供哪些类型、多少个参数,以及参数列表如何结束。

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

/* sum_ints(n, ...): pass exactly n int values */
int sum_ints(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum_ints(2, 5, 5));
    return 0;
}

彻底测试

由于错误可能悄无声息地隐藏起来,请使用多种参数数量和类型测试可变参数函数。

零参数或最大参数数量等边界情况,往往能暴露错误。

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

int sum(int n, ...) {
    va_list a; va_start(a, n);
    int s = 0;
    for (int i = 0; i < n; i++) s += va_arg(a, int);
    va_end(a);
    return s;
}

int main(void) {
    printf("%d\n", sum(0));
    printf("%d\n", sum(1, 9));
    return 0;
}

快速检查

测试您对可变参数类型风险的理解。

回顾

您学习了可变参数函数的危险:

  • 编译器不会对可变参数进行类型检查。
  • 错误的类型、数量和哨兵值都会导致未定义行为。
  • 请记住提升规则:小整数提升为 int,float 提升为 double。
  • 使用编译器的格式属性,记录约定,并尽可能选择类型安全的替代方案。

常见问题解答

「类型安全问题」课时是免费的吗?

是的 — 「类型安全问题」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。

「类型安全问题」这节课中我会学到什么?

可变参数的风险 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「类型安全问题」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C Academy 课中编写并运行代码吗?

能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. stdarg 宏
  2. 编写类似 printf 的函数
  3. 类型安全问题
  4. 实际示例
← 返回 C Academy