C言語で文字列を変数に代入する2つの方法を徹底解説

344, 2021-11-12

目次

C言語で文字列を変数に代入する

C言語では文字列を変数に代入することが出来ます。
「代入」と一言で言っても、その代入が表す挙動は色々です。
この記事では「代入」とは具体的に何なのか? というところから解説します。

また、C言語の文字列の種類についてもあわせて解説します。
さらに他言語で言うところの文字列の代入をC言語で実現するにはどうしたらいいのか?
というところも解説します。

具体的には↓を見ていきます。

  • 代入とは何か?

  • C言語における文字列の種類

  • C言語における文字列の構造

  • C言語における文字列の代入とは?

  • 文字配列に文字列をコピーする

代入とは何か?

代入」とは具体的には何なのでしょうか?
代入が表す意味とは?
これはある値にある値を代入(コピー)することを指します。

代入の本質は「コピー」

代入の本質は「コピー」です。
コピーとは、物質Aを物質Bに複製することを指します。
あるメモリ領域にある値に別のメモリ領域にある値をコピーすることが代入の本質です。
コピーは計算機の基本的な機能です。この機能が備わっていない計算機は珍しいと言えます。

コピーは計算結果をメモリ領域に保存したり、あるいは一時期なデータを退避しておく場合に使われます。
複雑な計算では計算結果を保存しておく必要があります。
そのためこのコピーが計算時に使われます。

代入の本質はコピーで、われわれ開発者はC言語を使う際に無意識的にこのコピーを行っています。
あるいは意識的に行っているという人もいるかもしれません。
代入の本質を知っておくとC言語の文字列の代入についても迷わないで済みます。

代入演算子

代入で使われる演算子を「代入演算子」と言います。
C言語における代入演算子は↓のような種類があります。

=  *=  /=  %=  +=  -=  <<=  >>=  &=  ^=  |=

このうち、文字列の代入で使われる代入演算子は=になります。
他の演算子は文字列の代入ではほとんど使われません。
つまり文字列の代入では=だけ覚えておけば良いことになります。

ただし後述しますが、C言語では文字列全体のコピーを行う場合は関数を使うことが一般的です。

左オペランド、右オペランド

代入演算子の左の値を「左オペランド」、右の値を「右オペランド」と言います。

左オペランド = 右オペランド

C言語の文字列の代入の場合、右オペランドは文字列になります。
左オペランドはポインタ変数になります。

代入演算の制約

C言語の規格では代入演算には制約が設けられています。
その制約は↓になります。

制約: 代入演算子の左オペランドは,変更可能な左辺値でなければならない。

左辺値とは、左オペランドに持ってくることが出来る値の総称のことを言います。
つまり代入演算子(=)の左側にある値は「変更可能な左辺値(modifiable lvalue)」でなければいけません。
これは例えばconstが付いていない変数です。
constをつけた変数には場合によって代入することはできません。
(厳密にはconstが修飾する対象により挙動が変わります)

C言語における文字列の種類

文字列の代入ではC言語の文字列の種類についての把握が必要になってきます。
C言語の文字列は大きく分けると↓の2つになります。

  • 文字配列

  • 文字列定数

文字配列

文字配列スタック・セグメントデータ・セグメントというメモリ領域に確保される配列のことを言います。
これは多くの場合char型の配列です。

    char s[10];  // 文字配列

C言語で「変更可能な文字列」と言った場合、それはほとんどのケースでこの文字配列のことになります。
文字配列はメモリ上に文字列の要素数分のメモリ領域が確保されます。
そのためこの配列を操作して配列内の値を変更することが可能です。

C言語で「変更可能な文字列」を扱いたいとなった場合は、この文字配列を思い浮かべると良いです。
これは静的に確保される配列ですが、動的な文字配列も存在します。
動的な文字配列はmalloc()calloc()などでメモリが動的に確保されます。

文字列定数

文字列定数とはテキスト・セグメントというメモリ領域に確保される文字列のことを言います。
この文字列定数は、文字配列と違って変更することができません。
そのため「定数」という名前が付いています。

C言語で文字列と言った場合、ほとんどがこの文字列定数のことを指していると言っていいでしょう。
文字列定数はダブルクオーテーションで囲まれた文字列のことを指します。

    "Hello, World!";  // 文字列定数

文字列定数はコンパイル後に実行ファイルに埋め込まれます。
この埋め込まれる部分がテキスト・セグメントというメモリ領域になります。
文字列定数を無理や変更しようとすると、セグフォなどになりプログラムがクラッシュします

C言語における文字列の構造

C言語における文字列は連続したメモリ領域に確保される文字の集合のことを言います。
文字は通常char型で表現されます。
またこの文字の集合のことを文字列と言います。

文字列の末尾にはナル文字と呼ばれる番兵の役割をする文字が保存されます。
このナル文字が存在しないと文字列は文字列として機能しません。
そのため文字列を代入でコピーする場合は、このナル文字がコピーされるかどうかを開発者が気にする必要があります。

ナル文字は文字列をループで参照するときなどに参照されます。
そのためナル文字が存在しない文字列はバッファーオーバーランになる可能性があります

ナル文字は通常「\0」という文字としてコード上で書かれます。
これの値は0で、実質0と変わりません。

C言語の文字列はこのようにシンプルな構造です。
構造がシンプルなため軽量で素早い処理が可能になっています。

C言語における文字列の代入とは?

C言語における文字列の代入とは具体的に何を指すのでしょうか?
変数に文字列を代入するには?
これは大きく分けて↓の2つになります。

  • ポインタ変数に文字列のアドレスを代入する

  • 文字配列に文字列をコピーする

変数に文字列を代入する場合、その変数は2つにわけることができます。
それはポインタ変数文字配列です。
具体的にこの2つについて解説します。

ポインタ変数に文字列のアドレスを代入する

ポインタ変数に文字列(文字配列、文字列定数)のアドレスを代入する場合です。
たとえば文字列定数をポインタ変数に代入するには↓のように書きます。

    // 文字列定数をポインタ変数sに代入
    const char *s = "Hello, World!";

文字列定数をポインタ変数に代入する場合、そのポインタ変数にはconstを付けておくのがマナーです。
なぜかというと文字列定数が保存されたポインタを変更しようとするとセグフォなどが起こりプログラムがクラッシュするからです
そのためconstを付けて、文字列の要素を変更できないようにしておきます。

↑の式ではHello, World!というテキスト・セグメント領域にある文字列定数のアドレスが、ポインタ変数sに代入されます。
sに文字列としてHello, World!のメモリ領域が確保されているわけではありません。
すでにテキスト・セグメント内で確保されているHello, World!という文字列定数のアドレスが、ポインタ変数に代入されているだけです。

また文字配列のアドレスをポインタ変数に代入することも可能です。

    // 文字配列hwのアドレスをポインタ変数pに代入
    char hw[] = "Hello, World!";
    char *p = hw;

文字配列の場合は文字配列用にメモリ領域が確保されます。
そしてその文字配列(hw)のアドレスをポインタ変数pに代入しています。

文字配列のアドレスを代入したポインタ変数にも場合によってはconstを付ける場合があります。
文字配列の値を変更不可にしたい場合はconst, 変更可能にしたい場合はconstを付けないという選択肢があります。

constを付けない文字配列のアドレスが入ったポインタは自由に変更することができます
ただしメモリの境界線エラー(あさっての添え字の参照など)などは気にする必要があります。

文字配列に文字列をコピーする

他言語を学んでいる人にとって「文字列の代入」と言った場合、その多くは「文字列全体のコピー」を指していると思います。
C言語でも代入演算による文字列のコピーは提供されていますが、それは他言語に比べるとかなり限定的なものです。
文字列全体のコピーを代入で実現する」と言った場合、C言語ではそのケースは「定義する文字配列に文字列定数をコピーする」というかなり限られたケースになります。

実際に見てみましょう。
↓が文字配列に文字列定数をコピーするコードです。

    // 文字配列sに文字列定数「Hello, World!」を代入(コピー)する
    char s[] = "Hello, World!";
    printf("%s\n", s);  // Hello, World!

↑の場合sが文字配列です。
そして"Hello, World!"が文字列定数です。
↑のように宣言と同時に定義する文脈では、文字配列には文字列定数がコピーされます。
文字配列sにはHello, World!の文字数分とナル文字分の要素数が自動で確保されます。

文字配列の要素数を省略した場合は↑のように要素数が自動で確保されます。
しかし要素数を明示的に指定することも出来ます。

    // 要素数20の文字配列sに文字列定数「Hello, World!」を代入(コピー)する
    char s2[20] = "Hello, World!";
    printf("%s\n", s);  // Hello, World!

このコードは文字配列の定義の場合にのみ有効です。
つまり↓のようなコードはコンパイルエラーになります。

    char s[20];

    s = "Hello, World!";
    // error: assignment to expression with array type (GCC 10.0.0)

このエラーは多くの開発者の直感に反するものだと思いますが、仕様なんで許してください。

仕様ならしようがない

文字配列に文字列をコピーする

それなら変数定義の文脈以外で文字配列に文字列を代入(コピー)するのはどうしたらいいのか?
という話になります。

答えは簡単です。
それは「専用の関数を使ってコピーをする」ということになります。
この領域に来るともはや「代入」というカテゴリからは外れます。
しかしC言語で文字列を代入(コピー)したいとなった場合、こういった関数を使う方法は一般的です。
この記事ではこれらは広義の意味での代入と捉えてもいいものとし、紹介します。

具体的には↓の関数を使います。

  • strcpy

  • memcpy

  • memmove

  • snprintf

strcpyを使った方法

strcpy()は第1引数のポインタに第2引数の文字列をコピーします。
第1引数には通常、文字配列が指定されます。
strcpy()は第2引数の文字列をナル文字も含めてコピーします。

また第1引数の文字配列は十分なサイズが確保されている必要があります
サイズが足りない場合はバッファーオーバーランなどのバグになる場合があります。

また第1引数と第2引数には違うメモリ上のオブジェクトを指定する必要があります。
メモリが同じ場合の動作は未定義です。

    // strcpyを使った方法
    char s1[20];
    strcpy(s1, "Hello, World!");

    printf("%s\n", s1);  // Hello, World!

strcpy()は手軽でよく使われますが、その扱いには注意が必要です。
バグでバッファーオーバーランになった場合、プログラムの脆弱性となりえます。
そのためよりセキュアなsnprintf()の使用を検討してください。

memcpyを使った方法

memcpy()は第1引数のポインタに第2引数を第3引数で指定する文字数だけコピーします。
memcpy()で文字列をコピーする場合は↓のようにコードを書きます。

    // memcpyを使った方法
    char s2[20];
    const char *hw1 = "Hello, World!";
    memcpy(s2, hw1, strlen(hw1) + 1);

    printf("%s\n", s2);  // Hello, World!

strlen(hw1) + 1とやってナル文字もコピーのカウントに含めている点に注意が必要です。
また第1引数と第2引数には違うメモリ上のオブジェクトを指定する必要があります。
メモリが同じ場合の動作は未定義です。

memcpy()はオブジェクトの途中にナル文字が含まれていても指定文字数だけコピーします。
そのため途中のナル文字も一緒にコピーされます。

memcpy()memmove()は引数はvoid型のポインタで受け取ります。
そのため文字列に限らずあらゆるオブジェクトを渡すことができます。

memmoveを使った方法

memmove()は第1引数のポインタに第2引数を第3引数で指定する文字数だけコピーします。

    // memmoveを使った方法
    char s3[20];
    const char *hw2 = "Hello, World!";
    memmove(s3, hw2, strlen(hw2) + 1);

    printf("%s\n", s3);  // Hello, World!

memmove()memcpy()の違いは、memmove()は第1引数と第2引数のメモリが被っていても正常に動作する点です。
内部で引数のコピーを取ってメモリ領域が被っていても大丈夫なように処理をしてくれます。

snprintfを使った方法

snprintf()は第1引数にフォーマットの内容をコピーします。
snprintf()は第1引数に文字配列のポインタ、第2引数に文字配列のサイズを取ります。
第3引数にフォーマットを指定出来て、第4引数にフォーマットに渡す引数を指定します。

    // snprintfを使った方法
    char s4[20];
    snprintf(s4, sizeof s4, "Hello, World!");

    printf("%s\n", s4);  // Hello, World!

snprintf()strcpy()memcpy()などと違って、コピー先のバッファのサイズを指定することができます
そのためそれらの関数と比べてセキュアな関数と言えます。
またフォーマットで自由に文字列を生成することができるため、非常に使い勝手の良い関数と言えます。

おわりに

今回はC言語の文字列の代入について詳しく解説しました。
文字列の代入(コピー)はプログラムで頻繁に行われる処理です。
C言語では文字列クラスなどが存在しないため、かなり低レイヤ―な処理が必要になります。
しかしその分動作は軽快で速いプログラムを作ることができます。
ぜひ習得しておきたい技術と言えます。

変数に文字列を代入しよう