第11章 文字列とポインタ

テキスト『新・明解C言語 入門編 第2版』第11章の内容に関する補足説明を提供します.

11-1 文字列とポインタ

配列による文字列とポインタによる文字列

第9章で学習したとおり(第1版 p.242,第2版 p.258),printf 関数での文字列の変換指定は %s です. 実引数は文字列を保存した配列名でした. 式の中では,配列名は配列の先頭要素へのポインタに読み替えられます. したがって,実引数として printf 関数に渡していたのは, 配列の先頭要素へのポインタだったのです. 第13章3節に掲載されている printf 関数の仕様を見てください. 変換指定子が %s のとき,「実引数は,文字型の配列へのポインタでなければならない(第1版)」 「実引数は,文字型配列の先頭要素へのポインタでなければならない(第2版)」 と書かれています.

List 11-2(第1版 List 11-1)での "123" のような文字列リテラルを使うとき, 配列の宣言は行っていません. しかし,こうした文字列リテラルは, 記憶域上のどこかで配列のように保持されます(第1版 Fig.11-1,第2版 Fig. 11-2). 配列を宣言していないのに, 自動的に配列が作成されたかのようです. 文字列リテラルは実質的に配列なので, 評価すると先頭要素(つまり,先頭の文字)へのポインタとなります. 文字列リテラルへのポインタを保持しておけば, 配列と同じように文字列リテラルにアクセスできるようになります.

List 11-1 での,char 型の配列 str, 文字列リテラル "123", char へのポインタ型の変数 ptr が占有している記憶域の大きさを調べてみましょう. List 11-1 を以下のように書き換えてください.


#include <stdio.h>

int main(void)
{
  char str[] = "ABC";
  char *ptr = "123";

  printf("sizeof(str) = %u\n", (unsigned) sizeof(str));
  printf("sizeof(\"123\") = %u\n", (unsigned) sizeof("123"));
  printf("sizeof(ptr) = %u\n", (unsigned) sizeof(ptr));

  putchar('\n');

  printf("str = \"%s\"\n", str);
  printf("ptr = \"%s\"\n", ptr);

  return 0;
}

変更を加えた List 11-1 を実行すると, 大学の演習室の環境では以下のような結果が得られます.

result of List 11-1

配列 str,文字列リテラル "123", char へのポインタ型の変数 ptr が占有している記憶域の大きさはそれぞれ, 4,4,8 でした. C言語では char 型が占有する記憶域の大きさが 1 と定義されています(第1版 p.179,第2版 p.192). 1文字が1バイトということですね. 配列と文字列リテラルの大きさが 4 となるのは, ナル文字を含めて4文字分の記憶域が必要だからです. ポインタ型変数が占める記憶域の大きさは環境依存ですので, 他の環境では 8 ではないかもしれません.

配列と文字列リテラルとでは, それらが配置される記憶域が異なります. List 11-1 を以下のように書き換えて,このことを確かめてみましょう.


#include <stdio.h>

int main(void)
{
  char str[] = "ABC";
  char *ptr = "123";

  char str2[] = "DEF";
  char *ptr2 = "456";

  printf("sizeof(str) = %u\n", (unsigned) sizeof(str));
  printf("sizeof(\"123\") = %u\n", (unsigned) sizeof("123"));
  printf("sizeof(ptr) = %u\n", (unsigned) sizeof(ptr));

  putchar('\n');

  printf("str = \"%s\"\n", str);
  printf("ptr = \"%s\"\n", ptr);
  printf("str2 = \"%s\"\n", str2);
  printf("ptr2 = \"%s\"\n", ptr2);

  putchar('\n');

  printf("str の先頭要素アドレス = \"%p\"\n", str);
  printf("ptr の先頭要素アドレス = \"%p\"\n", ptr);
  printf("str2 の先頭要素アドレス = \"%p\"\n", str2);
  printf("ptr2 の先頭要素アドレス = \"%p\"\n", ptr2);

  return 0;
}

書き換えたプログラムを実行すると,以下のような結果が得られました.

result of List 11-1

配列 str と str2 は,記憶域上で近くに配置されています. 文字列リテラル "123" と "456" は, やはり記憶域上で近い場所に配置されています. 文字列リテラルは記憶域上で配列のように保持されていますが, その場所はかなり異なっています.

関数の中で定義された, 記憶クラス指定子 static をつけずに定義されたオブジェクトには, 自動記憶期間が与えられるのでした(第1版 p.162,第2版 p.174). List 11-1 を書き換えたプログラムでの str および str2 はこれに該当します. たいていの処理系では, これらオブジェクトはメモリ上でスタックセグメントと呼ばれる領域に配置されます.

一方,文字列リテラルには静的記憶期間が与えられます(第1版 p.241,第2版 Column 9-1). たいていの処理系では, 文字列リテラルはメモリ上でテキストセグメントと呼ばれる領域に配置されます. この領域には関数(プログラム本体)も配置されるため, コードセグメントとも呼ばれます. テキストセグメント(コードセグメント)は読み出し専用で, 書き換えできません. そのため,テキストセグメントに配置された文字列リテラルを書き換えることはできません. これは,配列の内容を書き換えられることと対照的です. C言語の仕様では,文字列リテラルを書き換えようとしたときの動作は不定で, 何が起きるかはわかりません.

配列による文字列とポインタによる文字列の違い

このセクションで述べられているのは, 文字列リテラルを指すポインタと, char 型の配列の名前が読み替えられたポインタの違いです. 文字列リテラルを指すポインタには,char へのポインタ型である任意のアドレスを代入できます. 一方,配列の名前が読み替えられたポインタは, その値(配列の先頭要素へのアドレス)を変更できません. テキストで説明されているように, 記憶域上で配列のアドレスを変えてしまうことはおかしいからです. このことは,文字の場合に限らない,ポインタと配列の一般的な違いです.

文字列の配列

List 11-4 での char *p[] という表記は 「char へのポインタ型の配列 p[]」という意味になります. 配列 p の要素は char へのポインタです. これまで char へのポインタ型の変数を char *p と宣言してきました. ポインタを格納する変数 p を, ポインタを格納する配列 p[] に変えると, List 11-4 での char *p[] という表記になります.

List 11-4 で配列 a の各要素である文字列を printf 関数で表示するとき, 最後の実引数は a[i] と書かれています. これは &a[i][0] と同じ意味になります. つまり,配列の要素である文字列それぞれの,先頭文字へのポインタです. テキスト p.291(第1版 p.274)では,配列名 a は &a[0] と解釈されることを学習しました. これと対比すると,a[i] は &a[i][0] と解釈されるということがわかると思います.

少し長くなりますが,List 11-4 を次のように書き換えて, 配列 a のさまざまな場所を指すポインタを表示させてみましょう. ポインタ演算の結果も示します.


#include <stdio.h>

int main(void)
{
  int i;
  char a[][5] = {"LISP", "C", "Ada"};
  char *p[] = {"PAUL", "X", "MAC"};

  for (i = 0; i < 3; i++)
    printf("a[%d] = \"%s\"\n", i, a[i]);

  for (i = 0; i < 3; i++)
    printf("p[%d] = \"%s\"\n", i, p[i]);

  putchar('\n');

  printf("a が指すアドレス = %p\n", a);

  printf("&a[0] が指すアドレス = %p\n", &a[0]);

  for (i = 0; i < 3; i++)
    printf("a + %d が指すアドレス = %p\n", i, a + i);

  for (i = 0; i < 3; i++)
    printf("&a[0] + %d が指すアドレス = %p\n", i, &a[0] + i);

  putchar('\n');

  for (i = 0; i < 3; i++)
    printf("a[%d] が指すアドレス = %p\n", i, a[i]);

  for (i = 0; i < 3; i++)
    printf("&a[%d][0] が指すアドレス = %p\n", i, &a[i][0]);

  for (i = 0; i < 5; i++)
    printf("a[0] + %d が指すアドレス = %p\n", i, a[0] + i);

  for (i = 0; i < 5; i++)
    printf("&a[0][0] + %d が指すアドレス = %p\n", i, &a[0][0] + i);

  putchar('\n');

  for (i = 0; i < 5; i++)
    printf("&a[0][%d] が指すアドレス = %p\n", i, &a[0][i]);

  return 0;
}

配列名 a および &a[0] に関係する出力は以下のようになります.

result of List 11-4

最初はテキスト p.291(第1版 p.274)で学習したことの復習です. 配列名 a が &a[0] と解釈されることがわかります. これらを実引数としてアドレスを表示すると,同じになっていますね. このアドレスは,最初の文字列 "LISP" の先頭文字へのポインタです.

ポインタ演算 a + 1 および &a[0] + 1 は, 2番目の文字列 "C" の先頭文字へのポインタとなります. アドレスの値が 5 増えていますね.Fig. 11-4(第1版 Fig.11-3)を見て, "LISP" の先頭文字から "C" の先頭文字までが5つ離れていることを確認してください. 同様に,ポインタ演算 a + 2 および &a[0] + 2 は, 3番目の文字列 "Ada" の先頭文字へのポインタとなります.

続いて,a[i] と &a[i][0] に関係する出力です.

result of List 11-4

a[0] と &a[0][0] が同じアドレスを指していることがわかります. これは最初の文字列 "LISP" の先頭文字のアドレスです.同様に, a[1] と &a[1][0] は2番目の文字列("C")の先頭文字, a[2] と &a[2][0] は3番目の文字列("Ada")の先頭文字を指しています.

ポインタ演算 a[0] + 1 および &a[0][0] + 1 は, 最初の文字列 "LISP" の2文字目(I)へのポインタとなります. アドレスの値が1増えていますね. 他のポインタ演算も同様です.

演習 11-2 は List 7-5 がヒントになります.

11-2 ポインタによる文字列の操作

文字列の長さを調べる

List 11-5 の str_length 関数の仮引数では, const 型修飾子(第1版 p.152,第2版 p.162)が用いられています. これは,ポインタ s が指しているアドレスにあるものは const char 型であるという意味です. つまり,str_length 関数に配列が渡されたとき, この関数は配列の内容を書き換えないというということです. ポインタ s の値を書き換えないという意味ではありません.

List 11-5 の str_length 関数で用いている while 文


while (*s++)
    len++;

での制御式 *s++ は *(s++) という意味になります. 単項 * 演算子と後置増分演算子(++)では, 後置増分演算子の方が優先順位が高い(第1版 p.205,第2版 p.221)からです. インクリメントされるのはポインタ s の値です.

後置増分演算子 ++ は, これが適用された式全体を評価すると,増分前の値となります(第1版 p.79,第2版 p.81). したがって,s++ という式でポインタ s のインクリメントは実行されますが, 評価の結果として得られるのはインクリメント前の値(アドレス)です. その値(アドレス)に対して単項 * 演算子が適用され,文字が得られます. この文字がナル文字でないときに,len の値がひとつ増やされます. ナル文字ならば len の値は変わらず,while 文を抜けます.

Fig. 11-5(第1版 Fig.11-4)では,最後にポインタ s が指しているのは文字列の最後のナル文字です. このとき,制御式の値は 0 なので len の値は更新されませんが, ポインタ s のインクリメントは制御式の値に関係なく行われます. したがって,str_length 関数が実行された後では, ポインタはナル文字のひとつ後ろの領域を指しています. ポインタ s が最後に指していた場所を確かめるには, List 11-5 の str_length 関数を次のように書き換えて, while ループ前後でのポインタの値を比較するとよいでしょう.


int str_length(const char *s)
{
    int len = 0;

    printf("受け取ったポインタは%pです.\n", s);

    while (*s++)
        len++;

    printf("現在のポインタは%pです.\n", s);

    return len;
}

List 11-5 をこのように書き換えて実行すると, 以下のような結果が得られます.

result of List 11-5

「受け取ったポインタ」は文字列 "five" の先頭文字を指しています. 「現在のポインタ」は,「受け取ったポインタ」と比べると, 文字列の先頭から5文字後ろを指していることがわかります. 4文字後ろがナル文字ですから,そのひとつ後ろを指していることがわかります.

文字列のコピー

List 11-6 の main 関数で, 「コピーするのは」という文字列を提示する printf 関数に誤植があります. 実引数 tmp は不要です.

List 11-6 を clang でコンパイルすると,using the result of an assignment as a condition without parentheses という警告が出ます. 問題ないので無視してください. 警告が出ないようにしたければ,while 文の制御式を括弧で囲んで,


while ((*d++ = *s++))
  ;

としてください.これ以降のプログラムでも,この警告への対応は同じです.

この while 文の制御式は複雑ですね. テキストに説明がありますので,よく理解してください. 少しだけプログラムが長くなりますが,この while 文は


while ((*d = *s) != '\0') {
  d++;
  s++;
}

と書くこともできます.

List 11-6 では,コピー元とコピー先の配列の大きさを, いずれもナル文字を含めて128文字としています.これは, コピー先である配列 str の記憶領域を超えて書き込みが行われないようにするための対処です. この問題については Column 11-1(第1版では次のセクション「文字列の不正なコピー」)で説明されます.

文字列の不正なコピー

第1版でのこのセクションは,第2版では Column 11-1 になりました. List 11-6 に続けて学習する内容ですので,ここに残します.

文字列リテラルを書き換えてはいけないということは, 11-1 節でも学習しました. たいていの処理系では, 文字列リテラルはメモリ上でテキストセグメント(コードセグメント)と呼ばれる領域に配置されます. テキストセグメントは読み出し専用で,書き換えできません. C言語の仕様では,文字列リテラルを書き換えようとしたときの動作は不定で, 何が起きるかはわかりません.

List 11-6 を書き換えた list1106x.c(第1版 List 11-7)は,clang では何も警告なくコンパイルできます. しかし,コンパイルされたプログラムを実行すると, 下図のようにエラーとなります. Bus error とは,制限されているか, または存在しないメモリへのアクセスがあったことを示すエラーです.

error when executing List 11-7

ポインタを返す関数

List 11-6 で,printf 関数の書式文字列における変換指定は %s です. 第13章3節に掲載されている printf 関数の仕様を見てください. 変換指定子が %s のとき,「実引数は,文字型配列の先頭要素へのポインタでなければならない」と書かれています. 最後の printf 文の実引数は str で, これは「コピー先の文字列の先頭文字へのポインタ」です.

この実引数 str のかわりに str_copy 関数を用いることができるのは, 関数の返却値が char へのポインタ型の値だからです. 「コピー先の文字列の先頭文字へのポインタ」は, str_copy 関数に渡された後,この関数の中で char へのポインタ型の変数 t に保存されています. 関数の返却値は保存されていた t の値です. テキストでは,printf 関数に渡されるのは「コピー後の文字列 str の先頭文字へのポインタ」と書かれていますが, コピー前も後も,この先頭文字のアドレスは同じです.

11-3 文字列を扱うライブラリ関数

この節で紹介されている,文字列を扱うライブラリ関数は, <string.h> ヘッダをインクルードすることで使用可能になります.

文字列関連の標準ライブラリ

テキストの第1版では文字列を扱う関数の実現例を示していました. これは文字列を扱う関数の書き方を学習することが目的で, 実際のプログラミングでわざわざ定義する必要はありません.

テキスト第1版の List 11-8 から List 11-11 は,文字列を扱う関数を定義しているだけで, それらの関数を使う main 関数は書かれていません. 定義されている関数を利用するプログラムを書きながら学習を進めてください. これは演習 11-9(p.303)に取り組むことになります.

テキストの第2版では,文字列を扱う関数の作成は演習問題となりました.

strlen 関数:文字列の長さを求める

演習 11-3 で定義する strlen 関数(第1版では List 11-8 で定義しています)は List 11-5 の str_length 関数とほとんど同じです. 関数の返却値の型が size_t 型であることだけが異なります. List 11-5 の main 関数で strlen 関数を使ってみてください. 文字列の長さを printf 関数で表示するとき, strlen 関数の返却値の型をキャストしておきます(第1版 p.181,第2版 p.194). たとえば,


printf("文字列\"%s\"の長さは%zuです.\n", str, (unsigned) strlen(str));

とします.

テキスト第2版 p.325 にある演習11-7 から演習 11-9 は,ここで取り組むとよいでしょう. テキスト第1版では,これらは演習 11-4 から演習 11-6 でした. 演習 11-9 は演習 9-6(第1版 p.249,第2版 p.266)と同じ問題です. 今回は「添字演算子を用いずに実現すること」という条件がついていることが異なります.

演習 11-8 および 11-9 の main 関数では, 検索する文字 c は配列に保存するとよいでしょう. 1文字だけなので配列でなく変数を使うこともできますが, 配列を使った方が簡単です. 変数を使うと,main 関数の書き方によっては, ちょっとした工夫が必要になることがあります. これについては第9章3節の冒頭で説明しましたので, そちらを参照してください.

演習 11-9 を(main 関数を書いて)clang でコンパイルすると, returning 'const char *' from a function with result type 'char *' discards qualifiers という warning がでます. 問題ないので無視してください. 関数 str_chr の返却値のタイプが char へのポインタ型であるのに, const char へのポインタ型である s を返そうとするため, 警告がされています.

strcpy 関数 / strncpy 関数:文字列をコピーする

演習 11-4 で定義する strcpy 関数(第1版では List 11-9 で定義しています)は List 11-6 の str_copy 関数と同じです.

テキスト p.318(第1版 p.299)での strncpy 関数の「解説」は, s2 が指す文字列の長さが n よりも短いときの動作が実際と少し異なります. 「残りをナル文字で埋めつくす」という動作は行われますが, そのあともう1文字分多くナル文字を埋めます. テキスト第2版では Fig. 11-9 のパターン [3] のように動作します. たとえば,s1 が指す文字列が "1234567",s2 が指す文字列が "ABC" で, n が 5 であるとしましょう. コピー先の文字列の「4」の位置に,"ABC" の最後にあるナル文字がコピーされると, 最初の while ループを抜けます.このとき, n の値は 2 で,ポインタ s1 は「5」の位置を指しています. 次の while ループが実行されると,「5」および「6」の位置にナル文字が埋められます. この動作を確認するには,演習11-4 で定義する strncpy 関数の処理を, 以下のように可視化してやればよいでしょう. この関数を実際に呼び出す main 関数を書かないと動作を確認できないので, 実行可能なプログラムの全体を示します.


#include <stdio.h>

char *strncpy(char *s1, const char *s2, size_t n)
{
    char *tmp = s1;

    while (n) {
        printf("n = %d\n", (int) n);
        printf("s1 = %s\n", s1);
        putchar('\n');

        if (!(*s1++ = *s2++)) break;
        n--;
    }

    puts("while を抜けた");
    printf("n = %d\n", (int) n);
    printf("s1 = %s\n", s1);
    putchar('\n');

    while (n--)
        *s1++ = '\0';

    return tmp;
}

int main(void)
{
    char str1[128] = "1234567";
    char str2[128];
    int n;

    printf("コピー先の文字列:\"%s\"\n", str1);

    printf("コピー元の文字列:");
    scanf("%s", str2);

    printf("%sの,先頭から何文字をコピーしますか:", str2);
    scanf("%d", &n);

    printf("コピーの結果:\"%s\"\n", strncpy(str1, str2, (size_t) n));

    return 0;
}

このプログラムを,コピー元の文字列を "ABC",n を 5 で実行すると, 以下のような結果が得られます.

result of List 11-9

strcat 関数 / strncat関数:文字列を連結する

演習 11-5 で定義する strcat 関数(第1版では List 11-10 で定義しています)では, 2つの while 文が続けて使われます.


while (*s1)
    s1++;
while (*s1++ = *s2++)
    ;

最初の while 文を抜けるとき,ポインタ s1 は文字列末尾のナル文字を指しています. 次の while 文では,s2 が指す文字列が, s1 末尾のナル文字の位置からコピーされます. ポインタ s1 が指しているオブジェクトに, ポインタ s2 が指しているオブジェクトが代入されます. 代入された値が 0 (つまり,ナル文字)でない限り, ポインタ s1 および s2 の値が 1 進められます.

この節でここまでに学習した関数と異なり, 最初の while 文の制御式ではポインタのインクリメントは行っていません. もし制御式の中で s1 のインクリメントを行ってしまうと, while 文を抜けるとき,s1 はナル文字のもうひとつ後ろを指してしまいます.

演習 11-5 で定義する strncat 関数(第1版では List 11-10 で定義しています)は 演習 11-4 で定義した strncpy 関数(第1版では List 11-9 で定義しています)に似ています. しかしよく見ると,s2 の長さ n の値をディクリメントするタイミングが違います. 演習11-4(第1版 List 11-9)の strncpy 関数では,文字列をコピーする while ループの制御式は n であり, ディクリメントはループ本体で行われていました.


while (n) {
    if (!(*s1++ = *s2++)) break;
    n--;
}

これに対して,演習 11-5(第1版 List 11-10)の strncat 関数では, 文字列をコピーする while ループの制御式は n-- であり, 制御式の中で n をディクリメントしています.


while (n--)
    if (!(*s1++ = *s2++)) break;

もし,strncpy 関数での文字列コピーのための while 文を, strncat 関数での while 文のように書いてしまうと, 次の while 文(ナル文字で埋める)が無限ループになってしまうことがあります. 文字列をコピーする while 文の制御式が n-- であるとき, この式を評価した値(ディクリメント前の n の値)が 0 になると while 文を抜けます. このとき,while ループ本体は実行されませんが,n の値はディクリメントされて -1 になります. すると,次の while 文は,制御式の値が -1 から始まり, 1 ずつディクリメントされていくことになります. 制御式の値は 0 にならないので,無限ループとなってしまいます.

演習 11-5(第1版 List 11-10)の strncat 関数で文字列のコピーを行う while 文は, 演習 11-4(第1版 List 11-9)の strncpy 関数で文字列のコピーを行っている while 文のように書くこともできます. すなわち,制御式では n のインクリメントを行わず. while ループの最後に行っても問題ありません.

演習 11-5(第1版 List 11-10)の strncat 関数で文字列を連結するとき, s2 が指す文字列の長さ(ナル文字を含まない)が n 以上のときは, この文字列の最初から n 文字が連結され,その後にナル文字が入れられます. 文字列の長さが n より小さいときには, 最後にあるナル文字までが連結されて while ループを抜けるので, その後にもうひとつナル文字が入れられます.

strcmp 関数 / strncmp 関数:文字列の大小関係を求める

演習 11-6(第1版 List 11-11)で定義する strcmp 関数は, 2つの文字列を前から1文字ずつ比較していきます. ポインタ s1 が指している文字と s2 が指している文字が同じかどうかを, while 文の制御式で判定します.

これらの文字が同じとき, if 文の制御式で s1 が指す文字がナル文字であるかどうかを判定します. これがナル文字であれば,s2 もナル文字を指しています. これは2つの文字列が最後のナル文字まで同じであることを意味しますので, 値 0 を返します.

ポインタ s1 が指している文字と s2 が指している文字が異なると, while 文を抜けて,2つの文字の文字コードの差を返します. 文字コードの差を計算するときには,*s1 から *s2 を引きます. これらの値は char 型なので,この関数の「解説」に従って, unsigned char 型にキャストしてから引き算を行います.

第9章の演習 9-3 と演習 9-11 で, 読み込んだ文字列が "$$$$$"であるかどうかの判定に strcmp 関数を利用できるということを述べました. 復習してください.

演習 11-6(第1版 List 11-11)で定義する,strncmp 関数で使われている while 文での制御式 n && *s1 && *s2 は, n が 0 であるか,*s1 あるいは *s2 のいずれかがナル文字であると, 評価値が 0 となり,while ループを抜けます.

この while ループの本体では, ポインタ s1 が指している文字と s2 が指している文字を if 文の制御式で比較します. これらの文字が異なれば,文字コードの差を return 文で返して, 関数の処理は終わりです. 文字が同じときは,ポインタの位置を次の文字に進め,n の値を 1 小さくします.

2つの文字列が n 文字目まで同一であるとき. n の値が 0 となって while ループを抜けます.すぐ後の


if (!n) return 0;

という if 文によって,関数の返却値は 0 となります.

文字列での n 番目の文字に到達する前にナル文字が出現すると, そこで while ループを抜けます. ポインタ s1 が指す文字がナル文字であった場合,


if (*s1) return 0;

という if 文の制御式の値は 0 となりますから, 最後の return 文が実行されて -1 が返されます. 文字コードでの2つの文字の大小関係は, s2 が指す文字もナル文字ならば *s1 = *s2 であり, ナル文字以外ならば *s1 < *s2 となります.

文字列での n 番目の文字に到達する前にナル文字が出現して while ループを抜けたとき, ポインタ s1 が指す文字がナル文字以外だとすると, これは *s2 がナル文字であることを意味します(そうでなければ while ループを抜けていません). ポインタ s1 が指す文字はナル文字以外なので,


if (*s1) return 0;

という if 文の制御式の値は 1 となり,0 が返されます. 文字コードでの2つの文字の大小関係は,*s1 > *s2 です.

文字列の長さが比較する文字数 n よりも小さいとき, 2つの文字列が同一であっても,同一であるとは判定されません. 以下に示す main 関数を書いて,同じ文字列(たとえば,ABC)を, 文字列のサイズより大きな n(たとえば,4)まで比較してみましょう.


int main(void)
{
  char str1[128];
  char str2[128];
  int n;

  printf("比較する文字列1:");
  scanf("%s", str1);

  printf("比較する文字列2:");
  scanf("%s", str2);

  printf("何文字目まで比較しますか?");
  scanf("%d", &n);

  if (strncmp(str1, str2, (size_t) n) == 0)
    printf("2つの文字列は%d文字目まで同じです。\n", n);
  else {
    puts("2つの文字列は異なります。");
    printf("strncmp(str1, str2, n) = %d\n", strncmp(str1, str2, (size_t) n));
  }
  
  return 0;
}

結果は次のようになります.

result of List 11-11

atoi 関数 / atol 関数 / atoll 関数 / atof 関数:文字列を数値に変換する

このセクションで扱われている4つ(第1版では atoll 関数がないので3つ)の関数名は, a-to-i,a-to-l,a-to-ll,a-to-f と区切って読んでください. そうすると関数名の意味がわかります. たとえば,atoi 関数は「文字(a)から int型 の整数(i)へ」という意味です.

List 11-13(第1版 List 11-12)では,atoi 関数だけでなく,atol 関数と atof 関数の動作も確かめるとよいでしょう. 確認のための printf 文を以下のように追加します.


printf("int 型の整数に変換すると%dです。\n", atoi(str));
printf("long int 型の整数に変換すると%ldです。\n", atol(str));
printf("double 型の実数に変換すると%fです。\n", atof(str));

このセクションで扱われている4つの関数は, オーバーフロー(第1版 p.197,第2版 p.213)の発生時など, 入力が不適切な場合の処理に問題があります. そのため,実用のプログラムでは使用を避けられ, かわりに strtol あるいは strtoll という関数が使われます.

演習 11-11(第1版 演習 11-8)は演習 9-10(第1版 p.251,第2版 p.269)と同じ問題です. 今回は添字演算子を使わないプログラムを書きます.

第1版の『解きながら学ぶC言語』での演習 11-10(第2版 演習 11-12)の解答は, 入力された文字列の最初に空白類文字があるときに読み飛ばしたり, 符号を処理したりするなど,さまざまな入力に対応できるものになっています. 数字文字だけが入力されることを前提とするなら, もっと簡単なプログラムにできます. 以下に,atoi 関数(第1版では strtoi 関数として定義)の解答例をひとつ示しておきます. 数字文字を順に読み取って数値に変換する計算方法を, 『解きながら学ぶC言語』での解答例とは変えてみました.


size_t str_len(const char *s) /* List 11-8 */
{
  size_t len = 0;

  while (*s++)
    len++;
  return len;
}

int power10(int n) /* 10のべき乗を返す関数 */
{
  int ans = 1;

  while (n--) {
    ans = ans * 10;
  }
  return ans;
}

int atoi(const char *nptr)
{
  int n;
  int sum = 0;

  n = (int) str_len(nptr) - 1;

  while (*nptr) {
    sum = sum + ((*nptr - '0') * power10(n--));
    nptr++;
  }

  return sum;
}

まとめ

まとめのプログラムは,main 関数の最後に必要な return 0; が抜けています(初版で確認).