連載
» 2012年11月30日 12時00分 UPDATE

目指せ! Cプログラマ(13):文字操作・入出力ライブラリの使い方を理解しよう (1/2)

文字を操作する関数、入出力に関する関数など、Cでよく使われるライブラリについて理解を深めよう。

[長沼立巳, 小山博史,SSS(G)/ガリレオ]

Cの標準ライブラリとはどんな存在か?

 プログラミング言語Cでは、文法の他に、よく使われる機能をまとめたライブラリについて規定されています。ほとんどの環境ではすぐにこのライブラリを使うことができます。このライブラリについて特に呼び名はないのですが、ここでは標準ライブラリと呼ぶことにします。

 ライブラリとは、ある特定の分野の問題を解決するための機能について、型や関数などをまとめたものです。例えばCの標準ライブラリには文字や文字列を操作するためのライブラリがあり、その中には文字列の長さを知りたいときに使う関数や文字列連結のための関数などが含まれています。

 Cで使えるライブラリは、標準ライブラリだけではありません。開発環境によっては、標準ライブラリ以外にも、ターゲットとする実行環境に適したライブラリが用意されていることがあります。ネット上でも数多くのライブラリが配布されています。

 特定の分野の問題を解決するには、そういった標準ライブラリ以外のライブラリ、いわゆるサードパーティライブラリを用いたほうが便利な場合も多いでしょう。ただしそのような場合でも、標準ライブラリについての知識は無駄にはなりません。多くのライブラリは標準ライブラリの機能を使っていたり、またはその知識を前提として設計されている場合が多いからです。

 今回と次回は、その標準ライブラリについて説明します。

 なお、今回紹介する機能の一部は新しいCの規格によるもので、環境の制限によっては使えないものもあります。詳しくは環境ごとのマニュアルを参照してください。

Cにおける文字列とは?

 ライブラリの説明に入る前に、文字列について説明しておきましょう。この連載の中でもすでに文字列という言葉は出てきていますが、Cにおける文字列とはどのようなものなのか、正しく把握しておきましょう。

 Cの文字列(string)は、ナル文字(null character)で終わる文字の並びです。文字の並びは、文字の配列で表します。ナル文字は文字リテラルで表すと '\0' になります。

 ダブルクオート " で囲んだ部分は文字列リテラルとなります。"hello"という文字列があると、最後にナル文字で終わるので、それは 'h', 'e', 'l', 'l', 'o', '\0' という文字の配列と同じになります。

 CではいわゆるASCIIコードであれば問題なく扱えますが、英語以外の言語で使われることを想定して開発されたわけではないので、文字の型、対象とする文字集合の指定方法、エンコーディングの扱い方などの文字列を扱うにあたって必要となる事項がCの規格では決められていません。このため、文字がどのように扱われるかは、環境ごとの実装に任せられています。つまり、Cで文字を扱うときには、環境ごとの文字の扱い方を調べる必要があります。

 こういったことを踏まえ、本記事ではCで文字列を扱うときに必要な基本事項を確認することにします。

コラム●エスケープ シーケンス

 Cのソースコードを読むと、ナル文字の '\0' と同じように '\t' や '\n' といった '\' で始まる文字を見かけるはずです。 '\' で始まる文字はエスケープシーケンスといわれ、水平タブコード '\t' 、改行コード '\n' 、復帰コード '\r' といった制御コードを指定することができます。また、文字列内でダブルクオート " を指定するために "\"" と指定するときにも使います。


コラム●文字列操作関数

 一部の新しい環境では、以降で説明する文字列操作の関数を使ったプログラムをコンパイルした時に、警告が表示されたりエラーになったりする場合があります。これは、使用が推奨されていない関数を警告するためのものです。そのような場合はとりあえず、表示されるメッセージに従ってエラーを回避してください。詳しくは後ほど説明します。


文字列操作の基本「string.h」に含まれる関数

 では、標準ライブラリを使って文字列を操作してみましょう。

 文字列を扱うためには string.h というヘッダーをインクルードします。ソースコードの先頭に #include <string.h> と書きましょう。実際のコードでは他の機能を使うためのヘッダーもインクルードしますが、インクルードはどの順番で書いても構いません。

    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    // このコードでは、string.h、stdio.h、stdlib.h で提供されている機能を使えます。
    // インクルードの順番は問いません。
    
    int main(void) {
      // ...
      return EXIT_SUCCESS;
    }

 まず、strlen関数を使うと文字列の長さを得ることができます。strlen関数のプロトタイプは次のようになっています。

    size_t strlen ( const char *s );

 strlen関数の引数(仮引数s)には、長さを数える文字列を渡します。戻り値は文字列の長さで、その型はsize_tです。実際に使ってみましょう。printfでsize_t型の値を表示するには %zuを使います。

    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void) {
      const char str[] = "Hello";
      size_t length = strlen(str);
      printf("length = %zu\n", length); // length = 5
      return EXIT_SUCCESS;
    }

Visual C++では%zuはサポートされていません。%zuの代わりに、独自拡張の%Iuを使います。


 ”Hello"という文字列の長さは5です。ちなみに、配列strには文字列の最後を表す \0 も含まれていますので、配列の長さは6になっています。strlenは配列の長さとは無関係に、最初に現れた \0 の場所までの文字の数を数えます。

 次に、この文字列を操作してみます。ポインターの参照先と \0 について気にしながら確認してください。

 まずはコピーです。文字列を、別の配列にコピーするにはstrcpy関数を使います。strcpy関数のプロトタイプは次のようになっています。

    char* strcpy ( char * restrict dst,
                   const char * restrict src );

 先ほどと比べて若干複雑になっていて、引数が2つになっています。strcpy関数では、文字列srcがポインターdstが参照する領域にデータがコピーされます。dstには何でも指定していいというわけではなく、関数を呼び出す前に、コピーする文字列を入れるのに十分な領域を確保しておく必要があります。

    // インクルードはここまでのサンプルと同じですので省略します。
    int main(void) {
      const char str[] = "Hello";
      char dst[6];
      
      // 文字列 str を dst にコピー
      strcpy(dst, str);
      
      // str="Hello", dst="Hello"
      printf("str=\"%s\", dst=\"%s\"\n", str, dst);
      
      return EXIT_SUCCESS;
    }

 戻り値はコピー先のポインター、つまり第1引数と同じ値になります。

 コピー元(src)とコピー先(dst)の領域は、restrictが指定されていますので重なっていてはいけません。C99以前の環境ではrestrictがありませんが、やはり重なっていてはいけません。同じバッファの別の領域を指定する場合には特に注意しましょう。

 重なった領域をコピーしたいときはmemmove関数を使います。

    void * memmove ( void * dst,
                     const void * src,
                     size_t n );

 第3引数にコピーする領域の大きさ(メモリ操作をする関数へは\0を含む大きさを指定することに注意)を指定します。

 次は文字列の比較です。文字列と文字列を比べるにはstrcmp関数を使います。

    int strcmp ( const char * s1,
                 const char * s2 );

 文字列s1とs2が等しいときには0が返ります。等しくないときには、s1が大きければ0より大きい値が、s2が大きければ0より小さい値が返ります。文字列が大きいとか小さいというのは、文字を先頭から順に比較していって、最初に違う文字が表れたとき、その文字コードの大小で決まります。

 文字列から指定した文字や文字列を探す関数もあります。

    char * strchr (const char * s, int c);
    char * strrchr (const char * s, int c);
    char * strstr (cosnt char * s1, const char * s2);

 1つ目のstrchrは、文字列sの中から最初に文字cが現れた場所を、2つ目のstrrchrは最後に文字cが現れた場所を、ポインターで返します。3つ目のstrstrは、文字列s1から文字列s2を探します。s2が空文字(長さ0の文字列)の場合、s1の値が返ります。どの関数も、対象の文字列中から見つけられなかった場合はNULLが返ります。

 標準ライブラリには、他にも文字列操作のための機能が用意されています。

  • memcpy関数 : オブジェクトをコピーする
  • strncpy関数 : 長さを指定して文字列をコピーする
  • strcat、strncat関数 : 文字列を連結する
  • strtok関数 : 文字列をトークン分割する
  • strspn関数 : 指定した文字だけで構成されている長さを算出する
  • strcspn関数 : 指定した文字が含まれない長さを算出する
  • strpbrk関数 : 指定した文字が最初に現れる位置を探す

 まだこれ以外にもありますが、よく使われるのはこのくらいです。

 とはいえ、Cにおける文字列操作は、他のスクリプト言語などと比べると簡単ではありません。可変長(プログラムの実行中に長さが変わる)の文字列を簡単に扱う方法もありませんし、正規表現もありません。そのため、同じ事を実現するにしても、どうしてもコードが長くなってしまいます。

 文字列をたくさん扱うようなプログラムを作るのであれば、なんでもかんでも標準ライブラリでやろうとするのではなく、標準以外のライブラリを使用したり、他の言語と組み合わせたりすることをおすすめします。幸いにも、Cはそういったことをやりやすい環境が整っています。

文字を操作する方法(ctype.h wctype.h)

 次に、文字を操作する方法について説明します。

 ここで説明する機能を利用するには、ctype.hをインクルードする必要があります。その機能は大きく分けて、判定と変換の2つがあります。

 まず判定については、その文字がどういった種類のものであるか判定するためのものです。関数名はすべてisで始まっています。文字が数字かどうかを判定するisdigit関数のプロトタイプは次のようになっています。

    int isdigit ( int c );

 isdigit関数は、引数で与えられた文字が10進数字('0' から '9' のいずれかの文字)であれば0以外の値が、そうでなければ0が返ります。引数がcharではなくintであるのは、EOFという文字ではない(char型ではない)定数を渡すことができるようにするためです。EOFはEnd-of-Fileの頭文字で、ファイルの終わりを表す文字です。EOFの型はintで、負の値を持つと規定されています。したがって引数の型もintになっています。

    #include <ctype.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void) {
      // '1' is digit? yes
      printf("'1' is digit? %s\n",
             isdigit('1') ? "yes" : "no");
      
      // 'a' is digit? no
      printf("'a' is digit? %s\n",
             isdigit('a') ? "yes" : "no");
      return EXIT_SUCCESS;
    }

 同じように判定を行う関数として次のものが提供されています。どの関数もintの引数を1つ受け取り、結果をintで返します。関数名の後ろの説明は、関数が真(0以外)を返す条件です。

  • isxdigit関数 : 16進数字('0' から '9' あるいは 'A' から 'F'、小文字も含む)
  • isprint関数 : 表示文字(空白を含む)
  • isgraph関数 : 表示文字(空白を除く)
  • isalnum関数 : アルファベットまたは10進数字
  • isalpha関数 : アルファベット
  • isupper関数 : アルファベット大文字
  • islower関数 : アルファベット小文字
  • ispunct関数 : 区切り文字(空白以外のほとんどの記号)
  • isblank関数 : 行中の語を区切るための文字(空白かタブ)
  • iscntrl関数 : 制御文字(改行やタブ、バックスペース、エスケープなど)
  • isspace関数 : 空白文字類(空白や改行、タブなど)

 なお、isdigitとisxdigitの2つ以外の関数は、ロケールの設定により動作が変わります。ロケールとは地域ごとに異なる表示規則です。例えば日本で年月日を表示するときには「2012年11月1日」のような形式ですが、米国では「Nov 1, 2012」のような形式になります。日本を含む多くの地域では小数点の区切りはドットですが、ドイツやフランスなどではカンマになります。標準的に使われている文字のエンコードも異なりますし、そこに含まれている記号や、日時、通貨、数値、単位などの表記方法が異なります。

 これを吸収するための仕組みがロケールで、Cではlocale.hをインクルードすることでその機能が使えるようになります。locale.hで提供されている機能がsetlocale関数です。

    char * setlocale ( int category,
                       const char * locale );

 カテゴリーには次の6種類があります。

  • LC_ALL : プログラムのロケール全体(他のカテゴリより優先されます)
  • LC_COLLATE : 文字の順序
  • LC_CTYPE : 文字の種別
  • LC_MONETARY : 通貨の形式
  • LC_NUMERIC : 数の形式
  • LC_TIME : 日時の形式

 他に、環境ごとのカテゴリが用意されている場合もあります(LinuxにおけるLC_MESSAGESなど)。

 第2引数localeにはそのカテゴリに設定するロケールを指定します。ロケールを表す文字列の形式は実行環境によって異なります。

 プログラムの開始時は ”C” という特別なロケールになっています。これはどの環境でも動作できるような互換性のある設定で、実行環境ごとの設定、例えば日本語の設定になっていれば日本語に合わせた表示になるようにするには、第2引数localeに空文字列 "" を渡します。

    #include <locale.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void) {
      char * startup;
      char * base;
      
      // 現在のロケールを確認する。プログラム開始時は "C"
      startup = setlocale(LC_ALL, NULL);
      
      // 規定のロケールに変更する。結果は環境によって異なる。
      base = setlocale(LC_ALL, "");
      
      printf("%s , %s\n", startup, base);
      return EXIT_SUCCESS;
    }

 第2引数localeにNULLを渡すと、ロケールを変更せずに現在のロケールを表す文字列が返ります。手元の環境では次のような結果になりました。

  • 日本語環境のWindows 7 : C , Japanese_Japan.932
  • 英語環境のWindows 7 : C , English_United States.1252
  • 日本語環境のUbuntu 12.04 : C , ja_JP.UTF-8
  • 英語環境のUbuntu 12.04 : C , en_US.UTF-8

 一般的にロケールに応じて表示を変更するプログラムでは、プログラムの開始時に setlocale(LC_ALL, ""); というコードを入れます。

 ところで、ctype.hで定義されているisで始まる関数のリストを上に挙げましたが、このリストの関数には先頭のisをiswに置き換えたバージョンが存在します。それらはワイド文字を扱う関数で、wctype.hをインクルードすると使えるようになります。また、文字列操作のところで説明したstrlenやstrcmpなどのstrをwcsに置き換えたワイド文字列用のバージョン(wcslenやwcscmp)もあり、wchar.hをインクルードすると使えるようになります。

 ワイド文字とは、その時点のロケールでの任意の文字を表現できる文字コードです。charとは別にwchar_tという型が用意されており、ワイド文字はこの型に収まります。例えば日本語環境を想定すると、1バイトのサイズしか持たないchar型では日本語のすべての文字を表現することはできません。このため、日本語ロケールではワイド文字として主にUnicode(UTF-16やUTF-32)が使われます。wchar_tは実際には他の型の別名(typedef)になっており、文字を格納するのに必要なサイズの型が選ばれます。

 ワイド文字とワイド文字列(ワイド文字で構成される文字列)のリテラルは、char型のリテラルの前に L を付けます。

    const wchar_t wc = L'a';
    const wchar_t wstr[] = L"abc";

 もうひとつ、Cには多バイト(マルチバイト)文字というものもあります。こちらは実行環境で使われている文字です。多バイト文字用には型は用意されておらず、バイト列という扱いになります。文字列操作や文字操作のワイド文字バージョンに相当するようなものもありません。

 その代わり、ワイド文字と多バイト文字を変換する関数がstdlib.hに用意されています。

    // 長さ
    int mblen ( const char * mbs,
                size_t n );
    
    // 文字の変換
    int mbtowc ( wchar_t * restrict wc,
                 const char * restrict mbc,
                 size_t n );
    int wctomb ( char * mbc,
                 wchar_t wc );
    
    // 文字列の変換
    int mbstowcs ( wchar_t * restrict wstr,
                   const char * restrict mbstr,
                   size_t n );
    int wcstombs ( char * restrict mbstr,
                   const wchar_t * restrict wstr,
                   size_t n );

 charで表現されている部分が多バイト文字(列)で、wchar_tの部分がワイド文字(列)です。

 実際のプログラミングにおけるワイド文字とマルチバイト文字の扱いは複雑ですのでここでは説明できませんが、次のポイントだけは押さえておいてください。

  • ワイド文字はwchar_t、多バイト文字はcharの配列で文字を表す
  • ワイド文字はwchar_tオブジェクト1つで1文字だが、多バイト文字は複数のcharオブジェクトで1文字(文字によって長さが変わる可能性がある、シフトあり)
  • プログラム内部での処理はワイド文字を使う
  • 実行環境とのデータのやり取りは多バイト文字を使う

 Cの多言語サポートは基本的な機能だけが提供されていますので、実行環境によってはより高度な機能を用意している場合があります。あるいは、サードパーティのライブラリにもそういったものがあります。場合によってはそれらを使うことを選択肢として考慮した方が良いでしょう。

       1|2 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。