我知道聚会迟到了,但这里还有 2 个功能可供使用,并且可能会进一步调整以满足您的需求(源代码在帖子底部)
另请参阅下面的实施说明,以确定哪个功能更适合您的需求。
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h> // C99
// tokenize destructively
char **str_toksarray_alloc(
char **strp, /* InOut: pointer to the source non-constant c-string */
const char *delim, /* c-string containing the delimiting chars */
size_t *ntoks, /* InOut: # of tokens to parse/parsed (NULL or *ntoks==0 for all tokens) */
bool keepnulls /* false ignores empty tokens, true includes them */
);
// tokenize non-destructively
char **str_toksarray_alloc2(
const char *str, /* the source c-string */
const char *delim,
size_t *ntoks,
bool keepnulls
);
使用说明
它们的原型几乎相同,除了源字符串(分别为strp 和str)。
strp(指向字符串的指针)是已分配的非常量 c 字符串的地址,要就地标记化。 str 是一个未更改的 c 字符串(它甚至可以是字符串文字)。 c-string 我的意思是nul-终止的字符缓冲区。两个函数的其余参数相同。
要解析所有可用的标记,mute ntoks(意味着在将其传递给任何函数之前将其设置为 0 或将其作为 NULL 指针传递)。否则,函数会解析到 *ntoks 标记,或者直到没有更多标记(以先到者为准)。在任何情况下,当ntoks 是non-NULL 时,它会根据成功解析的令牌数进行更新。
另请注意,非静音 ntoks 决定了将分配多少指针。因此,如果源字符串包含 10 个标记,并且我们将 ntoks 设置为 1000,我们最终将得到 990 个不必要的分配指针。另一方面,如果源字符串包含 1000 个标记,但我们只需要前 10 个,则将 ntoks 设置为 10 听起来是一个更明智的选择。
这两个函数分配并返回一个字符指针数组,但str_toksarray_alloc() 使它们指向修改后的源字符串本身中的标记,而str_toksarray_alloc2() 使它们指向动态分配令牌的副本(名称末尾的 2 表示 2 级分配)。
返回的数组附加了一个NULL哨兵指针,在ntoks的回传值中没有考虑到(否则,当non-NULL,ntoks回传给调用者返回数组的长度,而不是它的第一级大小)。
当 keepnulls 设置为 true 时,生成的令牌类似于我们对 strsep() 函数的预期。主要意味着源字符串中的连续定界符产生空标记(null),如果delim 是一个空的 c 字符串,或者在源字符串中没有找到它包含的定界符,则结果只有 1 个标记:源字符串。与strsep()相反,可以通过将keepnulls设置为false来忽略空令牌。
失败的函数调用可以通过检查它们的返回值与NULL 或通过检查ntoks 的传回值与0 来识别(假设ntoks 是@987654362 @)。我建议在尝试访问返回的数组之前始终检查失败,因为这些函数包括完整性检查,可以推迟否则立即崩溃(例如,将 NULL 指针作为源字符串传递)。
成功时,调用者应该在完成后释放数组。
对于str_toksarray_alloc(),一个简单的free() 就足够了。对于str_toksarray_alloc2(),由于第二级分配,涉及一个循环。 NULL 哨兵(或 non-NULL ntoks 的回传值)使这变得微不足道,但我还在下面为所有懒惰的蜜蜂提供了 toksarray_free2() 函数:)
以下是使用这两个函数的简化示例。
准备:
const char *src = ";b,test,Tèst,;;cd;ελληνικά,nørmälize,;string to";
const char *delim = ";,";
bool keepnulls = true;
size_t ntoks = 0;
str_toksarray_alloc():
// destructive (use copy of src)
char *scopy = strdup( src );
if (!scopy) { ... }; // handle strdup failure
printf( "%s\n", src );
char **arrtoks = str_toksarray_alloc( &scopy, delim, &ntoks, keepnulls );
printf( "%lu tokens read\n", ntoks );
if ( arrtoks ) {
for (int i=0; arrtoks[i]; i++) {
printf( "%d: %s\n", i, arrtoks[i] );
}
}
free( scopy );
free( arrtoks );
/* OUTPUT
;b,test,Tèst,;;cd;ελληνικά,nørmälize,;string to
11 tokens read
0:
1: b
2: test
3: Tèst
4:
5:
6: cd
7: ελληνικά
8: nørmälize
9:
10: string to
*/
str_toksarray_alloc2():
// non-destructive
keepnulls = false; // reject empty tokens
printf( "%s\n", src );
arrtoks = str_toksarray_alloc2( src, delim, &ntoks, keepnulls );
printf( "%lu tokens read\n", ntoks );
if ( arrtoks ) {
for (int i=0; arrtoks[i]; i++) {
printf( "%d: %s\n", i, arrtoks[i] );
}
}
toksarray_free2( arrtoks ); // dangling arrtoks
// or: arrtoks = toksarray_free2( arrtoks ); // non-dangling artoks
/* OUTPUT
;b,test,Tèst,;;cd;ελληνικά,nørmälize,;string to
7 tokens read
0: b
1: test
2: Tèst
3: cd
4: ελληνικά
5: nørmälize
6: string to
*/
实施说明
这两个函数都使用 strsep() 进行标记化,这使得它们线程安全,但这不是标准函数。如果未提供,您始终可以使用开源实现(例如 GNU's 或 Apple's)。 str_toksarray_alloc2() 中使用的函数 strdup() 也是如此(它的实现很简单,但这里还是 GNU's 和 Apple's 的例子)。
在str_toksarray_alloc() 中使用strsep() 的副作用是源字符串的起始指针在解析循环的每一步中都不断移动到下一个标记。这意味着调用者将无法释放已解析的字符串,除非他们已将起始地址保存到额外的指针。 我们为他们省去了麻烦,方法是在函数中使用 strpSaved 指针在本地执行此操作。 str_toksarray_alloc2() 不受此影响,因为它不接触源字符串。
这两个函数之间的主要区别是str_toksarray_alloc() 不会为找到的令牌分配内存。它只是为数组指针分配空间,并将它们设置为直接指向源字符串。这是因为 strsep() nul-就地终止找到的令牌。这种依赖性会使您的支持代码复杂化,但对于大字符串,它也会对性能产生很大影响。如果保留源字符串并不重要,那么它也会对内存占用产生很大影响。
另一方面,str_toksarray_alloc2() 分配并返回一个由动态分配的令牌副本组成的自我维持数组,没有进一步的依赖关系。它首先通过从源字符串的本地副本创建数组,然后将实际令牌内容复制到数组中来实现。与str_toksarray_alloc() 相比,这要慢得多并且留下更大的内存占用,但它没有进一步的依赖关系,并且对源字符串的性质没有特殊要求。这使得编写更简单(因此更易于维护)的支持代码变得更加容易。
这两个函数之间的另一个区别是当ntoks 被静音 时的第一级分配(数组指针)。它们都解析所有可用的令牌,但它们采用完全不同的方法。 str_toksarray_alloc() 使用初始大小为 16(字符指针)的 alloc-ahead,在解析循环中按需加倍。 str_toksarray_alloc2() 第一次计算所有可用的令牌,然后它只分配一次这么多的字符指针。第一次通过使用标准函数 strpbrk() 和 strchr() 的辅助函数 str_toksfound() 完成。我也在下面提供该函数的源代码。
哪种方法更好由您决定,具体取决于您的项目需求。随意将每个函数的代码调整为任一方法并从那里获取。
我想说的是,对于非常大的字符串,alloc-ahead 的平均速度要快得多,尤其是当初始大小和增长因子根据每个案例进行微调时(例如,使它们成为函数参数)。用所有strchr() 和strpbrk() 保存额外的通行证可以在那里有所作为。然而,对于相对较小的字符串,这几乎是常态,提前分配一堆字符指针只是一种矫枉过正。这并没有什么坏处,但在这种情况下它确实会无缘无故地弄乱代码。无论如何,请随意选择最适合您的。
这两个功能也是如此。我想说在大多数情况下str_toksarray_alloc2() 处理起来要简单得多,因为内存和性能很少是中小型字符串的问题。如果您必须处理大字符串,请考虑使用str_toksarray_alloc()(尽管在这些情况下,您应该使用专门的字符串解析函数,接近您的项目需求和输入规范)。
哦,男孩,我认为这不仅仅是 2 美分(笑)。
无论如何,这是 2 个函数和辅助函数的代码(我已经删除了它们的大部分描述 cmets,因为我已经涵盖了几乎所有内容)。
源代码
str_toksarray_alloc():
// ----------------------------------------
// Tokenize destructively a nul-terminated source-string.
// Return a dynamically allocated, NULL terminated array of char-pointers
// each pointing to each token found in the source-string, or NULL on error.
//
char **str_toksarray_alloc(char **strp, const char *delim, size_t *ntoks, bool keepnulls)
{
// sanity checks
if ( !strp || !*strp || !**strp || !delim ) {
goto failed;
}
char *strpSaved = *strp; // save initial *strp pointer
bool ntoksOk = (ntoks && *ntoks); // false when ntoks is muted
size_t _ntoks = (ntoksOk ? *ntoks : 16); // # of tokens to alloc-ahead
// alloc array of char-pointers (+1 for NULL sentinel)
char **toksarr = malloc( (_ntoks+1) * sizeof(*toksarr) );
if ( !toksarr ) {
goto failed;
}
// Parse *strp tokens into the array
size_t i = 0; // # of actually parsed tokens
char *tok;
while ( (tok = strsep(strp, delim)) ) {
// if requested, ignore empty tokens
if ( *tok == '\0' && !keepnulls ) {
continue;
}
// non-muted ntoks reached? we are done
if ( ntoksOk && i == _ntoks ) {
*ntoks = i;
break;
}
// muted ntoks & ran out of space? double toksarr and keep parsing
if ( !ntoksOk && i == _ntoks ) {
_ntoks *= 2;
char **tmparr = realloc( toksarr, (_ntoks+1) * sizeof(*tmparr) );
if ( !tmparr ) {
*strp = strpSaved;
free( toksarr );
goto failed;
}
toksarr = tmparr;
}
toksarr[i++] = tok; // get token address
}
toksarr[i] = NULL; // NULL sentinel
*strp = strpSaved; // restore initial *strp pointer
if (ntoks) *ntoks = i; // pass to caller # of parsed tokens
return toksarr;
failed:
if (ntoks) *ntoks = 0;
return NULL;
}
str_toksarray_alloc2():
// ----------------------------------------
// Tokenize non-destructively a nul-terminated source-string.
// Return a dynamically allocated, NULL terminated array of dynamically
// allocated and nul-terminated string copies of each token found in the
// source-string. Return NULL on error.
// The 2 at the end of the name means 2-levels of allocation.
//
char **str_toksarray_alloc2( const char *str, const char *delim, size_t *ntoks, bool keepnulls )
{
// sanity checks
if ( !str || !*str || !delim ) {
if (ntoks) *ntoks = 0;
return NULL;
}
// make a copy of str to work with
char *_str = strdup( str );
if ( !_str ) {
if (ntoks) *ntoks = 0;
return NULL;
}
// if ntoks is muted we'll allocate str_tokscount() tokens, else *ntoks
size_t _ntoks = (ntoks && *ntoks) ? *ntoks : str_tokscount(_str, delim, keepnulls);
if ( _ntoks == 0 ) { // str_tokscount() failed
goto fail_free_str;
}
// alloc the array of strings (+1 for an extra NULL sentinel)
char **toksarr = malloc( (_ntoks+1) * sizeof(*toksarr) );
if ( !toksarr ) {
goto fail_free_str;
}
// Parse str tokens and duplicate them into the array
size_t i = 0; // # of actually parsed tokens
char *tok;
while ( i < _ntoks && (tok = strsep(&_str, delim)) ) {
// if requested, skip empty tokens
if ( *tok == '\0' && !keepnulls ) {
continue;
}
// duplicate current token into the array
char *tmptok = strdup( tok );
if ( !tmptok ) {
goto fail_free_arr;
}
toksarr[i++] = tmptok;
}
toksarr[i] = NULL; // NULL sentinel
free( _str ); // release the local copy of the source-string
if (ntoks) *ntoks = i; // pass to caller the # of parsed tokens
return toksarr;
// cleanup before failing
fail_free_arr:
for (size_t idx=0; idx < i; idx++) {
free( toksarr[idx] );
}
free( toksarr );
fail_free_str:
free( _str );
if (ntoks) *ntoks = 0;
return NULL;
}
str_tokscount() - str_toksarr_alloc2() 使用的辅助函数:
// ----------------------------------------
// Return the count of tokens present in a nul-terminated source-string (str),
// based on the delimiting chars contained in a 2nd nul-terminated string (delim).
// If the boolean argument is false, empty tokens are excluded.
//
// To stay consistent with the behavior of strsep(), the function returns 1 if
// delim is an empty string or none of its delimiters is found in str (in those
// cases the source-string is considered a single token).
// 0 is returned when str or delim are passed as NULL pointers, or when str is
// passed as an empty string.
//
size_t str_tokscount( const char *str, const char *delim, bool keepnulls )
{
// sanity checks
if ( !str || !*str || !delim ) {
return 0;
}
const char *tok = str;
size_t nnulls = strchr(delim, *str) ? 1 : 0;
size_t ntoks = 1; // even when no delims in str, str counts as 1 token
for (; (str = strpbrk(tok, delim)); ntoks++ ) {
tok = ++str;
if ( strchr(delim, *str) ) {
nnulls++;
}
}
return keepnulls ? ntoks : (ntoks - nnulls);
}
toksarray_free2() - 在 str_toksarr_alloc2() 返回的数组上使用它:
// ----------------------------------------
// Free a dynamically allocated, NULL terminated, array of char-pointers
// with each such pointer pointing to its own dynamically allocated data.
// Return NULL, so the caller has the choice of assigning it back to the
// dangling pointer. The 2 at the end of the name means 2-levels of deallocation.
//
// NULL terminated array means ending with a NULL sentinel.
// e.g.: toksarr[0] = tok1, ..., toksarr[len] = NULL
//
char **toksarray_free2( char **toksarr )
{
if ( toksarr ) {
char **toks = toksarr;
while ( *toks ) { // walk until NULL sentinel
free( *toks++ );
}
free( toksarr );
}
return NULL;
}