C言語の型について

C言語型について気になった点をメモしておきます。なお、かなり自己解釈しているので間違ったことを書いてるのを前提に読んで欲しいです。

型の分類

私のみたところ、C言語の型は、基本型、参照型、そして特殊な型に分類することができます。*1

  • 基本型 (int、char、long、float)
  • 参照型 (ポインタ)
  • 特殊な型
    • 参照型のように振舞う定数 (配列、関数)
    • 基本型のように振舞う定数 (構造体)

基本型と参照型

まず、もっとも基本的なのが基本型です。これは変数に目的のデータを直接入れるもので、intやcharといったものがあります。以下の例では整数10が目的のデータです。

int a = 10;

これに対して、参照型があります。ポインタのことです。こちらは、変数に直接目的のデータを入れることはせず、変数の中には、目的のデータへのアドレスが入っています。

int a = 10;
int *p;
p = &a; //アドレスを代入

なおこのとき、ポインタの型は、目的のデータの型に合わせる必要があります。今回の場合、目的のデータはint型の10なので、ポインタの型もint*型になっています。

特殊な型

  • 特殊な型
    • 参照型のように振舞う定数 (配列、関数)
    • 基本型のように振舞う定数 (構造体)

特殊な型は二種類に分類することができます。まずは、「参照型のように振舞う定数」を見ていきます。

配列

配列は、よく参照型(ポインタ)と間違えられます。例えば、以下のようなコードが誤解を生む元になっています。

int array[3] = { 2, 5, 11 };
int *p;
p = array;
printf("%d", p[1]); //5

このコードは正しく動作します。int型のポインタに配列を格納できてしまいました。しかも、ポインタに角括弧「[]」をつけて、配列のように扱っています。しかし、逆に配列にポインタを格納することはできません。

int array1[3] = { 2, 5, 11 };
int array2[3];
int *p;
p = array1;
array2 = p; //エラー

このことから、配列とポインタは別のものであるということがわかります。ではなぜ、int型のポインタに、配列を格納できたのでしょうか。それは配列が場合によって、ポインタのように振舞うからなのです。多くの場合、配列はポインタのように振舞うので、ポインタのように扱っていても、問題が起きないケースが多いというわけです。
では配列の正体は何なのでしょうか。配列の正体は、参照でもなければ、変数でもありません。配列は定数です。次に以下のコードをご覧ください。これは、配列を入れるための変数を定義しているように見えますが、実はこれだけで配列の定数が作られています。

int array[10];

定数に何かを代入することはできないので、以下のコードは誤りです。

int array1[3] = { 2, 5, 11 };
int array2[3];
array2 = array1; //エラー

また、ポインタは加算することができますが、配列は定数なので加算することができません。以下も誤りです。

int array[3] = { 2, 5, 11 };
array++; //エラー

配列は「参照型のように振舞う定数」と頭のなかに置いておくといいでしょう。ちなみにC言語ではmallocを使っても配列のようなものを作ることができますが、こちらの配列は性質が違うので注意してください。

関数

関数も、配列と同じ「参照型のように振舞う定数」です。まずは定数なので、代入や加算ができません。

int foo() {
    return 3;
}

int bar() {
    return 5;
}

main() {
    printf("%d", foo());
    foo = bar; //エラー
    printf("%d", foo());
    foo++; //エラー
}

上のコードを実行するとエラーがでます。定数なので代入や加算はできません。次に配列と同じで、参照型のように振舞うことができます。以下のコードをご覧ください。

int foo() {
    return 3;
}

main() {
    int (*fp)();
    fp = foo;
    printf("%d", fp()); //3
}

このコードは、ポインタが気持ち悪い形をしていますが、正しいものです。このポインタに関数を代入するとき括弧「()」をつけていないのがポイントです。これらのことから、関数も配列と似たような性質があることがわかりました。関数も「参照型のように振舞う定数」と覚えておきましょう。

構造体

最後に構造体をみてみます。配列や関数が「ポインタのように振舞う定数」だったように、構造体は「基本型のように振舞う定数」です。

定数

まず、以下のコードを書くだけで構造体が作られるのは配列や関数と同じです。構造体も定数のようなものです。

struct person {
    char name[50];
    int age;
};

main() {
    struct person john; //変数宣言ではなく、定数を作ってる
}

宣言と同時に値を入れるコードは以下のようになります。定数なのに宣言時には値を代入できるのは、配列と似た性質です。

struct person john = { "John", 20 };
printf("name:%s age:%d", john.name ,john.age); // name:John age:20
代入

構造体は、定数なのにもかかわらず、構造体の中に構造体を代入することができます。こっちは配列にはなかった性質です。配列は、配列に配列を代入しようとすると怒られます。最初に「基本型のように振舞う定数」といいましたが、まさに基本型のように振舞っています。

struct person {
    char name[50];
    int age;
};

main() {
    struct person john = { "John", 20 };
    struct person bob;
    bob = john; //代入可能
    printf("name:%s age:%d", bob.name ,bob.age); // name:John age:20
}

しかし、これは普通の変数の代入とは違います。構造体の持つメンバの値を代入しています。構造体自体を代入しているわけではありません。具体的には、johnの参照先をbobの参照先にしている訳ではありません。これは以下コードのように、代入を行った後で、johnに変更を加えてみると分かります。

struct person john = { "John", 20 };
struct person bob;
bob = john;
printf("name:%s age:%d", bob.name ,bob.age);

john.age = 30;
printf("name:%s age:%d", bob.name ,bob.age); //name:John age:20

john.ageを書き換えたのに、bobの方には反映されていません。参照先を代入しているわけではないことが分かります。あと、配列や関数と同じで加算はエラーになります。

struct person john = { "John", 20 };
john++; //エラー
ポインタ操作

構造体のポインタを扱う方法は、基本型と同じです。配列や関数のように&を省略することはできません。

struct person john = { "John", 20 };
struct person *p;
p = &john;
printf("name:%s age:%d", (*p).name, (*p).age);

また、配列や関数のように比較することはできません。基本型にもない性質ですね。

struct person john = { "John", 20 };
struct person bob = { "Bob", 30 };
if(john == bob) { //エラー
    ……
}

配列や関数の場合だと、比較の際にはポインタとみなされ、参照先が比較されます。しかし構造体では比較すること自体できません。

おさらい

  • 基本型 (int、char、long、float)
  • 参照型 (ポインタ)
  • 特殊な型
    • 参照型のように振舞う定数 (配列、関数)
    • 基本型のように振舞う定数 (構造体)

*1:Wikipediaを見てみたところ、参照型は基本型に含まれていたので、いきなり間違ったことを書いていますが、そこは目をつぶってください