初期化と代入 | Programming Place Plus C言語編 第7章

トップページC言語編

このページの概要 🔗

このページの解説は C99 をベースとしています

以下は目次です。


代入 🔗

変数が保持している値を変更する操作を代入 (assignment) といいます。

代入は次の構文で行えます。

変数名 =

この構文を、代入式 (assignment expression) と呼びます。=代入演算子 (assignment operator) と呼ばれる演算子です。

代入演算子には「=」、つまりイコールの記号を使いますが、「等しい」という意味はなく、「変数名 = 式」は「変数名と式が同じである」というようなことを意味しません。

プログラムの実行中、代入式の箇所に来ると、まず「式」の部分を処理します。式は、処理を行うと必ずなんらかの値になります(10 + 10 は 20 になりますし、a + b は変数a の値と変数b の値を足し合わせたものになります)。式から値を作り出すことを、式を評価 (evaluation) するといいます。「式」の部分を評価してつくられた値が、「変数名」に記述した変数に与えられます。

なお、代入演算子の左側の記述を左辺 (left-hand side)、右側の記述を右辺 (right-hand side) ということがあります。

変数はつねに1つの値しか持てませんから、左辺の変数が保持していた値は上書きされます。

代入は、すでに存在している変数の値を変更する行為ですから、「変数名」に書く変数は宣言されたものでなければなりません。そうでなければコンパイルエラーになります。

代入を行うことによって、未初期化状態だった変数に、はっきりした値を記憶させることができます。

int num;   // 未初期化状態
printf("%d\n", num);  // 危険
int num;    // 未初期化状態
num = 100;  // 代入
printf("%d\n", num);  // 安全

代入式の右辺は、定数だけでなく、変数を使ったり、計算式にしたりできます。

#include <stdio.h>

int main(void)
{
    int num1;
    int num2;
    int num3;

    num1 = 100;
    num2 = num1;
    num3 = num1 * 2;

    printf("num1: %d\n", num1);
    printf("num2: %d\n", num2);
    printf("num3: %d\n", num3);
}

実行結果:

num1: 100
num2: 100
num3: 200

num2 = num1 を実行しても、右辺側の num1 の値に変化は起こりません。つまり、代入は値を移動させているのではなく、コピーしています。

代入と型 🔗

代入先の変数の型と、代入しようとする値の型は、基本的には一致していなければなりません。一致していても(しているようにみえても)代入できないケースや、一致していなくても代入できてしまうケースもあるので、かなり分かりづらいですが、まずは型が一致していることが前提であると理解しておきましょう。

ともかく、型に対する意識は強く持つようにしてください。C言語は、型が重要な意味を持つ一方で、型を簡単に無視してしまえます。それでも、正しい型を使えているかしっかり意識してソースコードを書いておけば、プログラマーの意図を示すことができ、自分を含む誰かがソースコードを読み解くときにも助けになります。

次のプログラムにある4つの代入は、int型と、char型の配列とのあいだでの代入になっています。このうち、確実に問題ないのは1つだけで、2つはコンパイルエラー、1つは注意を要するものになっています。

#include <stdio.h>

int main(void)
{
    int value;
    char message[40];

    // OK. int型の変数に整数を代入できる
    value = 100;

    // 注意。int型の変数に文字列を代入できてしまう
    value = "Hello";

    // エラー。char型の配列に、整数を代入できない
    message = 100;

    // エラー. char型の配列に、文字列を代入できない
    message = "Hello";
}

int型の変数value に整数を代入することは問題ありません。ここで、100 という整数定数は int型です。つまり、int型の変数に int型の値を代入しようとしており、このように型が一致している代入はつねに問題なく行えます。

【上級】整数定数の型は基本的に int ですが、int型で表現できないほど巨大な数の場合には、long int型や long long int型として取り扱われます[1]

int型の変数value に文字列リテラルを代入することは、型がまったく異なるように思えますが可能です。文字列を int型の変数に代入することは、普通に考えて意図がみえない行為であり、するべきではないですが、コンパイルエラーにならないので注意が必要です(多くのコンパイラがこの代入には警告を出します)。

【上級】この代入が許されてしまうのは、文字列リテラルが char の配列であり[2]、配列はポインタへ暗黙的に型変換でき、ポインタは整数なので int型の変数に代入できるからです。

char型の配列message に int型の整数を代入することはできません。文字列を保持するための変数なので、1つの整数を代入できないことは理屈どおりです。

char型の配列message に文字列リテラルを代入することは、明らかにできてよさそうですが、コンパイルエラーになります。これはできないと困るので、代わりの手段として strcpy関数があります。

strcpy関数 🔗

strcpy関数は、文字列を別のところへコピーする関数です。strcpy関数を使用するには、#include <string.h> という記述が必要です。

strcpy(コピー先, コピー元);

「コピー先」には char型の配列を、「コピー元」には文字列リテラルや char型の配列を指定できます。

配列以外のものを指定する場合がありますが、当面はこのような使い方だけに限定します。

「コピー先」の配列には、「コピー元」の文字列を受け取れるだけの十分な大きさが必要であることに注意してください。この約束を破ると、バッファオーバーフロー (buffer overflow) と呼ばれる重大なバグにつながります。バッファオーバーフローとは、有効な範囲を超えたところにまで書き込みを行ってしまい、そこに元々あった別のデータを上書きしてしまうことをいいます。


たとえば、次のように使います。

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s1[40];
    char s2[40];

    strcpy(s1, "Hello");
    puts(s1);

    strcpy(s2, s1);
    puts(s2);
}

実行結果:

Hello
Hello

Visual Studio 2015 では、strcpy関数を使おうとすると警告が出ます。

warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.

警告される理由は、さきほど説明したバッファオーバーフローの危険性があるからです。原理的に strcpy関数は、プログラマーが徹底して注意をはらう以外に、バッファオーバーフローを避ける方法がありません。そのため、Visual Studio ではそもそも strcpy関数を使うことを自体を避け、代わりに strcpy_s関数という安全性を高めた関数を使うことを勧めています。ただし、strcpy_s関数は C99 には存在しない関数です。

【C11】strcpy_s関数が標準ライブラリに追加されました。

C言語編は C99 をベースに解説しているので、strcpy_s関数は使わずに進めます。C4996 の警告が気になるなら、「Visual Studio編>C4996警告」のページを参考にして対応してください。

変数の初期化 🔗

先ほどの例では、変数を宣言した後で値を代入していましたが、宣言と同時に値を与えることもできます。このような行為を、初期化 (initialize) といいます。

宣言と同時に値を与える構文は、次のようになります。

型名 変数名 = 初期化子;

char input_string[80]; のように、[整数] の部分がある場合は少し難しくなるのでここでは割愛します。

第25章で取り上げます。

「初期化子」の部分には、10 とか numnum + 1 といった記述が入ります。このように、初期化のために = に続けて記述するリテラルや式の部分を、初期化子 (initializer) と呼びます。

【C23】空の初期化子 (empty initializer) が追加され、int num = {}; のような初期化が許されるようになりました。この場合、型に応じたデフォルトの値で初期化されます。[5]

[【C++プログラマー】C23 で空の {} を使ったデフォルト初期化が可能になりましたが、= が必要です。]

何度か繰り返しているとおり、未初期化状態の変数は取り扱いに注意しなければなりません。そのため、変数を宣言するのと同時に値を与えてしまうのが安全といえます。

#include <stdio.h>

int main(void)
{
    int num1;        // できるだけ未初期化状態は避けたほうがいい

    //
    // ここに多くのコードがあると、未初期化状態の num1 の値を使うミスを犯す機会を増やしてしまう
    //

    num1 = 100;
    printf("%d\n", num1);

    int num2 = 200;  // こちらの方が良い習慣
    printf("%d\n", num2);
}

実行結果:

100
200

代入式の連結 🔗

代入式は連結できます。

#include <stdio.h>

int main(void)
{
    int value = 10;
    int value2 = 20;
    int value3 = 30;

    value = value2 = value3;

    printf("%d %d %d\n", value, value2, value3);
}

実行結果:

30,30,30

value = value2 = value3 のように、代入式が連続している場合、右側から順に処理されます。つまり、value = (value2 = value3) であるかのように扱われます[3]。まず、value3 の値が value2 に代入され、その結果が value に代入されるということです。

この書き方は、複数の変数に同じ値を代入したいときに、1つの文にまとめて書けるのだと理解すればいいです。

自己代入 🔗

ある変数に、自分自身の値を代入する記述も可能です。

#include <stdio.h>

int main(void)
{
    int value = 10;

    value = value;

    printf("%d\n", value);
}

実行結果:

10

value = value のように、自身の値を自身に代入するかたちを自己代入 (self assignment) と呼びます。

自己代入に特別なルールがあるわけではなく、いつもの代入のルールに従います。つまり、右辺の変数の値が、左辺の変数に代入され、右辺の変数に変化は起きません。結局のところ、何も変化は起こらないということになります。

わざわざ自己代入をすることに意味はありませんが、不正でないことを知っておくと便利な場面がいずれ登場します。

自己代入ではありませんが、似たようなかたちの代入はよく必要になります。たとえば、ある変数の値を「現在の3倍」にしたいとき、次のように書けます。

#include <stdio.h>

int main(void)
{
    int value = 10;

    value = value * 3;

    printf("%d\n", value);
}

実行結果:

30

変数 value の値を 3倍した値を得るには、乗算演算子を使って value * 3 という計算を行い、その結果を value に代入すればいいです。したがって、value = value * 3 という式になります。左辺と右辺に同じ変数があらわれる特徴的な代入式になりました。

代入にイコールの記号が使われる理由に頭を悩ませないでください。value = value * 3 は、= を「イコール」だと捉えてしまうとまったく意味不明にみえます。代入の「=」は単にそういう記号です。

複合代入演算子 🔗

value = value * 3 のように、ある変数に計算をおこなって、結果を同じ変数に入れたいという場面はよくあります。そのため、記述を少し簡単に済ませる方法が用意されています。

その方法は、複合代入演算子 (compound assignment operator) と呼ばれる演算子を使うことです。複合代入演算子はいくつかの演算子を総称した名称です。現時点では以下の5つを知っておくと良いです。

構文は以下のとおりです。

変数名 複合代入演算子 式

具体的には、たとえば value *= 3; のような記述になります。この意味は、value = value * 3; と同じです。つまり、「X op= E」は「X = X op E」と同じです。

【上級】E1 op= E2E1 = E1 op E2 と同じ意味ではありますが、複合代入演算子の場合は E1 が1回しか評価されない点でだけ異なっています[4]。通常これは好ましい動作です。

複合代入演算子を使ったプログラムは、次のようになります。

#include <stdio.h>

int main(void)
{
    int value = 10;

    value += 20;    // value = value + 20; と同じ
    printf("%d\n", value);

    value -= 10;    // value = value - 10; と同じ
    printf("%d\n", value);

    value *= 3;    // value = value * 3; と同じ
    printf("%d\n", value);

    value /= 2;    // value = value / 2; と同じ
    printf("%d\n", value);

    value %= 4;    // value = value % 4; と同じ
    printf("%d\n", value);
}

実行結果:

30
20
60
30
2

/= と %= については、除算と剰余の計算をしていることになるので、ゼロ除算の可能性があります。右辺を評価した結果が 0 にならないように注意してください(「第3章」を参照)。

value *= value2 + 1; のような、右側が複雑な記述も可能です。この場合、value = value * (value2 + 1); と同じになります。() の存在に注意してください。この例の場合は、複合代入演算子を使うとかえって分かりづらく見えるかもしれませんが、その感覚も正しいと思います。短く書かれたプログラムが、必ずしも優れているわけではありません。


練習問題 🔗

問題① 次のプログラムの実行結果はどうなりますか?

#include <stdio.h>

int main(void)
{
    int value = 10;
    int value2 = -10;

    value = value2;
    printf("%d,%d\n", value, value2);

    value2 = value + 5;
    printf("%d,%d\n", value, value2);

    value = value2 = 100;
    printf("%d,%d\n", value, value2);
}

問題② 整数を1つ入力させ、その数を2倍した数、さらに2倍した数、それをさらに2倍した数・・・を順々に出力するプログラムを作成してください(適当なところで止めてください)。

問題③ 2つの int型の変数 a, b があるとして、互いに値を入れ替えるプログラムを書いてください。

問題④ 次のプログラムを、複合代入演算子を使って書き直してください。

#include <stdio.h>

int main(void)
{
    int num = 15;
    int num2 = 5;
    int num3 = 30;

    num = num - 10;
    num2 = num2 + (num * 4);
    num3 = num3 % (num2 - 5);

    printf("%d %d %d\n", num, num2, num3);
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗



前の章へ (第6章 標準入力)

次の章へ (第8章 文字と文字列)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る