コンテンツにスキップ

C言語/ポインタ

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

ポインタとは何か

[編集]

ポインタは、メモリ上のデータのアドレスを指す変数です。つまり、ポインタ変数に格納されている値は、別の変数やデータ領域の場所を示しています。この場所のことを、ポインタが指し示していると言います。

ポインタを使うことで、以下のことが可能になります。

  • データを直接操作する:ポインタを使って、データの内容を直接読み書きすることができます。
  • データ構造を扱う:構造体や配列などの複雑なデータ構造を効率的に扱うことができます。
  • 動的メモリ管理:プログラム実行時に必要なメモリを動的に割り当てたり解放したりすることができます。

これらのことから、ポインタはC言語において非常に重要な機能の一つと言えます。

ポインタの重要性

[編集]

ポインタは、C言語を柔軟かつ効率的にプログラミングするために不可欠なものです。ポインタを使うことで、以下のことが可能になります。

柔軟なデータ処理
ポインタを使って、データの内容を直接操作したり、データ構造を扱うことができます。これは、従来の変数ではできない操作であり、より複雑なプログラムを書くことが可能になります。
効率的なメモリ管理
ポインタを使って、動的メモリ管理を行うことができます。これは、プログラム実行時に必要なメモリを動的に割り当てたり解放したりすることで、メモリの無駄遣いを防ぎ、プログラムのパフォーマンスを向上させることができます。
低水準のプログラミング
ポインタを使って、ハードウェアやオペレーティングシステムと直接やり取りすることができます。これは、システムプログラミングやデバイスドライバ開発など、低水準のプログラミングを行う際に必要不可欠な機能です。

このように、ポインタはC言語の機能を最大限に活用するために欠かせない存在です。

ポインタの基本

[編集]

宣言と初期化

[編集]

ポインタ変数は、通常の変数と同様に宣言することができます。ポインタ変数の宣言には、以下の形式が使われます。

型名 *変数名;
  • 型名 は、ポインタが指す変数の型を表します。
  • 変数名 は、ポインタ変数の名前を表します。
例:
int *p;
この宣言は、p という名前のポインタ変数を宣言します。このポインタ変数は、int 型の変数を指し示すことができます。

ポインタ変数は、宣言と同時に初期化することもできます。ポインタ変数の初期化には、以下の形式が使われます。

型名 *変数名 = 初期値;
* 初期値 は、ポインタ変数が指す変数の初期値を表します。
例:
int i = 10;
int *p = &i;

このコードは、i という名前の変数に 10 を代入し、p という名前のポインタ変数を i のアドレスに初期化します。

アドレス演算子 (& と *)

[編集]

ポインタ変数には、2 種類の重要な演算子があります。

  • アドレス演算子 &:変数のアドレスを取得します。
  • 間接参照演算子 *:ポインタが指す変数の値を取得または設定します。

アドレス演算子 (&)

[編集]

アドレス演算子 & は、変数のアドレスを取得します。変数のアドレスとは、メモリ上のその変数の場所を表す値です。

アドレス演算子 & は、以下の形式で使用されます。

型名 *変数名 = &実体名;
  • 実体名 は、アドレスを取得したい変数の名前を表します。
例:
int i = 10;
int *p = &i;

このコードは、i という名前の変数のアドレスを p というポインタ変数に格納します。

間接参照演算子 (*)

[編集]

間接参照演算子 * は、ポインタが指す変数の値を取得または設定します。

間接参照演算子 * は、以下の形式で使用されます。

型名 変数名 = *ポインタ名;
*ポインタ名 = ;
  • ポインタ名 は、ポインタ変数の名前を表します。
  • は、ポインタが指す変数の新しい値を表します。

ポインタによる値の取得

[編集]

間接参照演算子 * を使って、ポインタが指す変数の値を取得することができます。

例:
int i = 10;
int *p = &i;
int value = *p;

このコードは、i の値を value に代入します。

ポインタによる値の設定

[編集]

間接参照演算子 * を使って、ポインタが指す変数の値を設定することができます。

例:
int i = 10;
int *p = &i;
 
*p = 20;

このコードは、i の値を 20 に設定します。

ポインタと sizeof 演算子

[編集]

sizeof 演算子は、データ型または変数のサイズを取得するために使用されます。ポインタの場合、sizeof 演算子は、ポインタが指しているデータ型ではなく、ポインタ変数自体のサイズを取得します。

例:
int i = 10;
int *p = &i;
 
printf("%zu\n", sizeof(i)); // 4
printf("%zu\n", sizeof(p)); // 8

このコードは、i のサイズと p のサイズを出力します。結果は、4 と 8 であることがわかります。これは、64 ビット環境では、int 型とポインタ型がそれぞれ 4 バイトの 8 バイトのサイズであるためです。

ポインタと配列

[編集]

配列とポインタの関係

[編集]

配列名とポインタは、同じメモリ領域を指し示すことを理解することが重要です。つまり、配列要素にアクセスするには、配列名またはその要素のアドレスを使用することができます。

例:
int ary[] = {1, 2, 3, 4, 5};
 
int *p = ary;
 
printf("%d\n", ary[0]); // 1
printf("%d\n", p[0]);   // 1

このコードは、ary という名前の配列を宣言し、その要素に値を代入します。その後、p という名前のポインタ変数を ary に初期化します。最後に、ary[0]p[0] の値を出力します。結果は、両方の値が 1 であることがわかります。

配列要素へのアクセス

[編集]

ポインタを使って、配列要素にアクセスすることができます。ポインタが指している配列要素にアクセスするには、以下の形式を使用します。

型名 変数名 = ポインタ名[添字];
  • 添字 は、アクセスしたい配列要素のインデックスを表します。
例:
int ary[] = {1, 2, 3, 4, 5};
int *p = ary;
 
printf("%d\n", p[2]); // 3

このコードは、ary の 3 番目の要素の値を p[2] に代入します。結果は、p[2] の値が 3 であることがわかります。

文字列操作

[編集]

ポインタを使って、文字列を操作することができます。C 言語において、文字列は char 型の配列として表現されます。

例:
char str[] = "Hello, World!";
char *p = str;
 
printf("%c\n", *p); // H
printf("%s\n", p); // Hello, World!

このコードは、str という名前の文字列配列を宣言し、その要素に値を代入します。その後、p という名前のポインタ変数を str に初期化します。最後に、*pp の値を出力します。結果は、*p の値が 'H' であること、p の値が "Hello, World!" であることがわかります。

ポインタと関数

[編集]

関数引数としてのポインタ

[編集]

関数にポインタを渡すことができます。ポインタを渡すことで、関数内でポインタが指すデータに直接アクセスすることができます。

例:
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
 
int main() {
    int x = 5;
    int y = 10;
 
    swap(&x, &y);
 
    printf("%d, %d\n", x, y); // 10, 5
}

このコードは、swap という名前の関数を定義します。この関数は、2 つのポインタ引数を受け取り、そのポインタが指す値を入れ替えます。main 関数では、xy という変数を宣言し、swap 関数を呼び出して xy の値を入れ替えます。最後に、xy の値を出力します。結果は、x が 10 で y が 5 になっていることがわかります。

関数ポインタ

[編集]

関数ポインタは、関数を指すポインタです。関数ポインタを使って、以下のことが可能になります。

  • 関数を引数として渡す
  • 関数を戻り値として返す
  • 関数ポインタを配列に格納する
例:
#include <stdio.h>
#include <stdlib.h>

static int compare_fwd(const void *a, const void *b) {
    // const void * 型のポインタを int * にキャスト
    int x = *((const int *)a);
    int y = *((const int *)b);

    // 昇順にソートする場合
    return x - y;
}

static int compare_rev(const void *a, const void *b) {
    // const void * 型のポインタを int * にキャスト
    int x = *((const int *)a);
    int y = *((const int *)b);

    // 降順にソートする場合
    return y - x;
}


int main() {
    int ary[] = {5, 2, 4, 1, 3};
    int (*cmp)(const void*, const void*);

    cmp = compare_fwd;
    qsort(ary, sizeof(ary) / sizeof(*ary), sizeof(*ary), cmp);

    for (int i = 0; i < sizeof(ary) / sizeof(*ary); i++) {
        printf("%d ", ary[i]);
    }
    puts("");

    cmp = compare_rev;
    qsort(ary, sizeof(ary) / sizeof(*ary), sizeof(*ary), cmp);

    for (int i = 0; i < sizeof(ary) / sizeof(*ary); i++) {
        printf("%d ", ary[i]);
    }
    puts("");

    return 0;
}

このコードは、cmp という名前の関数ポインタ変数を宣言します。その後、compare_fwdcompare_rev という 2 つの関数を定義します。これらの関数は、2 つの整数を比較して、小さい/等しい/大きいを返します。main 関数では、ary という名前の配列を宣言し、qsort 関数を使って ary を昇順と降順にソートします。ソートには、compare_asccompare_desc 関数の関数ポインタが使用されます。

コールバック関数

[編集]

コールバック関数は、別の関数から呼び出される関数のことを指します。コールバック関数は、通常、非同期処理やイベント処理に使用されます。

例:
typedef void (*callback_t)(void *);

void do_something(void *data, callback_t callback) {
    // ... 何らかの処理を行う ...

    callback(data);
}

void callback_function(void *data) {
    // ... データを処理する ...
}

int main() {
    do_something(NULL, callback_function);

    return 0;
}

このコードは、callback_t という名前のtypedef を定義します。このtypedef は、void * 型の引数を受け取る void 型の関数を表します。その後、do_something という名前の関数を定義します。この関数は、data という名前のポインタと callback という名前の関数ポインタ引数を受け取り、callback 関数を呼び出して data を渡します。main 関数では、callback_function 関数のポインタを do_something 関数に渡し、do_something 関数を呼び出します。

ポインタの応用

[編集]

動的メモリ管理

[編集]

動的メモリ管理は、プログラム実行時に必要なメモリを動的に割り当てたり解放したりすることです。C 言語では、malloc()free() などの関数を使って、動的メモリ管理を行うことができます。

例:
int *p = malloc(sizeof(int) * 10);

if (p != NULL) {
    for (int i = 0; i < 10; i++) {
        p[i] = i;
    }

    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
    puts("");

    free(p);
} else {
    printf("メモリ確保に失敗しました。\n");
}

このコードは、malloc() 関数を使って 10 個の int 型の要素を格納できるメモリ領域を確保します。メモリ確保が成功した場合、for ループを使ってメモリ領域の要素に値を代入し、別の for ループを使ってメモリ領域の要素の値を出力します。最後に、free() 関数を使ってメモリ領域を解放します。メモリ確保が失敗した場合、エラーメッセージを出力します。

構造体とポインタ

[編集]

構造体とポインタを組み合わせて使うことで、複雑なデータ構造を効率的に扱うことができます。

例:
typedef struct student {
    char name[32];
    int age;
    double score;
} Student;

Student *p = malloc(sizeof(Student));

if (p != NULL) {
    strcpy(p->name, "Taro Yamada");
    p->age = 20;
    p->score = 80.0;

    printf("名前: %s\n", p->name);
    printf("年齢: %d歳\n", p->age);
    printf("点数: %f\n", p->score);

    free(p);
} else {
    printf("メモリ確保に失敗しました。\n");
}

このコードは、Student という名前のtypedef を定義します。このtypedef は、nameagescore というメンバを持つ構造体を表します。その後、malloc() 関数を使って Student 型の構造体を 1 つ格納できるメモリ領域を確保します。メモリ確保が成功した場合、構造体のメンバに値を代入し、構造体のメンバの値を出力します。最後に、free() 関数を使ってメモリ領域を解放します。

リンクドリスト

[編集]

リンクドリストは、ポインタを使って要素を連結したデータ構造です。リンクドリストは、挿入、削除、検索などの操作を効率的に行うことができます。

例:
typedef struct node {
    int data;
    struct node *next;
} Node;

Node *insert_node(Node *head, int data) {
    Node *new_node = malloc(sizeof(Node));
    if (new_node != NULL) {
        new_node->data = data;
        new_node->next = head;
        head = new_node;
    }
    return head;
}

void print_list(Node *head) {
    while (head != NULL) {
        printf("%d ", head->data);
        head = head->next;
    }
    printf("\n");
}

int main() {
    Node *head = NULL;

    head = insert_node(head, 10);
    head = insert_node(head, 20);
    head = insert_node(head, 30);

    print_list(head);

    return 0;
}

このコードは、Node という名前のtypedef を定義します。このtypedef は、data というメンバと next というメンバを持つ構造体を表します。insert_node 関数は、新しいノードをリンクドリストの先頭に挿入します。print_list 関数は、リンクドリストの要素をすべて出力します。main 関数では、リンクドリストを作成し、要素を挿入して出力します。

ポインタの注意点

[編集]

ヌルポインタ

[編集]

ヌルポインタは、何も指していないポインタのことを指します。ヌルポインタにアクセスしようとすると、プログラムがクラッシュする可能性があります。

例:
int *p = NULL;
 
*p = 10; // エラーが発生する可能性があります

このコードは、p という名前のポインタ変数を宣言し、初期化しません。その後、*p に 10 を代入しようとします。しかし、p はヌルポインタであるため、この操作はエラーが発生する可能性があります。

未定義動作

[編集]

ポインタを誤って使用すると、未定義動作が発生する可能性があります。未定義動作は、プログラムの動作が予測できなくなる状態です。

例:
int ary[10];
int *p = &ary[10];
 
*p = 20; // 未定義動作が発生する可能性があります

このコードは、ary という配列を宣言し、その要素に値を代入します。その後、p という名前のポインタ変数を ary[10] に初期化します。しかし、ary[10] は存在しない要素であるため、この操作は未定義動作が発生する可能性があります。

メモリリーク

[編集]

メモリリークは、プログラム実行中にメモリを割り当てたが、解放せずに放置してしまう状態です。メモリリークは、メモリ不足やプログラムの動作不良を引き起こす可能性があります。

例:
int *p = malloc(sizeof(int) * 10);
 
 // ... 処理を行う ...
 
 // メモリ解放を忘れている

このコードは、malloc() 関数を使って 10 個の int 型の要素を格納できるメモリ領域を確保します。その後、メモリ領域を使って処理を行います。しかし、free() 関数を使ってメモリ領域を解放することを忘れてしまうと、メモリリークが発生します。

まとめ

[編集]

ポインタは、C 言語において非常に重要な機能です。ポインタを使うことで、データ処理を柔軟かつ効率的に行うことができます。しかし、ポインタを誤って使用すると、プログラムがクラッシュしたり、メモリリークが発生したりする可能性があります。

ポインタを安全かつ効果的に使うためには、以下の点に注意する必要があります。

  • ヌルポインタにアクセスしない
  • 未定義動作を引き起こさないように注意する
  • メモリリークが発生しないように注意する

これらの点に注意することで、ポインタを安全かつ効果的に使いこなすことができます。

練習問題

[編集]
  1. 以下のコードを実行し、結果を説明してください。
    int ary[5] = {1, 2, 3, 4, 5};
    int *p = ary;
     
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    
  1. 以下のコードを実行し、結果を説明してください。
    int ary[5] = {1, 2, 3, 4, 5};
    int *p = &ary[2];
     
    for (int i = 0; i < 3; i++) {
        printf("%d ", *p++);
    }
    
  2. 以下のコードを実行し、結果を説明してください。
    void swap(int *a, int *b) {
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
     
    int main() {
        int x = 5;
        int y = 10;
     
        swap(&x, &y);
     
        printf("%d, %d\n", x, y);
    }
    
  3. 以下のコードを実行し、結果を説明してください。
    typedef struct student {
        char name[32];
        int age;
        double score;
    } Student;
    
    Student *create_student(char *name, int age, double score) {
        Student *new_student = malloc(sizeof(Student));
        if (new_student != NULL) {
            strcpy(new_student->name, name);
            new_student->age = age;
            new_student->score = score;
        }
        return new_student;
    }
    
    void print_student(Student *student) {
        if (student != NULL) {
            printf("名前: %s\n", student->name);
            printf("年齢: %d歳\n", student->age);
            printf("点数: %f\n", student->score);
        }
    }
    
    int main() {
        Student *s = create_student("Taro Yamada", 20, 80.0);
        if (s != NULL) {
            print_student(s);
            free(s);
        }
        return 0;
    }