C言語基礎|11章のまとめ

文字列は“配列”でも“ポインタ”でも書ける!違いをつかめば、Cの文字列操作が一気に読みやすくなる。

第11章では、文字列とポインタの“距離の近さ”を、かなり実感できたと思います。
配列で作った文字列も、ポインタで指す文字列も、見た目は同じように printf で表示できます。でも、中身(メモリの持ち方・書き換えの可否・サイズの意味)はけっこう違います。

さらに、文字列の配列(2次元配列版とポインタ配列版)も登場して、**「どこに文字が置かれて、何が可変で、何が固定なのか」**を意識できるようになったはずです。
最後に、標準ライブラリ(string.h / stdlib.h)の代表的な関数を使って、実務での基本動作も押さえました。ここで一度、要点を“整理して定着”させましょう。

文字列の2つの表現(配列の文字列 / ポインタの文字列)

まずは違いを表で整理

観点配列による文字列ポインタによる文字列
宣言例char a[] = "CIA";char *p = "FBI";
実体char の配列そのもの先頭文字を指すポインタ
書き換えa[0] などは書き換え可能文字列リテラル側は書き換え保証なし
代入(付け替え)a = "DEF" は不可p = "XYZ" は可
sizeof の意味配列の全バイト数ポインタのバイト数

図でイメージ(簡略)

配列:  a[] = "CIA"
a:   [ 'C' ][ 'I' ][ 'A' ][ '\0' ]   ← 配列が実体

ポインタ: p = "FBI"
p:   [ 先頭アドレス ]  →  [ 'F' ][ 'B' ][ 'I' ][ '\0' ]  ← リテラルは別の場所

文字列リテラルとポインタの関係(重要ポイント)

文字列リテラルは「先頭文字へのポインタ」として扱える

  • "FBI" を式として評価すると、先頭文字 'F' の位置(アドレス)を指すポインタとして扱われます。
  • だから char *p = "FBI"; で、p は 'F' を指すように初期化されます。

「両方が記憶域を占有する」の意味

何がメモリを使う?使われる領域
ポインタ変数そのものpsizeof(char *) バイト
文字列リテラルの実体"FBI"文字数+1 バイト(\0 含む)

ポインタに文字列リテラルを代入できる理由

代入すると「文字列をコピー」するのではなく「指す先が変わる」

p = "FBI";   → p は 'F' を指す
p = "CIA";   → p は 'C' を指す(指す先の付け替え)

この動きができるのは、p が「ポインタ(アドレスを入れる箱)」だからです。
一方、配列は「箱そのもの」なので、箱を別の箱に入れ替えるような代入はできません。

文字列の配列(2通り:2次元配列 / ポインタ配列)

2つの実現方法を表で比較

観点配列による文字列の配列(2次元配列)ポインタによる文字列の配列(ポインタ配列)
宣言例char a2[][5] = {"LISP","C","Ada"};char *p2[] = {"PAUL","X","MAC"};
実体char[5] が行数分並んだものchar * が要素数分並んだもの
文字の配置すべて連続領域連続する保証なし
サイズ(配列部分)sizeof(a2) = 行×列sizeof(p2) = sizeof(char*)×要素数
追加で必要な領域なし(文字も全部中にある)各文字列リテラル分の領域も必要

図でイメージ

a2(2次元配列):
[ L I S P \0 ][ C \0 ? ? ? ][ A d a \0 ? ]   ← 全体が連続

p2(ポインタ配列):
p2: [ →"PAUL" ][ →"X" ][ →"MAC" ]           ← 先の文字列は別々でもOK

ポインタのインクリメント / デクリメント(文字列操作の基本)

規則をひとことで

  • ポインタ p が配列の要素を指しているとき
    p++ は次の要素を指す
    p-- は前の要素を指す

例(文字列走査の発想)

const char *s = "CAT";
s が 'C' を指す
s++ で 'A' を指す
s++ で 'T' を指す
s++ で '\0' を指す

この考え方が、strlen っぽい処理や、コピー処理の基礎になります。

文字列リテラルへ書き込みは危険(やってはいけない)

なぜ危険?

  • 書き換え可能な場所に置かれている保証がない(処理系依存)
  • たとえ書けたとしても、長い文字列を書き込めば領域を越えて破壊する可能性がある

結論
文字列リテラルや、その周辺に書き込むコードは作らない。

「ポインタを返す関数」は使い回しが効く

strcpy や strcat が便利なのは、コピー先(連結先)の先頭ポインタを返すからです。
返り値をそのまま次の処理に渡せます。

例の発想(イメージ):

  • strcpy(dst, src) の結果(dst)を printf に渡せる。
  • 連続コピー、連続連結、表示と合成ができる。

文字列関連ライブラリの全体像(string.h / stdlib.h)

代表関数まとめ(超要点)

分類関数何をする?
長さstrlen\0 を除いた文字数
コピーstrcpy / strncpy文字列をコピー(後者は制限あり)
連結strcat / strncat後ろに連結(後者は制限あり)
比較strcmp / strncmp辞書順っぽい比較(文字コード依存)
変換atoi / atol / atoll / atof文字列を数値へ

各関数の書式と役割(最低限ここを押さえる)

strlen

  • ヘッダ:#include <string.h>
  • 形式:size_t strlen(const char *s);
  • すること:\0 を含まない長さを返す

strcpy / strncpy

  • strcpy 形式:char *strcpy(char * restrict s1, const char * restrict s2);
  • strncpy 形式:char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
  • すること:s2 を s1 へコピー(ただし重なりは未定義)
  • 注意:strncpy は必ずしも \0 を付けてくれないケースがある

strcat / strncat

  • strcat 形式:char *strcat(char * restrict s1, const char * restrict s2);
  • strncat 形式:char *strncat(char * restrict s1, const char * restrict s2, size_t n);
  • すること:s1 の末尾へ s2 を連結
  • 注意:s1 側のバッファに十分な空きが必要

strcmp / strncmp

  • strcmp 形式:int strcmp(const char *s1, const char *s2);
  • strncmp 形式:int strncmp(const char *s1, const char *s2, size_t n);
  • すること:大小比較(等しいなら0、s1>s2なら正、s1<s2なら負)
  • 注意:比較基準は文字コード依存

atoi / atof など

  • ヘッダ:#include <stdlib.h>
  • 形式:int atoi(const char *nptr); / double atof(const char *nptr);
  • すること:数字として読める部分を数値化
  • 注意:失敗しても 0 になりやすい、範囲外の動作が保証されない

サンプルプログラム

プロジェクト名:chap11-13-1 ソースファイル名:chap11-13-1.c

// 11章まとめ:文字列の持ち方と標準関数の基本
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 文字列を [ ] で囲んで表示する
void put_bracket(const char *s)
{
    putchar('[');
    while (*s) putchar(*s++);
    putchar(']');
    putchar('\n');
}

int main(void)
{
    // 配列による文字列(中身を持つ)
    char a[] = "DOG";

    // ポインタによる文字列(指す)
    const char *p = "CAT";

    // 文字列の配列:2次元配列版(連続)
    char a2[][6] = {"RED", "BLUE", "GREEN"};

    // 文字列の配列:ポインタ配列版(指す)
    const char *p2[] = {"TOKYO", "OSAKA", "NAGOYA"};

    puts("配列の文字列 a と ポインタの文字列 p を表示します。");
    printf("a = %s\n", a);
    printf("p = %s\n", p);

    puts("\nstrlen で長さを確認します。");
    printf("strlen(a) = %zu\n", strlen(a));
    printf("strlen(p) = %zu\n", strlen(p));

    puts("\n2種類の「文字列の配列」を表示します。");
    for (int i = 0; i < (int)(sizeof(a2) / sizeof(a2[0])); i++) {
        printf("a2[%d] = ", i);
        put_bracket(a2[i]);
    }

    for (int i = 0; i < (int)(sizeof(p2) / sizeof(p2[0])); i++) {
        printf("p2[%d] = ", i);
        put_bracket(p2[i]);
    }

    puts("\natoi の簡単な例:数字文字列を数にします。");
    printf("文字列 \"42\" を atoi すると %d です。\n", atoi("42"));

    return 0;
}

このプログラム内で登場する命令(関数)の役割

名前何をする?ここでの役割
puts文字列+改行を表示説明文を表示
printf書式付き表示値や結果を整形表示
putchar1文字出力[ ] の装飾表示に使用
strlen文字列長(\0除く)a と p の長さ比較
atoi文字列→int 変換"42" を数値化

11章の要点を一気に復習(まとめ表)

テーマ覚えておくポイント
配列の文字列実体を持つ、sizeof は全体サイズ、配列自体の代入は不可
ポインタの文字列指すだけ、代入で指す先変更OK、リテラル書き換えは危険
文字列の配列(2次元)文字が連続、固定幅ゆえ未使用領域が出ることがある
文字列の配列(ポインタ配列)柔軟、配置は保証なし、配列部分+リテラル分の領域
ポインタ演算p++ で次要素へ(文字列走査の基本)
標準関数string.h と stdlib.h の基本関数を用途別に使う