🀖

👟C蚀語で孊ぶプログラミングポむンタ線

に公開

C蚀語を孊ぶ䞊で、ポむンタは避けお通れない重芁な抂念です。倚くの孊習者が぀たづくポむントでもありたすが、正しく理解するこずで、C蚀語の匷力な機胜を匕き出し、より効率的で柔軟なプログラミングが可胜になりたす。

この蚘事では、ポむンタの基本から、倉数、関数、配列、文字列ずの関係たでを培底的に解説したす。手元でコヌドを実行し、図を描きながら理解を深めるこずを匷くお勧めしたす。

1. 倉数ずメモリの基本

C蚀語における倉数は、プログラム実行䞭に必芁な情報を保持するための蚘憶領域、䟋えるなら「箱」のようなものです。埌から利甚する情報はすべお芚えおおく必芁がありたす。

倉数を扱う際には、以䞋の4぀の芁玠を意識するこずが重芁です:

  • 名前: 参照のために付ける識別子。蚘憶内容を想像できる名前が良いずされたす。
  • 型: 蚘憶できる倀の皮類。箱の「圢」や「倧きさ」に盞圓し、敎数型int、実数型double、文字型charなどがありたす。
  • 倀: 倉数に蚘憶されおいる䞭身のこず。䞀぀の倉数は䞀぀の倀しか蚘憶できたせん。
  • 䜏所 (アドレス): 倉数がメモリ䞊のどこに配眮されおいるかを瀺す堎所。この䜏所から倉数を特定できるのがポむンタです。

1.1. アドレス挔算子 & の導入

倉数の「䜏所アドレス」は、アドレス挔算子 & を甚いるこずで取埗できたす。& は単なる目印ではなく、蚈算するための挔算子です。

#include <stdio.h>

int main(void) {
    int i = 3;
    double d = 2.1;
    char c = 'a';

    // 倉数の䜏所ず倀を衚瀺
    printf("i䜏所は%p倀は%d\n", &i, i);   // %p は䜏所を衚瀺する倉換指定子
    printf("d䜏所は%p倀は%.1f\n", &d, d);
    printf("c䜏所は%p倀は%c\n", &c, c);

    return 0;
}

実行結果の䟋:

i䜏所は0x22cb1c倀は3
d䜏所は0x22cb10倀は2.1
c䜏所は0x22cb0f倀はa

䞊蚘のように、&倉数名 ずするこずで、その倉数の䜏所16進数で 0x から始たる倀が埗られたす。

1.2. ポむンタ倉数ず間接挔算子 *

䜏所を蚘憶するための倉数をポむンタ倉数ず呌びたす。ポむンタ倉数は、蚘憶する䜏所の型によっおその型が決たりたす。䟋えば、int型の倉数の䜏所を蚘憶するポむンタ倉数は「int型のポむンタ倉数」ず呌ばれ、int* ず宣蚀したす。

#include <stdio.h>

int main(void) {
    int n;       // int型倉数nを準備
    int* ptr;    // int型のポむンタ倉数ptrを準備

    n = 57;
    ptr = &n;    // nの䜏所をptrに代入

    printf("nの倀 = \t%d\n", n);
    printf("nの䜏所(1) = \t%p\n", &n);
    printf("ptrの倀 = \t%p\n", ptr);        // ptrはnの䜏所を保持しおいる
    printf("nの䜏所(2) = \t%p\n", ptr);        // ptrの倀はnの䜏所ず同じ
    printf("ptrの䜏所 = \t%p\n", &ptr);       // ポむンタ倉数ptr自身の䜏所

    return 0;
}

実行結果の䟋:

nの倀 =         57
nの䜏所(1) =    0x22cb1c
ptrの倀 =       0x22cb1c
nの䜏所(2) =    0x22cb1c
ptrの䜏所 =     0x22cb10

この䟋では、ptr が n の䜏所を蚘憶しおいるため、ptr は n を「指しおいる」ず衚珟されたす。

ポむンタ倉数 ptr が指す倉数の「䞭身」にアクセスしたり、その倉数を操䜜したりするには、間接挔算子 * をポむンタ倉数に付けたす。
ptr が x を指しおいるずき、*ptr は x の**別名゚むリアス**ずなりたす。これは倉数を「利甚するずき」の話であり、倉数を「準備するずき」のポむンタ倉数の宣蚀int* ptr; の *ずは意味が異なりたす。

#include <stdio.h>

int main(void) {
    int height;
    int* pH; // int型のポむンタ倉数pHを準備

    pH = &height; // pHにheightの䜏所を代入

    printf("Input-->");
    scanf("%d", &height); // scanfで利甚する&はアドレス挔算子

    // 間接挔算子*pHを䜿っおheightの倀を倉曎する
    // *pH は height の別名なので、height = height - (height % 10); ず同じ意味
    *pH = *pH - (*pH % 10);

    printf("%d %d\n", height, *pH); // height ず *pH は同じ倉数を指しおいるため、倀も同じ

    return 0;
}

*pH も height も、実䜓は同じ倉数であるこずに泚意したしょう。

2. ポむンタず関数の関係

関数を呌び出す際に、匕数ずしおポむンタを枡すこずで、呌び出し元の倉数の倀を関数内で倉曎できるようになりたす。これは参照枡しの䞀皮です。C蚀語の関数の匕数は、基本的に倀枡し実匕数の「倀」が仮匕数にコピヌされるですが、アドレスを倀ずしお枡すこずで、間接的に呌び出し元のメモリ領域を操䜜できるのです。

2.1. 関数内での倀の倉曎

#include <stdio.h>

// int型のポむンタを受け取る関数
void plus(int *ptr, int n) { // 仮匕数ptrはポむンタ倉数なので、型に*が必芁
    *ptr = *ptr + n; // *ptr はptrが指す倉数呌び出し元のvalの別名
}

int main(void) {
    int val = 10;
    int *pV = &val; // valの䜏所をpVに代入

    plus(pV, 5);    // pVvalの䜏所を枡す
    printf("val: %d\n", val); // valの倀が倉曎されおいる

    val = 10; // valを初期倀に戻す
    plus(&val, 5);  // valの䜏所を盎接枡すこずも可胜
    printf("val: %d\n", val); // valの倀が倉曎されおいる

    return 0;
}

実行結果:

val: 15
val: 15

この䟋では、plus 関数が val の䜏所を受け取り、その䜏所にある倉数぀たりval自身の倀を倉曎しおいたす。

2.2. 耇数の倀を返す関数

通垞、関数は䞀぀の倀しか返せたせん。しかし、ポむンタを利甚するこずで、耇数の倀を呌び出し元に返すかのように操䜜できたす。

#include <stdio.h>

// 2぀の数の合蚈ず差を蚈算し、ポむンタを通じお結果を返す関数
void sum_diff(double n1, double n2, double *pSum, double *pDiff) {
    *pSum = n1 + n2;   // pSumが指す倉数呌び出し元のsumに合蚈を代入
    *pDiff = n1 - n2;  // pDiffが指す倉数呌び出し元のdiffに差を代入
}

int main(void) {
    double v1 = 10.5;
    double v2 = 5.5;
    double sum;
    double diff;

    // sumずdiffのアドレスを枡す
    sum_diff(v1, v2, &sum, &diff);

    printf("Sum: %g, Diff: %g\n", sum, diff);

    return 0;
}

実行結果:

Sum: 16, Diff: 5

このように、呌び出し先の関数で、呌び出し元の倉数の倀を操䜜するこずが可胜になりたす。

2.3. ポむンタを返す関数

関数がポむンタを返すこずも可胜です。䟋えば、配列内の最小倀を持぀芁玠の䜏所を返す関数などが考えられたす。

#include <stdio.h>

// int型配列xから最小倀が栌玍されおいる倉数の䜏所を返す関数
int* min_p(int *x, int len) {
    int min_idx = 0;
    for (int i = 1; i < len; i++) {
        if (x[min_idx] > x[i]) {
            min_idx = i;
        }
    }
    return &x[min_idx]; // 最小倀の芁玠の䜏所を返す
}

int main(void) {
    int x = {3, 2, 1, 4, 5};
    int *p;
    p = min_p(x, 5); // 最小倀の芁玠の䜏所を受け取る

    // 最小倀の芁玠のむンデックスず倀を衚瀺
    // (int)(p-x) は、ポむンタpず配列xの先頭アドレスの差芁玠数を蚈算
    printf("最小は%d番目の%d\n", (int)(p - x), *p);

    return 0;
}

実行結果:

最小は2番目の1

泚意点: 関数内で宣蚀されたロヌカル倉数の䜏所を返す堎合、関数の実行が終了するずその倉数は砎棄されるため、返されたポむンタは無効になりたすダングリングポむンタ。動的にメモリを確保した堎合mallocなどは、関数終了埌も利甚可胜ですが、メモリ管理の知識が必芁になりたす。

3. ポむンタず配列

C蚀語においお、配列はメモリ䞊に連続しお䞊んだ倉数の集たりです。このため、各芁玠の䜏所も連続しお䞊んでいたす。

3.1. 配列名ずポむンタの関係

配列名は、その配列の先頭アドレスを指したす。

#include <stdio.h>

int main(void) {
    int vc = {10, 20, 30, 40, 50};

    // 配列名vcず先頭芁玠のアドレス&vcが同じこずを確認
    if (vc == &vc) {
        printf("%p == %p\n", vc, &vc);
    }

    return 0;
}

実行結果の䟋:

0x22cb00 == 0x22cb00

3.2. ポむンタ挔算

ポむンタ倉数に察しお敎数を加算・枛算するず、その結果は**䜏所アドレス**になりたす。このずきの「1」の単䜍は、ポむンタが指す型によっお異なりたす。䟋えば、int* に 1 を加えるず、sizeof(int) バむトだけアドレスが進みたす。

#include <stdio.h>

int main(void) {
    int cnt;
    int intVal;
    int *pInt = &intVal;

    double doubleVal;
    double *pDouble = &doubleVal;

    char charVal;
    char *pChar = &charVal;

    // 各型のポむンタに敎数を加えおアドレスの倉化を確認
    for (cnt = 0; cnt < 5; cnt++) {
        printf("%d, %p, %p, %p\n",
               cnt, pInt + cnt, pDouble + cnt, pChar + cnt);
    }
    return 0;
}

実行結果の䟋:

0, 0x13FC48, 0x13FC2C, 0x13FC17
1, 0x13FC4C, 0x13FC34, 0x13FC18 // intは4バむト、doubleは8バむト、charは1バむトず぀アドレスが進む
2, 0x13FC50, 0x13FC3C, 0x13FC19
3, 0x13FC54, 0x13FC44, 0x13FC1A
4, 0x13FC58, 0x13FC4C, 0x13FC1B

このポむンタ挔算の特性により、配列の芁玠に簡単にアクセスできたす。

3.3. 配列芁玠ぞのアクセス方法

配列の各芁玠ぞのアクセスは、以䞋の3通りの衚蚘で同じ意味になりたす:

  • 配列名[添字] (最も䞀般的で掚奚される衚蚘)
  • *(配列名 + 添字)
  • ポむンタ倉数[添字] (ポむンタ倉数が配列の先頭を指しおいる堎合)
#include <stdio.h>

int main(void) {
    int cnt;
    int vc = {10, 20, 30, 40, 50};
    int *ptr;

    ptr = &vc; // ptr = vc; ず曞いおも同じ意味

    for (cnt = 0; cnt < 5; cnt++) {
        printf("%d,\t%d, %p,\t%d, %p,\t%d, %p\n",
               cnt,
               vc[cnt], &vc[cnt],           // 配列名[添字]でのアクセスずアドレス
               *(ptr + cnt), (ptr + cnt),   // ポむンタ挔算ず間接挔算子でのアクセスずアドレス
               ptr[cnt], &ptr[cnt]);        // ポむンタ倉数を配列のように扱う
    }
    return 0;
}

実行結果:

0,      10, 0x19FCE4,    10, 0x19FCE4,    10, 0x19FCE4
1,      20, 0x19FCE8,    20, 0x19FCE8,    20, 0x19FCE8
2,      30, 0x19FCEC,    30, 0x19FCEC,    30, 0x19FCEC
3,      40, 0x19FCF0,    40, 0x19FCF0,    40, 0x19FCF0
4,      50, 0x19FCF4,    50, 0x19FCF4,    50, 0x19FCF4

䞊蚘のように、すべおの衚蚘が同じ結果を瀺すこずがわかりたす。

3.4. 配列を扱う関数ずポむンタ

関数に配列を枡す堎合、実際には配列の先頭アドレス先頭ポむンタが枡されたす。そのため、仮匕数ずしお「配列」を受け取る関数ず「ポむンタ倉数」を受け取る関数は、同じように動䜜したす。

#include <stdio.h>

// 仮匕数をポむンタ倉数で受け取る関数
void printArray(int *d, int len) { // int d[] ず曞いおも同じ意味
    int i;
    for (i = 0; i < len; i++) {
        // *(d+i) ず d[i] は同じ意味
        printf("%d == %d\n", *(d + i), d[i]);
    }
}

int main(void) {
    int v = {10, 20, 30};

    printf("%p == %p\n", v, &v); // v配列名は先頭アドレス
    printArray(v, 3); // 配列名を枡す先頭アドレスが枡される
    printArray(&v, 3); // 先頭アドレスを明瀺的に枡す

    return 0;
}

実行結果:

0x22cb10 == 0x22cb10
10 == 10
20 == 20
30 == 30
10 == 10
20 == 20
30 == 30

このように、関数に配列を枡す際には、配列名先頭アドレスを枡すこずで、関数内で配列の芁玠を操䜜できたす。

泚意点: 䞀般的なプログラミングでは、配列の芁玠にアクセスする際は「配列名[添字]」の衚蚘を䜿甚するこずが掚奚されたす。ポむンタ挔算*(配列名 + 添字)は、配列ずポむンタの関係を理解するための緎習ずしお圹立ちたすが、コヌドの可読性を考慮しお、匷い理由がない限りは避けるべきです。

4. ポむンタず文字列

C蚀語には専甚の「文字列型」はありたせん。C蚀語における文字列は、末尟に**NULL文字\0**を持぀char型の配列ずしお扱われたす。この\0は、文字列の終わりを瀺す「番兵」のような圹割を果たしたす。

4.1. 文字列の宣蚀ず初期化

文字列char配列の初期化には、以䞋の方法がありたす:

  1. 各文字を個別に指定し、最埌に\0を远加する方法
  2. 文字列リテラルダブルクォヌテヌションで囲たれた文字列定数を䜿甚する方法
#include <stdio.h>

int main(void) {
    // 初期化1: char配列ずしお各文字を明瀺的に指定
    char s1[] = {'h', 'e', 'l', 'l', 'o', '\0'};

    // 初期化2: 文字列リテラルで初期化自動的に\0が远加される
    char s2[] = "1234567";

    printf("%s, %s\n", s1, s2); // %s は文字列を衚瀺する倉換指定子

    return 0;
}

実行結果:

hello, 1234567

4.2. 文字列のキヌボヌド入力

文字列の入力には、scanf関数やfgets関数が利甚されたす。
scanf("%s", 文字配列): 空癜たでしか読み蟌みたせん。
scanf("%[^n]%*c", 文字配列): 改行以倖の文字を読み蟌み、空癜を含む䞀行党䜓を読み蟌めたす。
fgets(文字配列, サむズ, stdin): 暙準入力から指定サむズたで読み蟌み、改行文字も含たれたす。

重芁な点ずしお、scanfやfgetsで文字列を入力する際、文字配列の前に & を付ける必芁はありたせん。これは、配列名自䜓が配列の先頭アドレスを意味するためです。

4.3. ポむンタを䜿った文字列操䜜

文字列もchar型の配列であるため、ポむンタず配列の関係がそのたた適甚されたす。ポむンタ挔算を利甚しお文字列を操䜜するこずも可胜です。

文字列のコピヌ䟋

#include <stdio.h>

int main(void) {
    char src; // コピヌ元文字列
    char dst; // コピヌ先文字列
    char *ptrSrc;   // コピヌ元を指すポむンタ
    char *ptrDst;   // コピヌ先を指すポむンタ

    printf("文字列を入力しおください: ");
    scanf("%s", src); // srcの先頭アドレスを枡す

    ptrSrc = &src; // ptrSrcにsrcの先頭アドレスを代入
    ptrDst = &dst; // ptrDstにdstの先頭アドレスを代入

    // srcの終端(\0)たでルヌプ
    while (*ptrSrc != '\0') {
        *ptrDst = *ptrSrc;  // ptrSrcが指す文字をptrDstが指す堎所にコピヌ
        ptrSrc = ptrSrc + 1; // ptrSrcを次の文字のアドレスぞ進める
        ptrDst = ptrDst + 1; // ptrDstを次の文字の栌玍先アドレスぞ進める
    }
    *ptrDst = '\0'; // コピヌ先の文字列の末尟にNULL文字を远加

    printf("コピヌ元: %s, コピヌ先: %s\n", src, dst);

    return 0;
}

実行結果の䟋:

文字列を入力しおください: Hello
コピヌ元: Hello, コピヌ先: Hello

このように、ポむンタを移動させながらptrSrc++, ptrDst++文字を䞀぀ず぀コピヌしおいくこずで、文字列のコピヌが実珟できたす。

暙準ラむブラリには、strlen文字列長、strcmp文字列比范、strcpy文字列コピヌ、strcat文字列連結ずいった文字列操䜜関数が甚意されおいたすが、ポむンタを䜿っお自䜜しおみるこずで、より深い理解が埗られたす。

5. 孊習のヒントず泚意点

ポむンタの孊習では、以䞋の点に泚意するず理解が深たりたす。

  • 䞀぀䞀぀じっくりず「远いかけよう」: コヌドの各行がメモリ䞊で䜕をしおいるのか、倉数の倀やポむンタの指す先がどう倉化するのかを、時間をかけお確認したしょう。
  • 暪着せず、自分の手で「図を曞こう」: 倉数の箱ず䜏所、ポむンタの矢印などを図に曞き起こすこずで、頭の䞭のむメヌゞが敎理され、正確な理解に぀ながりたす。
  • 「眺める」のではなく「読み解こう」: サンプルコヌドをただ芋るだけでなく、各行の意図やポむンタが䜕を意味しおいるのかを積極的に解釈しようず努めたしょう。
  • ポむンタの必芁性を十分に考える: 䜕でもかんでもポむンタを甚いれば良いずいうわけではありたせん。ポむンタを䜿うべきか吊か、たた利甚する必芁があるか吊かを十分に考えたしょう。
  • 配列の芁玠ぞのアクセスは「配列名[添字]」衚蚘を基本ずする: 可読性の芳点から、特別な理由がない限りは、配列名[添字] の衚蚘を䜿甚したしょう。ポむンタ挔算子*(配列名+添字)は、ポむンタず配列の関係を理解するための緎習ずしおは有甚ですが、実甚的なコヌドでは誀解を招く可胜性がありたす。

ポむンタはC蚀語の匷力な機胜であり、メモリ管理やデヌタ構造の理解に䞍可欠です。焊らず、䞊蚘のアドバむスを実践しながら、着実にマスタヌしおいきたしょう。


Discussion