我目前使用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
,因此如果您进行大量此类连接,它对性能非常关键。
有没有更标准的方法来做,看起来比我的解决方案更好?
答案 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)
假设您有两个字符串:s1
和s2
长度为l1
且l2
连接意味着您应生成一个长度为{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)
这是一个较晚的答案,但我遇到了同样的问题。为了找到起点,我决定重新阅读strcpy
,strncpy
,strlen
,strnlen
,strcat
和{{1} }。
我几乎错过了它,但是幸运的是……我的开发系统(Debian扩展版)上的strncat
中有一段有趣的段落。引用(格式化我的):
man strcpy
某些系统(BSD,Solaris和其他系统)提供以下内容 功能:
strlcpy()
此功能类似于
size_t strlcpy(char *dest, const char *src, size_t size);
,但最多复制strncpy()
个字节到size-1
,始终添加一个终止的空字节,并且不填充 具有(更多)空字节的目标。此功能修复了一些dest
和strcpy()
的问题,但呼叫者仍必须处理 如果strncpy()
太小,数据丢失的可能性。 返回值 函数的长度为size
,允许截断为 易于检测:如果返回值大于或等于src
, 发生截断。如果数据丢失很重要,则呼叫者必须 在调用之前检查参数,或测试函数返回 值。size
在strlcpy()
中不存在,并且没有被标准化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;
}