是否在 C 未定义行为中将任意数量的字符串与嵌套函数调用连接起来?

Is concatenating arbitrary number of strings with nested function calls in C undefined behavior?

我有一个应用程序,它通过使用文本片段创建完整文件路径名的一系列字符串连接来构建文件路径名。

问题是将少量但任意数量的文本字符串连接在一起的方法是否成功取决于未定义的行为

一系列嵌套函数的求值顺序是否一致运行?

我发现了这个问题 Nested function calls order of evaluation 但是它似乎更多地是关于参数列表中的多个函数而不是一系列嵌套函数。

请原谅以下代码示例中的名称。它与源代码的其余部分一致,我正在先测试一下。

我对连接多个字符串的需求的第一个切入点是一个如下所示的函数,它将最多三个文本字符串连接成一个字符串。

typedef wchar_t TCHAR;

TCHAR *RflCatFilePath(TCHAR *tszDest, int nDestLen, TCHAR *tszPath, TCHAR *tszPath2, TCHAR *tszFileName)
{
    if (tszDest && nDestLen > 0) {
        TCHAR *pDest = tszDest;
        TCHAR *pLast = tszDest;

        *pDest = 0;   // ensure empty string if no path data provided.

        if (tszPath) for (pDest = pLast; nDestLen > 0 && (*pDest++ = *tszPath++); nDestLen--) pLast = pDest;
        if (tszPath2) for (pDest = pLast; nDestLen > 0 && (*pDest++ = *tszPath2++); nDestLen--)  pLast = pDest;
        if (tszFileName) for (pDest = pLast; nDestLen > 0 && (*pDest++ = *tszFileName++); nDestLen--)  pLast = pDest;
    }

    return tszDest;
}

然后我 运行 进入了一个案例,我有四段文字要放在一起。

考虑到这一点,似乎很快就会发现五个的情况,所以我想知道是否有不同的方法来处理任意数量的字符串。

我想到的是如下两个函数。

typedef wchar_t TCHAR;

typedef struct {
    TCHAR *pDest;
    TCHAR *pLast;
    int    destLen;
} RflCatStruct;

RflCatStruct RflCatFilePathX(const TCHAR *pPath, RflCatStruct x)
{
    TCHAR *pDest = x.pLast;
    if (pDest && pPath) for ( ; x.destLen > 0 && (*pDest++ = *pPath++); x.destLen--)  x.pLast = pDest;
    return x;
}

RflCatStruct RflCatFilePathY(TCHAR *buffDest, int nLen, const TCHAR *pPath)
{
    RflCatStruct  x = { 0 };

    TCHAR *pDest = x.pDest = buffDest;
    x.pLast = buffDest;
    x.destLen = nLen;

    if (buffDest && nLen > 0) {   // ensure there is room for at least one character.
        *pDest = 0;   // ensure empty string if no path data provided.
        if (pPath) for (pDest = x.pLast; x.destLen > 0 && (*pDest++ = *pPath++); x.destLen--)  x.pLast = pDest;
    }
    return x;
}

这两个函数的使用示例如下。具有这两个功能的代码似乎适用于 Visual Studio 2013.

TCHAR buffDest[512] = { 0 };
TCHAR *pPath = L"C:\flashdisk\ncr\database";
TCHAR *pPath2 = L"\";
TCHAR *pFilename = L"filename.ext";

RflCatFilePathX(pFilename, RflCatFilePathX(pPath2, RflCatFilePathY(buffDest, 512, pPath)));
printf("dest t = \"%S\"\n", buffDest);


printf("dest t = \"%S\"\n", RflCatFilePathX(pFilename, RflCatFilePathX(pPath2, RflCatFilePathY(buffDest, 512, pFilename))).pDest);


RflCatStruct  dStr = RflCatFilePathX(pPath2, RflCatFilePathY(buffDest, 512, pPath));
//   other stuff then
printf("dest t = \"%S\"\n", RflCatFilePathX(pFilename, dStr).pDest);

函数调用的参数在调用函数之前被完全评估。因此对 RflCatFilePath* 的调用将按预期顺序进行评估。 (这由 §6.5.2.2/10 保证:"There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call.")

如评论中所述,snprintf 函数可能是解决此问题的更好选择。 (asprintf 会更好,并且有一个可免费使用的 shim,适用于 Windows。)snprintf 的唯一问题是您可能必须调用它两次。它总是 return 是 本来 存储在缓冲区中的字节数,如果有足够的 space,所以如果 return 值是不小于缓冲区的大小,您将需要分配一个更大的缓冲区(您现在知道其大小)并再次调用 snprintf

asprintf 为您做到了,但它是对标准库的 BSD/Gnu 扩展。

在连接文件路径的情况下,操作 system/file 系统支持最大字符串长度,您应该能够找出它是什么(尽管它可能需要 OS-specific调用 non-Posix 系统)。因此,如果连接不适合 512 字节缓冲区,则简单地 return 错误指示可能是合理的。

为了好玩,我包含了一个递归可变参数连接器:

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

static char* concat_helper(size_t accum, char* chunk, va_list ap) {
  if (chunk) {
    size_t chunklen = strlen(chunk);
    char* next_chunk = va_arg(ap, char*);
    char* retval = concat_helper(accum + chunklen, next_chunk, ap);
    memcpy(retval + accum, chunk, chunklen);
    return retval;
  } else {
    char* retval = malloc(accum + 1);
    retval[accum] = 0;
    return retval;
  }
}
char* concat_list(char* chunk, ...) {
    va_list ap;
    va_start(ap, chunk);
    char* retval = concat_helper(0, chunk, ap);
    va_end(ap);
    return retval;
}

由于 concat_list 是可变参数函数,您需要在参数末尾提供 (char*)NULL。另一方面,您不需要为每个新参数重复函数名称。因此,示例调用可能是:

concat_list(pPath, pPath2, pFilename, (char*)0);

(我想你需要一个 wchar_t* 版本,但变化应该很明显。注意 malloc。)为了生产目的,递归可能应该被一个迭代版本取代,它遍历参数列表两次(参见 va_copy),但我一直喜欢 "there-and-back" 递归模式。