少し前に、コンピューターサイエンスカレッジの教師の1人が関与した、かなり興味深い事件が私に起こりました。
Linuxプログラミングについての会話は、この人がシステムプログラミングの複雑さは実際には非常に誇張されていると主張し始めたという事実に徐々に変わりました。 C言語は、実際にはLinuxカーネルのように(彼の言葉で)一致するように単純です。
私はLinuxを搭載したラップトップを持っていて、そこにはC言語(gcc、vim、make、valgrind、gdb)で開発するための紳士用のユーティリティセットがありました。 そのときどのような目標を設定したか覚えていませんが、数分後、対戦相手はこのラップトップの後ろにいて、問題を完全に解決する準備ができていました。
そして、文字通り最初の行で、メモリを...行の下に割り当てるときに、彼は重大な間違いを犯しました。
char *str = (char *)malloc(sizeof(char) * strlen(buffer));
バッファ-キーボードからのデータが入力されたスタック変数。
「これに何か問題はありますか?」と尋ねる人が必ずいると思います。
多分私を信じて。
そして何を正確に-カットで読んでください。
ちょっとした理論-一種のFaceWithout。
わかっている場合は、次のヘッダーまでスクロールします。
Cの文字列は文字の配列であり、常に適切な '\ 0'-行末文字で終わる必要があります。 スタック上の行(静的)は、次のように宣言されます。
char str[n] = { 0 };
nは文字配列のサイズで、文字列の長さと同じです。
割り当て{0}-行の「ゼロ化」(オプション、それなしで宣言できます)。 結果は、memset(str、0、sizeof(str))およびbzero(str、sizeof(str))関数の実行と同じです。 ガベージが初期化されていない変数に存在するのを防ぐために使用されます。
スタック上でも、すぐに行を初期化できます。
char buf[BUFSIZE] = "default buffer text\n";
また、文字列をポインターとして宣言し、ヒープ(ヒープ)にメモリを割り当てることができます。
char *str = malloc(size);
size-文字列に割り当てるバイト数。 このような行は動的と呼ばれます(目的のサイズが動的に計算されるため、割り当てられたメモリサイズはrealloc()関数を使用していつでも増やすことができます)。
スタック変数の場合、表記法nを使用して配列のサイズを決定し、ヒープ上の変数の場合、表記法サイズを使用しました。 そして、これは、スタック上のアナウンスメントとヒープ上のメモリ割り当てを伴うアナウンスメントの違いの真の本質を完全に反映しています。これは、要素数について話すときに通常nが使用されるためです。 そして、サイズはまったく別の話です...
私は思う。 今のところ十分です。 どうぞ
Valgrindは私たちを助けます
前の記事で、彼についても言及しました。 Valgrind ( 時間 -wiki 記事 、 2-小さなハウツー )は、プログラマがメモリリークとコンテキストエラーを追跡するのに役立つ非常に便利なプログラムです。これらは、文字列を操作するときに最も頻繁に表示されるものです。
私が言及したプログラムに似たものを実装する小さなリストを見て、valgrindで実行してみましょう。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define HELLO_STRING "Hello, Habr!\n" void main() { char *str = malloc(sizeof(char) * strlen(HELLO_STRING)); strcpy(str, HELLO_STRING); printf("->\t%s", str); free(str); }
そして、実際には、プログラムの結果:
[indever@localhost public]$ gcc main.c [indever@localhost public]$ ./a.out -> Hello, Habr!
これまでのところ、異常なことは何もありません。 では、valgrindを使用してこのプログラムを実行しましょう。
[indever@localhost public]$ valgrind --tool=memcheck ./a.out ==3892== Memcheck, a memory error detector ==3892== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==3892== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info ==3892== Command: ./a.out ==3892== ==3892== Invalid write of size 2 ==3892== at 0x4005B4: main (in /home/indever/prg/C/public/a.out) ==3892== Address 0x520004c is 12 bytes inside a block of size 13 alloc'd ==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== ==3892== Invalid read of size 1 ==3892== at 0x4C30BC4: strlen (vg_replace_strmem.c:454) ==3892== by 0x4E89AD0: vfprintf (in /usr/lib64/libc-2.24.so) ==3892== by 0x4E90718: printf (in /usr/lib64/libc-2.24.so) ==3892== by 0x4005CF: main (in /home/indever/prg/C/public/a.out) ==3892== Address 0x520004d is 0 bytes after a block of size 13 alloc'd ==3892== at 0x4C2DB9D: malloc (vg_replace_malloc.c:299) ==3892== by 0x400597: main (in /home/indever/prg/C/public/a.out) ==3892== -> Hello, Habr! ==3892== ==3892== HEAP SUMMARY: ==3892== in use at exit: 0 bytes in 0 blocks ==3892== total heap usage: 2 allocs, 2 frees, 1,037 bytes allocated ==3892== ==3892== All heap blocks were freed -- no leaks are possible ==3892== ==3892== For counts of detected and suppressed errors, rerun with: -v ==3892== ERROR SUMMARY: 3 errors from 2 contexts (suppressed: 0 from 0)
== 3892 ==すべてのヒープブロックが解放されました-リークは発生しません-リークはありません 。 ただし、目を少し下げる価値があります(ただし、これは結果に過ぎず、基本的な情報は少し異なります)。
== 3892 ==エラー概要:2つのコンテキストから3つのエラー(抑制:0から0)
3エラー。 2つのコンテキスト。 このような単純なプログラムで。 どうやって!?
はい、とても簡単です。 全体の「トリック」は、strlen関数が行末文字「\ 0」を考慮しないことです。 入力行で明示的に指定されていても(#define HELLO_STRING "Hello、Habr!\ N \ 0")、無視されます。
プログラム実行の結果よりもわずかに高い行-> Hello、Habr! 私たちの貴重なバルグラインドが好きではなかった場所と場所の詳細なレポートがあります。 これらの線を独立して見て、結論を出すことを提案します。
実際には、プログラムの正しいバージョンは次のようになります。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define HELLO_STRING "Hello, Habr!\n" void main() { char *str = malloc(sizeof(char) * (strlen(HELLO_STRING) + 1)); strcpy(str, HELLO_STRING); printf("->\t%s", str); free(str); }
valgrindを通過します。
[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==3435== ==3435== HEAP SUMMARY: ==3435== in use at exit: 0 bytes in 0 blocks ==3435== total heap usage: 2 allocs, 2 frees, 1,038 bytes allocated ==3435== ==3435== All heap blocks were freed -- no leaks are possible ==3435== ==3435== For counts of detected and suppressed errors, rerun with: -v ==3435== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
素晴らしい。 エラーはありません。割り当てられたメモリの+1バイトが問題の解決に役立ちました。
興味深いことに、ほとんどの場合、最初と2番目のプログラムは同じように動作しますが、終了文字が収まらない行に割り当てられたメモリがゼロでない場合、printf()関数は、そのような行が出力されると、この行の-行末文字がprintf()のパスに現れるまで、すべてが表示されます。
ただし、(strlen(str)+ 1)はそのような解決策です。 2つの問題に直面しています。
- そして、例えばs(n)printf(..)で形成された行にメモリを割り当てる必要がある場合は? 引数をサポートしていません。
- 外観 変数宣言のある行はひどく見えます。 mallocの一部の人(char *)も、プラスの下に書くように固定することができます。 定期的に文字列を処理する必要があるプログラムでは、よりエレガントなソリューションを見つけることは理にかなっています。
私たちを満足させ、心を痛めるソリューションを考え出しましょう。
snprintf()
int snprintf(char *str, size_t size, const char *format, ...);
-関数-sprintf拡張。文字列をフォーマットし、最初の引数として渡されたポインターに従ってそれを書き込みます。 sprintf()と異なる点は、サイズで指定されたよりも多くのバイトがstrに書き込まれないことです。
この関数には興味深い機能が1つあります-いずれの場合でも、生成された文字列のサイズを返します(行末文字を考慮せずに)。 文字列が空の場合、0が返されます。
strlenを使用して説明した問題の1つは、sprintf()およびsnprintf()関数に関連しています。 文字列strに何かを書き込む必要があるとします。 最後の行には、他の変数の値が含まれています。 エントリは次のようになります。
char * str = /* */; sprintf(str, "Hello, %s\n", "Habr!");
問題は、文字列strに割り当てるメモリ量を決定する方法ですか?
char * str = malloc(sizeof(char) * (strlen(str, "Hello, %s\n", "Habr!") + 1));
-乗車ではありません。 strlen()のプロトタイプは次のようになります。
#include <string.h> size_t strlen(const char *s);
const char * sは、sに渡される文字列が可変数の引数を持つ書式文字列であることを意味しません。
ここで、上記で説明したsnprintf()関数の有用なプロパティに助けられます。 次のプログラムのコードを見てみましょう。
#include <stdio.h> #include <stdlib.h> #include <string.h> void main() { /* .. snprintf() , */ size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0'); char *str = malloc(needed_mem); snprintf(str, needed_mem, "Hello, %s!\n", "Habr"); printf("->\t%s", str); free(str); }
valgrindでプログラムを実行します。
[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==4132== ==4132== HEAP SUMMARY: ==4132== in use at exit: 0 bytes in 0 blocks ==4132== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated ==4132== ==4132== All heap blocks were freed -- no leaks are possible ==4132== ==4132== For counts of detected and suppressed errors, rerun with: -v ==4132== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) [indever@localhost public]$
素晴らしい。 持っている引数のサポート。 snprintf()関数の2番目の引数としてゼロを渡すという事実により、nullポインターへの書き込みがSeagfaultにつながることはありません。 ただし、これにもかかわらず、関数は文字列に必要なサイズを返します。
しかし一方で、追加の変数を作成する必要があり、設計
size_t needed_mem = snprintf(NULL, 0, "Hello, %s!\n", "Habr") + sizeof('\0');
strlen()よりもさらに悪く見えます。
一般に、フォーマット文字列の最後に明示的に「\ 0」を指定すると、+ sizeof( '\ 0')を削除できます(size_t needed_mem = snprintf(NULL、0、“ Hello、%s!\ N \ 0 ”、“ Habr”) ;)、しかし、これは常に可能というわけではありません(文字列処理メカニズムに応じて、余分なバイトを割り当てることができます)。
何かする必要があります。 私は少し考えて、今が古代人の知恵に訴える時だと決めました。 最初の引数としてヌルポインターを、2番目の引数としてゼロを指定してsnprintf()を呼び出すマクロ関数について説明します。 そして、我々は行の終わりを忘れないでしょう!
#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')
はい、それは誰かにとってはニュースかもしれませんが、Cのマクロは可変数の引数をサポートし、省略記号は指定されたマクロ関数引数(この場合はargs)がいくつかの実引数に対応することをプリプロセッサに伝えます。
ソリューションを実際にテストしてみましょう。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0') void main() { char *str = malloc(strsize("Hello, %s\n", "Habr!")); sprintf(str, "Hello, %s\n", "Habr!"); printf("->\t%s", str); free(str); }
valgrundから始めます。
[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==6432== ==6432== HEAP SUMMARY: ==6432== in use at exit: 0 bytes in 0 blocks ==6432== total heap usage: 2 allocs, 2 frees, 1,041 bytes allocated ==6432== ==6432== All heap blocks were freed -- no leaks are possible ==6432== ==6432== For counts of detected and suppressed errors, rerun with: -v ==6432== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
はい、エラーはありません。 すべてが正しいです。 そして、valgrindは幸せで、プログラマーはついに眠りにつくことができます。
しかし、最後に、別のことを言います。 (引数があっても)任意の行にメモリを割り当てる必要がある場合は、すでに完全に機能する既製のソリューションがあります。
これはasprintf関数です。
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <stdio.h> int asprintf(char **strp, const char *fmt, ...);
最初の引数として、文字列(** strp)へのポインターを受け取り、参照解除されたポインターにメモリを割り当てます。
asprintf()を使用して記述されたプログラムは次のようになります。
#include <stdio.h> #include <stdlib.h> #include <string.h> void main() { char *str; asprintf(&str, "Hello, %s!\n", "Habr"); printf("->\t%s", str); free(str); }
そして、実際には、valgrindで:
[indever@localhost public]$ valgrind --tool=memcheck ./a.out -> Hello, Habr! ==6674== ==6674== HEAP SUMMARY: ==6674== in use at exit: 0 bytes in 0 blocks ==6674== total heap usage: 3 allocs, 3 frees, 1,138 bytes allocated ==6674== ==6674== All heap blocks were freed -- no leaks are possible ==6674== ==6674== For counts of detected and suppressed errors, rerun with: -v ==6674== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
すべて問題ありませんが、ご覧のとおり、すべてのメモリがより多く割り当てられ、allocsは2つではなく3つになりました。 弱い組み込みシステムでは、この機能の使用は望ましくありません。
さらに、コンソールでman asprintfと記述すると、次のように表示されます。
CONFORMING TO These functions are GNU extensions, not in C or POSIX. They are also available under *BSD. The FreeBSD implementation sets strp to NULL on error.
これにより、この機能がGNUソースでのみ利用可能であることが明らかになります。
おわりに
結論として、Cでの文字列の操作は非常に複雑なトピックであり、多くのニュアンスがあります。 たとえば、メモリを動的に割り当てるときに「安全な」コードを作成するには、malloc()の代わりにcalloc()関数を使用することをお勧めします。callocは割り当てられたメモリをゼロで詰まらせます。 さて、またはメモリを割り当てた後、memset()関数を使用します。 そうしないと、割り当てられたメモリに元々置かれていたガベージが、デバッグ時、および場合によっては文字列の操作時に質問を引き起こす可能性があります。
私のリクエストで文字列にメモリを割り当てる問題を解決した私の馴染みのあるCプログラマーの半分以上(ほとんど初心者)は、最終的にコンテキストエラーを引き起こしました。 ある場合には-メモリーリークが発生した場合でも(まあ、人が解放するのを忘れた(str)、彼とは一緒にいない)。 実際、これは私があなたが今読んだこの創造物を作成するよう促しました。
誰かがこの記事が役立つことを願っています。 なぜ私はこれをすべてトラブルに巻き込んだのですか-簡単な言葉はありません どこにも独自の微妙さがあります。 そして、あなたが知っている言語の繊細さがあればあるほど、コードは良くなります。
この記事を読んだ後、あなたのコードは少し良くなると思います:)
頑張って、ハブル!