閉じる

【C言語】 typedefの使い方入門|例で理解する型定義・構造体・関数ポインタ

C言語のtypedefは、型に別名(エイリアス)をつけるための機能です。

特に構造体や関数ポインタのように長くなりがちな宣言を短く読みやすくするのに大きな力を発揮します。

本記事では、基本的な使い方から構造体、関数ポインタまで、実用でよく出てくるパターンに絞って丁寧に解説します。

C言語の入門者から、宣言の見通しを良くしたい方までを対象に進めていきます。

C言語のtypedefとは何か

typedefとは

typedefは、既存の型に新しい名前(別名)をつけるためのキーワードです。

「新しい型を作る」のではなく、「同じ型に別の呼び名を与える」機能だという点が重要です。

基本的なイメージとしては、次のようになります。

  • int 型にMyIntという別名をつける
  • struct PointPointという短い名前をつける
  • 複雑な関数ポインタ型に、シンプルな別名をつける

このようにすることで、プログラム中で「型っぽい名前」だけを見れば意味が分かるように整理できます。

typedefを使うメリット

typedefを使う主なメリットは次のような点です。

1つ目は読みやすさの向上です。

たとえば、関数ポインタや構造体のポインタが絡むと宣言が長くなりますが、typedefで「1つの型」として名前を与えておくと、宣言部分が大幅にスッキリします。

2つ目は変更に強くなる(保守性の向上)ことです。

コードのあちこちにunsigned longと書く代わりに、typedef unsigned long Size;のようにしておけば、将来サイズ表現を変更したいときにtypedefだけ書き換えれば済みます。

3つ目は抽象化(役割ベースの命名)ができることです。

intでもfloatでも、意味的には「温度」「ID」「フラグ」など異なる役割があります。

typedefTemperatureUserIdといった名前を与えることで、コードから「意図」が読み取りやすくなります。

どんなときにtypedefを使うべきか

typedefは、むやみに使うと逆に分かりにくくなることがあります。

適切な場面を意識して使うことが大切です。

よく使われる場面をいくつか挙げます。

  • 構造体や共用体の宣言を簡潔にしたいとき
  • 関数ポインタのように、宣言が複雑で読みにくい型を整理したいとき
  • IDやサイズなど、意味のある名前を型レベルで付けたいとき
  • ライブラリのAPIなどで、実装詳細を隠しつつ型だけ公開したいとき

逆に、単純なintcharばかりに別名を乱用すると、どれがどれだか分かりにくくなります。

長くて読みにくいもの」「役割をはっきりさせたいもの」を中心に使うとよいです。

typedefの基本的な使い方

基本構文と書き方

typedefの基本構文は次の通りです。

C言語
typedef 元の型 別名;

通常の変数宣言とほとんど同じ形で、先頭にtypedefを付けるだけです。

たとえば、int型にMyIntという別名をつける場合は次のように書きます。

C言語
typedef int MyInt;  // int型にMyIntという別名をつける

この宣言のあとでは、MyIntを通常の型名と同じように使用できます。

既存の型に別名をつける例

具体的なコードで見てみます。

C言語
#include <stdio.h>

// int型にMyIntという別名を定義
typedef int MyInt;

// unsigned long型にSizeという別名を定義
typedef unsigned long Size;

// double型にTemperatureという別名を定義
typedef double Temperature;

int main(void) {
    MyInt count = 10;          // int count = 10; と同じ
    Size buffer_size = 1024;   // unsigned long buffer_size = 1024; と同じ
    Temperature temp = 36.5;   // double temp = 36.5; と同じ

    printf("count = %d\n", count);
    printf("buffer_size = %lu\n", buffer_size);
    printf("temp = %.1f\n", temp);

    return 0;
}
実行結果
count = 10
buffer_size = 1024
temp = 36.5

この例では、用途を示す名前を型レベルで与えています。

「これはサイズを表す数値です」「これは温度です」という情報が、変数名だけでなく「型名」からも読み取れるようになるため、コードの意図が伝わりやすくなります。

ポインタ型にtypedefを使う例

ポインタ型は*がつくことで宣言が読みにくくなりがちです。

typedefを使うと、ポインタを1つの型として扱うことができます。

C言語
#include <stdio.h>

// char *型にStringという別名を定義
typedef char * String;

int main(void) {
    // Stringは「char *」の別名
    String s1 = "Hello";
    String s2 = "World";

    printf("%s %s\n", s1, s2);

    // 通常のポインタ宣言と混在させることも可能
    char *p = s1;
    printf("p = %s\n", p);

    return 0;
}
実行結果
Hello World
p = Hello

注意点として、typedefでポインタ型を定義すると*も含めて1つの型になることを意識する必要があります。

これは後述するconstとの組み合わせでも重要なポイントになります。

constとtypedefの組み合わせの注意点

typedefconstを組み合わせるときに特に重要なのが、「constが何にかかるか」です。

次の例を見てください。

C言語
#include <stdio.h>

// char * の別名をStringとして定義
typedef char * String;

int main(void) {
    const String s = "Hello";

    // sは「ポインタがconst」なのか「文字列がconst」なのか?
    printf("%s\n", s);

    return 0;
}

一見するとconst char *のように「文字列が変更できない」ように見えますが、実際にはchar * constと同じ意味になります。

つまり、s「変更できないポインタ(指す先は変えられない)」ですが、「指している文字列は変更可能」です。

整理すると次のようになります。

  • typedef char * String;
    Stringchar *そのもの
  • const String s;
    const char * const s;ではなく、char * const s;と同じ扱い

この違いを理解しやすくするために、比較表にまとめます。

宣言実際の型何がconstか
const char *p;const char *指す先の文字がconst
char * const p;char * constポインタ自体がconst
typedef char * String;<br>const String s;char * constポインタ自体がconst

「typedefは*を含めて1つの型にする」という点を忘れないようにしてください。

文字列を変更不可にしたい場合は、もともとの型をconst char *としてtypedefするのが確実です。

C言語
typedef const char * ConstString;  // 「const char *」に別名をつける

構造体(struct)とtypedefの実用例

構造体とtypedefの基本パターン

構造体とtypedefは、C言語でよく組み合わせて使われます。

典型的なパターンをまずは2種類見てみます。

C言語
// パターン1: structタグを使う
struct Point {
    int x;
    int y;
};

struct Point p1;  // struct Point を毎回書く

// パターン2: typedefで別名をつける
typedef struct {
    int x;
    int y;
} Point;

Point p2;         // struct を書かずに Point だけでOK

両者はメモリ構造としては全く同じですが、宣言の書き方が違います。

後者のパターンではPointが「型名」として使えるので、変数宣言が簡潔になります。

structタグとtypedefを両方使う理由

よく見かけるパターンとして、structタグtypedefを両方使う書き方があります。

C言語
typedef struct Point {
    int x;
    int y;
} Point;

この形にはいくつかのメリットがあります。

1つはタグ名Pointと、typedef名Pointを同一にすることで、C++との互換性が良くなることです。

C++ではstruct Point { ... };と書いたあとでPoint p;とだけ書けますが、Cでは通常struct Point p;と書く必要があります。

上記のようにtypedefしておくと、Pointだけで書けるようになり、C++風の書き方ができます。

もう1つは、前方宣言(宣言だけ先にしておくこと)に便利な点です。

特に構造体同士が互いにポインタで参照し合うようなケースなどで役立ちます。

構造体の宣言を簡潔にするtypedefテクニック

構造体を多用するプログラムでは、structを毎回書くのが煩雑になります。

そこで、typedefを使って宣言を簡潔にするテクニックがよく用いられます。

C言語
#include <stdio.h>

// ユーザー情報を表す構造体を定義し、Userという別名をつける
typedef struct {
    int  id;
    char name[32];
    int  age;
} User;

// 座標を表す構造体
typedef struct {
    double x;
    double y;
} Point;

// 長方形を表す構造体
typedef struct {
    Point left_top;
    Point right_bottom;
} Rect;

void print_user(const User *u) {
    printf("User(id=%d, name=%s, age=%d)\n", u->id, u->name, u->age);
}

int main(void) {
    User u = {1, "Alice", 20};
    Point p = {10.0, 20.0};
    Rect  r = {{0.0, 0.0}, {100.0, 50.0}};

    print_user(&u);

    printf("Point(%.1f, %.1f)\n", p.x, p.y);
    printf("Rect left_top (%.1f, %.1f)\n",  r.left_top.x,  r.left_top.y);
    printf("Rect right_bottom (%.1f, %.1f)\n",
           r.right_bottom.x, r.right_bottom.y);

    return 0;
}
実行結果
User(id=1, name=Alice, age=20)
Point(10.0, 20.0)
Rect left_top (0.0, 0.0)
Rect right_bottom (100.0, 50.0)

構造体の定義部にtypedefを組み込むことで、以後のコードでは構造体名だけで宣言・引数・戻り値を扱えるようになります。

これにより、ヘッダファイルなども読みやすくなり、APIの設計がしやすくなります。

構造体ポインタにtypedefを使う例

リンクリストのようなデータ構造では、構造体のポインタを多用します。

このとき、typedefでポインタ型に別名をつけると、宣言が揃って読みやすくなる場合があります。

C言語
#include <stdio.h>
#include <stdlib.h>

// リストのノードを表す構造体
typedef struct Node {
    int value;
    struct Node *next;
} Node;

// Node* に NodePtr という別名をつける
typedef Node * NodePtr;

// 新しいノードを作成する関数
NodePtr create_node(int value) {
    NodePtr node = (NodePtr)malloc(sizeof(Node));
    if (node == NULL) {
        return NULL;
    }
    node->value = value;
    node->next  = NULL;
    return node;
}

// リストをたどって値を表示する関数
void print_list(NodePtr head) {
    for (NodePtr cur = head; cur != NULL; cur = cur->next) {
        printf("%d ", cur->value);
    }
    printf("\n");
}

int main(void) {
    NodePtr head = create_node(1);
    head->next   = create_node(2);
    head->next->next = create_node(3);

    print_list(head);

    // 本来はfreeで解放が必要(今回は説明を簡略化)
    return 0;
}
実行結果
1 2 3

「ノードへのポインタ」を意味するNodePtrという型名にすることで、関数の引数や戻り値を見ただけで、「ノードそのもの」ではなく「ノードを指すポインタ」を扱っていることが分かりやすくなります。

ネストした構造体とtypedefの使い方

複数の構造体が入れ子(ネスト)になっている場合でもtypedefは有効です。

C言語
#include <stdio.h>

// 住所を表す構造体
typedef struct {
    char prefecture[32];
    char city[32];
    char street[64];
} Address;

// ユーザー情報を表す構造体(中にAddressを含む)
typedef struct {
    int     id;
    char    name[32];
    Address address;  // ネストした構造体をメンバとして持つ
} User;

void print_user(const User *u) {
    printf("id: %d\n", u->id);
    printf("name: %s\n", u->name);
    printf("address: %s %s %s\n",
           u->address.prefecture,
           u->address.city,
           u->address.street);
}

int main(void) {
    User u = {
        1,
        "Bob",
        {"Tokyo", "Shinjuku", "1-2-3"}
    };

    print_user(&u);

    return 0;
}
実行結果
id: 1
name: Bob
address: Tokyo Shinjuku 1-2-3

ネストした構造体名に短いtypedef名をつけておくことで、上位の構造体の宣言も簡潔になります。

特に、階層の深いデータ構造を設計するときには、型名を整理しておくことで見通しが大きく改善します。

関数ポインタとtypedefの使い方

関数ポインタの基本形と読みづらさ

関数ポインタはC言語の中でも特に宣言が読みにくい部分です。

たとえば、次の宣言を初めて見たとき、直感的に理解しにくいかもしれません。

C言語
int (*func)(int, int);

これは「int型を返し、int型を2つ引数に取る関数へのポインタfuncという意味です。

括弧の位置(func)が重要で、func全体が「ポインタ」であり、それが「関数」を指します。

このように、関数ポインタの宣言は形自体が複雑で、「戻り値」「引数」「ポインタ」の情報が1カ所に詰め込まれています。

ここでtypedefを使うことで、関数ポインタという複雑な型にシンプルな名前を与えることができます。

typedefで関数ポインタをわかりやすくする

関数ポインタにtypedefを使う典型的な例を見てみます。

C言語
#include <stdio.h>

// 2つのintを受け取ってintを返す関数型にCalcFuncという別名をつける
typedef int (*CalcFunc)(int, int);

// 足し算をする関数
int add(int a, int b) {
    return a + b;
}

// 掛け算をする関数
int mul(int a, int b) {
    return a * b;
}

// CalcFunc型の関数ポインタを引数にとる関数
int apply(CalcFunc func, int x, int y) {
    return func(x, y);  // 渡された関数を呼び出す
}

int main(void) {
    CalcFunc f;     // 関数ポインタ変数

    f = add;
    printf("add: %d\n", apply(f, 2, 3));  // 2 + 3 = 5

    f = mul;
    printf("mul: %d\n", apply(f, 2, 3));  // 2 * 3 = 6

    return 0;
}
実行結果
add: 5
mul: 6

CalcFuncは「2つのintを受け取ってintを返す関数ポインタ」という意味の型名です。

以後は「CalcFunc」という1語を見るだけで、関数ポインタの役割が分かるようになり、コードを読む負荷が大きく下がります。

コールバック関数でのtypedef活用例

コールバック関数(関数を引数として渡す仕組み)では、typedefで関数ポインタ型に名前をつけておくと、APIが非常に読みやすくなります。

C言語
#include <stdio.h>

// コールバック関数の型
// intを1つ受け取り、戻り値なし(void)の関数ポインタ
typedef void (*VisitFunc)(int value);

// 配列の全要素に対してコールバック関数を呼び出す
void for_each(int *array, int size, VisitFunc visitor) {
    for (int i = 0; i < size; ++i) {
        visitor(array[i]);  // 渡されたコールバックを実行
    }
}

// コールバック関数の例1: 値を表示する
void print_value(int value) {
    printf("value: %d\n", value);
}

// コールバック関数の例2: 2倍にして表示する
void print_double(int value) {
    printf("double: %d\n", value * 2);
}

int main(void) {
    int data[] = {1, 2, 3};
    int size   = sizeof(data) / sizeof(data[0]);

    // 1つ目のコールバック
    for_each(data, size, print_value);

    // 2つ目のコールバック
    for_each(data, size, print_double);

    return 0;
}
実行結果
value: 1
value: 2
value: 3
double: 2
double: 4
double: 6

for_each関数の宣言部VisitFunc visitorだけを見れば、「これは何らかの訪問処理(visit)をする関数を受け取るんだな」と想像できます。

APIを設計するときには、コールバックの型に意味のある名前を付けることが、読みやすいインターフェースの重要なポイントになります。

typedefと関数ポインタ配列の例

複数の関数を配列としてまとめたいときにも、typedefは有効です。

例えば、簡単なメニューシステムやステートマシンなどで「状態ごとに処理関数を切り替える」ようなコードを書きたい場合を考えてみましょう。

C言語
#include <stdio.h>

// 引数なし・戻り値なしの処理関数を表す型
typedef void (*Handler)(void);

// いくつかの処理関数を定義
void handle_start(void) {
    printf("Start\n");
}

void handle_stop(void) {
    printf("Stop\n");
}

void handle_pause(void) {
    printf("Pause\n");
}

int main(void) {
    // Handler型の配列として宣言
    Handler handlers[] = {
        handle_start,  // index 0
        handle_stop,   // index 1
        handle_pause   // index 2
    };

    int command;

    printf("0: start, 1: stop, 2: pause\n");
    printf("command? ");
    if (scanf("%d", &command) != 1) {
        return 1;
    }

    if (command >= 0 && command < 3) {
        // コマンドに対応する関数を呼び出す
        handlers[command]();
    } else {
        printf("Unknown command\n");
    }

    return 0;
}
実行結果
0: start, 1: stop, 2: pause
command? 0
Start

ここでtypedefを使わずに書くと、次のように長くなります。

C言語
// typedefを使わない場合の配列宣言例
void (*handlers2[])(void) = { handle_start, handle_stop, handle_pause };

1行に「配列」「ポインタ」「関数」の要素が混在するため、一目で理解するのは少し大変です。

typedefHandlerという型名を導入することで、「関数ポインタの配列である」という構造が明確になります。

まとめ

typedefは「新しい型を作る」のではなく、「既存の型に分かりやすい別名をつける」ための仕組みです。

特に、構造体やポインタ、関数ポインタのように宣言が長くなりがちな型で威力を発揮します。

役割を反映した型名を付けることで、コードの意図が伝わりやすくなり、変更にも強くなります。

一方で、過剰なtypedefはかえって混乱を招くこともあるため、「長くて読みにくい型」「意味を強調したい型」に絞って活用することが大切です。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!