16

「大学生が疑問に思ったC言語の文字列型:その謎を解く」

最終更新日 投稿日 2023年07月26日

最近宇宙開発系の企業に興味を持っていて、その会社に行くためにはC言語の理解が必要不可欠なのではないかと感じたので、ここ最近C言語を触っています。

これまではJavaScriptTypeScriptRubyなどのインタープリタ言語をメインでやってきたので、C言語を学ぶとまた違う発見があって面白いです。

その中で、文字列型が他の言語と違うことに戸惑ったという話をしていこうと思います。

インタープリタ言語の文字列型

例えばTypeScriptにおいて文字列型を定義しようとすると、下記のように定義します。

hoge.ts
const str: string = "Hoge";
console.log(str)

上記の例はTypeScriptTypeScriptには文字列型という型があります。PythonRubyも同様です。しかし、これが当たり前だと思っていた僕は、C言語には文字列型が存在しないことに驚きました。

C言語における文字列型

結論から言うと、そもそも文字列型というものありません。Hello Worldを書くと以下のようになります。

hoge1.c
#include <stdio.h>

int main() {
	char a[] = "Hello World";
	printf("値を出力します。%s\n", a);
	return 0;
}
// 値を出力します。Hello World
hoge2.c
#include <stdio.h>

int main() {
    char *a = "Hello World";
	printf("値を出力します。%s\n", a);
    return 0;
}
// 値を出力します。Hello World

hoge1.cのコード見て気づくかと思いますが、何やら配列に格納されているように感じますよね。まず、そのファイルから見ていこうとおもいます。

なぜ配列に文字を入れるのか?

先ほどC言語には文字列がないと説明しました。どうするかというとC言語にある文字型という型であるchar型を使っていきます。char型は1文字を表す型です。例えば、AとかRは1文字なのでchar型ですね。このchar型を配列にするんです。そして各文字がメモリ上の連続した位置に格納され、最後にヌル文字('\0')が格納されます。
先ほどの例のHello Worldでいえば、'H'、'e'、'l'、'l'、'o'、' '、'W'、'o'、'r'、'l'、'd'、'\0'が格納されていくんです。

ここで大事なのは各文字がメモリ上の連続した位置に格納されているというところです。実際にコードを書いて見ていきましょう。

hoge1.c
#include <stdio.h>

int main() {
    char a[] = "Hello World";
    printf("値を出力します。%s\n", a);
    
    for (int i = 0; a[i] != '\0'; i++) {
        printf("%cのアドレスは%p\n", a[i], &a[i]);
    }
    printf("\\0のアドレスは%p\n", &a[sizeof(a) - 1]);

    return 0;
}

// 値を出力します。Hello World
// Hのアドレスは0x16f286fc8
// eのアドレスは0x16f286fc9
// lのアドレスは0x16f286fca
// lのアドレスは0x16f286fcb
// oのアドレスは0x16f286fcc
//  のアドレスは0x16f286fcd
// Wのアドレスは0x16f286fce
// oのアドレスは0x16f286fcf
// rのアドレスは0x16f286fd0
// lのアドレスは0x16f286fd1
// dのアドレスは0x16f286fd2
// \0のアドレスは0x16f286fd3

実際にアドレスが連続していることがわかるかと思います。これが何を意味しているのでしょうか?
文字列がメモリ上の連続した位置に格納されているというのは、文字列の効率的な操作において重要です。なぜかというと、文字列の開始地点さえ分かっていれば、その後の連続するメモリアドレスを追っていけば、文字列の終端(ヌル文字)に到達できるからです。これは文字列の長さが変わっても同じです。

具体的には、文字列を引数として関数に渡すとき、配列全体をコピーするのではなく、配列の先頭へのポインタだけを渡すことで配列の大きさに関係なく、常に一定のメモリを使用するため、効率的です。また、文字列の検索や置換などの操作も、文字列がメモリ上の連続した位置に格納されていることを利用して行われます。

次にhoge2.cファイルの内容を見ていきましょう。

hoge2.c
#include <stdio.h>

int main() {
    char *a = "Hello World";
	printf("値を出力します。%s\n", a);
    return 0;
}
// 値を出力します。Hello World

hoge2.cに記述されている内容も同じように、
'H'、'e'、'l'、'l'、'o'、' '、'W'、'o'、'r'、'l'、'd'、'\0'が連続したメモリアドレスに保存されます。具体的には、文字列の先頭である'H'のアドレスがポインタaに保存されます。これは文字列の"開始点"を示しています。そして、その開始点から一文字ずつメモリを読み進めることで、全体の文字列を取得することができるんです。

char a[] = "Hello World";char *a = "Hello World";のどちらの場合も、文字列"Hello World"の各文字('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0')はメモリ上の連続したアドレスに保存されます。
hoge1.choge2.cHello Worldを出力するという点においては違いはないんですよね。

では何が違うのか?

char a[] = "Hello World";char *a = "Hello World";とで何が違うのか、それはメモリ上での扱い方と変更可能性です。

メモリ上での扱い

まず、char a[] = "Hello World";の場合ですが、この宣言はスタックメモリ上にHello Worldという文字列を保存します。スタックメモリとは、一時的なデータ保存場所で、関数の実行が終了すると自動的に開放される部分のことを指します。

次に、char *a = "Hello World";の場合ですが、これはリテラル(文字列)"Hello World"が保存されているメモリアドレスをポインタaが指しているだけです。そしてこの文字列は通常、読み取り専用の領域(静的メモリ領域)に格納されます。

変更可能性

ここで、もう一つ重要な違いがあります。それは変更可能性です。

char a[] = "Hello World";の場合、aはスタック上に配置されているので、後からその値を変更することが可能です。しかし、char *a = "Hello World";の場合、aが指す文字列は読み取り専用領域に格納されているため、その文字列自体を変更しようとするとエラーが発生します。

具体的なコードを見てみましょう。

change1.c
#include <stdio.h>

int main() {
    char a[] = "Hello World";
    a[0] = 'h';
    printf("値を出力します。%s\n", a); // 出力: "hello World"
    return 0;
}

ですが、文字列リテラルに対してはそれができないのです。

change2.c
#include <stdio.h>

int main() {
    char *a = "Hello World";
    a[0] = 'h'; // エラーが発生
    printf("値を出力します。%s\n", a);
    return 0;
}

上記のコードchange2.cを実行すると、書き込みエラーが発生します。これは、文字列リテラルが静的メモリ領域(読み取り専用領域)に配置されており、それを書き換えることはできないからです。したがって、C言語では文字列リテラルを書き換えようとすると、実行時にエラーが発生します。
これは実行環境によるものです。具体的な内容は下記のコメント欄で詳しく記されているのでご確認いただけると幸いです!

C言語に文字列型がない理由

さて、そんなC言語にはなぜ文字列型がないのでしょうか? その理由はC言語が作られた時代背景にあります。C言語は1970年代にベル研究所のデニス・リッチーによって開発されました。当時のコンピュータは今と比べると非常に低性能で、メモリも限られていました。

そんな中で効率よくプログラムを動かすためには、メモリの節約や直接的なメモリ操作が求められました。その要求に答えるためにC言語では、文字列をメモリ上の連続した位置に格納する文字型の配列として扱うことにしました。これにより、メモリ使用量を最小限に抑えつつ、効率的に文字列操作を行うことができました。

また、C言語はUNIX上のシステム開発言語として生まれました。そのため、メモリ管理や処理速度に対する細かな制御が可能で、低レベルな操作が必要なシステムプログラミングに適していました。

したがって、C言語には高レベルな抽象化が提供される文字列型がなく、代わりに効率的なメモリ操作が可能な文字型の配列が提供されています。

まとめ

C言語を学び始めてまだ日が浅いですが、これまで全く気にしたことがなかったポインタやメモリなどの低レイヤーことを意識することが必要になるのでとても面白い発見がありますね!
ではでは!

【参考資料】

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
16