strtok_s 的第一个参数始终保持为 NULL 是否正确?

Is it right to always keep the first parameter of strtok_s as NULL?

我曾经考虑过什么时候第一次调用strtok_s()应该将包含令牌的字符串作为第一个参数传递,如下代码:

char testString[100] = "1|2|3";
char *context = testString;
const char *token = strtok_s( testString, "|", &context );
while ( token )
    token = strtok_s( NULL, "|", &context );

但是,我看到有人总是把第一个参数设置为NULL,就像下面的代码:

char testString[100] = "1|2|3";
char *context = testString;
const char *token = strtok_s( NULL, "|", &context );
while ( token )
    token = strtok_s( NULL, "|", &context );

我知道它的工作原理以及它的工作原理。因为 context 指向与 testString 相同的缓冲区。但是我觉得有点奇怪,我的疑问是:

  1. 使用 strtok_s() 是个好习惯吗?它可能面临哪些潜在错误?
  2. 如果这是一个好的做法,为什么 strtok_s() 仍然需要保留第一个参数?它可以像往常一样 NULL ,不是吗?

根据函数documentation,函数的正确用法 是你提到的第一个。

进一步引用 C11 标准(强调我的),第 K.3.7.3.1 节(第 616 页):

  1. A sequence of calls to the strtok_s function breaks the string pointed to by s1 into a sequence of tokens, each of which is delimited by a character from the string pointed to by s2. The fourth argument points to a caller-provided char pointer into which the strtok_s function stores information necessary for it to continue scanning the same string.

  2. The first call in a sequence has a non-null first argument and s1max points to an object whose value is the number of elements in the character array pointed to by the first argument. The first call stores an initial value in the object pointed to by ptr and updates the value pointed to by s1max to reflect the number of elements that remain in relation to ptr. Subsequent calls in the sequence have a null first argument and the objects pointed to by s1max and ptr are required to have the values stored by the previous call in the sequence, which are then updated. The separator string pointed to by s2 may be different from call to call.

因此,标准所说的是正确的用法是使用非 NULL 第一个参数调用 strtok_s,然后使用 NULL 第一个参数调用它。在第一次调用时,该函数初始化一些状态,并使用提供的指针(最后一个参数)来存储它。

标准没有提到应该如何使用最后一个参数,而不是保持状态,以便函数在使用 unmodified 指针调用时可以继续搜索相同的字符串.基本上,它消除了对 strtok 内部状态的需要,例如,您可以同时标记多个字符串。

状态 space 的使用方式因此是 实现定义的 。很可能是这种情况,在某些实现中,它可以简单地将初始字符串放在那里并始终使用第一个参数 NULL 调用它,如您所示。但是不能保证所有实现都会发生这种情况,或者这种行为在库的未来版本中将保持不变。

直接回答你的问题,是的,它可能有效,但不,这样做不是一个好主意。

  1. Is it a good practice of using strtok_s()?

不,这是不好的做法。

即使它碰巧起作用(就像它在这里所做的那样),它也很糟糕因为你不得不问这个问题。你问这个问题是因为代码看起来令人惊讶,这意味着这段代码的作者浪费了你的时间,让代码变得比必要的更难理解。

... What are the potential bugs it may face?

  • 另一个 C 库可能以不同的方式实现函数
  • 另一位开发人员可能会将分配移动或更改为 context,因为像这样使用它是不寻常的
  1. If this is a good practice, why strtok_s() still needs to keep the first parameter?

由于这个问题不是很好的做法,因此没有实际意义,但值得指出另一个原因,与您的前任明显未能遵守的最小惊奇原则相关:一致性.

一致的界面不那么令人惊讶,更容易推理,也更容易避免混乱。这个原型保持与其他现有接口的一致性(尽管我看到你使用的是 MS strtok_s 而不是标准的 C11 版本) - 如果你删除第一个参数,则源字符串和定界符参数的明显顺序是相反的到其他 strtok 函数。

我个人认为第二种形式是无害的。因为:

  1. 很多代码已经写成第二种形式了。更改实现会破坏它们并造成不必要的麻烦。
  2. 没有更好的实施空间。

我什至在代码中只使用了一次 strtok_s():

char* context = testString;
// 1. with 'for' loop: 'token' will not leak into outer scope
for (const char* token; (token = strtok_s(NULL, "|", &context)) != NULL;)
    use(token);

// 2. with 'while' loop
char* context = testString;
const char* token2;
while((token2 = strtok_s(NULL, "|", &context)) != NULL)
    use(token);

下面是我的团队一直在使用的实现。

char* my_strtok_s(char* buf, const char* splitters, char** context)
{
    char* p = *context;
    char* token;

    if (buf != NULL)
        p = buf;

    if (p == NULL) 
    {
        *context = p;
        return NULL;
    }

    while(strchr(splitters, *p) && *p != 0)
        p++;

    token = p;
    while(*p != 0)
    {
        if (strchr(splitters, *p))
        {
            *p = 0;
            p++;
            break;
        }
        p++;
    }

    *context = p;

    return *token != 0 ? token : NULL;
}