什么是格式化字符串?
在 C 语言中,格式化字符串是一个字符序列,它主要用在两个标准库函数中:

printf系列函数:用于将格式化后的数据输出到标准输出(如屏幕)。scanf系列函数:用于从标准输入(如键盘)读取格式化数据。
格式化字符串的核心作用是作为模板或蓝图,告诉这些函数如何处理后续要输出的数据(对于 printf)或如何解释读取到的数据(对于 scanf)。
一个格式化字符串通常包含两种类型的字符:
- 普通字符:这些字符会原样输出或输入,在
printf("Hello, ");中,"Hello, "就是普通字符,会直接显示在屏幕上。 - 格式说明符:以百分号 () 开头,后面跟着一个或多个特定的字符,它是一个占位符,用于指定一个特定类型的数据应该如何被格式化。
printf 系列中的格式说明符
printf 的通用语法是:
printf("格式化字符串", 变量1, 变量2, ...);
格式说明符的通用结构是:%[标志][宽度][.精度][长度]类型

下面我们分解来看各个部分,并提供最常用的示例。
核心类型
这是格式说明符中必不可少的部分,它定义了要显示的数据类型。
| 类型说明符 | 数据类型 | 示例 |
|---|---|---|
%d 或 %i |
int (有符号十进制整数) |
int age = 25; printf("Age: %d", age); |
%c |
char (单个字符) |
char grade = 'A'; printf("Grade: %c", grade); |
%s |
char * (字符串,以 \0
| |
%f |
double (浮点数,默认6位小数) |
double pi = 3.14159; printf("Pi: %f", pi); |
%lf |
double (用于 scanf 读取 double) |
double price; scanf("%lf", &price); |
%p |
void * (指针地址) |
int num = 10; printf("Address: %p", &num); |
%x 或 %X |
unsigned int (十六进制数) |
int color = 0xFF00FF; printf("Color: %x", color); |
%o |
unsigned int (八进制数) |
int permissions = 0755; printf("Permissions: %o", permissions); |
%u |
unsigned int (无符号十进制整数) |
unsigned int count = 100; printf("Count: %u", count); |
| 输出百分号字符本身 | printf("100%%"); |
示例代码:
#include <stdio.h>
int main() {
int integer = 42;
float floating = 3.14f;
double double_precision = 123.456789;
char character = 'Z';
char string[] = "Hello C";
int hex_value = 255;
printf("Integer: %d\n", integer);
printf("Float: %f\n", floating);
printf("Double: %lf\n", double_precision); // 注意:printf中%f和%lf对于double通常可以互换,但%lf是标准
printf("Character: %c\n", character);
printf("String: %s\n", string);
printf("Hexadecimal: %x\n", hex_value);
return 0;
}
修饰符
修饰符可以用来控制输出的格式。

-
宽度:指定输出的最小字符数。
%5d:输出一个整数,如果不足5位,则在左边用空格补齐。%-5d:输出一个整数,如果不足5位,则在右边用空格补齐(左对齐)。%10.2f:输出一个浮点数,总宽度为10位,其中小数部分占2位。
-
精度:
- 对于浮点数 (
%f,%e,%g),.2表示小数点后保留2位。 - 对于字符串 (
%s),.5表示只输出前5个字符。 - 对于整数 (
%d),.5表示输出至少5位,不足则在左边补0。
- 对于浮点数 (
示例代码:
#include <stdio.h>
int main() {
int num = 123;
double pi = 3.1415926535;
char text[] = "Format String";
printf("Width 5: |%5d|\n", num); // '| 123|'
printf("Left align: |%-5d|\n", num); // '|123 |'
printf("Precision 2: |%.2f|\n", pi); // '|3.14|'
printf("Width 10, Precision 2: |%10.2f|\n", pi); // '| 3.14|'
printf("String precision 5: |%.5s|\n", text); // '|Forma|'
return 0;
}
scanf 系列中的格式说明符
scanf 的通用语法是:
scanf("格式化字符串", &变量1, &变量2, ...);
注意:scanf 需要变量的地址(& 运算符),因为它需要知道在哪里存储读取到的值。
scanf 的格式说明符比 printf 简单一些,但有一些非常重要的规则。
| 类型说明符 | 对应的变量类型 | 示例 |
|---|---|---|
%d |
int * (指向整数的指针) |
int age; scanf("%d", &age); |
%c |
char * (指向字符的指针) |
char initial; scanf("%c", &initial); |
%s |
char * (指向字符数组的指针,即字符串) |
char name[50]; scanf("%s", name); |
%f |
float * (指向浮点数的指针) |
float price; scanf("%f", &price); |
%lf |
double * (指向双精度浮点数的指针) |
double pi; scanf("%lf", &pi); |
scanf 的关键注意事项
-
空白字符(空格、制表符、换行符)的跳过:
scanf会自动跳过输入中的所有空白字符(包括空格、Tab、回车)。scanf("%d %d", &a, &b);,你输入10 20和10\n20的效果是一样的。 -
%s的问题:%s会读取字符,直到遇到下一个空白字符为止,这意味着它不能读取包含空格的字符串(如 "Hello World")。- 解决方案:使用
fgets函数来读取整行输入。
- 解决方案:使用
-
缓冲区溢出: 当使用
%s、%c或%[...]时,如果用户输入的数据超过了你为变量分配的空间,就会发生缓冲区溢出,这是一个严重的安全漏洞。- 解决方案:指定最大读取宽度。
char name[20]; scanf("%19s", name);,这里的19保证了最多读取19个字符,留1位给字符串结束符\0。
- 解决方案:指定最大读取宽度。
-
%c与空白字符: 如果你先使用scanf("%d", ...)读取一个数字,然后按回车,这个回车符会留在输入缓冲区中,如果你接着用scanf("%c", ...)读取一个字符,它会直接读取这个回车符,而不是等待你输入。- 解决方案:在读取
%c之前,可以先清空输入缓冲区,一个简单的方法是while (getchar() != '\n');,它会读取并丢弃所有字符,直到遇到换行符。
- 解决方案:在读取
示例代码:
#include <stdio.h>
int main() {
int id;
char name[50];
double salary;
printf("Enter your ID: ");
scanf("%d", &id);
// 清除输入缓冲区中的换行符,防止它被 fgets 读取
while (getchar() != '\n');
printf("Enter your full name: ");
// 使用 fgets 安全地读取一行
fgets(name, sizeof(name), stdin);
printf("Enter your salary: ");
scanf("%lf", &salary);
printf("\n--- Information ---\n");
printf("ID: %d\n", id);
printf("Name: %s", name); // fgets 会保留换行符,所以这里不用加\n
printf("Salary: %.2f\n", salary);
return 0;
}
高级和特殊格式说明符
-
%[](扫描集): 这是scanf的一个强大功能,允许你指定一组要读取的字符。%[a-z]:读取所有小写字母。%[0-9]:读取所有数字。%[^,]:读取所有字符,直到遇到逗号 () 为止。
-
: 在
printf中,如果你想输出一个字面的 字符,你需要使用 。
安全性问题:格式化字符串漏洞
这是 C 语言中一个非常著名且危险的安全漏洞,它主要与 printf 系列函数有关。
漏洞原理
printf 函数的第一个参数是格式化字符串,如果这个字符串来自用户输入,而程序员又没有提供足够的其他参数来匹配格式说明符,就会导致未定义行为,甚至安全漏洞。
漏洞代码示例:
#include <stdio.h>
void vulnerable_function(char *user_input) {
printf(user_input); // 危险!直接使用用户输入作为格式化字符串
}
int main() {
char input[100];
printf("Enter a format string: ");
fgets(input, sizeof(input), stdin);
vulnerable_function(input);
return 0;
}
攻击方式
攻击者可以输入类似这样的字符串:
%x %x %x %x
程序会尝试在栈上寻找四个整数参数并打印它们的十六进制值,这可以用来泄露内存信息。
更严重的是,攻击者可以输入:
%n
%n 比较特殊,它不打印任何东西,而是将到目前为止已经打印的字符数量写入到一个对应的 int * 参数指向的内存地址中。
攻击示例:
AAAA %x %x %x %x %n
如果攻击者能精确控制栈上的数据,他们可以将 AAAA 的地址作为 printf 的一个参数(通过将 AAAA 压入栈中),然后使用 %n 将一个特定的值(shellcode 的地址)写入到 AAAA 指向的内存,从而劫持程序执行流。
如何防御
- 永远不要将用户输入直接用作格式化字符串。
- 始终为
printf提供足够的参数。 - 使用现代的安全函数:
printf->printf_s(MSVC特有)sprintf->sprintf_sscanf->scanf_s
这些安全版本的函数会检查参数数量是否匹配格式说明符,如果不匹配,它们会直接拒绝执行并报错,从而防止漏洞的发生。
| 特性 | printf |
scanf |
|---|---|---|
| 目的 | 输出格式化数据 | 输入并解析格式化数据 |
| 参数 | 变量的值 | 变量的地址 (&var) |
| 核心 | 格式说明符定义输出格式 | 格式说明符定义输入解析规则 |
| 安全性 | 漏洞风险较低 | 漏洞风险高(缓冲区溢出、格式化字符串漏洞) |
| 关键函数 | printf, fprintf, sprintf |
scanf, fscanf, sscanf |
掌握格式化字符串是 C 语言编程的基础,理解它的语法、规则以及潜在的安全风险,是写出健壮、安全 C 代码的关键一步。
