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

在C中用嵌套函数调用连接任意数量的字符串是否为未定义行为?,c,function,standards,C,Function,Standards,我有一个应用程序,它通过一系列字符串连接来构建文件路径名,使用文本片段来创建完整的文件路径名 问题是,处理将少量但任意数量的文本字符串连接在一起的方法是否取决于未定义的行为才能成功 一系列嵌套函数的求值顺序是否得到保证 我发现这个问题似乎更多的是关于参数列表中的多个函数,而不是一系列嵌套函数 请原谅以下代码示例中的名称。它与源代码的其余部分是一致的,我先测试一下 我对连接多个字符串的需求的第一个切入点是一个函数,它看起来像下面这样,将最多三个文本字符串连接成一个字符串 typedef wchar

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

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

一系列嵌套函数的求值顺序是否得到保证

我发现这个问题似乎更多的是关于参数列表中的多个函数,而不是一系列嵌套函数

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

我对连接多个字符串的需求的第一个切入点是一个函数,它看起来像下面这样,将最多三个文本字符串连接成一个字符串

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;
}
然后我遇到了一个案例,我有四段文字要拼在一起

考虑到这一点,似乎很有可能也会有一个关于5的案例很快就会被发现,所以我想知道对于任意数量的字符串是否有不同的方法

我想到的是两个函数,如下所示

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保证:“在函数指示符和实际参数的评估之后,但在实际调用之前,有一个序列点。”)

如注释所示,
snprintf
函数可能是解决此问题的更好选择。(
asprintf
会更好,并且有一个在Windows上可以免费使用的垫片。)
snprintf
唯一的问题是您可能需要调用它两次。它总是返回缓冲区中存储的字节数(如果有足够的空间),因此如果返回值不小于缓冲区的大小,则需要分配一个更大的缓冲区(您现在知道其大小),然后再次调用
snprintf

asprintf
可以为您实现这一点,但它是标准库的BSD/Gnu扩展

在串联文件路径的情况下,操作系统/文件系统支持最大字符串长度,您应该能够找到它是什么(尽管在非Posix系统上可能需要特定于操作系统的调用)。因此,如果级联不适合512字节的缓冲区,则直接返回错误指示可能是合理的

为了好玩,我提供了一个递归varargs串联器:

#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;
}

(我想你需要一个
wchar\u t*
版本,但变化应该是显而易见的。注意
malloc
)出于生产目的,递归可能应该被一个迭代版本所取代,该版本将遍历参数列表两次(请参见
va\u copy
),但我一直喜欢“那里和后面”递归模式

未指定同一函数的参数求值顺序。因此,如果传递多个具有副作用的表达式(例如函数调用),则可能会遇到问题。但在您的情况下,最多只能传递一个。我在块的第一个代码中看到的唯一问题是,如果
nDestLen
运行为0,则不设置0终止字节。如果(nDestLen==0)*pDest=0,我会在最后做
以确保目标存储了有效字符串。是否有原因不使用
strcat
snprintf
?这将使代码变得更小:
snprintf(tszDest,nDestLen,“%ls%ls%ls”,tszPath?tszPath:”,tszPath2?tszPath2:”,tszFileName?tszFileName:)@Pablo如果目标缓冲区长度为零,则假定没有空间放置任何内容,包括字符串结束符。这遵循了标准的
strncpy()
strncat()
@Pablo,以及安全版本的字符串操作函数、
strcat_()
和其他函数,目标缓冲区长度是预期的,这些函数的安全版本是目前预期的。至少VisualStudio会抱怨每一个
strcat()
sprintf()
,所以在这个特定的应用程序中,我定义了一个预处理器来关闭该检查。这种方法的一个原因是具有一个通用的字符串复制函数,可以跨用于构建文本字符串的函数使用该函数。这种方法也有一种我喜欢的函数式编程风格。这里是类似asprintf的函数:。但是我认为Windows版本已经没有必要了。谢谢你提供C标准参考。我很感谢您花时间研究字符串连接函数的变量参数版本。我用这个方法和其他一些东西,包括使用一个结束参数列表值,例如一个空指针,但是没有考虑考虑这个问题。我真的不想走一条
malloc()
的路线,也不想做需要的内务管理,因为这主要是在堆栈上和临时的。我不相信
snprintf()
是一个更好的选择,只是一个不同的选择。@richard:如果你不打算分配内存,那么snprintf的输入量就会少很多,但口味当然不同。瓦拉格的事情是C&P,所以我实际上没有花太多时间在上面。如果我有,我可能已经写了一个版本,采取缓冲区地址和长度,而不是做malloc;修改将是微不足道的。但这是一个厚颜无耻的解决方案;它来自于书桌抽屉里的“有趣的递归的东西”,这更具说教性
concat_list(pPath, pPath2, pFilename, (char*)0);