它有什么意义?为什么sscanf函数仍然有效?

时间:2018-08-26 18:43:31

标签: c scanf

如您所见,我仅在循环内为sizeof(char)分配了1个字节,而sscanf()仍然读取整个块,直到将空白写入string_of_letters。这怎么可能?

sscanf()的定义是什么?

例如:str = "rony is a man"但在string_of_letters位置i上我看到“讽刺”。

char **string_of_letters;
int i;
char *read = str;

string_of_letters = (char**)malloc(3 * sizeof(char*));
for (i = 0; i < 3; i++) {
    string_of_letters[i] = (char*)malloc(sizeof(char));
    sscanf(read,"%[^, ]", &(*string_of_letters[i]));
    printf("%s\n", string_of_letters[i]);
}

2 个答案:

答案 0 :(得分:6)

C不会强加运行时内存边界检查,因此您仅分配一个字节的事实与sscanf的功能无关紧要:它将很乐意尝试将整个字符串存储到所指向的内存位置通过您提供的指针。如果缓冲区不够大,则结果将是不确定的行为,其确切后果取决于要考虑的太多因素(所使用的编译器及其版本,操作系统,内存的当前状态等)。

在像您这样的小型玩具程序中,它似乎正常工作也就不足为奇了,因为缓冲区足够小,并且没有太多其他事情了。但是,在较大的程序中,sscanf可能会写在传入缓冲区的末尾,并写入另一个缓冲区,分配给其他缓冲区,更改您不想更改的内存,或者例如,很幸运,进入受保护的内存,导致访问冲突。

答案 1 :(得分:4)

有很多方法可以修复所示的代码片段。此代码显示其中三个。如对该问题的注释所述,您需要在循环内分配至少2个字符(因为%[…]扫描集创建了一个以空值结尾的字符串),但是您可以使用%1[^, ]作为对一次得到一个字符。请注意,您需要测试sscanf()的返回值以检查您是否达到了预期。您还需要增加读取,以免一遍又一遍地读取相同的字符。在更一般的情况下,您可以使用%n告知扫描在哪里停止(请参阅Using sscanf() in a loop)。扫描集不会跳过空白(%c%n也不会跳过-所有其他标准转换都会跳过前导空白,包括换行符)。

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

enum { LIST_SIZE = 3 };

static void free_array(size_t n, char **arr)
{
    for (size_t i = 0; i < n; i++)
        free(arr[i]);
    free(arr);
}

int main(void)
{
    char str[] = "rony is a man";
    char **string_of_letters;
    char *read = str;

    printf("Variant 1:\n");
    string_of_letters = (char **)malloc(LIST_SIZE * sizeof(char *));
    for (int i = 0; i < LIST_SIZE; i++)
    {
        string_of_letters[i] = (char *)malloc(2 * sizeof(char));
        if (sscanf(&read[i], "%1[^, ]", string_of_letters[i]) != 1)
            printf("Conversion failed on %d\n", i);
        else
            printf("%s\n", string_of_letters[i]);
    }

    free_array(LIST_SIZE, string_of_letters);

    printf("Variant 2:\n");
    string_of_letters = (char **)malloc(LIST_SIZE * sizeof(char *));
    for (int i = 0; i < LIST_SIZE; i++)
    {
        string_of_letters[i] = (char *)malloc(sizeof(char));
        *string_of_letters[i] = read[i];
        printf("%c\n", *string_of_letters[i]);
    }

    free_array(LIST_SIZE, string_of_letters);

    printf("Variant 3:\n");
    strcpy(str, "  r o  n");

    char char_list[LIST_SIZE + 1];      // NB: + 1 provides space for null byte
    int offset = 0;
    for (int i = 0; i < LIST_SIZE; i++)
    {
        int pos;
        printf("Offset = %d: ", offset);
        if (sscanf(&read[offset], " %1[^, ]%n", &char_list[i], &pos) != 1)
        {
            printf("Conversion failed on character index %d\n", i);
            break;
        }
        else
            printf("%c\n", char_list[i]);
        offset += pos;
    }

    return 0;
}

所示的代码可以在运行Valgrind的Mac上运行带有Valgrind 3.14.0.GIT(从Git提取的版本,而不是正式发布的源代码的版本)的macOS 10.13.6 High Sierra的Mac上正常运行。 / p>

输出:

Variant 1:
r
o
n
Variant 2:
r
o
n
Variant 3:
Offset = 0: r
Offset = 3: o
Offset = 5: n

正如已经观察到的那样,问题sorta中的代码起作用的原因更多是偶然而不是设计。 malloc()返回的指针受到约束,因此它指向可以用于任何目的的内存位置:

  

C11 §7.22.3 Memory management functions

     

¶1…   如果分配成功,则返回的指针将适当对齐,以便可以将其分配给   指向具有基本对齐要求的任何类型的对象的指针,然后使用   在分配的空间中访问此类对象或此类对象的数组(...)。 …

这意味着,由于其他类型的对齐要求,单个char的连续分配将不是连续的。通常,您会发现分配的最小空间是8或16个字节(在32位或64位平台上),但这绝不是必需的。这确实意味着分配的空间通常比您请求的空间更多(尤其是如果您请求一个字节)。但是,访问该额外空间会导致不确定的行为。您对示例代码的运行表明,有时“未定义的行为”的行为或多或少符合预期。