椋鸟C语言笔记#35

scanf/fscanf/sscanf、printf/fprintf/sprintf/snprintf,以及它们的边界检查函数

Featured image

萌新的学习笔记,写错了恳请斧正。

scanf/fscanf/sscanf

#include <stdio.h>
int scanf( const char* format, ... );
int fscanf( FILE* stream, const char* format, ... );
int sscanf( const char* buffer, const char* format, ... );

这三个函数从各种资源读取数据,按照格式控制字符串(format)转译,并将结果存储到指定位置

这三个函数返回成功赋值的接受参数的数量(可以为零),出错则返回 EOF。

他们的区别在于:

scanf 和 fscanf 已经具体的讲过了

sscanf 的特性与 fscanf 等同,抵达字符串尾(’\0’)等价于 fscanf 抵达文件尾的条件

sscanf 使用实例
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
	int a = 0;
	char* arr = (int*)malloc(100 * sizeof(int));
	char str[100] = { 0 };
	sscanf(" 114514 afjoiweaj jiojl", "%d%s%s", &a, arr, str);
	printf("%d - %s - %s", a, arr, str);
	return 0;
}

printf/fprintf/sprintf/snprintf

#include <stdio.h>
int printf( const char* format, ... );
int fprintf( FILE* stream, const char* format, ... );
int sprintf( char* buffer, const char* format, ... );
int snprintf( char* buffer, int bufsz, const char* format, ... );

前三个函数从给定位置加载数据,按一定要求转换为字符串等价物,并写结果到各种地方

第四个函数 snprintf 就是增加了长度限制的 sprintf,其他没有区别

对比四个函数:

sprintf 使用实例
#include <stdio.h>
 
int main()
{
	char a[10] = "1919810";
	sprintf(a, "%s", "114514");
	printf(a);
 
	return 0;
}
snprintf 使用实例
#include <stdio.h>
 
int main()
{
	char str[100];
	snprintf(str, 100, "Hello %s", "World!");
	printf(str);
	return 0;
}

scanf 系列的边界检查函数(scanf_s/fscanf_s/sscanf_s)

首先声明一点,这些为了安全创造的带有下划线 s 结尾的边界检查函数是在 C11 标准中定义的可选函数,所以并不是每一个编译器都能编译这个函数,只有在使用的编译器实现定义 “__STDC_LIB_EXT1__” 而且在包含 < stdio.h > 前定义 “__STDC_WANT_LIB_EXT1__” 为整数常量 1 时才能保证这些函数一定可以使用。当然,不少编译器都默认开启了这些函数,比如在 Visual Studio 中这些函数就是默认开启的,甚至如果使用不安全的不带边界检查的这些函数还会警告并中断编译(在文件前定义 “_CRT_SECURE_NO_WARNINGS” 为整数常量 1 可以忽略不安全函数警告)

//就是加上这一行以禁用不安全函数警告
#define _CRT_SECURE_NO_WARNINGS 1
//后面的1可以省略,默认定义值为1

这些函数相比于传统函数,多了边界检查,可以防止缓冲区溢出

#include <stdio.h>
int scanf_s(const char* format, ...);
int fscanf_s(FILE* stream, const char* format, ...);
int sscanf_s(const char* buffer, const char* format, ...);

机制与传统函数相同,除了:

1. 如果在格式限定中使用了 %c 、 %s 或 %[,后面可变参数列表中与其对应的参数变成两个,第一个仍为传统函数的参数,第二个则是类型为 rsize_t 的一个值,用于限定这个格式匹配符最多能匹配多少个字节的内容。比方说:

#include <stdio.h>
 
int main()
{
	int n = 0, m = 0;
	char arr[10] = { 0 };
	scanf_s("%d%x%s", &n, &m, arr, (rsize_t)sizeof arr);
	printf("n=%d m=%d arr=%s\n", n, m, arr);
	return 0;
}

这段代码中,scanf_s 中 %s 对应的参数包括 arr 和 (rsize_t)sizeof arr 两个 arr 是读取后保存的位置,后面则是限定最多读取 10 个字节(空终止符’\0’是算在内的,所以实际最多读 9 个)

2. 在运行时额外检测以下违规行为,如果出现违规也返回 EOF:

printf 系列的边界检查函数(printf_s/fprintf_s/sprintf_s/snprintf_s)

其使用条件与优越性与 scanf 系列的边界检查函数相同

#include <stdio.h>
int printf_s(const char* format, ...);
int fprintf_s(FILE* stream, const char* format, ...);
int sprintf_s(char* buffer, rsize_t bufsz, const char* format, ...);
int snprintf_s(char* buffer, rsize_t bufsz, const char* format, ...);

其机制与传统函数相同,除了在运行时检测如下违规行为:

关于返回值:

利用 snprintf 决定包含输出的缓冲区大小

当我们不确定输出字符串的大小时,我们可以利用以零为 bufsz 和以空指针为 buffer 的 snprintf 来决定缓冲区大小。为什么呢?

当 bufsz 为零、buffer 为空指针时,根据上面的介绍,我们知道这只会输出忽略 bufsz 限制情况下本应写入的字符数,而且不会产生其他影响。而这个输出值,不就是我们需要的缓冲区大小值吗?

下面是一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
int main()
{
	const char* format = "sqrt(2) = %f";
	int sz = snprintf(NULL, 0, format, sqrt(2));
	char* buf = (char*)malloc(sz + 1);	// +1 for '\0'
	snprintf(buf, sz + 1, format, sqrt(2));
	printf("%s\n", buf);
 
	return 0;
}