C:连接字符串的最佳和最快方法是什么

时间:2014-02-19 12:38:51

标签: c string performance concatenation

我目前使用strcat()库中的string.h函数连接c中的字符串。

我想到了,我得出的结论是它应该是非常昂贵的函数,因为它开始连接之前,它必须遍历char数组,直到它找到'\0' char。

例如,如果我使用"horses"将字符串strcat()连接1000次,我将需要付费 (1 + 2 + 3 + ... + 1000) * strlen("horses") = (1000*1001)/2 * 6 = 3003000

我想到了非标准的方法,维护一个字符串长度的整数,然后发送到strcat()指向字符串末尾的指针:

strcat(dest + dest_len, "string");

在这种情况下,我只需支付1000 * strlen("horses") = 1000 * 6 = 6000

6000远低于3003000,因此如果您进行大量此类连接,它对性能非常关键。

有没有更标准的方法来做,看起来比我的解决方案更好?

9 个答案:

答案 0 :(得分:26)

Joel Spolsky在他的Back to Basics文章中描述了使用strcat作为 Shlemiel the painter算法的低效字符串连接问题(阅读文章,它非常好) 。作为低效代码的一个例子,他给出了这个在O(n 2 )时间运行的例子:

char bigString[1000];     /* I never know how much to allocate... */
bigString[0] = '\0';
strcat(bigString,"John, ");
strcat(bigString,"Paul, ");
strcat(bigString,"George, ");
strcat(bigString,"Joel ");

第一次遍历第一个字符串并不是一个真正的问题;因为我们必须遍历第二个字符串,所以 one strcat的运行时间在结果的长度上是线性的。但是多个strcat是有问题的,因为我们一次又一次地遍历先前连接的结果。他提供了这种选择:

  

我们如何解决这个问题?一些聪明的C程序员实现了自己的   mystrcat如下:

char* mystrcat( char* dest, char* src )
{
     while (*dest) dest++;
     while (*dest++ = *src++);
     return --dest;
}
     

我们在这做了什么?只需很少的额外费用我们就会回来了   指向新的更长字符串结尾的指针。那样的代码   调用此函数可以决定在不重新扫描的情况下进一步追加   字符串:

char bigString[1000];     /* I never know how much to allocate... */
char *p = bigString;
bigString[0] = '\0';
p = mystrcat(p,"John, ");
p = mystrcat(p,"Paul, ");
p = mystrcat(p,"George, ");
p = mystrcat(p,"Joel ");
     

这当然是线性的,而不是n平方,所以它   当你有很多东西时,它不会遭受退化   串连。

当然,如果您想使用标准C字符串,这就是您可以做的。您正在描述缓存字符串长度并使用特殊连接函数(例如,使用稍微不同的参数调用strcat)的替代方法是对Pascal字符串的一种变体,Joel也提到过:

  

Pascal的设计者意识到了这个问题,并“修复”了它   在字符串的第一个字节中存储字节数。这些被称为   帕斯卡尔弦乐队。它们可以包含零并且不会以空值终止。   因为一个字节只能存储0到255之间的数字,Pascal   字符串的长度限制为255个字节,但因为它们不是   null终止它们占用与ASCIZ相同的内存量   字符串。关于Pascal字符串的好处是你从来没有   有一个循环只是为了弄清楚你的字符串的长度。查找   Pascal中字符串的长度是一条汇编指令   整个循环。它的速度更快。

     

...

     

很长一段时间,如果你想把Pascal字符串文字放在你的   C代码,你必须写:

char* str = "\006Hello!";
     

是的,你必须自己手动计算字节数,并对其进行硬编码   进入字符串的第一个字节。懒惰的程序员会这样做,   并且程序缓慢:

char* str = "*Hello!";
str[0] = strlen(str) - 1;

答案 1 :(得分:12)

如果你想要它简单,快速,一般,安全,我建议使用open_memstream()功能(它是POSIX-2008标准的一部分,不幸的是它没有成功进入C11标准,思考)。它的工作原理如下:

首先,你把指针的地址和大小

交给它
char* result = NULL;
size_t resultSize = 0;
FILE* stream = open_memstream(&result, &resultSize);

返回值是一个文件流,就像您使用fopen()打开文件一样。因此,您可以使用fprintf()&的整个库。合。将您喜欢的任何内容流式传输到内存缓冲区,并自动为您分配和管理。最重要的是,它还会跟踪累积字符串的大小,因此不必重新扫描它来计算其大小。

for(int i = 0; i < 1000000; i++) {
    fprintf(stream, "current number is %d, or 0x%x\n", i, i);
}

最后,关闭流,它将更新结果指针和大小变量,以反映写入的实际字符串数据量。

fclose(stream);
//Now you have a zero terminated C-string in result, and also its size in resultSize.
//You can do with it whatever you like.
//Just remember to free it afterwards:
free(result);

答案 2 :(得分:2)

要连接多个字符串,代码可以使用strlen()memcpy(),它们通常都是经过优化的函数。

使用这种方法,可以轻松添加便宜的size限制 鉴于目标缓冲区可能会溢出的可能性,否则大小限制是必不可少的。

与字符串长度之和成比例的运行时间:O(len(S [0])+ len(S [1])+ len(S [2])+ ...)

char *strsncat(char *dest, size_t size, char * strs[], size_t n) {
  assert(size > 0);
  size--;
  char *p = dest;
  while (n-- > 0) {
    size_t len = strlen(*strs);
    if (len >= size) {
      len = size;
    }
    size -= len;
    memcpy(p, *strs, len);
    strs++;
    p += len;
  }
  *p = '\0';
  return dest;
}

void cat_test(void) {
  char dest[10];
  char *strs[]  = { "Red", "Green", "Blue" };
  printf("'%s'\n",strsncat(dest, sizeof dest, strs, sizeof strs/sizeof strs[0]));
  // 'RedGreenB'
}

答案 3 :(得分:1)

假设您有两个字符串:s1s2长度为l1l2连接意味着您应生成一个长度为{s3的新字符串l1+l2 {1}}。此操作的时间复杂度为O(l1+l2)。从这个角度来看,strcat()似乎是最好的选择。

但是,如果要指示连接两个字符串的状态,则只需记录其指针O(1)。一个简单的例子是这样的:

typedef struct ConcatStr {
    char* str1;
    char* str2;
} ConcatStr;
ConcatStr myStrcat( char* str1, char* str2 )
{
    ConcatStr cstr;
    cstr.str1 = str1;
    cstr.str2 = str2;
}

答案 4 :(得分:1)

这是一个较晚的答案,但我遇到了同样的问题。为了找到起点,我决定重新阅读strcpystrncpystrlenstrnlenstrcat和{{1} }。

我几乎错过了它,但是幸运的是……我的开发系统(Debian扩展版)上的strncat中有一段有趣的段落。引用(格式化我的):

  

man strcpy

     

某些系统(BSD,Solaris和其他系统)提供以下内容   功能:

strlcpy()
     

此功能类似于size_t strlcpy(char *dest, const char *src, size_t size); ,但最多复制strncpy()   个字节到size-1,始终添加一个终止的空字节,并且不填充   具有(更多)空字节的目标。此功能修复了一些   deststrcpy()的问题,但呼叫者仍必须处理   如果strncpy()太小,数据丢失的可能性。 返回值   函数的长度为size ,允许截断为   易于检测:如果返回值大于或等于src,   发生截断。如果数据丢失很重要,则呼叫者必须   在调用之前检查参数,或测试函数返回   值。 sizestrlcpy()中不存在,并且没有被标准化   glibc,但可通过POSIX库在Linux上使用。

是的,您正在阅读以下内容:glibc函数的手册页包含另一个库中非标准化函数的提示,该函数做得更好。这可能证明这个问题有多重要。

顺便说一句,我永远不会明白为什么libbsd函数的设计者没有选择复制的字节数或指向{{1的新 end 的指针}}作为返回值。仅返回str(n)cpy()似乎很愚蠢,因为这些函数不会更改该参数,因此在每种情况下,函数返回时调用者仍然知道它,因此此选择没有任何意义。我错过了什么吗?

直到我了解dest之前,我主要使用自己的字符串连接功能,例如@Joshua Taylor的答案中已经显示了该内容。但是,这个想法有其自身的问题:

逐字节扫描/复制字符串可能效率很低。根据目标CPU,我们应该使用32位甚至64位寄存器,并一次复制多个字节。当然,这使函数更加复杂,因为我们必须检查是否还有足够的字节要复制,否则,请使用下一个更小的寄存器大小。为了进一步提高性能,我们应该使用汇编代码来实现我们的功能。

AFAIK,像glibc和libbsd这样的库都以这种方式实现。因此,最好使用libbsd实现。不过,我还没有进行性能评估。

答案 5 :(得分:0)

我使用这个变体,它更像是strcat的替代品,但不完全是:

char* mystrcat(char** dest, const char* src) {

    int i = 0;
    char cur;
    while(1) {
        cur = src[i];
        (*dest)[i] = cur;
        if(cur == 0) break;
        i++;
    }

    *dest += i;

    return *dest;
}

这里的返回值并不重要。 char数组char str[32]不包含指向字符的实际指针的存储(以再次获取指针),因此您可以这样做:

char str[32];
char* pStr = str; //storage for pointer
mystrcat(&pStr, "bla");
mystrcat(&pStr, "de");
mystrcat(&pStr, "bla\n");
printf(str);

myfunction(char* pStr) {

    mystrcat(&pStr, "bla");
    mystrcat(&pStr, "de");
    mystrcat(&pStr, "bla\n");
}

char str[32];
myfunction(str);
printf(str);

因为现在在myfunction()的堆栈上创建了指针的存储空间。

长度受限的版本是:

char* mystrcat(char** dest, const char* src, int max) {

    int i = 0;
    char cur;
    while(1) {
        if(i == max) {
            (*dest)[i] = 0;
            break;
        }
        cur = src[i];
        (*dest)[i] = cur;
        if(cur == 0) break;
        i++;
    }

    *dest += i;

    return *dest;
}

答案 6 :(得分:0)

检查

https://john.nachtimwald.com/2017/02/26/efficient-c-string-builder/

它帮助我在眨眼之间将char **复制到剪贴板

    str_builder_t *sb;
     sb = str_builder_create();

                        int colcnt=0;
                        for (int i=0;i<nrF;i++)  // nrF = number of Fileds 
                    {
                            //strcat(DATA,sqlite_array[i]);
                     str_builder_add_str(sb, sqlite_array[i], 0); 
                            if (colcnt<nrofcolumns)  // my list view 
                                {
                            str_builder_add_str(sb, "\t", 0); 
                                colcnt++;

                            }
                                if (colcnt==nrofcolumns) 
                            {

                            str_builder_add_str(sb, "\n", 0); 
                                    colcnt=0;
                            }

                    }

    HANDLE  glob =GlobalAlloc(GMEM_FIXED,str_builder_len(sb)+1);
    memcpy(glob,str_builder_peek(sb),str_builder_len(sb)+1);
    OpenClipboard(NULL);
    EmptyClipboard();
    SetClipboardData(CF_TEXT,glob);
    CloseClipboard();   

答案 7 :(得分:0)

这就是我所做的,它比 strcat 更快,但我不知道它与其他解决方案相比如何。假设您有一个包含 1000 个字符串的数组,并且想要将它们用空格连接起来,并且您有一个 100,000 个字符的缓冲区来保存它。

int L=0;
char buffer[100000];
char *str[1000]; // assume this is already populated
for (int i=0; i<1000; i++) // 1000 or whatever number you actually have
{
 L+=sprintf(buffer+L,"%s ",str[i]); // this is the important part
}

sprintf 将返回写入的字符数,并不断前进指针缓冲区+L。 这没有任何安全检查。您可以检查 L 是否超过 100000,但这取决于您。如果 buffer+L 超出字符串的末尾,它会使您的应用程序崩溃。

答案 8 :(得分:0)

这是一个简单安全高效的连接函数:

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

char *strwrite(char *dest, size_t size, size_t *ppos, const char *src) {
    size_t pos = *ppos;
    if (pos < size) {
        size_t len = strlen(src);
        if (pos + len < size)
            memcpy(dest + pos, src, len + 1);
            *ppos += len;
        } else {
            memcpy(dest + pos, src, size - pos - 1);
            dest[size - 1] = '\0';
            *ppos = size - 1;
        }
    }
    return dest;
}

int main() {
    char dest[10];
    size_t pos = 0;
    for (int i = 0; i < 3; i++) {
        strwrite(dest, sizeof dest, &pos, "Test");
    }
    printf("%s\n", dest);   // TestTestT
    return 0;
}

在 POSIX 系统上,可以使用 strnlen() 简化代码:

char *strwrite(char *dest, size_t size, size_t *ppos, const char *src) {
    size_t pos = *ppos;
    if (pos < size) {
        size_t len = strnlen(src, size - pos - 1);
        memcpy(dest + pos, src, len);
        pos += len;
        dest[pos] = '\0';
        *ppos = pos;
    }
    return dest;
}