web-dev-qa-db-ja.com

Cライブラリの関数は常に文字列の長さを期待するべきですか?

私は現在、Cで書かれたライブラリに取り組んでいます。このライブラリの多くの関数は、引数に_char*_または_const char*_の文字列を必要とします。私は、文字列の長さが_size_t_であると常に想定している関数から始めたため、null終了は必要ありませんでした。ただし、テストを作成する場合、次のようにstrlen()が頻繁に使用されます。

_const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));
_

適切に終了した文字列を渡すことをユーザーに信頼すると、安全性は低下しますが、コードはより簡潔で(私の意見では)読みやすくなります。

_libFunction("I hope there's a null-terminator there!");
_

それで、ここで賢明な習慣は何ですか? APIの使用をより複雑にしますが、ユーザーに入力を考えさせるか、ヌル終了文字列の要件を文書化して呼び出し元を信頼しますか?

15

最も明確かつ絶対的に長さを持ちます。標準Cライブラリは、この方法で悪名高く壊れており、バッファオーバーフローの処理に終わりがありません。このアプローチは非常に憎悪と苦痛の焦点であり、現代のコンパイラーはこの種の標準ライブラリー関数を使用するときに実際に警告、口笛、不平を言うでしょう。

面接でこの質問に出くわしたことがあり、テクニカルインタビュアーが数年の経験を持っているように見える場合-純粋な熱心さが仕事を着陸させる可能性があるので、引用できれば、実際にはかなり先を行くことができます。 shootingの前例で、C文字列ターミネータを探すAPIを実装する誰か。

感情をすべて残しておくと、文字列の最後のNULLは、読み取りと操作の両方で問題が発生する可能性があります。さらに、多層防御などの最新の設計概念に直接違反しています。 (必ずしもセキュリティには適用されませんが、API設計に適用されます)。長さが豊富なC APIの例-例。 Windows API。

実際、この問題は90年代に解決されました。今日の新たなコンセンサスは 文字列には触れないでください です。

後の編集:これは非常に活発な議論ですので、上と下のすべての人にニースであると信頼して、ライブラリstr *関数を使用することを追加しますOK、output = malloc(strlen(input)); strcpy(output, input);while(*src) { *dest=transform(*src); dest++; src++; }などの古典的なものが表示されるまで。モーツァルトのラクリモサが背景で聞こえそうです。

4
vski

Cでは、イディオムは文字列がNULで終了するということです。そのため、一般的な慣習を順守することは理にかなっています-ライブラリのユーザーが非NULで終了する文字列を持つことは実際には比較的ありません(これらは印刷するために追加の作業が必要なため) printfを使用し、他のコンテキストで使用します)。他の種類の文字列を使用するのは不自然で、おそらく比較的まれです。

また、この状況では、テストが少し奇妙に見えます。なぜなら、(strlenを使用して)正しく機能するために、最初にNULで終了する文字列を想定しているからです。ライブラリを使用する場合は、NULで終了しない文字列のケースをテストする必要があります。

16
James McLeod

あなたの「安全」論は実際には成り立ちません。あなたが文書化したものであるときにユーザーがnullで終了する文字列を渡すことを信頼していない場合(そしてプレーンCの「標準」とは何ですか)、彼らがあなたに与える長さを本当に信頼することはできません(彼らはおそらくstrlenを使用することで、手元にない場合と同じように使用できます。「文字列」が文字列でない場合は失敗します)。

ただし、長さを必要とする正当な理由があります。関数に部分文字列を処理させる場合、長さを渡す方が、ユーザーにコピーマジックを行ってnullバイトを取得させるよりもはるかに簡単(かつ効率的)です。適切な場所(および途中で1つずれるエラーのリスク)。
nullバイトが終端ではないエンコーディングを処理できること、またはnullが埋め込まれている文字列を(意図的に)処理できることは、状況によっては役立ちます(関数の実行内容によって異なります)。
nullで終了しないデータ(固定長配列)を処理できることも便利です。
簡単に言うと、ライブラリで何をしているか、およびユーザーが処理することを期待しているデータの種類によって異なります。

これにはおそらくパフォーマンスの側面もあります。関数が文字列の長さを事前に知る必要があり、ユーザーが少なくとも通常はその情報をすでに知っていることを期待している場合、(計算するのではなく)それらを渡すことで数サイクルを削ることができます。

ただし、ライブラリが通常のプレーンなASCIIテキスト文字列を想定しており、パフォーマンスの制約があまりなく、ユーザーがライブラリと対話する方法を十分に理解している場合は、長さパラメータを追加しても良い考えのように聞こえます。文字列が適切に終端されていない場合は、長さパラメータが偽のようになる可能性があります。

10
Mat

いいえ。文字列は常に定義によりnullで終了します。文字列の長さは冗長です。

Nullで終了しない文字データを「文字列」と呼ぶことはできません。それを処理する(そして長さを投げる)通常 APIの一部ではなく、ライブラリ内にカプセル化する必要があります。単一のstrlen()呼び出しを回避するためだけにパラメーターとして長さを要求することは、おそらく時期尚早の最適化です。

API関数の呼び出し元を信頼することはnsafeではありません。文書化された前提条件が満たされていない場合、未定義の動作は完全に問題ありません。

もちろん、適切に設計されたAPIには落とし穴を含めず、正しく簡単に使用できるようにする必要があります。そしてこれは、冗長性を避け、言語の慣習に従うことで、可能な限りシンプルでわかりやすいものにする必要があることを意味します。

2
dpi

常に長さを保つ必要があります。 1つは、ユーザーがNULLを含めることを望む場合です。 2番目に、strlenはO(N)であり、文字列全体のバイバイキャッシュに触れる必要があることを忘れないでください。3番目に、サブセットを簡単に渡すことができます。 -たとえば、実際の長さよりも短くなる可能性があります。

1
DeadMG

stringの前後とbufferの前後を区別する必要があります。

Cでは、文字列は伝統的にNULで終了します。これを期待することは完全に合理的です。したがって、通常は文字列の長さを渡す必要はありません。必要に応じて、strlenで計算できます。

buffer、特に書き込まれているものを渡すときは、絶対にバッファーサイズを渡す必要があります。宛先バッファの場合、これにより、呼び出し先は、バッファがオーバーフローしないようにすることができます。入力バッファーの場合、特に信頼できないソースからの任意のデータが入力バッファーに含まれている場合は特に、呼び出し先が最後を超えて読み取ることを回避できます。

文字列とバッファの両方がchar*である可能性があり、多くの文字列関数が宛先バッファに書き込むことによって新しい文字列を生成するため、おそらくいくつかの混乱があります。その後、一部の人々は、文字列関数は文字列の長さを取る必要があると結論付けます。ただし、これは不正確な結論です。バッファーにサイズを含めること(そのバッファーが文字列、整数の配列、構造体などに使用されるかどうか)は、より有用で一般的なマントラです。

(信頼できないソース(ネットワークソケットなど)から文字列を読み取る場合、入力がNULで終了しない可能性があるため、長さを指定することが重要です。ただしnot入力を文字列と見なしますmight文字列を含む任意のデータバッファーとして扱う必要があります(ただし、実際に検証するまでわかりません)。それでも、バッファにはサイズが関連付けられている必要があり、文字列にはサイズが必要ないという原則に従います)。

1
jamesdlin

関数が主に文字列リテラルで使用される場合、いくつかのマクロを定義することにより、明示的な長さを処理する手間を最小限に抑えることができます。たとえば、API関数が与えられた場合:

void use_string(char *string, int length);

マクロを定義できます:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

次に示すようにそれを呼び出します。

void test(void)
{
  use_strlit("Hello");
}

コンパイルされても実際には機能しないマクロを渡すために「クリエイティブ」なものを思いつくことは可能かもしれませんが、「sizeof」の評価内の文字列の両側で""を使用すると、分解された文字列リテラル以外の文字ポインターを誤って使用しようとした場合[これらの""がない場合、文字ポインターを渡そうとすると、誤って長さがポインターのサイズから1を引いたものになります。

C99の代替アプローチは、「ポインターと長さ」の構造タイプを定義し、文字列リテラルをその構造タイプの複合リテラルに変換するマクロを定義することです。例えば:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

このようなアプローチを使用する場合、アドレスを渡すのではなく、値でそのような構造を渡す必要があることに注意してください。それ以外の場合:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

複合リテラルの存続期間はそれらを囲むステートメントの終わりで終了するため、失敗する可能性があります。

0
supercat