关于sprintf和snprintf的比较

本文探讨了在GNUMake(Windows)和Linux环境下,sprintf与snprintf函数的使用和差异,重点在于安全性与内存管理方面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#include <stdio.h>
#include <string.h>

typedef unsigned char uchar;

#define BUF_SIZE 10						// 缓冲区大小
#define CLEAR_SIZE (BUF_SIZE+2)			// 操作的缓冲区大小, 需要全部重置

void printRuler(int len)
{
	putchar('\n');
	for(int i = 1; i <= len; i++)
	{
		printf("%02d ", i);
		if(i == BUF_SIZE)
			printf("\t|\t");
	}
	putchar('\n');
}

void dis(char *buf, int len)
{
	for(int i = 0; i < len; i++)
	{
		printf("%02x ", (uchar)buf[i]);
		if(i == BUF_SIZE - 1)
			printf("\t|\t");
	}
//	puts("\n");				// 另起一行输出字符串并换行, 不会接在原来的后面
	putchar('\n');			// 直接输出字符
	printf("buf=[%s]\n\n", buf);
}

int main()
{
	char buf[BUF_SIZE];

	memset(buf, 0xcc, CLEAR_SIZE);
	dis(buf, CLEAR_SIZE);

	printRuler(CLEAR_SIZE);
	printf("-------------------------------------- snprintf\n");

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "12345678");		// 未溢出
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789");		// 未溢出, 刚好填满
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789A");	// 未溢出, 被截断1个字符
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789AB");	// 未溢出, 被截断2个字符
	dis(buf, CLEAR_SIZE);

//////////////////////////////////////////////////////////////////////////////
	printRuler(CLEAR_SIZE);
	printf("-------------------------------------- sprintf\n");
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "12345678");					// 未溢出
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789");				// 刚刚好
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789A");				// 溢出1个字符
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789AB");				// 溢出2个字符
	dis(buf, CLEAR_SIZE);

	return 0;
}

/*
	运行情况:
D:\profile\Desktop\test>make
g++ -o a.exe a.cpp

D:\profile\Desktop\test>a
cc cc cc cc cc cc cc cc cc cc   |       cc cc
buf=[烫烫烫烫烫烫@]


01 02 03 04 05 06 07 08 09 10   |       11 12
-------------------------------------- snprintf
31 32 33 34 35 36 37 38 00 cc   |       cc cc
buf=[12345678]

31 32 33 34 35 36 37 38 39 00   |       cc cc
buf=[123456789]

31 32 33 34 35 36 37 38 39 00   |       cc cc
buf=[123456789]

31 32 33 34 35 36 37 38 39 00   |       cc cc
buf=[123456789]


01 02 03 04 05 06 07 08 09 10   |       11 12
-------------------------------------- sprintf
31 32 33 34 35 36 37 38 00 cc   |       cc cc		// 空间有余
buf=[12345678]

31 32 33 34 35 36 37 38 39 00   |       cc cc		// 刚好填充満
buf=[123456789]										

31 32 33 34 35 36 37 38 39 41   |       00 cc		// 溢出1个字符
buf=[123456789A]

31 32 33 34 35 36 37 38 39 41   |       42 00		// 溢出2字符
buf=[123456789AB]


	结论:
		1. sprintf和snprintf都会在字符串末尾加上'\0'
		2. snprintf比sprintf安全,即不会造成缓冲区溢出
*/

以上的测试环境是GNUMake(windows), 和Linux.


===============================================================================

以下的代码测试环境是windows vc6和vc2010

#define _CRT_SECURE_NO_WARNINGS			// 针对vc2010添加

#include <stdio.h>
#include <string.h>

typedef unsigned char uchar;

#ifdef WIN32
	#define snprintf _snprintf			// windows平台无snprintf, 但是有_snprintf
#endif

#define BUF_SIZE 10						// 缓冲区大小
#define CLEAR_SIZE (BUF_SIZE+2)			// 操作的缓冲区大小, 需要全部重置

void printRuler(int len)
{
	putchar('\n');
	for(int i = 1; i <= len; i++)
	{
		printf("%02d ", i);
		if(i == BUF_SIZE)
			printf("\t|\t");
	}
	putchar('\n');
}

void dis(char *buf, int len)
{
	for(int i = 0; i < len; i++)
	{
		printf("%02x ", (uchar)buf[i]);
		if(i == BUF_SIZE - 1)
			printf("\t|\t");
	}
//	puts("\n");				// 另起一行输出字符串并换行, 不会接在原来的后面
	putchar('\n');			// 直接输出字符
	printf("buf=[%s]\n\n", buf);
}

int main()
{
	char buf[BUF_SIZE];

	memset(buf, 0xcc, CLEAR_SIZE);
	dis(buf, CLEAR_SIZE);

	printRuler(CLEAR_SIZE);
	printf("-------------------------------------- snprintf\n");

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "12345678");		// 未溢出
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789");		// 未溢出, 刚好填满
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789A");	// 未溢出, 被截断1个字符
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	snprintf(buf, BUF_SIZE, "%s", "123456789AB");	// 未溢出, 被截断2个字符
	dis(buf, CLEAR_SIZE);

//////////////////////////////////////////////////////////////////////////////
	printRuler(CLEAR_SIZE);
	printf("-------------------------------------- sprintf\n");
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "12345678");					// 未溢出
	dis(buf, CLEAR_SIZE);

	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789");				// 刚刚好
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789A");				// 溢出1个字符
	dis(buf, CLEAR_SIZE);
	
	memset(buf, 0xcc, CLEAR_SIZE);
	sprintf(buf, "%s", "123456789AB");				// 溢出2个字符
	dis(buf, CLEAR_SIZE);

	return 0;
}

/*
	运行情况(vc6/vc2010):
	cc cc cc cc cc cc cc cc cc cc   |       cc cc
	buf=[烫烫烫烫烫烫?]


	01 02 03 04 05 06 07 08 09 10   |       11 12
	-------------------------------------- snprintf
	31 32 33 34 35 36 37 38 00 cc   |       cc cc		// 未溢出时会填充字符串结束符('\0')
	buf=[12345678]

	31 32 33 34 35 36 37 38 39 00   |       cc cc		// 未溢出时会填充字符串结束符('\0')
	buf=[123456789]

	31 32 33 34 35 36 37 38 39 41   |       cc cc		// 溢出了将不会填充字符串结束符('\0')
	buf=[123456789A烫?]

	31 32 33 34 35 36 37 38 39 41   |       cc cc		// 溢出了将不会填充字符串结束符('\0')
	buf=[123456789A烫?]


	01 02 03 04 05 06 07 08 09 10   |       11 12
	-------------------------------------- sprintf
	31 32 33 34 35 36 37 38 00 cc   |       cc cc
	buf=[12345678]

	31 32 33 34 35 36 37 38 39 00   |       cc cc
	buf=[123456789]

	31 32 33 34 35 36 37 38 39 41   |       00 cc
	buf=[123456789A]

	31 32 33 34 35 36 37 38 39 41   |       42 00
	buf=[123456789AB]

	结论:
		1. windows上无snprintf,但是有_snprintf可以达到同样的功能,但是细节之处略有不同
		2. 未溢出时,sprintf和snprintf都会在字符串末尾加上'\0';
		3. 超出缓冲区大小时,_snprintf不会造成溢出,但是不会在缓冲区中添加字符结束符
		4. sprintf始终会在字符串末尾添加结束符,但是存在缓冲区溢出的情况
		5. _snprintf比sprintf安全,即不会造成缓冲区溢出
		6. vc6中对于_snprintf缓冲区溢出时不会出现崩溃信息,但是在vc2010中却会出现
*/




### 三、`sprintf` `snprintf` 的区别分析 在 C 语言中,`sprintf` `snprintf` 都用于将格式化的字符串输出到目标字符串中,它们的用法类似于 `printf`,但输出目标是字符串而非标准输出[^3]。然而,两者在行为安全性方面存在显著差异。 #### 3.1 缓冲区大小控制 `sprintf` 不检查目标缓冲区的大小,它会将格式化的字符串完整地写入目标字符串,直到遇到字符串结束符 `\0` 为止。这种行为可能导致缓冲区溢出,尤其是在目标缓冲区空间不足时。例如: ```c char buffer[10]; sprintf(buffer, "%s", "This string is too long"); // 缓冲区溢出风险 ``` 而 `snprintf` 多了一个参数 `n`,用于指定目标缓冲区的大小,它在写入时会限制最多写入 `n` 个字符(包括终止符 `\0`),从而有效防止缓冲区溢出。例如: ```c char buffer[10]; snprintf(buffer, sizeof(buffer), "%s", "This string is too long"); // 安全写入 ``` #### 3.2 返回值差异 `sprintf` 返回写入目标字符串的字符数(不包括终止符 `\0`)[^3]。这个返回值不能用于判断是否发生了缓冲区溢出。 `snprintf` 返回的是“将要写入”的字符数(不包括终止符),如果这个值大于或等于给定的缓冲区大小,则表示写入被截断。这一特性可以用于判断输出是否完整,从而增强程序的健壮性[^2]。 #### 3.3 安全性比较 由于 `sprintf` 不对缓冲区大小进行检查,它在使用不当的情况下容易引发缓冲区溢出问题,这在现代软件开发中被视为潜在的安全隐患。 相比之下,`snprintf` 在设计上更安全,因为它会确保写入的字符数不超过指定的缓冲区大小,从而避免溢出风险。因此,在需要将格式化字符串写入字符串的场景中,推荐优先使用 `snprintf`。 ### 总结 | 特性 | `sprintf` | `snprintf` | |------------------|----------------------------------|----------------------------------------| | 缓冲区大小控制 | 不检查,可能溢出 | 检查,防止溢出 | | 返回值含义 | 写入字符数(不含 `\0`) | 应写入字符数,可能被截断 | | 安全性 | 不安全,易引发溢出 | 安全,推荐使用 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值