web-dev-qa-db-ja.com

C:文字列を連結するための最良かつ最速の方法は何ですか

現在、_string.h_ライブラリのstrcat()関数を使用して、cで文字列を連結しています。

私はそれについて考えました、そしてそれは非常に高価な関数であるはずであるという結論に達しました、それはそれが連結し始める前に、それは_'\0'_ charを見つけるまでchar配列を反復しなければならないからです。

たとえば、strcat()を使用して文字列_"horses"_を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_よりもはるかに低いため、このような連結を多数行うと、パフォーマンスにとって非常に重要になる可能性があります。

それを行うためのいくつかのより標準的な方法はありますか、私のソリューションよりも良く見えますか?

23

Joel Spolskyが彼の Back to Basics の記事で、strcatを非効率的な文字列連結の問題として説明しています画家のシュレミエルのアルゴリズム(記事を読んでください、それはかなり良いです)。非効率的なコードの例として、彼はO(n2)時間:

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 ");

最初の文字列を初めて歩くことは、実際には問題ではありません;すでに2番目の文字列をウォークする必要があるため、onestrcatの実行時間は、結果の長さに対して線形です。 strcatsが複数ある場合は、以前に連結した結果を何度も繰り返すため、問題があります。彼はこの代替案を提供します:

これをどのように修正しますか?数人の賢い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の設計者はこの問題を認識しており、文字列の最初のバイトにバイトカウントを格納することで「修正」しました。これらはPascal文字列と呼ばれます。ゼロを含めることができ、ヌルで終了しません。 1バイトは0〜255の数値しか格納できないため、Pascal文字列の長さは255バイトに制限されますが、nullで終了しないため、ASCIZ文字列と同じ量のメモリを使用します。 Pascal文字列の優れた点は、文字列の長さを計算するためだけにループする必要がないことです。 Pascalで文字列の長さを見つけることは、ループ全体ではなく、1つのアセンブリ命令です。それは記念碑的に速いです。

長い間、Pascal文字列リテラルをCコードに入れたい場合は、次のように記述する必要がありました。

char* str = "\006Hello!";

はい、手動でバイトをカウントし、文字列の最初のバイトにハードコーディングする必要がありました。怠惰なプログラマはこれを行い、遅いプログラムを持っています:

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

簡単、高速、一般的なand安全が必要な場合は、open_memstream()関数を使用することをお勧めします(これはPOSIX-2008標準の一部ですが、残念ながら、 C11標準だと思った)。それはこのように動作します:

まず、ポインタのアドレスとサイズを渡します

_char* result = NULL;
size_t resultSize = 0;
FILE* stream = open_memstream(&result, &resultSize);
_

戻り値は、fopen()を使用してファイルを開いたかのように、ファイルストリームです。そのため、fprintf()&coのすべての武器を使用できます。自動的に割り当てられ管理されるメモリバッファに、好きなものをストリーミングします。最も重要なのは、蓄積された文字列のサイズも追跡するため、サイズを計算するために再スキャンする必要がないことです。

_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);
_

複数の文字列を連結するために、コードは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'
}

これは遅い答えですが、同じ問題に遭遇しました。出発点を見つけるために、strcpystrncpystrlenstrnlenstrcatのマニュアルページをもう一度読むことにしました。およびstrncat

私はほとんどそれを見逃しましたが、幸い...開発システム(Debianストレッチ)の_man strcpy_に興味深い箇所があります。それを引用する(私のフォーマット):

strlcpy()

一部のシステム(BSD、Solaris、その他)は、次の機能を提供します。

_size_t strlcpy(char *dest, const char *src, size_t size);
_

この関数はstrncpy()に似ていますが、最大で_size-1_バイトをdestにコピーし、常に終了nullバイトを追加し、ターゲットに(さらに)nullを埋め込みませんバイト。この関数はstrcpy()およびstrncpy()の問題の一部を修正しますが、sizeが小さすぎる場合でも、呼び出し元はデータ損失の可能性を処理する必要があります。 関数の戻り値はsrcの長さで、切り捨てを簡単に検出できます。戻り値がsize以上の場合、切り捨て発生した。データの損失が問題になる場合、呼び出し元は呼び出しの前に引数を確認するか、関数の戻り値をテストする必要があります。 strlcpy()glibcには存在せず、POSIXによって標準化されていませんが、Linuxでlibbsdライブラリを介して使用できます。 =

はい、あなたはこの権利を読んでいます:glibc関数のmanページには、別のライブラリーにある標準化されていない関数のヒントが含まれており、より効果的です。これは、この問題がいかに重要であるかを証明するかもしれません。

ところで、str(n)cpy()関数の設計者がコピーされたバイト数または新しいendへのポインターを選択しなかった理由は決してわかりません。 destの戻り値として。これらの関数はパラメーターを変更しないため、destだけを返すのはばかげているように思われます。したがって、関数が戻ったときに呼び出し元がそれを知っているため、この選択は意味がありません。私は何か見落としてますか?

strlcpy()について知るまでは、@ Joshua Taylorが彼の回答で示したような、独自の文字列連結関数をほとんど使用していました。ただし、このアイデアには独自の問題があります。

文字列をバイト単位でスキャン/コピーすることは、非常に非効率的です。ターゲットCPUに応じて、32ビットまたは64ビットのレジスタを使用して、一度に複数のバイトをコピーする必要があります。もちろん、コピーするのに十分なバイトが残っているかどうかを確認する必要があるため、これにより関数がより複雑になります。そうでない場合は、次に小さいレジスタサイズを使用します。パフォーマンスをさらに向上させるには、アセンブリコードを使用して関数を実装する必要があります。

私の知る限り、glibcやlibbsdのようなライブラリはそのように実装されています。したがって、libbsd実装を使用するのが最善の場合があります。ただし、パフォーマンスの測定は行っていません。

2
Binarus

私はこのバリアントを使用していますが、これは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;
}
0
Rik Ruiter

これをチェックして

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();   
0
Adrian